diff --git a/.github/workflows/dotnet-blazor-app-publish-to-gh-pages.yml b/.github/workflows/dotnet-blazor-app-publish-to-gh-pages.yml index c204ff23a..7c8f92f20 100644 --- a/.github/workflows/dotnet-blazor-app-publish-to-gh-pages.yml +++ b/.github/workflows/dotnet-blazor-app-publish-to-gh-pages.yml @@ -16,11 +16,14 @@ env: PROJECT_FILE: "src/apps/Highbyte.DotNet6502.App.WASM/Highbyte.DotNet6502.App.WASM.csproj" PROJECT_GH_PAGES_DIR: "app" + PROJECT_GH_PAGES_DIR_WEBGL: "app2" BUILD_OUTPUT_WORKING_DIR: "build" GH_PAGES_WORKING_DIR: "ghpages" GH_PAGES_BRANCH_NAME: "gh-pages" jobs: + + #Deploy the default version (SkiaSharp) of the Blazor WASM site deploy: runs-on: ubuntu-latest steps: @@ -66,3 +69,51 @@ jobs: folder: ${{ env.GH_PAGES_WORKING_DIR }} # The folder the action should deploy. #target-folder: app #If not specified, the contents for folder above is copied to the root of the GH pages branch. single-commit: true + + #Deploy WebGL version of the Blazor WASM site + deploy2: + runs-on: ubuntu-latest + steps: + # Checkout the code + - uses: actions/checkout@v4 + + # Get version from tag + - name: Get version from tag with 'v' prefix + run: echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_ENV + + # Install .NET Core SDK + - name: Setup .NET Core + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + + - name: Install DotNet workload - WASM tools + run: | + dotnet workload install wasm-tools + + # Create GitHub Pages root folder contents + - name: Create GH root folder and add .nojekyll file do disable built-in Jekyll CMS + run: | + mkdir ${{ env.GH_PAGES_WORKING_DIR }} + echo '' > ${{ env.GH_PAGES_WORKING_DIR }}/.nojekyll + + # Publish the Blazor WASM site to a build working folder + - name: Publish + # Note: Could not get -p:GHPagesInjectBrotliLoader=true to work. Gets error "Uncaught ReferenceError: Blazor is not defined" from "brotliloader.min.js" + run: dotnet publish ${{ env.PROJECT_FILE }} -p:Version=${{ env.VERSION }} -c:${{ env.CONFIGURATION }} -p:GHPages=true -p:GHPagesBase="/${{ github.event.repository.name }}/${{ env.PROJECT_GH_PAGES_DIR_WEBGL }}/" -p:GHPagesInjectBrotliLoader=false -o:${{ env.BUILD_OUTPUT_WORKING_DIR }}/${{ env.PROJECT_GH_PAGES_DIR }} /p:DefineConstants=SILKNETWASM + + # Copy the published Blazor WASM site from wwwroot folder in publish output dir + - name: Copy published wwwroot contents to a subdirectory in GH pages working dir root + run: | + cp -r ${{ env.BUILD_OUTPUT_WORKING_DIR }}/${{ env.PROJECT_GH_PAGES_DIR }}/wwwroot/ ${{ env.GH_PAGES_WORKING_DIR }}/${{ env.PROJECT_GH_PAGES_DIR_WEBGL }}/ + + # Deploy the Blazor WASM site + - name: Deploy to Github Pages + uses: JamesIves/github-pages-deploy-action@v4 + with: + # token: ${{ secrets.ACCESS_TOKEN }} # Use if custom token needed to another repo + branch: ${{ env.GH_PAGES_BRANCH_NAME }} # The branch the action should deploy to. + folder: ${{ env.GH_PAGES_WORKING_DIR }} # The folder the action should deploy. + #target-folder: app #If not specified, the contents for folder above is copied to the root of the GH pages branch. + single-commit: true + 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/Skia/EmulatorConfig.cs b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/EmulatorConfig.cs similarity index 71% rename from src/apps/Highbyte.DotNet6502.App.WASM/Skia/EmulatorConfig.cs rename to src/apps/Highbyte.DotNet6502.App.WASM/Emulator/EmulatorConfig.cs index a337b3035..cbe8b80bc 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Skia/EmulatorConfig.cs +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/EmulatorConfig.cs @@ -1,16 +1,18 @@ +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; -namespace Highbyte.DotNet6502.App.WASM.Skia; +namespace Highbyte.DotNet6502.App.WASM.Emulator; 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; } @@ -23,7 +25,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/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/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/Emulator/Skia/SkiaWasmHost.cs b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/Skia/SkiaWasmHost.cs new file mode 100644 index 000000000..b7b4ed152 --- /dev/null +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/Skia/SkiaWasmHost.cs @@ -0,0 +1,146 @@ +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; + + } + + 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 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); + } + + protected override void OnAfterPause() + { + _updateTimer?.Stop(); + } + + protected override Task OnAfterStop() + { + _initialized = false; + return Task.CompletedTask; + } + + protected override Task 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(); + + return Task.CompletedTask; + } + + 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/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 64% 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..47d76885e 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Skia/C64HostConfig.cs +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/SystemSetup/C64HostConfig.cs @@ -1,13 +1,16 @@ - using Highbyte.DotNet6502.Impl.AspNet.Commodore64; +using Highbyte.DotNet6502.Impl.SilkNet.Commodore64.Video; using Highbyte.DotNet6502.Systems; -namespace Highbyte.DotNet6502.App.WASM.Skia +namespace Highbyte.DotNet6502.App.WASM.Emulator.SystemSetup { + public class C64HostConfig : IHostSystemConfig, ICloneable { 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/Skia/C64Setup.cs b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/SystemSetup/C64Setup.cs similarity index 82% 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..6a16410ca 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; @@ -17,12 +18,14 @@ public class C64Setup : SystemConfigurer GetNewConfig(string configurationVariant) @@ -69,12 +72,29 @@ 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 (_emulatorConfig.Renderer) + { + case RendererType.SkiaSharp: + renderer = new C64SkiaRenderer(); + renderContext = renderContextContainer.SkiaRenderContext; + break; + case RendererType.SilkNetOpenGl: + renderer = new C64SilkNetOpenGlRenderer(c64HostConfig.SilkNetOpenGlRendererConfig); + renderContext = renderContextContainer.SilkNetOpenGlRenderContext; + break; + default: + throw new NotImplementedException($"Renderer {_emulatorConfig.Renderer} not implemented."); + } + var inputHandler = new C64AspNetInputHandler(_loggerFactory, _hostConfig.InputConfig); var audioHandler = new C64WASMAudioHandler(_loggerFactory); 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/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/WasmHost.cs b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/WasmHostBase.cs similarity index 63% rename from src/apps/Highbyte.DotNet6502.App.WASM/Skia/WasmHost.cs rename to src/apps/Highbyte.DotNet6502.App.WASM/Emulator/WasmHostBase.cs index 0b5400b9f..c259a0638 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Skia/WasmHost.cs +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/WasmHostBase.cs @@ -1,41 +1,43 @@ 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.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; + protected 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; + + public WASMRenderContextContainer RenderContextContainer { get; protected set; } = default!; + public WASMAudioHandlerContext AudioHandlerContext { get; protected set; } = default!; + public AspNetInputHandlerContext InputHandlerContext { get; protected set; } = default!; + + protected readonly SystemList _systemList; + public SystemList SystemList => _systemList; + + protected EmulatorConfig EmulatorConfig => _emulatorConfig; - private SkiaRenderContext _skiaRenderContext = 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 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 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!; @@ -57,30 +59,29 @@ 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, + SystemList systemList, Action updateStats, Action updateDebug, Func setMonitorState, 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,90 +92,160 @@ 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."); - _skiaRenderContext = new SkiaRenderContext(GetCanvas, GetGRContext); - InputHandlerContext = new AspNetInputHandlerContext(_loggerFactory, gamepadList); - AudioHandlerContext = new WASMAudioHandlerContext(audioContext, jsRuntime, _initialMasterVolume); + _audioContext = audioContext; - _systemList.InitContext(() => _skiaRenderContext, () => InputHandlerContext, () => AudioHandlerContext); + OnInit(); + } - _systemRunner = await _systemList.BuildSystemRunner(_systemName); + protected abstract void OnInit(); + 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; + + _logger.LogInformation($"System started: {_systemName}"); - Initialized = true; + await OnAfterStart(); } - public void Stop() + protected virtual async Task OnAfterStart() { await Task.CompletedTask; } + + 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() { - _updateTimer?.Stop(); + if (EmulatorState == EmulatorState.Uninitialized) + return; + + Stop(); - _systemRunner.AudioHandler.PausePlaying(); + await Start(); + + OnAfterReset(); + } + + protected virtual void OnAfterReset() { } + + public async Task Stop() + { + if (EmulatorState == EmulatorState.Running) + Pause(); + + _systemRunner = default!; + EmulatorState = EmulatorState.Uninitialized; _logger.LogInformation($"System stopped: {_systemName}"); + + await OnAfterStop(); } + protected virtual async Task OnAfterStop() { await Task.CompletedTask; } + - 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 - _skiaRenderContext.GetCanvas().Clear(); + protected virtual void OnBeforeRender() { } - // Clean up Skia resources - _skiaRenderContext?.Cleanup(); + public virtual void OnAfterUpdateCanvasSize(int width, int height) { } + + 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; @@ -210,44 +281,12 @@ 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) - { - //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() { - string stats = ""; + var stats = ""; var allStats = InstrumentationBag.Stats .Union(_systemRunner.System.Instrumentations.Stats.Select(x => (Name: $"{HostStatRootName}-{SystemTimeStatName}-{x.Name}", x.Stat))) @@ -255,7 +294,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 +308,7 @@ private string GetStats() private string GetDebugMessages() { - string debugMessages = ""; + var debugMessages = ""; var inputDebugInfo = _systemRunner.InputHandler.GetDebugInfo(); var inputStatsOneString = string.Join(" # ", inputDebugInfo); @@ -294,7 +333,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 +353,7 @@ public void OnKeyDown(KeyboardEventArgs e) var key = e.Key; if (key == "F11") - { _toggleDebugStatsState(); - - } else if (key == "F12") { ToggleMonitor(); @@ -327,9 +363,7 @@ public void OnKeyDown(KeyboardEventArgs e) public void ToggleMonitor() { if (Monitor.Visible) - { Monitor.Disable(); - } else { Monitor.Enable(); @@ -343,3 +377,10 @@ public void OnKeyPress(KeyboardEventArgs e) { } } + +public enum EmulatorState +{ + Uninitialized, + Running, + Paused +} 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..2b5224d3f 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 @@ + + + 1.0 + @@ -28,6 +32,7 @@ + @@ -45,7 +50,8 @@ - + + @@ -64,4 +70,8 @@ + + + + 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 a83015c69..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.Skia; +@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; @@ -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"); @@ -115,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; @@ -144,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; @@ -178,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 @@ -203,7 +209,7 @@ if (Parent.WasmHost == null) return; - if (Parent.CurrentEmulatorState == EmulatorState.Uninitialized) + if (Parent.EmulatorState == EmulatorState.Uninitialized) return; // Only expect one file @@ -249,7 +255,7 @@ /// private async Task OnBasicFilePicker(MouseEventArgs mouseEventArgs) { - _wasRunningBeforeFileDialog = Parent.CurrentEmulatorState == EmulatorState.Running; + _wasRunningBeforeFileDialog = Parent.EmulatorState == EmulatorState.Running; await Js.InvokeVoidAsync("clickId", "filePickerBasic"); } @@ -263,7 +269,7 @@ if (Parent.WasmHost == null) return; - if (Parent.CurrentEmulatorState == EmulatorState.Uninitialized) + if (Parent.EmulatorState == EmulatorState.Uninitialized) return; // Only expect one file @@ -314,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; @@ -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() ?? ""; } 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/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/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/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..08b26bb61 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,44 +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. -

-
+ @if (_emulatorConfig.Renderer == Emulator.SystemSetup.RendererType.SkiaSharp) + { + @*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;" + + }
@@ -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 @@