natecurtiss / n8engine

An open-source C# game engine that's going to be the best thing ever.
MIT License
49 stars 3 forks source link

Added Bindings For Copying CHAR_INFO array to output buffer #1

Closed Andrew900460 closed 3 years ago

Andrew900460 commented 3 years ago

hope this helps!

Andrew900460 commented 3 years ago

From looking at the code, I would suggest taking the CharInfo array and only having 1 allocation of it. So if there is another class which holds "characters", "foregroundColors", and "backgroundColors", put the CharInfo Array in that class as well. Because that will ultimately represent the "final buffer" that is sent to the Windows API.

I'm not sure how you use the Write function outside of the ConsoleWriting.cs file. I'm assuming you would call it once per frame. If that's the case, you are always doing "new CharInfo[]" and allocating a new array every time. So however you choose to do it, you should only allocate that array once. Then you can transfer the characters and colors to that already existing array.

It's kinda odd that WriteConsoleOutput would allocate a string??? In theory WriteConsoleOutput should give best performance, of course. Unless there was another function I've overlooked 1000+ times! But I'm sure that's the one you should use if you want to write blocks of character data. I could run my own performance tests, but I'm very busy with my own project. But If I have a chance I would test it.

Maybe Rider thinks its a String because of the char member variable in CharInfo??? That's all I could imagine.

Anyway, I hope you can get it working, I'm sure there is something up, and this will lead to some nice performance boosts.

Andrew900460 commented 3 years ago

Also, from looking at the image you attached. I see a function called "ConsoleWriting.Write", but the first parameter is an array of chars[]. So I am also wondering if Rider thought that was the String?

I don't see that version of the function in the repo so idk.

After some looking, that looks like the same function it's just that you added arguments.

Before I go to bed here finally :P I noticed you just have your arrays of chars and color structs in the renderer class. So you could also just create a CharInfo array there as well and pass that as a parameter, and hopefully, that fixes the allocation issue.

Andrew900460 commented 3 years ago

After some time of thinking, I am also wondering If the allocations were caused by the "marshaling" stuff that is going on internally?

I'm wondering if you could use the Span class along with Marshal.AllocHGlobal to create the CharInfo buffer in Unmanaged memory, and then you can pass it into the WriteConsoleOutput function.

https://docs.microsoft.com/en-us/dotnet/api/system.span-1?view=net-5.0

// Create a span from native memory.
// Should probably use  Marshal.SizeOf( typeof( MY_TYPE ) );
var native = Marshal.AllocHGlobal(100); // change 100 to total bytes that make up the array ( sizeof(type) * arrayLength )
Span<byte> nativeSpan;
unsafe {
    nativeSpan = new Span<byte>(native.ToPointer(), 100); // change 100 to the total elements in array buffer ( arrayLength )
}

// access data through
// nativeSpan[index];

Marshal.FreeHGlobal(native);

The only thing is that you would have to alter the function binding so you don't get c# compiler errors. But as long as the underlying data is the same, it should be fine. I mean, at its core, [In] CharInfo[] lpBuffer is actually just a pointer to the actual array. The difference is that instead of that array being held in "managed c#", it's held in "unmanaged c/c++" And maybe that will eliminate the slowdowns. Because maybe inside of c# it has to convert the "c# array to a c array".

Anyway!!! Hope this helps.

Andrew900460 commented 3 years ago

I ran a benchmark test in my own source code.

I generated 1000 frames of a 64x64 buffer To add some CPU load to each frame, I would randomly generate characters and FG/BG colors for the buffer, before calling the Windows API. I've only created the CharInfo array once beforehand and then reused it each frame. I also ran in release mode in Visual Studio, so it executed with optimized code.

I managed to get 88.6 FPS but that is with a small buffer size (64x64)

This also considers that I am still using the C# array. And it could be the case that it is more performant to use an array that is in unmanaged memory.

EDIT: ok I'm getting way too deep into this

So I did another benchmark, but instead of altering each "buffer pixel" on each frame (because that would be 4096 operations), I instead just altered 1 pixel on each frame. So the visual effect would be that each frame gains a new random colored pixel. So there is less work being done.

That time around I got 311.17 FPS

So it appears based on this result that altering that array of 4096 pixels was very slow for some reason. But WriteConsoleOutput by itself isn't that slow. But maybe I'm wrong. I also just made the buffer have random colors and characters on the first frame, but no changes on the next frames. And it still went at 88.6 FPS. Even though it wasn't doing any new work on each frame, just using the same buffer. But then I just tried sending an empty buffer, all black. And I got super fast FPS. So I think WriteConsoleOutput performs differently when the buffer has more "data variety"??? Which is odd...

natecurtiss commented 3 years ago

interesting. An actually usable version of n8engine is about 99% complete, so when that all gets finished I'll do another fork that I'll experiment with. Thank you for taking the time to do this!

Andrew900460 commented 3 years ago

Idk if you saw that, but ignore those "closed and reopened" I misclicked. Anyway.

Also, I wanted to say your welcome, I hope this engine really shines.

Also, I spoke with some other programmers who have a lot more "low-level knowledge" regarding c#. And they said it might've been that C# was doing extra work behind the scenes when the function was being called. So, because the "WriteConsoleOutput" function used a "C# array" that was passed in for the buffer. C# had to do "marshaling", which included allocating an array in the unmanaged memory. Copying all the data, and then substituting for that.

But that can be solved by using C#'s namespace for interacting with native memory. https://docs.microsoft.com/en-us/dotnet/standard/memory-and-spans/ And you'll probably end up using the "Memory" class.

Only thing is that you need to make sure your project is using either .Net 5.0/higher or .Net Core 2.1/higher.

natecurtiss commented 3 years ago

Awesome! This already uses .NET 5.0 so we're good. The engine is finished enough, so feel free to make another PR if you're still interested in doing this.