Best way to write thousands of strokes in WPF canvas - c#

I have to implement writing by touch on Wacom tablet ( like on paper) and I need to render up to 200 strokes per second in WPF Canvas. The problem is that after about 20s of constant writing it gets a bit laggs and the laggs grows. I store all strokes in HashSet. I thought about taking screen shoot when HashSet contains over 600 elements and set it as a background image and clear the HashSet, but after ~30 screen shoots it gets a bit blurred. Do you have any idea how to make it better?

You can use WriteableBitmap. This is very fast, but it works by writing individual bytes to bitmap, so if you need to apply effects to the drawing, then you have to write them yourself. This is an example of how you can use WriteableBitmap. It takes ~100 ms in debug mode to draw 1000 lines (each made out of 1600 points) on 1600 x 1200 px image on my computer, but I'm sure this can be optimized.
I'm just randomly drawing lines, but you can get events from Canvas and capture positions of stylus and pass them after each stroke.
public partial class MainWindow : Window
{
private WriteableBitmap _bitmap;
private readonly Random _random = new Random();
private readonly Stopwatch _stopwatch = new Stopwatch();
private const int White = 0x00000000;
private const int Red = 0x00FF0000;
private int _width;
private int _height;
public MainWindow()
{
InitializeComponent();
CanvasImage.Loaded += OnLoaded;
}
private async void OnLoaded(object sender, RoutedEventArgs e)
{
_width = (int)DrawableCanvas.ActualWidth;
_height = (int)DrawableCanvas.ActualHeight;
_bitmap = new WriteableBitmap(_width, _height, 96, 96, PixelFormats.Bgr32, null);
CanvasImage.Source = _bitmap;
while (true)
{
unsafe
{
for (var index = 0; index < _width * _height; index++)
*((int*)_bitmap.BackBuffer + index) = White;
}
_stopwatch.Start();
for (var index = 0; index < 1000; index++)
{
var start = _random.Next(0, _width);
var points = Enumerable.Range(0, _width).Select(x => new Point((x + start) % _width, x % _height));
UpdateImage(points);
}
Debug.WriteLine($"Last 1000 draws took: {_stopwatch.ElapsedMilliseconds} ms");
_stopwatch.Reset();
await Task.Delay(300);
}
}
private void UpdateImage(IEnumerable<Point> points)
{
_bitmap.Lock();
foreach (var point in points)
{
var x = (int)point.X;
var y = (int)point.Y;
var offset = _width * y + x;
unsafe
{
*((int*)_bitmap.BackBuffer + offset) = Red;
}
}
_bitmap.AddDirtyRect(new Int32Rect(0, 0, _width, _height));
_bitmap.Unlock();
}
}
And view:
<Grid>
<Border Width="1604" Height="1204" BorderThickness="2" BorderBrush="Blue">
<Canvas x:Name="DrawableCanvas">
<Image x:Name="CanvasImage"></Image>
</Canvas>
</Border>
</Grid>

Related

Skiasharp - reduce GPU load by decreasing the resolution

in my project (WinUi 3) i draw some polylines to plot some "real time" datas with Skiasharp. I saw the GPU-load is very high. So i tryed, what happens when less datas are drawn. ....the GPU load is still the same. Only if i change the size of the "SKXamlCanvas" it reduces the GPU load. (but the plot shout have a certain size...) So i tryed to render my chart in a bitmap with a lower resolution and scale it. But still it needs the same recources.
Is there any possibility to reduce the basic GPU-load? (as example an possibility to render in a lower resolution and scale it afterwords)
Example Application
(.net 6.0 Winui-3)
(Installed Modules: SkiaSharp, Skiasharp.Views.Winui)
XAML:
x:Class="skEfficientDraw.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:skEfficientDraw"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:skia="using:SkiaSharp.Views.Windows"
mc:Ignorable="d">
<Grid>
<skia:SKXamlCanvas x:Name="skCanvas" PaintSurface="skCanvas_PaintSurface" />
</Grid>
C#:
public sealed partial class MainWindow : Window
{
SKPaint linePaint = new SKPaint
{
Color = SKColor.Parse("#003366"),
StrokeWidth = 5,
IsAntialias = true,
Style = SKPaintStyle.Stroke
};
public MainWindow()
{
this.InitializeComponent();
DispatcherTimer DrawTimer;
DrawTimer = new DispatcherTimer();
DrawTimer.Tick += DrawTimer_Tick;
DrawTimer.Interval = new TimeSpan(0, 0, 0, 0, 100);
DrawTimer.Start();
}
private void DrawTimer_Tick(object sender, object e)
{
skCanvas.Invalidate();
}
private void skCanvas_PaintSurface(object sender, SkiaSharp.Views.Windows.SKPaintSurfaceEventArgs e)
{
///
/// - The GPU load is very high, also if lineCount is 0
/// - Is there a possibility to reduce the GPU-load? Maybe rendering the "skCanvas" in a lower resolution?
///
SKImageInfo info = e.Info;
SKSurface surface = e.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
int lineCount = 5;
Random rand = new Random();
for (int i = 0; i < lineCount; i++)
{
int x1 = rand.Next(info.Width);
int y1 = rand.Next(info.Height);
int x2 = rand.Next(info.Width);
int y2 = rand.Next(info.Height);
canvas.DrawLine(x1, y1, x2, y2, linePaint);
}
}
}

WPF Display a portion of bigger image in ImageBox

I'm coming from Winforms trying to rewrite a program in WPF, and I want to display a certain portion of the whole image, depending on an Id I use for a list, that I load each portion of the whole image in. I was able to do it successfully in Winforms, but I want to perform the same task in WPF using Controls.Image. Heres what I did in Winforms.
PictureBox picBox;
List<Image> tileImageList;
Image FullImage;
public TileFrame(PictureBox pbox)
{
picBox = pbox;
FullImage = picBox.Image; //The source of the picBox is set to the full image on init
tileImageList = new List<Image>();
PopTileList();
}
void PopTileList()
{
const int SIZE = 32;
Bitmap bitFullImage = new Bitmap(FullImage);
for (int y = 0; y < 48; y++)
{
for (int x = 0; x < 64; x++)
{
var portion = bitFullImage.Clone(new Rectangle((x * SIZE), (y * SIZE), SIZE, SIZE), bitFullImage.PixelFormat);
tileImageList.Add(portion);
}
}
picBox.Image = tileImageList[10];//The first image that shows when this is done
}
public void ShowTilePic(int selectedId)
{
picBox.Image = tileImageList[--selectedId];
}
Since the image being displayed will change based on the selected item of a listbox, the tileImageList is crucial for relating the list box selected index and the tileImageList index. Other answers I've searched for seemed to make it much more complicated than what I've done here. Is there a simple way to do this in WPF and in code?
Nvm I figured it out.
List<CroppedBitmap> tileImageList;
Image imageBox;
public TileFrame(MainWindow mWindow)
{
tileImageList = new List<CroppedBitmap>();
imageBox = mWindow.ImageBox;
PopTileList();
}
void PopTileList()
{
const int SIZE = 32;
var bitmapImage = (BitmapSource)imageBox.Source;
for (int y = 0; y < 48; y++)
{
for (int x = 0; x < 64; x++)
{
var portion = new CroppedBitmap(bitmapImage, new Int32Rect((x * SIZE), (y * SIZE), SIZE, SIZE));
tileImageList.Add(portion);
}
}
}
public void ShowTilePic(int selectedId)
{
imageBox.Source = tileImageList[selectedId];
}

How to optimize draw area in pixel art editor

I have pixel art creator program, and I have rectangles on canvas that are one field (pixel?). And this is good solution on not huge amount of it (for example 128x128). if i want to create 1024x1024 rectangles on canvas this process is very long, ram usage is about 1-2 gb and after that program runs very slowly. How to optimize this, or create better solution?
Using a Rectangle to represent each pixel is the wrong way to do this. As a FrameworkElement, every rectangle participates in layout and input hit testing. That approach is too heavy weight to be scalable. Abandon it now.
I would recommend drawing directly to a WriteableBitmap and using a custom surface to render the bitmap as the user draws.
Below is a minimum proof of concept that allows simple drawing in a single color. It requires the WriteableBitmapEx library, which is available from NuGet.
public class PixelEditor : FrameworkElement
{
private readonly Surface _surface;
private readonly Visual _gridLines;
public int PixelWidth { get; } = 128;
public int PixelHeight { get; } = 128;
public int Magnification { get; } = 10;
public PixelEditor()
{
_surface = new Surface(this);
_gridLines = CreateGridLines();
Cursor = Cursors.Pen;
AddVisualChild(_surface);
AddVisualChild(_gridLines);
}
protected override int VisualChildrenCount => 2;
protected override Visual GetVisualChild(int index)
{
return index == 0 ? _surface : _gridLines;
}
private void Draw()
{
var p = Mouse.GetPosition(_surface);
var magnification = Magnification;
var surfaceWidth = PixelWidth * magnification;
var surfaceHeight = PixelHeight * magnification;
if (p.X < 0 || p.X >= surfaceWidth || p.Y < 0 || p.Y >= surfaceHeight)
return;
_surface.SetColor(
(int)(p.X / magnification),
(int)(p.Y / magnification),
Colors.DodgerBlue);
_surface.InvalidateVisual();
}
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
if (e.LeftButton == MouseButtonState.Pressed && IsMouseCaptured)
Draw();
}
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonDown(e);
CaptureMouse();
Draw();
}
protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonUp(e);
ReleaseMouseCapture();
}
protected override Size MeasureOverride(Size availableSize)
{
var magnification = Magnification;
var size = new Size(PixelWidth* magnification, PixelHeight * magnification);
_surface.Measure(size);
return size;
}
protected override Size ArrangeOverride(Size finalSize)
{
_surface.Arrange(new Rect(finalSize));
return finalSize;
}
private Visual CreateGridLines()
{
var dv = new DrawingVisual();
var dc = dv.RenderOpen();
var w = PixelWidth;
var h = PixelHeight;
var m = Magnification;
var d = -0.5d; // snap gridlines to device pixels
var pen = new Pen(new SolidColorBrush(Color.FromArgb(63, 63, 63, 63)), 1d);
pen.Freeze();
for (var x = 1; x < w; x++)
dc.DrawLine(pen, new Point(x * m + d, 0), new Point(x * m + d, h * m));
for (var y = 1; y < h; y++)
dc.DrawLine(pen, new Point(0, y * m + d), new Point(w * m, y * m + d));
dc.Close();
return dv;
}
private sealed class Surface : FrameworkElement
{
private readonly PixelEditor _owner;
private readonly WriteableBitmap _bitmap;
public Surface(PixelEditor owner)
{
_owner = owner;
_bitmap = BitmapFactory.New(owner.PixelWidth, owner.PixelHeight);
_bitmap.Clear(Colors.White);
RenderOptions.SetBitmapScalingMode(this, BitmapScalingMode.NearestNeighbor);
}
protected override void OnRender(DrawingContext dc)
{
base.OnRender(dc);
var magnification = _owner.Magnification;
var width = _bitmap.PixelWidth * magnification;
var height = _bitmap.PixelHeight * magnification;
dc.DrawImage(_bitmap, new Rect(0, 0, width, height));
}
internal void SetColor(int x, int y, Color color)
{
_bitmap.SetPixel(x, y, color);
}
}
}
Just import it into your Xaml, preferably inside a ScrollViewer:
<Window x:Class="WpfTest.PixelArtEditor"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:l="clr-namespace:WpfTest"
Title="PixelArtEditor"
Width="640"
Height="480">
<ScrollViewer HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto">
<l:PixelEditor />
</ScrollViewer>
</Window>
Obviously, this is a far cry from being a fully-featured pixel art editor, but it's functional, and it's enough to get you on the right track. The difference in memory usage between editing a 128x128 image vs. 1024x1024 is about ~30mb. Fire it up and see it in action:
Hey, that was fun! Thanks for the diversion.
Just to improve Mike Strobel solution to snap gridlines to device pixels.
var d = -0.5d; // snap gridlines to device pixels
using (DrawingContext dc = _dv.RenderOpen())
{
GuidelineSet guidelineSet = new GuidelineSet();
guidelineSet.GuidelinesX.Add(0.5);
guidelineSet.GuidelinesY.Add(0.5);
dc.PushGuidelineSet(guidelineSet);
// Draw grid
}

Downloading intersecting but not duplicating map download tiles from list of points?

I have a WPF user control with which the user can draw a rectangle on the map canvas to define an area for downloading background imagery tiles from a web service. The tiles are in Lat long 1x1 degree.
This is working, I pass a point as a parameter & download the tile. However I am now attempting to pass a List<Point> for each corner of the user defined rectangle & therefore determine which tiles intersect each point. This works to an extent however if the user defines a rectangle completely within a single tile then the same tile is downloaded 4 times (once for each point):
ForEach(point in rectanglePointsList)
{
DownloadTile(point);
}
I need to iterate over the points & determine whether to download the subsequent tile or not. This code is dumb to the tiles, I only have the point parameters that I'm passing in. A colleague suggested a nested for loop whereby I convert the X & Y from each point, find the min & max & then somehow determine whether a tile should be downloaded knowing that the tiles are always 1x1 degree. Is there an algorithm to achieve this? Don't really know where to start.
List<int>xValuesList = new List<int>();
List<int> yValuesList = new List<int>();
ForEach(point in RectanglePointsList)
{
xValuesList.Add(Convert.ToInt32(point.X);
yValuesList.Add(Convert.ToInt32(point.Y);
}
int maxX = xValuesList.Select(value => value.X).Max();
int maxY = yValuesList.Select(value => value.Y).Max();
//Lost after here...
Here's a quick sample :D
selects a group of tiles
adds them or not to the cache
shows which have been added or not
supports a single mouse click, no need to draw a rectangle
encompasses selected tiles no matter where you start/end
Here I've clicked on the first twos, this time I've drawn a rectangle encompassing the twos below, they haven't been added twice.
XAML:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WpfApplication1"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
x:Name="Window"
Title="MainWindow"
Width="525"
Height="350"
Background="Transparent"
SnapsToDevicePixels="True"
UseLayoutRounding="True"
mc:Ignorable="d">
<Grid />
</Window>
Code:
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
namespace WpfApplication1
{
public partial class MainWindow : Window
{
private readonly HashSet<Int32Point> _set = new HashSet<Int32Point>();
private readonly int columns = 10;
private readonly int rows = 10;
private bool _down;
private Point _position1;
private Point _position2;
private Size size = new Size(500, 500);
public MainWindow()
{
InitializeComponent();
MouseDown += MainWindow_MouseDown;
MouseMove += MainWindow_MouseMove;
MouseUp += MainWindow_MouseUp;
}
private void MainWindow_MouseUp(object sender, MouseButtonEventArgs e)
{
_down = false;
InvalidateVisual();
// find rects selected
var x1 = (int) Math.Floor(_position1.X/(size.Width/columns));
var y1 = (int) Math.Floor(_position1.Y/(size.Height/rows));
var x2 = (int) Math.Ceiling(_position2.X/(size.Width/columns));
var y2 = (int) Math.Ceiling(_position2.Y/(size.Height/rows));
var w = x2 - x1;
var h = y2 - y1;
var builder = new StringBuilder();
for (var y = 0; y < h; y++)
{
for (var x = 0; x < w; x++)
{
var int32Point = new Int32Point(x1 + x, y1 + y);
var add = _set.Add(int32Point);
if (add)
{
// download image !!!
}
else
{
// image already downloaded, do something !
}
builder.AppendLine(string.Format("{0} : {1}", int32Point, (add ? "added" : "ignored")));
}
}
MessageBox.Show(builder.ToString());
}
private void MainWindow_MouseMove(object sender, MouseEventArgs e)
{
_position2 = e.GetPosition(this);
InvalidateVisual();
}
private void MainWindow_MouseDown(object sender, MouseButtonEventArgs e)
{
_position1 = e.GetPosition(this);
_down = true;
}
protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
{
InvalidateVisual();
}
protected override void OnRender(DrawingContext drawingContext)
{
// draw a mini-map
for (var y = 0; y < rows; y++)
{
for (var x = 0; x < columns; x++)
{
var color = Color.FromRgb((byte) ((double) x/columns*255), (byte) ((double) y/rows*255), 255);
var brush = new SolidColorBrush(color);
var w = size.Width/columns;
var h = size.Height/rows;
var rect = new Rect(w*x, h*y, w, h);
drawingContext.DrawRectangle(brush, null, rect);
}
}
// draw selection rectangle
if (_down)
{
drawingContext.DrawRectangle(null, new Pen(new SolidColorBrush(Colors.White), 2.0),
new Rect(_position1, _position2));
}
}
private struct Int32Point
{
public readonly int X, Y;
public Int32Point(int x, int y)
{
X = x;
Y = y;
}
public override string ToString()
{
return $"X: {X}, Y: {Y}";
}
}
}
}
Go on and improve on that !

Is there a fast way to manipulate and buffer a screen in Windows Forms?

I am working on a game for learning purposes, I want to make it only with the .NET-Framework and a Windows Forms project in C#.
I want to get the 'screen' (Something that can be displayed on the window) as an int[]. Modify the array and reapply the altered array to the 'screen' in a buffered manner (So that it doesn't flicker).
I am currently using a Panel, which I draw a Bitmap on with Graphics. The Bitmap is converted to an int[] which I then can modify and reapply to the Bitmap and redraw. It works, but is very slow, especially because I have to scale up the image every frame because my game is only 300x160 and the screen 900x500.
Build up:
// Renders 1 frame
private void Render()
{
// Buffer setup
_bufferedContext = BufferedGraphicsManager.Current;
_buffer = _bufferedContext.Allocate(panel_canvas.CreateGraphics(), new Rectangle(0, 0, _scaledWidth, _scaledHeight));
_screen.clear();
// Get position of player on map
_xScroll = _player._xMap - _screen._width / 2;
_yScroll = _player._yMap - _screen._height / 2;
// Indirectly modifies the int[] '_pixels'
_level.render(_xScroll, _yScroll, _screen);
_player.render(_screen);
// Converts the int[] into a Bitmap (unsafe method is faster)
unsafe
{
fixed (int* intPtr = &_screen._pixels[0])
{
_screenImage = new Bitmap(_trueWidth, _trueHeight, _trueWidth * 4, PixelFormat.Format32bppRgb, new IntPtr(intPtr));
}
}
// Draw generated image on buffer
Graphics g = _buffer.Graphics;
g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor;
g.DrawImage(_screenImage, new Rectangle(0, 0, 900, 506));
// Update panel buffered
_buffer.Render();
}
Is there a faster way without external libraries to make this work?
I'm not to sure about the unsafe code , But I do know about the buffered graphics manager. I think you should create a class for it instead of creating a new one every time.As well as having all of your sprites widths and heights be determined at the load instead of scaling them. That sped up my small game engine a good bit.
class Spritebatch
{
private Graphics Gfx;
private BufferedGraphics bfgfx;
private BufferedGraphicsContext cntxt = BufferedGraphicsManager.Current;
public Spritebatch(Size clientsize, Graphics gfx)
{
cntxt.MaximumBuffer = new Size(clientsize.Width + 1, clientsize.Height + 1);
bfgfx = cntxt.Allocate(gfx, new Rectangle(Point.Empty, clientsize));
Gfx = gfx;
}
public void Begin()
{
bfgfx.Graphics.Clear(Color.Black);
}
public void Draw(Sprite s)
{
bfgfx.Graphics.DrawImageUnscaled(s.Texture, new Rectangle(s.toRec.X - s.rotationOffset.Width,s.toRec.Y - s.rotationOffset.Height,s.toRec.Width,s.toRec.Height));
}
public void drawImage(Bitmap b, Rectangle rec)
{
bfgfx.Graphics.DrawImageUnscaled(b, rec);
}
public void drawImageClipped(Bitmap b, Rectangle rec)
{
bfgfx.Graphics.DrawImageUnscaledAndClipped(b, rec);
}
public void drawRectangle(Pen p, Rectangle rec)
{
bfgfx.Graphics.DrawRectangle(p, rec);
}
public void End()
{
bfgfx.Render(Gfx);
}
}
This is a example of what I used. It's set up to mimic the Spritebatch in Xna. Drawing the images Unscaled will really increase the speed of it.Also creating one instance of the buffered graphics and Context will be faster then creating a new one every time you have to render. So I would advise you to change the line g.DrawImage(_screenImage, new Rectangle(0, 0, 900, 506)); to DrawImageUnscaled(_screenImage, new Rectangle(0, 0, 900, 506));
Edited : Example of how to scale code on sprite load
public Sprite(Bitmap texture, float x, float y, int width, int height)
{
//texture is the image you originally start with.
Bitmap b = new Bitmap(width, height);
// Create a bitmap with the desired width and height
using (Graphics g = Graphics.FromImage(b))
{
g.DrawImage(texture, 0, 0, width, height);
}
// get the graphics from the new image and draw the old image to it
//scaling it to the proper width and height
Texture = b;
//set Texture which is the final picture to the sprite.
//Uppercase Texture is different from lowercase
Scaling of the image is expensive enough, even when is done without any interpolation. To speed up the things, you should minimize memory allocations: when you create brand new Bitmap every frame, it leads to object creation and pixmap buffer allocation. This fact negates all the benefits you get from BufferedGraphics. I advise you to do the following:
Create the Bitmap instance of required size (equal to screen size) only once, outside of Render method.
Use direct access to bitmap data through LockBits method, and try to implement the scaling be hand using nearest pixel.
Of course, using some sort of hardware acceleration for scaling operation is the most preferred option (for example, in opengl all images are usually drawn using textured rectangles, and rendering such rectangles implicitly involves the process of "scaling" when texture sampling is performed).
I'm wondering why do you call this "very slow", because I did some tests and the performance doesn't seem bad. Also have you measured the performance of your rendering code into int[] '_pixels' (unfortunately you haven't provided that code) separately from the bitmap operations, because it might be the slow part.
About your concrete question. As others mentioned, using preallocated buffered graphics and bitmap objects would speed up it a bit.
But do you really need that int[] buffer? BufferedGraphics is already backed internally with a bitmap, so what really happens is:
(1) You fill the int[] buffer
(2) int[] buffer is copied to the new/preallocated Bitmap
(3) Bitmap from step 2 is copied (applying scale) to the BufferedGraphics internal bitmap (via DrawImage)
(4) BufferedGraphics internal bitmap is copied to the screen (via Render)
As you can see, there are a lot of copy operations. The intended usage of BufferedGraphics is:
(1) You fill the BufferedGraphics internal bitmap via drawing methods of the BufferedGraphics.Graphics property. If setup, the Graphics will do the scaling (as well other transformations) for you.
(2) BufferedGraphics internal bitmap is copied to the screen (via Render)
I don't know what your drawing code is doing, but if you can afford it, this definitely should provide the best performance.
Here is my quick and dirty test in case you are interested in:
using System;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.Threading;
using System.Windows.Forms;
namespace Test
{
enum RenderMode { NewBitmap, PreallocatedBitmap, Graphics }
class Screen
{
Control canvas;
public Rectangle area;
int[,] pixels;
BitmapData info;
Bitmap bitmap;
BufferedGraphics buffer;
float scaleX, scaleY;
public RenderMode mode = RenderMode.NewBitmap;
public Screen(Control canvas, Size size)
{
this.canvas = canvas;
var bounds = canvas.DisplayRectangle;
scaleX = (float)bounds.Width / size.Width;
scaleY = (float)bounds.Height / size.Height;
area.Size = size;
info = new BitmapData { Width = size.Width, Height = size.Height, PixelFormat = PixelFormat.Format32bppRgb, Stride = size.Width * 4 };
pixels = new int[size.Height, size.Width];
bitmap = new Bitmap(size.Width, size.Height, info.PixelFormat);
buffer = BufferedGraphicsManager.Current.Allocate(canvas.CreateGraphics(), bounds);
buffer.Graphics.InterpolationMode = InterpolationMode.NearestNeighbor;
ApplyMode();
}
public void ApplyMode()
{
buffer.Graphics.ResetTransform();
if (mode == RenderMode.Graphics)
buffer.Graphics.ScaleTransform(scaleX, scaleY);
}
public void FillRectangle(Color color, Rectangle rect)
{
if (mode == RenderMode.Graphics)
{
using (var brush = new SolidBrush(color))
buffer.Graphics.FillRectangle(brush, rect);
}
else
{
rect.Intersect(area);
if (rect.IsEmpty) return;
int colorData = color.ToArgb();
var pixels = this.pixels;
for (int y = rect.Y; y < rect.Bottom; y++)
for (int x = rect.X; x < rect.Right; x++)
pixels[y, x] = colorData;
}
}
public unsafe void Render()
{
if (mode == RenderMode.NewBitmap)
{
var bounds = canvas.DisplayRectangle;
using (var buffer = BufferedGraphicsManager.Current.Allocate(canvas.CreateGraphics(), bounds))
{
Bitmap bitmap;
fixed (int* pixels = &this.pixels[0, 0])
bitmap = new Bitmap(info.Width, info.Height, info.Stride, info.PixelFormat, new IntPtr(pixels));
buffer.Graphics.InterpolationMode = InterpolationMode.NearestNeighbor;
buffer.Graphics.DrawImage(bitmap, bounds);
buffer.Render();
}
}
else
{
if (mode == RenderMode.PreallocatedBitmap)
{
fixed (int* pixels = &this.pixels[0, 0])
{
info.Scan0 = new IntPtr(pixels); info.Reserved = 0;
bitmap.LockBits(area, ImageLockMode.WriteOnly | ImageLockMode.UserInputBuffer, info.PixelFormat, info);
bitmap.UnlockBits(info);
}
buffer.Graphics.DrawImage(bitmap, canvas.DisplayRectangle);
}
buffer.Render();
}
}
}
class Game
{
[STAThread]
public static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
var game = new Game();
game.Run();
}
Form form;
Control canvas;
Screen screen;
Level level;
Player player;
private Game()
{
form = new Form();
canvas = new Control { Parent = form, Bounds = new Rectangle(0, 0, 900, 506) };
form.ClientSize = canvas.Size;
screen = new Screen(canvas, new Size(300, 160));
level = new Level { game = this };
player = new Player { game = this };
}
private void Run()
{
bool toggleModeRequest = false;
canvas.MouseClick += (sender, e) => toggleModeRequest = true;
var worker = new Thread(() =>
{
int frameCount = 0;
Stopwatch drawT = new Stopwatch(), applyT = new Stopwatch(), advanceT = Stopwatch.StartNew(), renderT = Stopwatch.StartNew(), infoT = Stopwatch.StartNew();
while (true)
{
if (advanceT.ElapsedMilliseconds >= 3)
{
level.Advance(); player.Advance();
advanceT.Restart();
}
if (renderT.ElapsedMilliseconds >= 8)
{
frameCount++;
drawT.Start(); level.Render(); player.Render(); drawT.Stop();
applyT.Start(); screen.Render(); applyT.Stop();
renderT.Restart();
}
if (infoT.ElapsedMilliseconds >= 1000)
{
double drawS = drawT.ElapsedMilliseconds / 1000.0, applyS = applyT.ElapsedMilliseconds / 1000.0, totalS = drawS + applyS;
var info = string.Format("Render using {0} - Frames:{1:n0} FPS:{2:n0} Draw:{3:p2} Apply:{4:p2}",
screen.mode, frameCount, frameCount / totalS, drawS / totalS, applyS / totalS);
form.BeginInvoke(new Action(() => form.Text = info));
infoT.Restart();
}
if (toggleModeRequest)
{
toggleModeRequest = false;
screen.mode = (RenderMode)(((int)screen.mode + 1) % 3);
screen.ApplyMode();
frameCount = 0; drawT.Reset(); applyT.Reset();
}
}
});
worker.IsBackground = true;
worker.Start();
Application.Run(form);
}
class Level
{
public Game game;
public int pos = 0; bool right = true;
public void Advance() { Game.Advance(ref pos, ref right, 0, game.screen.area.Right - 1); }
public void Render()
{
game.screen.FillRectangle(Color.SaddleBrown, new Rectangle(0, 0, pos, game.screen.area.Height));
game.screen.FillRectangle(Color.DarkGreen, new Rectangle(pos, 0, game.screen.area.Right, game.screen.area.Height));
}
}
class Player
{
public Game game;
public int x = 0, y = 0;
public bool right = true, down = true;
public void Advance()
{
Game.Advance(ref x, ref right, game.level.pos, game.screen.area.Right - 5, 2);
Game.Advance(ref y, ref down, 0, game.screen.area.Bottom - 1, 2);
}
public void Render() { game.screen.FillRectangle(Color.Yellow, new Rectangle(x, y, 4, 4)); }
}
static void Advance(ref int pos, ref bool forward, int minPos, int maxPos, int delta = 1)
{
if (forward) { pos += delta; if (pos < minPos) pos = minPos; else if (pos > maxPos) { pos = maxPos; forward = false; } }
else { pos -= delta; if (pos > maxPos) pos = maxPos; else if (pos < minPos) { pos = minPos; forward = true; } }
}
}
}

Categories