From ee0032ad5c6b71dff268e65ce98b3ae2f4fcb721 Mon Sep 17 00:00:00 2001 From: Highbyte Date: Fri, 12 Apr 2024 18:07:25 +0200 Subject: [PATCH 1/5] Fix WASM C64 Menu rendering --- .../Pages/Commodore64/C64Menu.razor | 48 +++++++++++-------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Commodore64/C64Menu.razor b/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Commodore64/C64Menu.razor index a83015c69..f8edee668 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Commodore64/C64Menu.razor +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Commodore64/C64Menu.razor @@ -1,4 +1,5 @@ -@using Highbyte.DotNet6502.App.WASM.Skia; +@using Highbyte.DotNet6502.App.WASM.Emulator.SystemSetup +@using Highbyte.DotNet6502.App.WASM.Skia; @using Highbyte.DotNet6502.Systems.Commodore64.Config; @using Highbyte.DotNet6502.Systems.Commodore64; @using Highbyte.DotNet6502.Systems.Commodore64.Video; @@ -8,21 +9,24 @@ - -

- - - + @if (_c64HostConfig!=null) + { + +

+ + + + }

Load/save files

@@ -68,9 +72,11 @@ .systemCommandsStyle { display: @Parent.GetSystemVisibilityDisplayStyle("Commands", SYSTEM_NAME) } + .systemHelpStyle { display: @Parent.GetSystemVisibilityDisplayStyle("Help", SYSTEM_NAME) } + .systemConfigStyle { display: @Parent.GetSystemVisibilityDisplayStyle("Config", SYSTEM_NAME) } @@ -82,9 +88,9 @@ @* Fix for compiler warning CS8669 https://github.com/dotnet/razor/issues/8720 *@ @{ #pragma warning disable CS8669 -} +} @code { -@inject IJSRuntime Js + @inject IJSRuntime Js @inject HttpClient HttpClient @inject ILoggerFactory LoggerFactory @@ -96,7 +102,7 @@ private C64Config _c64Config => (Parent.SystemConfig as C64Config)!; private C64HostConfig _c64HostConfig => (Parent.HostSystemConfig as C64HostConfig)!; - protected override void OnInitialized() + protected override async Task OnInitializedAsync() { _logger = LoggerFactory.CreateLogger(); _logger.LogDebug("OnInitializedAsync() was called"); @@ -343,13 +349,13 @@ catch (Exception ex) { _latestFileError = $"Save error: {ex.Message}"; - _logger.LogError($"Save error: {ex.Message}"); + _logger.LogError($"Save error: {ex.Message}"); } } } - private void OnAssemblyExampleChanged(ChangeEventArgs e) + private async Task OnAssemblyExampleChanged(ChangeEventArgs e) { SelectedAssemblyExample = e.Value?.ToString() ?? ""; } From 2a7d84cca675c3b6211657d65f34902e28c8db74 Mon Sep 17 00:00:00 2001 From: Highbyte Date: Fri, 12 Apr 2024 18:13:27 +0200 Subject: [PATCH 2/5] Start of refactor WASM host app to allow for different rendering implementations. --- .../{Skia => Emulator}/EmulatorConfig.cs | 4 +- .../{ => Emulator}/Skia/PeriodicAsyncTimer.cs | 2 +- .../{ => Emulator}/Skia/WasmHost.cs | 38 +++++++++---------- .../SystemSetup}/BrowserContext.cs | 2 +- .../SystemSetup}/C64HostConfig.cs | 10 ++++- .../SystemSetup}/C64Setup.cs | 27 +++++++++++-- .../SystemSetup}/GenericComputerHostConfig.cs | 2 +- .../SystemSetup}/GenericComputerSetup.cs | 23 ++++++----- .../Emulator/WASMRenderContextContainer.cs | 26 +++++++++++++ .../{Skia => Emulator}/WasmMonitor.cs | 14 +++---- .../Highbyte.DotNet6502.App.WASM.csproj | 2 + .../Pages/Commodore64/C64ConfigUI.razor | 6 +-- .../Pages/Commodore64/C64HelpUI.razor | 3 +- .../Pages/Commodore64/C64Menu.razor | 1 - .../Pages/GeneralHelpUI.razor | 3 +- .../Pages/Generic/GenericConfigUI.razor | 3 +- .../Pages/Generic/GenericHelpUI.razor | 3 +- .../Pages/Index.razor.cs | 18 ++++++--- 18 files changed, 117 insertions(+), 70 deletions(-) rename src/apps/Highbyte.DotNet6502.App.WASM/{Skia => Emulator}/EmulatorConfig.cs (84%) rename src/apps/Highbyte.DotNet6502.App.WASM/{ => Emulator}/Skia/PeriodicAsyncTimer.cs (95%) rename src/apps/Highbyte.DotNet6502.App.WASM/{ => Emulator}/Skia/WasmHost.cs (90%) rename src/apps/Highbyte.DotNet6502.App.WASM/{Skia => Emulator/SystemSetup}/BrowserContext.cs (78%) rename src/apps/Highbyte.DotNet6502.App.WASM/{Skia => Emulator/SystemSetup}/C64HostConfig.cs (66%) rename src/apps/Highbyte.DotNet6502.App.WASM/{Skia => Emulator/SystemSetup}/C64Setup.cs (83%) rename src/apps/Highbyte.DotNet6502.App.WASM/{Skia => Emulator/SystemSetup}/GenericComputerHostConfig.cs (81%) rename src/apps/Highbyte.DotNet6502.App.WASM/{Skia => Emulator/SystemSetup}/GenericComputerSetup.cs (91%) create mode 100644 src/apps/Highbyte.DotNet6502.App.WASM/Emulator/WASMRenderContextContainer.cs rename src/apps/Highbyte.DotNet6502.App.WASM/{Skia => Emulator}/WasmMonitor.cs (96%) diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Skia/EmulatorConfig.cs b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/EmulatorConfig.cs similarity index 84% rename from src/apps/Highbyte.DotNet6502.App.WASM/Skia/EmulatorConfig.cs rename to src/apps/Highbyte.DotNet6502.App.WASM/Emulator/EmulatorConfig.cs index a337b3035..3b79e399a 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Skia/EmulatorConfig.cs +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/EmulatorConfig.cs @@ -3,7 +3,7 @@ using Highbyte.DotNet6502.Monitor; using Highbyte.DotNet6502.Systems; -namespace Highbyte.DotNet6502.App.WASM.Skia; +namespace Highbyte.DotNet6502.App.WASM.Emulator; public class EmulatorConfig { @@ -23,7 +23,7 @@ public EmulatorConfig() CurrentDrawScale = DefaultDrawScale; } - public void Validate(SystemList systemList) + public void Validate(SystemList systemList) { if (!systemList.Systems.Contains(DefaultEmulator)) throw new DotNet6502Exception($"Setting {nameof(DefaultEmulator)} value {DefaultEmulator} is not supported. Valid values are: {string.Join(',', systemList.Systems)}"); diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Skia/PeriodicAsyncTimer.cs b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/Skia/PeriodicAsyncTimer.cs similarity index 95% rename from src/apps/Highbyte.DotNet6502.App.WASM/Skia/PeriodicAsyncTimer.cs rename to src/apps/Highbyte.DotNet6502.App.WASM/Emulator/Skia/PeriodicAsyncTimer.cs index 1367bc8aa..e56d3bcfc 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Skia/PeriodicAsyncTimer.cs +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/Skia/PeriodicAsyncTimer.cs @@ -1,6 +1,6 @@ using System.Diagnostics; -namespace Highbyte.DotNet6502.App.WASM.Skia; +namespace Highbyte.DotNet6502.App.WASM.Emulator.Skia; public class PeriodicAsyncTimer { diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Skia/WasmHost.cs b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/Skia/WasmHost.cs similarity index 90% rename from src/apps/Highbyte.DotNet6502.App.WASM/Skia/WasmHost.cs rename to src/apps/Highbyte.DotNet6502.App.WASM/Emulator/Skia/WasmHost.cs index 0b5400b9f..047f86854 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Skia/WasmHost.cs +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/Skia/WasmHost.cs @@ -6,7 +6,7 @@ using Highbyte.DotNet6502.Systems; using Toolbelt.Blazor.Gamepad; -namespace Highbyte.DotNet6502.App.WASM.Skia; +namespace Highbyte.DotNet6502.App.WASM.Emulator.Skia; public class WasmHost : IDisposable { @@ -22,13 +22,13 @@ public class WasmHost : IDisposable private SKCanvas _skCanvas = default!; private GRContext _grContext = default!; - private SkiaRenderContext _skiaRenderContext = default!; + private WASMRenderContextContainer _renderContext = default!; public WASMAudioHandlerContext AudioHandlerContext { get; private set; } = default!; public AspNetInputHandlerContext InputHandlerContext { get; private set; } = default!; private readonly string _systemName; - private readonly SystemList _systemList; - public SystemList SystemList => _systemList; + private readonly SystemList _systemList; + public SystemList SystemList => _systemList; private readonly Action _updateStats; private readonly Action _updateDebug; private readonly Func _setMonitorState; @@ -60,7 +60,7 @@ public class WasmHost : IDisposable public WasmHost( IJSRuntime jsRuntime, string systemName, - SystemList systemList, + SystemList systemList, Action updateStats, Action updateDebug, Func setMonitorState, @@ -99,11 +99,14 @@ public async Task Init(SKCanvas canvas, GRContext grContext, AudioContextSync au _skCanvas = canvas; _grContext = grContext; - _skiaRenderContext = new SkiaRenderContext(GetCanvas, GetGRContext); + _renderContext = new WASMRenderContextContainer( + new SkiaRenderContext(GetCanvas, GetGRContext), + null + ); InputHandlerContext = new AspNetInputHandlerContext(_loggerFactory, gamepadList); AudioHandlerContext = new WASMAudioHandlerContext(audioContext, jsRuntime, _initialMasterVolume); - _systemList.InitContext(() => _skiaRenderContext, () => InputHandlerContext, () => AudioHandlerContext); + _systemList.InitContext(() => _renderContext, () => InputHandlerContext, () => AudioHandlerContext); _systemRunner = await _systemList.BuildSystemRunner(_systemName); @@ -133,7 +136,7 @@ public void Start() { var screen = _systemList.GetSystem(_systemName).Result.Screen; // Number of milliseconds between each invokation of the main loop. 60 fps -> (1/60) * 1000 -> approx 16.6667ms - double updateIntervalMS = (1 / screen.RefreshFrequencyHz) * 1000; + double updateIntervalMS = 1 / screen.RefreshFrequencyHz * 1000; _updateTimer = new PeriodicAsyncTimer(); _updateTimer.IntervalMilliseconds = updateIntervalMS; _updateTimer.Elapsed += UpdateTimerElapsed; @@ -152,10 +155,10 @@ public void Cleanup() } // Clear canvas - _skiaRenderContext.GetCanvas().Clear(); + _renderContext.SkiaRenderContext.GetCanvas().Clear(); // Clean up Skia resources - _skiaRenderContext?.Cleanup(); + _renderContext.SkiaRenderContext?.Cleanup(); // Clean up input handler resources InputHandlerContext?.Cleanup(); @@ -210,9 +213,7 @@ private void EmulatorRunOneFrame() // Show monitor if we encounter breakpoint or other break if (execEvaluatorTriggerResult.Triggered) - { Monitor.Enable(execEvaluatorTriggerResult); - } } public void Render(SKCanvas canvas, GRContext grContext) @@ -247,7 +248,7 @@ private GRContext GetGRContext() private string GetStats() { - string stats = ""; + var stats = ""; var allStats = InstrumentationBag.Stats .Union(_systemRunner.System.Instrumentations.Stats.Select(x => (Name: $"{HostStatRootName}-{SystemTimeStatName}-{x.Name}", x.Stat))) @@ -255,7 +256,7 @@ private string GetStats() .Union(_systemRunner.AudioHandler.Instrumentations.Stats.Select(x => (Name: $"{HostStatRootName}-{AudioTimeStatName}-{x.Name}", x.Stat))) .Union(_systemRunner.InputHandler.Instrumentations.Stats.Select(x => (Name: $"{HostStatRootName}-{InputTimeStatName}-{x.Name}", x.Stat))) .ToList(); - foreach ((string name, IStat stat) in allStats.OrderBy(i => i.Name)) + foreach ((var name, var stat) in allStats.OrderBy(i => i.Name)) { if (stat.ShouldShow()) { @@ -269,7 +270,7 @@ private string GetStats() private string GetDebugMessages() { - string debugMessages = ""; + var debugMessages = ""; var inputDebugInfo = _systemRunner.InputHandler.GetDebugInfo(); var inputStatsOneString = string.Join(" # ", inputDebugInfo); @@ -294,7 +295,7 @@ private string GetDebugMessages() private string BuildHtmlString(string message, string cssClass, bool startNewLine = false) { - string html = ""; + var html = ""; if (startNewLine) html += "
"; html += $@"{HttpUtility.HtmlEncode(message)}"; @@ -314,10 +315,7 @@ public void OnKeyDown(KeyboardEventArgs e) var key = e.Key; if (key == "F11") - { _toggleDebugStatsState(); - - } else if (key == "F12") { ToggleMonitor(); @@ -327,9 +325,7 @@ public void OnKeyDown(KeyboardEventArgs e) public void ToggleMonitor() { if (Monitor.Visible) - { Monitor.Disable(); - } else { Monitor.Enable(); diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Skia/BrowserContext.cs b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/SystemSetup/BrowserContext.cs similarity index 78% rename from src/apps/Highbyte.DotNet6502.App.WASM/Skia/BrowserContext.cs rename to src/apps/Highbyte.DotNet6502.App.WASM/Emulator/SystemSetup/BrowserContext.cs index fa81e6ce3..cba522629 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Skia/BrowserContext.cs +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/SystemSetup/BrowserContext.cs @@ -1,6 +1,6 @@ using Blazored.LocalStorage; -namespace Highbyte.DotNet6502.App.WASM.Skia; +namespace Highbyte.DotNet6502.App.WASM.Emulator.SystemSetup; public class BrowserContext { diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Skia/C64HostConfig.cs b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/SystemSetup/C64HostConfig.cs similarity index 66% rename from src/apps/Highbyte.DotNet6502.App.WASM/Skia/C64HostConfig.cs rename to src/apps/Highbyte.DotNet6502.App.WASM/Emulator/SystemSetup/C64HostConfig.cs index 1a41c5965..a202906f9 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Skia/C64HostConfig.cs +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/SystemSetup/C64HostConfig.cs @@ -1,11 +1,17 @@ - using Highbyte.DotNet6502.Impl.AspNet.Commodore64; using Highbyte.DotNet6502.Systems; -namespace Highbyte.DotNet6502.App.WASM.Skia +namespace Highbyte.DotNet6502.App.WASM.Emulator.SystemSetup { + public enum C64HostRenderer + { + SkiaSharp, + SilkNetOpenGl + } public class C64HostConfig : IHostSystemConfig, ICloneable { + public C64HostRenderer Renderer { get; set; } = C64HostRenderer.SkiaSharp; + public C64AspNetConfig InputConfig { get; set; } = new C64AspNetConfig(); public object Clone() diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Skia/C64Setup.cs b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/SystemSetup/C64Setup.cs similarity index 83% rename from src/apps/Highbyte.DotNet6502.App.WASM/Skia/C64Setup.cs rename to src/apps/Highbyte.DotNet6502.App.WASM/Emulator/SystemSetup/C64Setup.cs index 44d164672..76a705d54 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Skia/C64Setup.cs +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/SystemSetup/C64Setup.cs @@ -1,15 +1,16 @@ using Highbyte.DotNet6502.Impl.AspNet; using Highbyte.DotNet6502.Impl.AspNet.Commodore64.Audio; using Highbyte.DotNet6502.Impl.AspNet.Commodore64.Input; +using Highbyte.DotNet6502.Impl.SilkNet.Commodore64.Video; using Highbyte.DotNet6502.Impl.Skia; using Highbyte.DotNet6502.Impl.Skia.Commodore64.Video; using Highbyte.DotNet6502.Systems; using Highbyte.DotNet6502.Systems.Commodore64; using Highbyte.DotNet6502.Systems.Commodore64.Config; -namespace Highbyte.DotNet6502.App.WASM.Skia; +namespace Highbyte.DotNet6502.App.WASM.Emulator.SystemSetup; -public class C64Setup : SystemConfigurer +public class C64Setup : SystemConfigurer { public string SystemName => C64.SystemName; @@ -69,12 +70,30 @@ public SystemRunner BuildSystemRunner( ISystem system, ISystemConfig systemConfig, IHostSystemConfig hostSystemConfig, - SkiaRenderContext renderContext, + WASMRenderContextContainer renderContextContainer, AspNetInputHandlerContext inputHandlerContext, WASMAudioHandlerContext audioHandlerContext ) { - var renderer = new C64SkiaRenderer(); + var c64HostConfig = (C64HostConfig)hostSystemConfig; + + IRenderer renderer; + IRenderContext renderContext; + switch (c64HostConfig.Renderer) + { + case C64HostRenderer.SkiaSharp: + renderer = new C64SkiaRenderer(); + renderContext = renderContextContainer.SkiaRenderContext; + break; + case C64HostRenderer.SilkNetOpenGl: + throw new NotImplementedException(); + //renderer = new C64SilkNetOpenGlRenderer(c64HostConfig.SilkNetOpenGlRendererConfig); + //renderContext = renderContextContainer.SilkNetOpenGlRenderContext; + break; + default: + throw new NotImplementedException($"Renderer {c64HostConfig.Renderer} not implemented."); + } + var inputHandler = new C64AspNetInputHandler(_loggerFactory, _hostConfig.InputConfig); var audioHandler = new C64WASMAudioHandler(_loggerFactory); diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Skia/GenericComputerHostConfig.cs b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/SystemSetup/GenericComputerHostConfig.cs similarity index 81% rename from src/apps/Highbyte.DotNet6502.App.WASM/Skia/GenericComputerHostConfig.cs rename to src/apps/Highbyte.DotNet6502.App.WASM/Emulator/SystemSetup/GenericComputerHostConfig.cs index 8d768403d..946355b19 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Skia/GenericComputerHostConfig.cs +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/SystemSetup/GenericComputerHostConfig.cs @@ -1,6 +1,6 @@ using Highbyte.DotNet6502.Systems; -namespace Highbyte.DotNet6502.App.WASM.Skia +namespace Highbyte.DotNet6502.App.WASM.Emulator.SystemSetup { public class GenericComputerHostConfig : IHostSystemConfig, ICloneable { diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Skia/GenericComputerSetup.cs b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/SystemSetup/GenericComputerSetup.cs similarity index 91% rename from src/apps/Highbyte.DotNet6502.App.WASM/Skia/GenericComputerSetup.cs rename to src/apps/Highbyte.DotNet6502.App.WASM/Emulator/SystemSetup/GenericComputerSetup.cs index 6d58fc60f..5bdbdd6ec 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Skia/GenericComputerSetup.cs +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/SystemSetup/GenericComputerSetup.cs @@ -7,9 +7,9 @@ using Highbyte.DotNet6502.Systems.Generic.Config; using Microsoft.AspNetCore.WebUtilities; -namespace Highbyte.DotNet6502.App.WASM.Skia; +namespace Highbyte.DotNet6502.App.WASM.Emulator.SystemSetup; -public class GenericComputerSetup : SystemConfigurer +public class GenericComputerSetup : SystemConfigurer { public string SystemName => GenericComputer.SystemName; @@ -36,7 +36,7 @@ public async Task GetNewConfig(string configurationVariant) var prgBytes = await Load6502Binary(_browserContext.HttpClient, _browserContext.Uri); // Get screen size specified in url - (int? cols, int? rows, ushort? screenMemoryAddress, ushort? colorMemoryAddress) = GetScreenSize(_browserContext.Uri); + (var cols, var rows, var screenMemoryAddress, var colorMemoryAddress) = GetScreenSize(_browserContext.Uri); cols = cols ?? 40; rows = rows ?? 25; @@ -101,7 +101,7 @@ public SystemRunner BuildSystemRunner( ISystem system, ISystemConfig systemConfig, IHostSystemConfig hostSystemConfig, - SkiaRenderContext skiaRenderContext, + WASMRenderContextContainer renderContextContainer, AspNetInputHandlerContext inputHandlerContext, WASMAudioHandlerContext audioHandlerContext) { @@ -113,7 +113,8 @@ public SystemRunner BuildSystemRunner( var genericComputer = (GenericComputer)system; - renderer.Init(genericComputer, skiaRenderContext); + // Currently only SkiaSharp renderer is supported for GenericComputer + renderer.Init(genericComputer, renderContextContainer.SkiaRenderContext); inputHandler.Init(genericComputer, inputHandlerContext); audioHandler.Init(genericComputer, audioHandlerContext); @@ -135,28 +136,28 @@ public SystemRunner BuildSystemRunner( if (QueryHelpers.ParseQuery(uri.Query).TryGetValue("cols", out var colsParameter)) { - if (int.TryParse(colsParameter, out int colsParsed)) + if (int.TryParse(colsParameter, out var colsParsed)) cols = colsParsed; else cols = null; } if (QueryHelpers.ParseQuery(uri.Query).TryGetValue("rows", out var rowsParameter)) { - if (int.TryParse(rowsParameter, out int rowsParsed)) + if (int.TryParse(rowsParameter, out var rowsParsed)) rows = rowsParsed; else rows = null; } if (QueryHelpers.ParseQuery(uri.Query).TryGetValue("screenMem", out var screenMemParameter)) { - if (ushort.TryParse(screenMemParameter, out ushort screenMemParsed)) + if (ushort.TryParse(screenMemParameter, out var screenMemParsed)) screenMemoryAddress = screenMemParsed; else screenMemoryAddress = null; } if (QueryHelpers.ParseQuery(uri.Query).TryGetValue("colorMem", out var colorMemParameter)) { - if (ushort.TryParse(colorMemParameter, out ushort colorMemParsed)) + if (ushort.TryParse(colorMemParameter, out var colorMemParsed)) colorMemoryAddress = colorMemParsed; else colorMemoryAddress = null; @@ -170,7 +171,6 @@ private async Task Load6502Binary(HttpClient httpClient, Uri uri) { byte[] prgBytes; if (QueryHelpers.ParseQuery(uri.Query).TryGetValue("prgEnc", out var prgEnc)) - { // Query parameter prgEnc must be a valid Base64Url encoded string. // Examples on how to generate it from a compiled 6502 binary file: // Linux: @@ -188,7 +188,6 @@ private async Task Load6502Binary(HttpClient httpClient, Uri uri) // Linux: // qrencode -s 3 -l L -o "myprogram.png" "http://localhost:5000/?prgEnc=THE_PROGRAM_ENCODED_AS_BASE64URL" prgBytes = Base64UrlDecode(prgEnc.ToString()); - } else if (QueryHelpers.ParseQuery(uri.Query).TryGetValue("prgUrl", out var prgUrl)) { prgBytes = await httpClient.GetByteArrayAsync(prgUrl.ToString()); @@ -214,7 +213,7 @@ private async Task Load6502Binary(HttpClient httpClient, Uri uri) /// private byte[] Base64UrlDecode(string arg) { - string s = arg; + var s = arg; s = s.Replace('-', '+'); // 62nd char of encoding s = s.Replace('_', '/'); // 63rd char of encoding switch (s.Length % 4) // Pad with trailing '='s diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/WASMRenderContextContainer.cs b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/WASMRenderContextContainer.cs new file mode 100644 index 000000000..317994e8c --- /dev/null +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/WASMRenderContextContainer.cs @@ -0,0 +1,26 @@ +using Highbyte.DotNet6502.Impl.SilkNet; +using Highbyte.DotNet6502.Impl.Skia; +using Highbyte.DotNet6502.Systems; + +namespace Highbyte.DotNet6502.App.WASM.Emulator; + +public class WASMRenderContextContainer : IRenderContext +{ + private SkiaRenderContext _skiaRenderContext; + private SilkNetOpenGlRenderContext _silkNetOpenGlRenderContext; + + public SkiaRenderContext SkiaRenderContext => _skiaRenderContext; + public SilkNetOpenGlRenderContext SilkNetOpenGlRenderContext => _silkNetOpenGlRenderContext; + + public WASMRenderContextContainer(SkiaRenderContext skiaRenderContext, SilkNetOpenGlRenderContext silkNetOpenGlRenderContext) + { + _skiaRenderContext = skiaRenderContext; + _silkNetOpenGlRenderContext = silkNetOpenGlRenderContext; + } + + internal void Cleanup() + { + SkiaRenderContext?.Cleanup(); + SilkNetOpenGlRenderContext?.Cleanup(); + } +} diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Skia/WasmMonitor.cs b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/WasmMonitor.cs similarity index 96% rename from src/apps/Highbyte.DotNet6502.App.WASM/Skia/WasmMonitor.cs rename to src/apps/Highbyte.DotNet6502.App.WASM/Emulator/WasmMonitor.cs index abb48ed71..973ceb247 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Skia/WasmMonitor.cs +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/WasmMonitor.cs @@ -1,7 +1,7 @@ using Highbyte.DotNet6502.Monitor; using Highbyte.DotNet6502.Systems; -namespace Highbyte.DotNet6502.App.WASM.Skia; +namespace Highbyte.DotNet6502.App.WASM.Emulator; public class WasmMonitor : MonitorBase { @@ -45,7 +45,7 @@ public void Enable(ExecEvaluatorTriggerResult? execEvaluatorTriggerResult = null } if (execEvaluatorTriggerResult != null) - base.ShowInfoAfterBreakTriggerEnabled(execEvaluatorTriggerResult); + ShowInfoAfterBreakTriggerEnabled(execEvaluatorTriggerResult); //else // WriteOutput("Monitor enabled manually."); @@ -116,10 +116,8 @@ private void ProcessMonitorCommand(string cmd) var commandResult = SendCommand(cmd); DisplayStatus(); if (commandResult == CommandResult.Quit) - { //Quit = true; Disable(); - } else if (commandResult == CommandResult.Continue) { Disable(); @@ -185,8 +183,8 @@ public void LoadBinaryFromUser(byte[] fileData) BinaryLoader.Load( Mem, fileData, - out ushort loadedAtAddress, - out ushort fileLength, + out var loadedAtAddress, + out var fileLength, _lastTriggeredLoadBinaryForceLoadAddress); WriteOutput($"File loaded at {loadedAtAddress.ToHex()}, length {fileLength.ToHex()}"); @@ -211,7 +209,7 @@ public override async void SaveBinary(string fileName, ushort startAddress, usho try { // Ensure file has .prg extension if not specfied. When saving by issuing a browser file download, and saving a file with no extension, the browser will add .txt extension. - string ext = Path.GetExtension(fileName); + var ext = Path.GetExtension(fileName); if (string.IsNullOrEmpty(ext)) fileName += ".prg"; @@ -249,7 +247,7 @@ public override void WriteOutput(string message, MessageSeverity severity) private string BuildHtmlString(string message, string cssClass, bool startNewLine = false) { - string html = ""; + var html = ""; if (startNewLine) html += "
"; html += $@"{HttpUtility.HtmlEncode(message)}"; diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Highbyte.DotNet6502.App.WASM.csproj b/src/apps/Highbyte.DotNet6502.App.WASM/Highbyte.DotNet6502.App.WASM.csproj index 7971ea29a..a6215e09c 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Highbyte.DotNet6502.App.WASM.csproj +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Highbyte.DotNet6502.App.WASM.csproj @@ -28,6 +28,7 @@ + @@ -47,6 +48,7 @@ + diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Commodore64/C64ConfigUI.razor b/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Commodore64/C64ConfigUI.razor index 86db0bc40..7de956943 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Commodore64/C64ConfigUI.razor +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Commodore64/C64ConfigUI.razor @@ -1,7 +1,7 @@ -@using Highbyte.DotNet6502.App.WASM.Skia -@using Highbyte.DotNet6502.Impl.AspNet.Commodore64.Input; -@using Highbyte.DotNet6502.Systems; +@using Highbyte.DotNet6502.App.WASM.Emulator.SystemSetup +@using Highbyte.DotNet6502.Systems @using Highbyte.DotNet6502.Systems.Commodore64.Config +@using Highbyte.DotNet6502.Impl.AspNet.Commodore64.Input; @using Highbyte.DotNet6502.Systems.Commodore64.TimerAndPeripheral; @using static Highbyte.DotNet6502.Systems.Commodore64.TimerAndPeripheral.C64Joystick; @using static Highbyte.DotNet6502.App.WASM.Pages.Index diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Commodore64/C64HelpUI.razor b/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Commodore64/C64HelpUI.razor index 2209435e9..091f760b4 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Commodore64/C64HelpUI.razor +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Commodore64/C64HelpUI.razor @@ -1,5 +1,4 @@ -@using Highbyte.DotNet6502.App.WASM.Skia -@using Highbyte.DotNet6502.Systems.Commodore64.Config +@using Highbyte.DotNet6502.Systems.Commodore64.Config @using static Highbyte.DotNet6502.App.WASM.Pages.Index Useful C64 monitor commands diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Commodore64/C64Menu.razor b/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Commodore64/C64Menu.razor index f8edee668..8d541df8f 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Commodore64/C64Menu.razor +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Commodore64/C64Menu.razor @@ -1,5 +1,4 @@ @using Highbyte.DotNet6502.App.WASM.Emulator.SystemSetup -@using Highbyte.DotNet6502.App.WASM.Skia; @using Highbyte.DotNet6502.Systems.Commodore64.Config; @using Highbyte.DotNet6502.Systems.Commodore64; @using Highbyte.DotNet6502.Systems.Commodore64.Video; diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Pages/GeneralHelpUI.razor b/src/apps/Highbyte.DotNet6502.App.WASM/Pages/GeneralHelpUI.razor index ba8137a00..4ebb6d75b 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Pages/GeneralHelpUI.razor +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Pages/GeneralHelpUI.razor @@ -1,5 +1,4 @@ -@using Highbyte.DotNet6502.App.WASM.Skia -@using Highbyte.DotNet6502.Systems.Commodore64.Config +@using Highbyte.DotNet6502.Systems.Commodore64.Config @using static Highbyte.DotNet6502.App.WASM.Pages.Index

diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Generic/GenericConfigUI.razor b/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Generic/GenericConfigUI.razor index 60872a313..e1643aab2 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Generic/GenericConfigUI.razor +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Generic/GenericConfigUI.razor @@ -1,5 +1,4 @@ -@using Highbyte.DotNet6502.App.WASM.Skia -@using Highbyte.DotNet6502.Systems.Generic.Config +@using Highbyte.DotNet6502.Systems.Generic.Config @using Highbyte.DotNet6502.Systems; @using static Highbyte.DotNet6502.App.WASM.Pages.Index diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Generic/GenericHelpUI.razor b/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Generic/GenericHelpUI.razor index 4a2e2f5d1..c29ef0dc0 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Generic/GenericHelpUI.razor +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Generic/GenericHelpUI.razor @@ -1,5 +1,4 @@ -@using Highbyte.DotNet6502.App.WASM.Skia -@using Highbyte.DotNet6502.Systems.Commodore64.Config +@using Highbyte.DotNet6502.Systems.Commodore64.Config @using static Highbyte.DotNet6502.App.WASM.Pages.Index Useful Generic Computer monitor commands diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Index.razor.cs b/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Index.razor.cs index bfff47b33..190459c79 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Index.razor.cs +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Index.razor.cs @@ -1,7 +1,6 @@ using Microsoft.AspNetCore.Components; using Blazored.Modal.Services; using Blazored.Modal; -using Highbyte.DotNet6502.App.WASM.Skia; using Highbyte.DotNet6502.Impl.AspNet; using Highbyte.DotNet6502.Impl.Skia; using Highbyte.DotNet6502.Systems; @@ -13,6 +12,10 @@ using Toolbelt.Blazor.Gamepad; using AutoMapper; using Highbyte.DotNet6502.Systems.Commodore64; +using Highbyte.DotNet6502.App.WASM.Emulator; +using Highbyte.DotNet6502.App.WASM.Emulator.SystemSetup; +using Highbyte.DotNet6502.Systems.Generic; +using Highbyte.DotNet6502.App.WASM.Emulator.Skia; namespace Highbyte.DotNet6502.App.WASM.Pages; @@ -91,7 +94,7 @@ private double Scale private ElementReference _monitorInputRef = default!; private EmulatorConfig _emulatorConfig = default!; - private SystemList _systemList = default!; + private SystemList _systemList = default!; private WasmHost _wasmHost = default!; public WasmHost WasmHost => _wasmHost; @@ -142,9 +145,12 @@ protected override async Task OnInitializedAsync() }; // Add systems - _systemList = new SystemList(); + _systemList = new(); - var c64HostConfig = new C64HostConfig(); + var c64HostConfig = new C64HostConfig + { + Renderer = C64HostRenderer.SkiaSharp, + }; var c64Setup = new C64Setup(_browserContext, LoggerFactory, c64HostConfig); _systemList.AddSystem(c64Setup); @@ -167,8 +173,8 @@ protected override async Task OnInitializedAsync() }, HostSystemConfigs = new Dictionary { - { C64.SystemName, c64HostConfig } - //{ GenericComputer.SystemName, new GenericComputerHostConfig() } + { C64.SystemName, c64HostConfig }, + { GenericComputer.SystemName, new GenericComputerHostConfig() } } }; _emulatorConfig.Validate(_systemList); From 8c48a9ceb7b14bd05aa15762d5114c604f235187 Mon Sep 17 00:00:00 2001 From: Highbyte Date: Sun, 14 Apr 2024 14:22:10 +0200 Subject: [PATCH 3/5] Refactor WasmHost class to support different implementations for rendering. Decouple Index.razor page from specific WasmHost implementation. --- .../Emulator/Skia/SkiaWasmHost.cs | 140 +++++++++++ .../{Skia/WasmHost.cs => WasmHostBase.cs} | 224 +++++++++++------- .../Pages/Commodore64/C64Menu.razor | 21 +- .../Pages/Generic/GenericMenu.razor | 9 +- .../Pages/Index.razor | 32 +-- .../Pages/Index.razor.cs | 125 ++++------ .../wwwroot/index.html | 2 +- 7 files changed, 360 insertions(+), 193 deletions(-) create mode 100644 src/apps/Highbyte.DotNet6502.App.WASM/Emulator/Skia/SkiaWasmHost.cs rename src/apps/Highbyte.DotNet6502.App.WASM/Emulator/{Skia/WasmHost.cs => WasmHostBase.cs} (69%) diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/Skia/SkiaWasmHost.cs b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/Skia/SkiaWasmHost.cs new file mode 100644 index 000000000..f75a7d3da --- /dev/null +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/Skia/SkiaWasmHost.cs @@ -0,0 +1,140 @@ +using Highbyte.DotNet6502.Impl.AspNet; +using Highbyte.DotNet6502.Impl.Skia; +using Highbyte.DotNet6502.Systems; +using Toolbelt.Blazor.Gamepad; + +namespace Highbyte.DotNet6502.App.WASM.Emulator.Skia; + +public class SkiaWasmHost : WasmHostBase +{ + private bool _initialized; + + private PeriodicAsyncTimer? _updateTimer; + + private SKCanvas _skCanvas = default!; + private GRContext _grContext = default!; + + private readonly ILogger _logger; + private readonly ILoggerFactory _loggerFactory; + + public SkiaWasmHost( + IJSRuntime jsRuntime, + SystemList systemList, + Action updateStats, + Action updateDebug, + Func setMonitorState, + EmulatorConfig emulatorConfig, + Func toggleDebugStatsState, + ILoggerFactory loggerFactory, + GamepadList gamepadList, + float initialMasterVolume = 50.0f + ) : base( + jsRuntime, + systemList, + updateStats, + updateDebug, + setMonitorState, + emulatorConfig, + toggleDebugStatsState, + loggerFactory, + gamepadList, + initialMasterVolume) + { + _logger = loggerFactory.CreateLogger(typeof(SkiaWasmHost).Name); + _loggerFactory = loggerFactory; + + } + + protected override WASMRenderContextContainer BuildRenderContext() + { + return new WASMRenderContextContainer( + new SkiaRenderContext(GetCanvas, GetGRContext), + null); + } + + private SKCanvas GetCanvas() + { + return _skCanvas; + } + + private GRContext GetGRContext() + { + return _grContext; + } + + public void OnPaintSurface(SKPaintGLSurfaceEventArgs e) + { + if (EmulatorState != EmulatorState.Running) + return; + + if (!(e.Surface.Context is GRContext grContext && grContext != null)) + return; + + if (!_initialized) + { + _grContext = grContext; + _skCanvas = e.Surface.Canvas; + _initialized = true; + } + + Render(); + } + + protected override void OnBeforeRender() + { + _skCanvas.Scale((float)EmulatorConfig.CurrentDrawScale); + } + + protected override void OnAfterPause() + { + _updateTimer?.Stop(); + } + + protected override void OnAfterStop() + { + _initialized = false; + } + + protected override void OnAfterStart() + { + if (_updateTimer != null) + { + } + else + { + var screen = SystemRunner.System.Screen; + // Number of milliseconds between each invokation of the main loop. 60 fps -> (1/60) * 1000 -> approx 16.6667ms + double updateIntervalMS = 1 / screen.RefreshFrequencyHz * 1000; + _updateTimer = new PeriodicAsyncTimer(); + _updateTimer.IntervalMilliseconds = updateIntervalMS; + _updateTimer.Elapsed += UpdateTimerElapsed; + } + _updateTimer!.Start(); + } + + protected override void OnAfterCleanup() + { + if (_updateTimer != null) + { + _updateTimer.Stop(); + _updateTimer = null; + } + + //// Clear canvas + //_renderContext.SkiaRenderContext.GetCanvas().Clear(); + + //// Clean up Skia resources + //_renderContext.SkiaRenderContext?.Cleanup(); + + //// Clean up input handler resources + //InputHandlerContext?.Cleanup(); + + //// Stop any playing audio + //_systemRunner.AudioHandler.StopPlaying(); + //// Clean up audio resources + ////AudioHandlerContext?.Cleanup(); + } + + private void UpdateTimerElapsed(object? sender, EventArgs e) => EmulatorRunOneFrame(); + +} diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/Skia/WasmHost.cs b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/WasmHostBase.cs similarity index 69% rename from src/apps/Highbyte.DotNet6502.App.WASM/Emulator/Skia/WasmHost.cs rename to src/apps/Highbyte.DotNet6502.App.WASM/Emulator/WasmHostBase.cs index 047f86854..56d421909 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/Skia/WasmHost.cs +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/WasmHostBase.cs @@ -1,40 +1,42 @@ using Highbyte.DotNet6502.Impl.AspNet; using Highbyte.DotNet6502.Impl.AspNet.JSInterop.BlazorWebAudioSync; -using Highbyte.DotNet6502.Impl.Skia; using Highbyte.DotNet6502.Instrumentation; using Highbyte.DotNet6502.Instrumentation.Stats; using Highbyte.DotNet6502.Systems; using Toolbelt.Blazor.Gamepad; -namespace Highbyte.DotNet6502.App.WASM.Emulator.Skia; +namespace Highbyte.DotNet6502.App.WASM.Emulator; -public class WasmHost : IDisposable +public abstract class WasmHostBase : IDisposable { - public bool Initialized { get; private set; } + private string _systemName; private readonly IJSRuntime _jsRuntime; - private PeriodicAsyncTimer? _updateTimer; private SystemRunner _systemRunner = default!; public SystemRunner SystemRunner => _systemRunner; - private SKCanvas _skCanvas = default!; - private GRContext _grContext = default!; + public EmulatorState EmulatorState { get; private set; } = EmulatorState.Uninitialized; - private WASMRenderContextContainer _renderContext = default!; + public WASMRenderContextContainer RenderContextContainer { get; private set; } = default!; public WASMAudioHandlerContext AudioHandlerContext { get; private set; } = default!; public AspNetInputHandlerContext InputHandlerContext { get; private set; } = default!; - private readonly string _systemName; private readonly SystemList _systemList; public SystemList SystemList => _systemList; + + protected EmulatorConfig EmulatorConfig => _emulatorConfig; + + private readonly Action _updateStats; private readonly Action _updateDebug; private readonly Func _setMonitorState; private readonly EmulatorConfig _emulatorConfig; private readonly Func _toggleDebugStatsState; private readonly ILoggerFactory _loggerFactory; + private AudioContextSync _audioContext; + private readonly GamepadList _gamepadList; private readonly float _initialMasterVolume; private readonly ILogger _logger; @@ -57,9 +59,8 @@ public class WasmHost : IDisposable private const int DEBUGMESSAGE_EVERY_X_FRAME = 1; private int _debugFrameCount = 0; - public WasmHost( + public WasmHostBase( IJSRuntime jsRuntime, - string systemName, SystemList systemList, Action updateStats, Action updateDebug, @@ -67,20 +68,20 @@ public WasmHost( EmulatorConfig emulatorConfig, Func toggleDebugStatsState, ILoggerFactory loggerFactory, - float scale = 1.0f, + GamepadList gamepadList, float initialMasterVolume = 50.0f) { _jsRuntime = jsRuntime; - _systemName = systemName; _systemList = systemList; _updateStats = updateStats; _updateDebug = updateDebug; _setMonitorState = setMonitorState; _emulatorConfig = emulatorConfig; _toggleDebugStatsState = toggleDebugStatsState; + _gamepadList = gamepadList; _initialMasterVolume = initialMasterVolume; _loggerFactory = loggerFactory; - _logger = loggerFactory.CreateLogger(typeof(WasmHost).Name); + _logger = loggerFactory.CreateLogger(typeof(WasmHostBase).Name); // Init stats InstrumentationBag.Clear(); @@ -91,93 +92,161 @@ public WasmHost( _updateFps = InstrumentationBag.Add($"{HostStatRootName}-OnUpdateFPS"); _renderFps = InstrumentationBag.Add($"{HostStatRootName}-OnRenderFPS"); - Initialized = false; } - public async Task Init(SKCanvas canvas, GRContext grContext, AudioContextSync audioContext, GamepadList gamepadList, IJSRuntime jsRuntime) + public void Init(AudioContextSync audioContext) { - _skCanvas = canvas; - _grContext = grContext; + if (EmulatorState != EmulatorState.Uninitialized) + throw new DotNet6502Exception("Internal error. Cannot init emulator if not in uninitialized state."); - _renderContext = new WASMRenderContextContainer( - new SkiaRenderContext(GetCanvas, GetGRContext), - null - ); - InputHandlerContext = new AspNetInputHandlerContext(_loggerFactory, gamepadList); - AudioHandlerContext = new WASMAudioHandlerContext(audioContext, jsRuntime, _initialMasterVolume); + _audioContext = audioContext; - _systemList.InitContext(() => _renderContext, () => InputHandlerContext, () => AudioHandlerContext); + RenderContextContainer = BuildRenderContext(); + InputHandlerContext = new AspNetInputHandlerContext(_loggerFactory, _gamepadList); + AudioHandlerContext = new WASMAudioHandlerContext(_audioContext, _jsRuntime, _initialMasterVolume); + _systemList.InitContext(() => RenderContextContainer, () => InputHandlerContext, () => AudioHandlerContext); + } - _systemRunner = await _systemList.BuildSystemRunner(_systemName); + protected abstract WASMRenderContextContainer BuildRenderContext(); + private async Task InitSystem() + { + if (EmulatorState != EmulatorState.Uninitialized) + throw new DotNet6502Exception("Internal error. Cannot init emulator system if not in uninitialized state."); + + _systemRunner = await _systemList.BuildSystemRunner(_systemName); Monitor = new WasmMonitor(_jsRuntime, _systemRunner, _emulatorConfig, _setMonitorState); + OnAfterInitSystem(); + } + + protected virtual void OnAfterInitSystem() { } + + public async Task Start() + { + if (EmulatorState == EmulatorState.Running) + return; + + if (!_systemList.IsValidConfig(_systemName).Result) + throw new DotNet6502Exception("Internal error. Cannot start emulator if current system config is invalid."); + + // Only create a new instance of SystemRunner if we previously has not started (so resume after pause works). + if (EmulatorState == EmulatorState.Uninitialized) + { + await InitSystem(); + } + + if (_systemRunner != null && _systemRunner.AudioHandler != null) + _systemRunner.AudioHandler.StartPlaying(); + + EmulatorState = EmulatorState.Running; - Initialized = true; + _logger.LogInformation($"System started: {_systemName}"); + + OnAfterStart(); + } + + protected virtual void OnAfterStart() { } + + public void Pause() + { + if (EmulatorState == EmulatorState.Paused || EmulatorState == EmulatorState.Uninitialized) + return; + + _systemRunner.AudioHandler?.PausePlaying(); + + EmulatorState = EmulatorState.Paused; + + _logger.LogInformation($"System paused: {_systemName}"); + + OnAfterPause(); } + protected virtual void OnAfterPause() { } + + public async Task Reset() + { + if (EmulatorState == EmulatorState.Uninitialized) + return; + + Stop(); + + await Start(); + + OnAfterReset(); + } + + protected virtual void OnAfterReset() { } + public void Stop() { - _updateTimer?.Stop(); + if (EmulatorState == EmulatorState.Running) + Pause(); - _systemRunner.AudioHandler.PausePlaying(); + _systemRunner = default!; + EmulatorState = EmulatorState.Uninitialized; _logger.LogInformation($"System stopped: {_systemName}"); + + OnAfterStop(); } + protected virtual void OnAfterStop() { } - public void Start() + + public async Task SetCurrentSystem(string systemName) { - if (_systemRunner != null && _systemRunner.AudioHandler != null) - _systemRunner.AudioHandler.StartPlaying(); + if (EmulatorState != EmulatorState.Uninitialized) + throw new DotNet6502Exception("Internal error. Cannot change system while running"); - if (_updateTimer != null) - { - } - else + if (!_systemList.IsValidConfig(systemName).Result) { - var screen = _systemList.GetSystem(_systemName).Result.Screen; - // Number of milliseconds between each invokation of the main loop. 60 fps -> (1/60) * 1000 -> approx 16.6667ms - double updateIntervalMS = 1 / screen.RefreshFrequencyHz * 1000; - _updateTimer = new PeriodicAsyncTimer(); - _updateTimer.IntervalMilliseconds = updateIntervalMS; - _updateTimer.Elapsed += UpdateTimerElapsed; + throw new Exception($"Invalid system config: {systemName}"); } - _updateTimer!.Start(); - _logger.LogInformation($"System started: {_systemName}"); + _systemName = systemName; + _logger.LogInformation($"System selected: {_systemName}"); } - public void Cleanup() + protected void Render() { - if (_updateTimer != null) + if (EmulatorState != EmulatorState.Running) + return; + + if (Monitor.Visible) + return; + + _renderFps.Update(); + + OnBeforeRender(); + + using (_renderTime.Measure()) { - _updateTimer.Stop(); - _updateTimer = null; + _systemRunner.Draw(); } + } - // Clear canvas - _renderContext.SkiaRenderContext.GetCanvas().Clear(); + protected virtual void OnBeforeRender() { } - // Clean up Skia resources - _renderContext.SkiaRenderContext?.Cleanup(); + public virtual void Cleanup() + { + // Clean up render context resources + RenderContextContainer?.Cleanup(); // Clean up input handler resources InputHandlerContext?.Cleanup(); // Stop any playing audio - _systemRunner.AudioHandler.StopPlaying(); + _systemRunner.AudioHandler?.StopPlaying(); // Clean up audio resources //AudioHandlerContext?.Cleanup(); + + OnAfterCleanup(); } - private void UpdateTimerElapsed(object? sender, EventArgs e) => EmulatorRunOneFrame(); + protected virtual void OnAfterCleanup() { } [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "")] - private void EmulatorRunOneFrame() + protected void EmulatorRunOneFrame() { - - if (!Initialized) - return; - if (Monitor.Visible) return; @@ -216,36 +285,6 @@ private void EmulatorRunOneFrame() Monitor.Enable(execEvaluatorTriggerResult); } - public void Render(SKCanvas canvas, GRContext grContext) - { - //if (Monitor.Visible) - // return; - - _renderFps.Update(); - - _grContext = grContext; - _skCanvas = canvas; - _skCanvas.Scale((float)_emulatorConfig.CurrentDrawScale); - using (_renderTime.Measure()) - { - _systemRunner.Draw(); - //using (new SKAutoCanvasRestore(skCanvas)) - //{ - // _systemRunner.Draw(skCanvas); - //} - } - } - - private SKCanvas GetCanvas() - { - return _skCanvas; - } - - private GRContext GetGRContext() - { - return _grContext; - } - private string GetStats() { var stats = ""; @@ -339,3 +378,10 @@ public void OnKeyPress(KeyboardEventArgs e) { } } + +public enum EmulatorState +{ + Uninitialized, + Running, + Paused +} diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Commodore64/C64Menu.razor b/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Commodore64/C64Menu.razor index 8d541df8f..dd90f217c 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Commodore64/C64Menu.razor +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Commodore64/C64Menu.razor @@ -1,4 +1,5 @@ -@using Highbyte.DotNet6502.App.WASM.Emulator.SystemSetup +@using Highbyte.DotNet6502.App.WASM.Emulator +@using Highbyte.DotNet6502.App.WASM.Emulator.SystemSetup @using Highbyte.DotNet6502.Systems.Commodore64.Config; @using Highbyte.DotNet6502.Systems.Commodore64; @using Highbyte.DotNet6502.Systems.Commodore64.Video; @@ -120,7 +121,7 @@ if (_c64Config == null) return; _c64Config.KeyboardJoystickEnabled = value; - if (Parent.CurrentEmulatorState != EmulatorState.Uninitialized) + if (Parent.EmulatorState != EmulatorState.Uninitialized) { C64 c64 = (C64)Parent.WasmHost.SystemList.GetSystem(SYSTEM_NAME).Result; c64.Cia.Joystick.KeyboardJoystickEnabled = value; @@ -149,7 +150,7 @@ if (_c64Config == null) return; _c64Config.KeyboardJoystick = value; - if (Parent.CurrentEmulatorState != EmulatorState.Uninitialized) + if (Parent.EmulatorState != EmulatorState.Uninitialized) { C64 c64 = (C64)Parent.WasmHost.SystemList.GetSystem(SYSTEM_NAME).Result; c64.Cia.Joystick.KeyboardJoystick = value; @@ -183,11 +184,11 @@ [Parameter] public Highbyte.DotNet6502.App.WASM.Pages.Index Parent { get; set; } = default!; - protected bool ShowConfigDisabled => Parent.CurrentEmulatorState != EmulatorState.Uninitialized; + protected bool ShowConfigDisabled => Parent.EmulatorState != EmulatorState.Uninitialized; - protected bool OnFilePickerDisabled => Parent.CurrentEmulatorState == EmulatorState.Uninitialized; + protected bool OnFilePickerDisabled => Parent.EmulatorState == EmulatorState.Uninitialized; - protected bool OnBasicFilePickerDisabled => Parent.CurrentEmulatorState == EmulatorState.Uninitialized; + protected bool OnBasicFilePickerDisabled => Parent.EmulatorState == EmulatorState.Uninitialized; ///

/// Open Load binary file dialog @@ -208,7 +209,7 @@ if (Parent.WasmHost == null) return; - if (Parent.CurrentEmulatorState == EmulatorState.Uninitialized) + if (Parent.EmulatorState == EmulatorState.Uninitialized) return; // Only expect one file @@ -254,7 +255,7 @@ /// private async Task OnBasicFilePicker(MouseEventArgs mouseEventArgs) { - _wasRunningBeforeFileDialog = Parent.CurrentEmulatorState == EmulatorState.Running; + _wasRunningBeforeFileDialog = Parent.EmulatorState == EmulatorState.Running; await Js.InvokeVoidAsync("clickId", "filePickerBasic"); } @@ -268,7 +269,7 @@ if (Parent.WasmHost == null) return; - if (Parent.CurrentEmulatorState == EmulatorState.Uninitialized) + if (Parent.EmulatorState == EmulatorState.Uninitialized) return; // Only expect one file @@ -319,7 +320,7 @@ { if (Parent.WasmHost == null) return; - if (Parent.CurrentEmulatorState == EmulatorState.Uninitialized) + if (Parent.EmulatorState == EmulatorState.Uninitialized) return; var result = await Parent.Modal.Show().Result; diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Generic/GenericMenu.razor b/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Generic/GenericMenu.razor index 385284543..5a0a08cde 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Generic/GenericMenu.razor +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Generic/GenericMenu.razor @@ -1,4 +1,5 @@ -@using Highbyte.DotNet6502.Systems.Generic; +@using Highbyte.DotNet6502.App.WASM.Emulator +@using Highbyte.DotNet6502.Systems.Generic; @using static Highbyte.DotNet6502.App.WASM.Pages.Index;
@@ -49,9 +50,9 @@ [Parameter] public Highbyte.DotNet6502.App.WASM.Pages.Index Parent { get; set; } = default!; - protected bool ShowConfigDisabled => Parent.CurrentEmulatorState != EmulatorState.Uninitialized; + protected bool ShowConfigDisabled => Parent.EmulatorState != EmulatorState.Uninitialized; - protected bool OnFilePickerDisabled => Parent.CurrentEmulatorState == EmulatorState.Uninitialized; + protected bool OnFilePickerDisabled => Parent.EmulatorState == EmulatorState.Uninitialized; /// @@ -73,7 +74,7 @@ if (Parent.WasmHost == null) return; - if (Parent.CurrentEmulatorState == EmulatorState.Uninitialized) + if (Parent.EmulatorState == EmulatorState.Uninitialized) return; // Only expect one file diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Index.razor b/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Index.razor index ff7bab8d1..870efe615 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Index.razor +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Index.razor @@ -1,4 +1,6 @@ @page "/" +@using Highbyte.DotNet6502.App.WASM.Emulator +@using Highbyte.DotNet6502.App.WASM.Emulator.Skia @using Highbyte.DotNet6502.App.WASM.Pages.Commodore64 @using Highbyte.DotNet6502.App.WASM.Pages.Generic @@ -6,10 +8,9 @@ @*@implements IDisposable*@ @*Note: Page title set in index.html title element*@ -@*dotnet-6502 CPU emulator in Blazor WebAssembly with Skia rendering -*@
+ @@ -17,7 +18,6 @@
System: - @* @foreach (var systemName in _systemList.Systems) { @@ -26,7 +26,7 @@
Status: @( - CurrentEmulatorState + EmulatorState )
@@ -46,7 +46,6 @@
Volume: - @* *@
@@ -55,7 +54,7 @@
- @_selectedSystemConfigValidationMessage + @_selectedSystemConfigValidationMessage
@@ -83,19 +82,20 @@
+ @*Skia OpenGl View Blazor WASM component*@
+ + IgnorePixelScaling="true" + EnableRenderLoop="true" + OnPaintSurface="OnPaintSurface" + id="emulatorSKGLView" + tabindex="0" + oncontextmenu="return false;" + @onkeydown="OnKeyDown" + @onkeyup="OnKeyUp" + @onfocus="OnFocus" />
diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Index.razor.cs b/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Index.razor.cs index 190459c79..636c90b0c 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Index.razor.cs +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Index.razor.cs @@ -2,7 +2,6 @@ using Blazored.Modal.Services; using Blazored.Modal; using Highbyte.DotNet6502.Impl.AspNet; -using Highbyte.DotNet6502.Impl.Skia; using Highbyte.DotNet6502.Systems; using Highbyte.DotNet6502.Impl.AspNet.JSInterop.BlazorWebAudioSync; using Microsoft.AspNetCore.WebUtilities; @@ -28,14 +27,6 @@ public partial class Index private AudioContextSync _audioContext = default!; - public enum EmulatorState - { - Uninitialized, - Running, - Paused - } - public EmulatorState CurrentEmulatorState { get; private set; } = EmulatorState.Uninitialized; - private string _selectedSystemName = default!; private bool IsSelectedSystemConfigOk => string.IsNullOrEmpty(_selectedSystemConfigValidationMessage); @@ -47,9 +38,11 @@ public enum EmulatorState private IHostSystemConfig _currentHostSystemConfig = default!; public IHostSystemConfig HostSystemConfig => _currentHostSystemConfig; + public EmulatorState EmulatorState => _wasmHost == null ? EmulatorState.Uninitialized : _wasmHost.EmulatorState; + private bool AudioEnabledToggleDisabled => ( (!(_currentSystemConfig?.AudioSupported ?? true)) || - (CurrentEmulatorState == EmulatorState.Running || CurrentEmulatorState == EmulatorState.Paused) + (EmulatorState == EmulatorState.Running || EmulatorState == EmulatorState.Paused) ); private bool AudioEnabled @@ -96,8 +89,8 @@ private double Scale private EmulatorConfig _emulatorConfig = default!; private SystemList _systemList = default!; - private WasmHost _wasmHost = default!; - public WasmHost WasmHost => _wasmHost; + private WasmHostBase _wasmHost = default!; + public WasmHostBase WasmHost => _wasmHost; private string _statsString = "Instrumentations: calculating..."; private string _debugString = ""; @@ -194,6 +187,18 @@ protected override async Task OnInitializedAsync() } ); _mapper = mapperConfiguration.CreateMapper(); + + _wasmHost = new SkiaWasmHost( + Js!, + _systemList, + UpdateStats, + UpdateDebug, + SetMonitorState, + _emulatorConfig, + ToggleDebugStatsState, + LoggerFactory, + GamepadList, + MasterVolumePercent); } private async Task SetDefaultsFromQueryParams(Uri uri) @@ -217,14 +222,30 @@ private async Task SetDefaultsFromQueryParams(Uri uri) } } + + /// + /// SkiaSharp WASM-specific method, only used if _wasmHost is SkiaWasmHost. + /// + /// + public async void OnPaintSurface(SKPaintGLSurfaceEventArgs e) + { + if (_wasmHost != null && _wasmHost is SkiaWasmHost skiaWasmHost) + { + skiaWasmHost.OnPaintSurface(e); + } + } + protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { _audioContext = await AudioContextSync.CreateAsync(Js!); + _wasmHost.Init(_audioContext); } } + + //protected override async void OnAfterRender(bool firstRender) //{ // if (firstRender) @@ -272,47 +293,13 @@ private async void UpdateCanvasSize() this.StateHasChanged(); } - private void InitEmulator() - { - _wasmHost = new WasmHost(Js!, _selectedSystemName, _systemList, UpdateStats, UpdateDebug, SetMonitorState, _emulatorConfig, ToggleDebugStatsState, LoggerFactory, (float)Scale, MasterVolumePercent); - CurrentEmulatorState = EmulatorState.Paused; - } - private void CleanupEmulator() { _debugVisible = false; - _wasmHost!.Monitor.Disable(); - - CurrentEmulatorState = EmulatorState.Paused; + _wasmHost.Monitor.Disable(); + _wasmHost.Stop(); _wasmHost?.Cleanup(); _wasmHost = default!; - CurrentEmulatorState = EmulatorState.Uninitialized; - } - - protected async void OnPaintSurface(SKPaintGLSurfaceEventArgs e) - { - if (CurrentEmulatorState != EmulatorState.Running) - return; - - if (!(e.Surface.Context is GRContext grContext && grContext != null)) - return; - - if (_wasmHost == null) - return; - - if (!_wasmHost.Initialized) - { - await _wasmHost.Init(e.Surface.Canvas, grContext, _audioContext, GamepadList, Js!); - } - - //_emulatorRenderer.SetSize(e.Info.Width, e.Info.Height); - //if (e.Surface.Context is GRContext context && context != null) - //{ - // // If we draw our own images (not directly on the canvas provided), make sure it's within the same contxt - // _emulatorRenderer.SetContext(context); - //} - - _wasmHost.Render(e.Surface.Canvas, grContext); } internal async Task UpdateCurrentSystemConfig(ISystemConfig config, IHostSystemConfig? hostSystemConfig) @@ -504,11 +491,11 @@ private string GetDisplayStyle(string displayData) { case "Canvas": { - return CurrentEmulatorState != EmulatorState.Uninitialized ? VISIBLE : HIDDEN; + return EmulatorState != EmulatorState.Uninitialized ? VISIBLE : HIDDEN; } case "CanvasUninitialized": { - return CurrentEmulatorState == EmulatorState.Uninitialized ? VISIBLE_BLOCK : HIDDEN; + return EmulatorState == EmulatorState.Uninitialized ? VISIBLE_BLOCK : HIDDEN; } case "Debug": { @@ -552,60 +539,52 @@ public string GetSystemVisibilityDisplayStyle(string displayData, string systemN } } - private bool OnSelectSystemNameDisabled => CurrentEmulatorState == EmulatorState.Running || CurrentEmulatorState == EmulatorState.Paused; - private bool OnStartDisabled => CurrentEmulatorState == EmulatorState.Running || !IsSelectedSystemConfigOk; - private bool OnPauseDisabled => CurrentEmulatorState == EmulatorState.Paused || CurrentEmulatorState == EmulatorState.Uninitialized; - private bool OnResetDisabled => CurrentEmulatorState == EmulatorState.Uninitialized; - private bool OnStopDisabled => CurrentEmulatorState == EmulatorState.Uninitialized; + private bool OnSelectSystemNameDisabled => EmulatorState == EmulatorState.Running || EmulatorState == EmulatorState.Paused; + private bool OnStartDisabled => EmulatorState == EmulatorState.Running || !IsSelectedSystemConfigOk; + private bool OnPauseDisabled => EmulatorState == EmulatorState.Paused || EmulatorState == EmulatorState.Uninitialized; + private bool OnResetDisabled => EmulatorState == EmulatorState.Uninitialized; + private bool OnStopDisabled => EmulatorState == EmulatorState.Uninitialized; public async Task OnStart(MouseEventArgs mouseEventArgs) { - if (CurrentEmulatorState == EmulatorState.Uninitialized) + if (EmulatorState == EmulatorState.Uninitialized) { bool isOk = await _systemList.IsValidConfig(_selectedSystemName); if (!isOk) return; - InitEmulator(); + await _wasmHost.SetCurrentSystem(_selectedSystemName); } - _wasmHost.Start(); - CurrentEmulatorState = EmulatorState.Running; + await _wasmHost.Start(); await FocusEmulator(); - this.StateHasChanged(); - - _logger.LogInformation($"System started: {_selectedSystemName}"); } public void OnPause(MouseEventArgs mouseEventArgs) { - if (CurrentEmulatorState == EmulatorState.Uninitialized) + if (EmulatorState == EmulatorState.Uninitialized) return; - _wasmHost!.Stop(); - CurrentEmulatorState = EmulatorState.Paused; + _wasmHost.Pause(); this.StateHasChanged(); - - _logger.LogInformation($"System paused: {_selectedSystemName}"); } public async Task OnReset(MouseEventArgs mouseEventArgs) { - OnPause(mouseEventArgs); - OnStop(mouseEventArgs); + await _wasmHost.Reset(); await OnStart(mouseEventArgs); } + public void OnStop(MouseEventArgs mouseEventArgs) { - CleanupEmulator(); + _wasmHost.Stop(); this.StateHasChanged(); - _logger.LogInformation($"System stopped: {_selectedSystemName}"); - } + private void OnMonitorToggle(MouseEventArgs mouseEventArgs) { - _wasmHost!.ToggleMonitor(); + _wasmHost.ToggleMonitor(); this.StateHasChanged(); } diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/wwwroot/index.html b/src/apps/Highbyte.DotNet6502.App.WASM/wwwroot/index.html index dcaf65a9b..8cb0c5552 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/wwwroot/index.html +++ b/src/apps/Highbyte.DotNet6502.App.WASM/wwwroot/index.html @@ -4,7 +4,7 @@ - dotnet-6502 CPU emulator in Blazor WebAssembly with Skia rendering + dotnet-6502 CPU emulator in Blazor WebAssembly with SkiaSharp or Silk.NET/WebGL rendering From 16e22095c32f19981f8b88c5daccb7c049ec6c4f Mon Sep 17 00:00:00 2001 From: Highbyte Date: Tue, 16 Apr 2024 21:53:37 +0200 Subject: [PATCH 4/5] Silk.NET WASM experiment with fragment shader. Change emulator renderer code for fragment shader rendering to work with both Silk.NET native and WASM. Shaders changed to be ES compatible (works in browser and native). --- .../Program.cs | 6 + .../Emulator/EmulatorConfig.cs | 6 +- .../Emulator/SilkNet/SilkNetWasmHost.cs | 277 ++++++ .../Emulator/Skia/SkiaWasmHost.cs | 24 +- .../Emulator/SystemSetup/C64HostConfig.cs | 11 +- .../Emulator/SystemSetup/C64Setup.cs | 17 +- .../Emulator/SystemSetup/Enums.cs | 8 + .../Emulator/WasmHostBase.cs | 37 +- .../Highbyte.DotNet6502.App.WASM.csproj | 12 +- .../Pages/GeneralSettingsUI.razor | 53 +- .../Pages/Index.razor | 133 ++- .../Pages/Index.razor.cs | 211 +++-- .../SilkNetWASMExperiment-import.props | 46 + .../Highbyte.DotNet6502.App.WASM/global.json | 2 +- .../wwwroot/css/app.css | 4 +- .../wwwroot/index.html | 2 +- .../Video/C64SilkNetOpenGlRenderer.cs | 169 ++-- .../Video/C64SilkNetOpenGlRendererConfig.cs | 18 + .../Commodore64/Video/C64shader_es.frag | 633 ++++++++++++++ .../Commodore64/Video/C64shader_es.vert | 13 + .../Commodore64/Video/TestShader.frag | 798 ++++++++++++++++++ .../Highbyte.DotNet6502.Impl.SilkNet.csproj | 40 +- .../OpenGLHelpers/Shader.cs | 85 +- .../SilkNetOpenGlRenderContext.cs | 22 +- 24 files changed, 2388 insertions(+), 239 deletions(-) create mode 100644 src/apps/Highbyte.DotNet6502.App.WASM/Emulator/SilkNet/SilkNetWasmHost.cs create mode 100644 src/apps/Highbyte.DotNet6502.App.WASM/Emulator/SystemSetup/Enums.cs create mode 100644 src/apps/Highbyte.DotNet6502.App.WASM/SilkNetWASMExperiment-import.props create mode 100644 src/libraries/Highbyte.DotNet6502.Impl.SilkNet/Commodore64/Video/C64shader_es.frag create mode 100644 src/libraries/Highbyte.DotNet6502.Impl.SilkNet/Commodore64/Video/C64shader_es.vert create mode 100644 src/libraries/Highbyte.DotNet6502.Impl.SilkNet/Commodore64/Video/TestShader.frag diff --git a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Program.cs b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Program.cs index f9cbd22a5..7559c5568 100644 --- a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Program.cs +++ b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Program.cs @@ -36,6 +36,12 @@ SilkNetOpenGlRendererConfig = new C64SilkNetOpenGlRendererConfig() { UseFineScrollPerRasterLine = false, // Setting to true may work, depending on how code is written. Full screen scroll may not work (actual screen memory is not rendered in sync with raster line). + + ShaderEmbeddedResourceType = typeof(C64SilkNetOpenGlRendererConfig), + VertexShaderPath = "Highbyte.DotNet6502.Impl.SilkNet.Commodore64.Video.C64shader_es.vert", + FragmentShaderPath = "Highbyte.DotNet6502.Impl.SilkNet.Commodore64.Video.C64shader_es.frag", + + //UseTestShader = true // Set to true to use a test fragment shader instead of the C64 fragment shader. } }; var c64Setup = new C64Setup(loggerFactory, c64HostConfig); diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/EmulatorConfig.cs b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/EmulatorConfig.cs index 3b79e399a..cbe8b80bc 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/EmulatorConfig.cs +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/EmulatorConfig.cs @@ -1,5 +1,5 @@ +using Highbyte.DotNet6502.App.WASM.Emulator.SystemSetup; using Highbyte.DotNet6502.Impl.AspNet; -using Highbyte.DotNet6502.Impl.Skia; using Highbyte.DotNet6502.Monitor; using Highbyte.DotNet6502.Systems; @@ -10,7 +10,9 @@ public class EmulatorConfig public const int DEFAULT_CANVAS_WINDOW_WIDTH = 640; public const int DEFAULT_CANVAS_WINDOW_HEIGHT = 400; - public required string DefaultEmulator { get; set; } + public RendererType Renderer { get; set; } = RendererType.SkiaSharp; + + public string DefaultEmulator { get; set; } public double DefaultDrawScale { get; set; } public double CurrentDrawScale { get; set; } public required MonitorConfig Monitor { get; set; } diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/SilkNet/SilkNetWasmHost.cs b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/SilkNet/SilkNetWasmHost.cs new file mode 100644 index 000000000..9bfea33c8 --- /dev/null +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/SilkNet/SilkNetWasmHost.cs @@ -0,0 +1,277 @@ +#if SILKNETWASM + +using Highbyte.DotNet6502.Impl.AspNet; +using Highbyte.DotNet6502.Impl.SilkNet; +using Highbyte.DotNet6502.Systems; +using Silk.NET.Core.Loader; +using Silk.NET.Core.Native; +using Silk.NET.Input.Sdl; +using Silk.NET.Maths; +using Silk.NET.OpenGL; +using Silk.NET.Windowing; +using Silk.NET.Windowing.Sdl; +using Toolbelt.Blazor.Gamepad; + +namespace Highbyte.DotNet6502.App.WASM.Emulator.SilkNet; + + +public class SilkNetWasmHost : WasmHostBase +{ + private readonly ILogger _logger; + private readonly ILoggerFactory _loggerFactory; + + private readonly IJSRuntime _jsRuntime; + + [System.Runtime.InteropServices.DllImport("SDL", EntryPoint = "SDL_GetPlatform", CallingConvention = (System.Runtime.InteropServices.CallingConvention)2)] + static extern unsafe byte* I_SDL_GetPlatform(); + [System.Runtime.InteropServices.UnmanagedCallersOnly(CallConvs = new[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })] + static unsafe byte* S_SDL_GetPlatform() => I_SDL_GetPlatform(); + private static IView s_window; + private int _currentCanvasWidth; + private int _currentCanvasHeight; + + public SilkNetWasmHost( + IJSRuntime jsRuntime, + SystemList systemList, + Action updateStats, + Action updateDebug, + Func setMonitorState, + EmulatorConfig emulatorConfig, + Func toggleDebugStatsState, + ILoggerFactory loggerFactory, + GamepadList gamepadList, + float initialMasterVolume = 50.0f + ) : base( + jsRuntime, + systemList, + updateStats, + updateDebug, + setMonitorState, + emulatorConfig, + toggleDebugStatsState, + loggerFactory, + gamepadList, + initialMasterVolume) + { + _logger = loggerFactory.CreateLogger(typeof(SilkNetWasmHost).Name); + _jsRuntime = jsRuntime; + _loggerFactory = loggerFactory; + } + + protected async override void OnInit() + { + // Init and start Silk.NET render loop + var canvasId = "canvas"; + await _jsRuntime.InvokeVoidAsync("setCanvas", typeof(SilkNetWasmHost).Assembly.GetName().Name, $"{typeof(SilkNetWasmHost).FullName}.CanvasDropped", canvasId); + unsafe + { + Console.WriteLine(SilkMarshal.PtrToString((nint)I_SDL_GetPlatform())); + delegate* unmanaged[Cdecl] gp = &S_SDL_GetPlatform; + if (gp == null) + { + throw new("what"); + } + } + SearchPathContainer.Platform = UnderlyingPlatform.Browser; + + s_window = GetSilkNetWindow(); + + Console.WriteLine("Before setting window event handlers"); + s_window.Load += OnSilkNetLoad; + s_window.Render += OnSilkNetRender; + s_window.Update += OnSilkNetUpdate; + s_window.Resize += OnSilkNetResize; + s_window.FramebufferResize += OnSilkNetFramebufferResize; + s_window.Closing += OnSilkNetClosing; + + Console.WriteLine("Before window run"); + s_window.Run(); + Console.WriteLine("After window run"); + + } + + [JSInvokable("Highbyte.DotNet6502.App.WASM.Emulator.SilkNet.SilkNetWasmHost.CanvasDropped")] + public static void CanvasDropped() => + Silk.NET.Windowing.Window.CanvasDropped(s_window); + + private IView GetSilkNetWindow() + { + Console.WriteLine("Start GetSilkNetWindow"); +#if SILKNETWASM //NOTE: not having this WASM check changes nothing, but it's here for clarity + // Register SDL windowing and Input manually + // Silk.NET doesn't have all the reflection facilities to find them automatically on WASM + Console.WriteLine("Before Window registration"); + SdlWindowing.RegisterPlatform(); + Console.WriteLine("Before Input registration"); + SdlInput.RegisterPlatform(); +#endif + + var opts = ViewOptions.Default; + opts.FramesPerSecond = 90; + opts.UpdatesPerSecond = 90; + opts.API = GraphicsAPI.Default; + +#if WASM + //On WASM, we should be using OpenGLES 3.0 + opts.API = new GraphicsAPI(ContextAPI.OpenGLES, new APIVersion(3, 0)); +#endif + + opts.VSync = false; + Console.WriteLine("Before window creation"); + Console.WriteLine(Silk.NET.Windowing.Window.Platforms.Count); + + IView window; + if (Silk.NET.Windowing.Window.IsViewOnly) + { + Console.WriteLine("View only"); + window = Silk.NET.Windowing.Window.GetView(opts); + } + else + { + Console.WriteLine("Not view only"); + window = Silk.NET.Windowing.Window.Create(new(opts)); + } + Console.WriteLine($"After window creation"); + return window; + } + + protected void OnSilkNetLoad() + { + RenderContextContainer = new WASMRenderContextContainer( + null, + new SilkNetOpenGlRenderContext(s_window, (float)_emulatorConfig.CurrentDrawScale)); + InputHandlerContext = new AspNetInputHandlerContext(_loggerFactory, _gamepadList); + AudioHandlerContext = new WASMAudioHandlerContext(_audioContext, _jsRuntime, _initialMasterVolume); + + _systemList.InitContext(() => RenderContextContainer, () => InputHandlerContext, () => AudioHandlerContext); + } + + /// + /// Runs on every Render Frame event. + /// + /// Use this method to render the world. + /// + /// This method is called at a RenderFrequency set in the GameWindowSettings object. + /// + /// + protected void OnSilkNetRender(double deltaTime) + { + Render(); + } + + /// + /// Runs on every Update Frame event. + /// + /// Use this method to run logic. + /// + /// + /// + protected void OnSilkNetUpdate(double deltaTime) + { + if (EmulatorState != EmulatorState.Running) + return; + EmulatorRunOneFrame(); + } + + protected void OnSilkNetClosing() + { + Cleanup(); + } + + private void OnSilkNetResize(Vector2D size) + { + _logger.LogInformation($"OnSilkNetResize: {size}"); + } + + private void OnSilkNetFramebufferResize(Vector2D size) + { + _logger.LogInformation($"OnSilkNetFramebufferResize: {size}"); + + //RenderContextContainer.SilkNetOpenGlRenderContext.Gl.Viewport(size); + + //// Temporary hack for Silk.NET viewport size. Parameter size seems to be 1,1 when canvas is hidden... + //Vector2D sizeWorkaround; + //if (size.X <= 1 && size.Y <= 1) + //{ + // if (SystemRunner != null) + // { + // var screen = SystemRunner.System.Screen; + // var scale = 2.0f; // TODO: Get scale from UI. + // sizeWorkaround = new Vector2D((int)(screen.VisibleWidth * scale), (int)(screen.VisibleHeight * scale)); + // } + // else + // { + // sizeWorkaround = new Vector2D(836, 470); + // //sizeWorkaround = new Vector2D(_currentCanvasWidth, _currentCanvasHeight); + // } + + //} + //else + //{ + // sizeWorkaround = size; + //} + + //RenderContextContainer.SilkNetOpenGlRenderContext.Gl.Viewport(sizeWorkaround); + } + + /// + /// Canvas size change event triggered by the GUI. + /// + /// + /// + public override void OnAfterUpdateCanvasSize(int width, int height) + { + _logger.LogInformation($"OnAfterUpdateCanvasSize: {width} {height}"); + + // Hack: remember last time canvas size was changed to use in OnSilkNetFramebufferResize + _currentCanvasWidth = width; + _currentCanvasHeight = height; + + //Vector2D size = new(width, height); + //RenderContextContainer.SilkNetOpenGlRenderContext.Gl.Viewport(size); + } + + + protected override void OnBeforeRender() + { + } + + protected override void OnAfterPause() + { + } + + protected override async Task OnAfterStop() + { + // Workaround for Silk.NET WASM canvas that doesn't seem to work if it was invisible first. + // Also, make sure to resize canvas to 0,0 when stopped (done in general code at Index.razor.cs?) + RenderContextContainer.SilkNetOpenGlRenderContext.Gl.Clear((uint)ClearBufferMask.ColorBufferBit); + await _jsRuntime!.InvokeVoidAsync("triggerResize"); // hack to make sure canvas is refreshed + } + + protected override async Task OnAfterStart() + { + // Workaround for Silk.NET WASM canvas that doesn't seem to work if it was invisible first. + // Make sure to resize back canvas to original size when started (done in general code at Index.razor.cs?) + await _jsRuntime!.InvokeVoidAsync("triggerResize"); // hack to make sure canvas is refreshed + } + + protected override void OnAfterCleanup() + { + + //// Clear canvas + //_renderContext.SkiaRenderContext.GetCanvas().Clear(); + + //// Clean up Skia resources + //_renderContext.SkiaRenderContext?.Cleanup(); + + //// Clean up input handler resources + //InputHandlerContext?.Cleanup(); + + //// Stop any playing audio + //_systemRunner.AudioHandler.StopPlaying(); + //// Clean up audio resources + ////AudioHandlerContext?.Cleanup(); + } +} + +#endif diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/Skia/SkiaWasmHost.cs b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/Skia/SkiaWasmHost.cs index f75a7d3da..b7b4ed152 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/Skia/SkiaWasmHost.cs +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/Skia/SkiaWasmHost.cs @@ -45,13 +45,6 @@ public SkiaWasmHost( } - protected override WASMRenderContextContainer BuildRenderContext() - { - return new WASMRenderContextContainer( - new SkiaRenderContext(GetCanvas, GetGRContext), - null); - } - private SKCanvas GetCanvas() { return _skCanvas; @@ -80,6 +73,16 @@ public void OnPaintSurface(SKPaintGLSurfaceEventArgs e) Render(); } + protected override void OnInit() + { + RenderContextContainer = new WASMRenderContextContainer( + new SkiaRenderContext(GetCanvas, GetGRContext), + null); + InputHandlerContext = new AspNetInputHandlerContext(_loggerFactory, _gamepadList); + AudioHandlerContext = new WASMAudioHandlerContext(_audioContext, _jsRuntime, _initialMasterVolume); + _systemList.InitContext(() => RenderContextContainer, () => InputHandlerContext, () => AudioHandlerContext); + } + protected override void OnBeforeRender() { _skCanvas.Scale((float)EmulatorConfig.CurrentDrawScale); @@ -90,12 +93,13 @@ protected override void OnAfterPause() _updateTimer?.Stop(); } - protected override void OnAfterStop() + protected override Task OnAfterStop() { _initialized = false; + return Task.CompletedTask; } - protected override void OnAfterStart() + protected override Task OnAfterStart() { if (_updateTimer != null) { @@ -110,6 +114,8 @@ protected override void OnAfterStart() _updateTimer.Elapsed += UpdateTimerElapsed; } _updateTimer!.Start(); + + return Task.CompletedTask; } protected override void OnAfterCleanup() diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/SystemSetup/C64HostConfig.cs b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/SystemSetup/C64HostConfig.cs index a202906f9..47d76885e 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/SystemSetup/C64HostConfig.cs +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/SystemSetup/C64HostConfig.cs @@ -1,19 +1,16 @@ using Highbyte.DotNet6502.Impl.AspNet.Commodore64; +using Highbyte.DotNet6502.Impl.SilkNet.Commodore64.Video; using Highbyte.DotNet6502.Systems; namespace Highbyte.DotNet6502.App.WASM.Emulator.SystemSetup { - public enum C64HostRenderer - { - SkiaSharp, - SilkNetOpenGl - } + public class C64HostConfig : IHostSystemConfig, ICloneable { - public C64HostRenderer Renderer { get; set; } = C64HostRenderer.SkiaSharp; - public C64AspNetConfig InputConfig { get; set; } = new C64AspNetConfig(); + public C64SilkNetOpenGlRendererConfig SilkNetOpenGlRendererConfig { get; set; } = new C64SilkNetOpenGlRendererConfig(); + public object Clone() { var clone = (C64HostConfig)MemberwiseClone(); diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/SystemSetup/C64Setup.cs b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/SystemSetup/C64Setup.cs index 76a705d54..6a16410ca 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/SystemSetup/C64Setup.cs +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/SystemSetup/C64Setup.cs @@ -18,12 +18,14 @@ public class C64Setup : SystemConfigurer GetNewConfig(string configurationVariant) @@ -79,19 +81,18 @@ WASMAudioHandlerContext audioHandlerContext IRenderer renderer; IRenderContext renderContext; - switch (c64HostConfig.Renderer) + switch (_emulatorConfig.Renderer) { - case C64HostRenderer.SkiaSharp: + case RendererType.SkiaSharp: renderer = new C64SkiaRenderer(); renderContext = renderContextContainer.SkiaRenderContext; break; - case C64HostRenderer.SilkNetOpenGl: - throw new NotImplementedException(); - //renderer = new C64SilkNetOpenGlRenderer(c64HostConfig.SilkNetOpenGlRendererConfig); - //renderContext = renderContextContainer.SilkNetOpenGlRenderContext; + case RendererType.SilkNetOpenGl: + renderer = new C64SilkNetOpenGlRenderer(c64HostConfig.SilkNetOpenGlRendererConfig); + renderContext = renderContextContainer.SilkNetOpenGlRenderContext; break; default: - throw new NotImplementedException($"Renderer {c64HostConfig.Renderer} not implemented."); + throw new NotImplementedException($"Renderer {_emulatorConfig.Renderer} not implemented."); } var inputHandler = new C64AspNetInputHandler(_loggerFactory, _hostConfig.InputConfig); diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/SystemSetup/Enums.cs b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/SystemSetup/Enums.cs new file mode 100644 index 000000000..5a44b7cee --- /dev/null +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/SystemSetup/Enums.cs @@ -0,0 +1,8 @@ +namespace Highbyte.DotNet6502.App.WASM.Emulator.SystemSetup +{ + public enum RendererType + { + SkiaSharp, + SilkNetOpenGl + } +} diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/WasmHostBase.cs b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/WasmHostBase.cs index 56d421909..c259a0638 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/WasmHostBase.cs +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/WasmHostBase.cs @@ -11,7 +11,7 @@ public abstract class WasmHostBase : IDisposable { private string _systemName; - private readonly IJSRuntime _jsRuntime; + protected readonly IJSRuntime _jsRuntime; private SystemRunner _systemRunner = default!; @@ -19,11 +19,11 @@ public abstract class WasmHostBase : IDisposable public EmulatorState EmulatorState { get; private set; } = EmulatorState.Uninitialized; - public WASMRenderContextContainer RenderContextContainer { get; private set; } = default!; - public WASMAudioHandlerContext AudioHandlerContext { get; private set; } = default!; - public AspNetInputHandlerContext InputHandlerContext { get; private set; } = default!; + public WASMRenderContextContainer RenderContextContainer { get; protected set; } = default!; + public WASMAudioHandlerContext AudioHandlerContext { get; protected set; } = default!; + public AspNetInputHandlerContext InputHandlerContext { get; protected set; } = default!; - private readonly SystemList _systemList; + protected readonly SystemList _systemList; public SystemList SystemList => _systemList; protected EmulatorConfig EmulatorConfig => _emulatorConfig; @@ -32,12 +32,12 @@ public abstract class WasmHostBase : IDisposable private readonly Action _updateStats; private readonly Action _updateDebug; private readonly Func _setMonitorState; - private readonly EmulatorConfig _emulatorConfig; + protected readonly EmulatorConfig _emulatorConfig; private readonly Func _toggleDebugStatsState; private readonly ILoggerFactory _loggerFactory; - private AudioContextSync _audioContext; - private readonly GamepadList _gamepadList; - private readonly float _initialMasterVolume; + protected AudioContextSync _audioContext; + protected readonly GamepadList _gamepadList; + protected readonly float _initialMasterVolume; private readonly ILogger _logger; public WasmMonitor Monitor { get; private set; } = default!; @@ -101,13 +101,10 @@ public void Init(AudioContextSync audioContext) _audioContext = audioContext; - RenderContextContainer = BuildRenderContext(); - InputHandlerContext = new AspNetInputHandlerContext(_loggerFactory, _gamepadList); - AudioHandlerContext = new WASMAudioHandlerContext(_audioContext, _jsRuntime, _initialMasterVolume); - _systemList.InitContext(() => RenderContextContainer, () => InputHandlerContext, () => AudioHandlerContext); + OnInit(); } - protected abstract WASMRenderContextContainer BuildRenderContext(); + protected abstract void OnInit(); private async Task InitSystem() { @@ -142,10 +139,10 @@ public async Task Start() _logger.LogInformation($"System started: {_systemName}"); - OnAfterStart(); + await OnAfterStart(); } - protected virtual void OnAfterStart() { } + protected virtual async Task OnAfterStart() { await Task.CompletedTask; } public void Pause() { @@ -177,7 +174,7 @@ public async Task Reset() protected virtual void OnAfterReset() { } - public void Stop() + public async Task Stop() { if (EmulatorState == EmulatorState.Running) Pause(); @@ -187,9 +184,9 @@ public void Stop() _logger.LogInformation($"System stopped: {_systemName}"); - OnAfterStop(); + await OnAfterStop(); } - protected virtual void OnAfterStop() { } + protected virtual async Task OnAfterStop() { await Task.CompletedTask; } public async Task SetCurrentSystem(string systemName) @@ -226,6 +223,8 @@ protected void Render() protected virtual void OnBeforeRender() { } + public virtual void OnAfterUpdateCanvasSize(int width, int height) { } + public virtual void Cleanup() { // Clean up render context resources diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Highbyte.DotNet6502.App.WASM.csproj b/src/apps/Highbyte.DotNet6502.App.WASM/Highbyte.DotNet6502.App.WASM.csproj index a6215e09c..e24f4a8d8 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Highbyte.DotNet6502.App.WASM.csproj +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Highbyte.DotNet6502.App.WASM.csproj @@ -12,7 +12,11 @@ + + $(DefineConstants);SILKNETWASM + 1.0 + @@ -46,9 +50,9 @@ - + + - @@ -66,4 +70,8 @@ + + + + diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Pages/GeneralSettingsUI.razor b/src/apps/Highbyte.DotNet6502.App.WASM/Pages/GeneralSettingsUI.razor index b1669ef7c..642abb835 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Pages/GeneralSettingsUI.razor +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Pages/GeneralSettingsUI.razor @@ -1,26 +1,59 @@ -@using Highbyte.DotNet6502.Logging.Console; +@using Highbyte.DotNet6502.App.WASM.Emulator +@using Highbyte.DotNet6502.App.WASM.Emulator.SystemSetup +@using Highbyte.DotNet6502.Logging.Console; @using static Highbyte.DotNet6502.App.WASM.Pages.Index -
- - +
+
+

General settings

+
+
+
Renderer
+
+ +
+
+ +
+
Log level
+
+ +
+
+ +
+
+

@inject DotNet6502ConsoleLoggerConfiguration LoggerConfiguration + @code { [CascadingParameter] BlazoredModalInstance BlazoredModal { get; set; } = default!; - private async Task Close() => await BlazoredModal.CloseAsync(); + [Parameter] public EmulatorConfig EmulatorConfig { get; set; } = default!; + + + private async Task Close() => await BlazoredModal.CloseAsync(ModalResult.Ok(EmulatorConfig)); private void OnSelectedLogLevelChanged(ChangeEventArgs e) => LoggerConfiguration.LogLevel = Enum.Parse(e.Value!.ToString()!); + + private void OnSelectedRendererChanged(ChangeEventArgs e) => + EmulatorConfig.Renderer = Enum.Parse(e.Value!.ToString()!); + } diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Index.razor b/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Index.razor index 870efe615..08b26bb61 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Index.razor +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Index.razor @@ -82,45 +82,65 @@
- - @*Skia OpenGl View Blazor WASM component*@
- + +
+

+ The DotNet 6502 emulator! +

+

+ A 6502 CPU emulator written in .NET, rendered with SkiaSharp, compiled to WebAssembly via Blazor, running in a browser. +

+

+ Select the computer to emulate in the System drop down menu on the left. +

+

+ For certain computers (like the C64) you have to provide ROM files via a Config button. +

+

+ Press Start button to run. +

+

+ When running, press F12 to start machine code monitor and F11 to show statistics. +

+
+ } + else if (_emulatorConfig.Renderer == Emulator.SystemSetup.RendererType.SilkNetOpenGl) + { +
+

+ The DotNet 6502 emulator! - Expermimental version with WebGL fragment shader rendering via Silk.NET +

+
+ + @*Canvas for Silk.NET WASM rendering with WebGL experminent*@ + // class="silkNetGLViewStyle" + // class="" + // style="width: 0px; height: 0px;" + // style="width: 836px; height: 470px;" + - -
- -

- The DotNet 6502 emulator! -

- -

- A 6502 CPU emulator written in .NET, rendered with SkiaSharp, compiled to WebAssembly via Blazor, running in a browser. -

-

- Select the computer to emulate in the System drop down menu on the left. -

-

- For certain computers (like the C64) you have to provide ROM files via a Config button. -

-

- Press Start button to run. -

-

- When running, press F12 to start machine code monitor and F11 to show statistics. -

-
- + }
@@ -168,6 +188,11 @@ width: @_windowWidthStyle; height: @_windowHeightStyle; } + .silkNetGLViewStyle { + display: true; /* Workaround for Silk.NET WASM experiment, doesn't work if canvas is initially invisible */ + width: @_windowWidthStyle; + height: @_windowHeightStyle; + } .canvasUninitializedStyle { display: @GetDisplayStyle("CanvasUninitialized"); @@ -187,3 +212,45 @@