Blazor Webassembly에서 BlazorExtensions/Canvas를 이용하면 HTML5/Canvas를 사용할 수 있습니다.
앞전에 소개한 Blazor and 2D game development – part 1: intro - David Guida 를 이용해서 눈 내리는 데모를 만들어 보았습니다.
Blazor Webassembly에서 BlazorExtensions/Canvas를 이용하면 HTML5/Canvas를 사용할 수 있습니다.
앞전에 소개한 Blazor and 2D game development – part 1: intro - David Guida 를 이용해서 눈 내리는 데모를 만들어 보았습니다.
AMD 4800HS일 때 43.5 fps, Release 모드일 때도 큰 차이가 없는것으로 보아 async Javascript interop의 문제인 듯
InvokeUnmarshalled
을 이용하면 Javascript interop을 통해 인자가 JSON으로 직렬화 하지 않고, 바로바로 전달 할 수 있어서 빠르다고 합니다. 얼마나 빠른가 너무 궁금해져서 InvokeUnmarshalled
버젼을 구현하게 되었습니다.
진행하다 보니 생각보다 잔일이 있네요. 호출하는 canvas 함수의 인자를 변환 처리해야 하기 때문에 하나하나 구현해야 합니다.
clearRect: function (fields) {
const c = window.game.context;
const x = Blazor.platform.readInt32Field(fields, 0);
const y = Blazor.platform.readInt32Field(fields, 4);
const width = Blazor.platform.readInt32Field(fields, 8);
const height = Blazor.platform.readInt32Field(fields, 12);
c.clearRect(x, y, width, height);
},
strokeText: function (fields) {
const c = window.game.context;
const text = Blazor.platform.readStringField(fields, 0);
const x = Blazor.platform.readInt32Field(fields, 8);
const y = Blazor.platform.readInt32Field(fields, 12);
c.strokeText(text, x, y);
},
beginPath: function () {
const c = window.game.context;
c.beginPath();
},
//await cc.ArcAsync(snow.X, snow.Y, snow.Size, 0, 2 * Math.PI, true);
arc: function (fields) {
const c = window.game.context;
const x = Blazor.platform.readFloatField(fields, 0);
const y = Blazor.platform.readFloatField(fields, 4);
const radius = Blazor.platform.readFloatField(fields, 8);
const startAngle = Blazor.platform.readFloatField(fields, 12);
const endAngle = Blazor.platform.readFloatField(fields, 16);
const anticlockwise = Blazor.platform.readInt32Field(fields, 20);
c.arc(x, y, radius, startAngle, endAngle, anticlockwise);
},
setFillStyle: function(pFields) {
const c = window.game.context;
const fields = Blazor.platform.readStringField(pFields, 0);
c.fillStyle = fields;
},
fill: function () {
const c = window.game.context;
c.fill();
}
그리고 C#쪽으로 코드도 InvokeUnmarshalled
을 쓰는 것으로 바꾸었습니다.
[JSInvokable]
public void GameLoop2(float timeStamp, int width, int height)
{
var fps = 1000d / (sw.ElapsedMilliseconds - lastMs);
lastMs = sw.ElapsedMilliseconds;
if (snowList.Count < MaxSnow)
{
snowList.Add(new()
{
X = rand.Next(0, width),
Y = -rand.Next(0, height),
Kind = snowList.Count / (MaxSnow / 5)
});
}
//await cc.BeginBatchAsync();
//await cc.ClearRectAsync(0, 0, width, height);
JSUnmarshalledRuntime.InvokeUnmarshalled<(int, int, int, int), bool>("game.clearRect", (0, 0, width, height));
//await cc.StrokeTextAsync($"count: {fpsCount}, {fps:00.0}", 16, 16);
JSUnmarshalledRuntime.InvokeUnmarshalled<(string, int, int), bool>("game.strokeText", ($"count: {fpsCount}, {fps:00.0}", 16, 16));
foreach (var snow in snowList)
{
//await cc.BeginPathAsync();
JSUnmarshalledRuntime.InvokeUnmarshalled<bool>("game.beginPath");
//await cc.ArcAsync(snow.X, snow.Y, snow.Size, 0, 2 * Math.PI, true);
JSUnmarshalledRuntime.InvokeUnmarshalled<(float, float, float, float, float, bool), bool>("game.arc", (snow.X, snow.Y, snow.Size, 0f, (float)(2 * Math.PI), true));
//await cc.SetFillStyleAsync(snow.Color);
JSUnmarshalledRuntime.InvokeUnmarshalled<(string, bool), bool>("game.setFillStyle", (snow.Color, true));
//await cc.FillAsync();
JSUnmarshalledRuntime.InvokeUnmarshalled<bool>("game.fill");
snow.Y += snow.Kind switch { 0 => 1, 1 => 2, 2 => 3, 3 => 4, 4 => 5, _ => 5 };
if (snow.Y > height)
snow.Y = 0;
snow.X += rand.Next(0, 2);
if (snow.X > width)
snow.X = 0;
}
//await cc.EndBatchAsync();
fpsCount++;
}
그리고 돌리자, 아주 놀라운 일이 일어났습니다. 앞전의 fps가 43.5 fps
였는데요, 이제 250 fps
가 나옵니다.
아무것도 안하는 requestAnimationFrame
호출도 250 fps
가 나온것을 보았을 때,
여기서 250은 엣지에서 requestAnimationFrame
의 최대 인 것으로 추측이 됩니다.
오 디스플레이 새로고침 빈도일 가능성이 높군요
눈을 500개로 늘리고 InvokeAsync vs InvokeUnmarshalled
를 비교해보니 7 fps vs 100 fps 100
차이가 나네요. 대략 10배 이상의 성능 차이입니다.
이제 여기서 생각해 볼 내용이, 비동기 호출을 하는 이유는 Blazor Server와의 호환성 때문입니다. 왜냐하면 서버의 작업은 동기
보다는 비동기
가 효과적으로 동작하기 때문인데요, 덕분에 Webassembly에서 Javascript의 호출 성능하락의 원인이됩니다. 또한 인자를 넘길 때 JSON 직렬화를 한다고 하는데요, 이부분에서 상당한 성능하락이 발생하나봅니다.
결론은, Webassembly 전용 프로그램을 Blazor로 개발할 경우 동기
호출을 쓰거나, InvokeUnmarshalled
를 쓰는게 좋다. 단, InvokeUnmarshalled
방식은 문서화가 아직 되지 않고 언제든지 메소드명이 바뀌거나 없어질 수 있다고 하니, 조심스럽게 사용해야 하겠습니다.