From c1eba9acc7d98ba8267f0b9af73b7b838a6e785c Mon Sep 17 00:00:00 2001 From: Highbyte Date: Fri, 19 Jul 2024 23:08:00 +0200 Subject: [PATCH 01/40] WIP: * A common base class HostApp for hosting emulator apps to reduce code duplication between the Skia and WASM host apps. * Refactor the Skia host app to use the new HostApp base class. * Move SystemLists and and SystemConfigurer classes from project Highbyte.DotNet6502.Systems to project Highbyte.DotNet6502. --- .../ISilkNetHostViewModel.cs | 31 + .../Program.cs | 12 +- .../SilkNetHostApp.cs | 513 +++++++++++++++ .../SilkNetImGuiMenu.cs | 100 ++- .../SilkNetWindow.cs | 622 ------------------ .../Highbyte.DotNet6502/Systems/HostApp.cs | 276 ++++++++ .../Systems}/SystemConfigurer.cs | 0 .../Systems}/SystemList.cs | 0 8 files changed, 874 insertions(+), 680 deletions(-) create mode 100644 src/apps/Highbyte.DotNet6502.App.SilkNetNative/ISilkNetHostViewModel.cs create mode 100644 src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetHostApp.cs delete mode 100644 src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetWindow.cs create mode 100644 src/libraries/Highbyte.DotNet6502/Systems/HostApp.cs rename src/libraries/{Highbyte.DotNet6502.Systems => Highbyte.DotNet6502/Systems}/SystemConfigurer.cs (100%) rename src/libraries/{Highbyte.DotNet6502.Systems => Highbyte.DotNet6502/Systems}/SystemList.cs (100%) diff --git a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/ISilkNetHostViewModel.cs b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/ISilkNetHostViewModel.cs new file mode 100644 index 00000000..b1bb407c --- /dev/null +++ b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/ISilkNetHostViewModel.cs @@ -0,0 +1,31 @@ +using Highbyte.DotNet6502.Systems; + +namespace Highbyte.DotNet6502.App.SilkNetNative +{ + public interface ISilkNetHostViewModel + { + public EmulatorState EmulatorState { get; } + public Task Start(); + public void Pause(); + public void Stop(); + public void Reset(); + + public void SetVolumePercent(float volumePercent); + public float Scale { get; set; } + + public void ToggleMonitor(); + public void ToggleStatsPanel(); + public void ToggleLogsPanel(); + + public HashSet AvailableSystemNames { get; } + public string SelectedSystemName { get; } + public void SelectSystem(string systemName); + + public Task IsSystemConfigValid(); + public Task GetSystemConfig(); + public IHostSystemConfig GetHostSystemConfig(); + public void UpdateSystemConfig(ISystemConfig newConfig); + + public ISystem? CurrentRunningSystem { get; } + } +} diff --git a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Program.cs b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Program.cs index b2e74a00..ea2e7bc0 100644 --- a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Program.cs +++ b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Program.cs @@ -81,12 +81,12 @@ // Silk.NET Window // ---------- -int windowWidth = SilkNetWindow.DEFAULT_WIDTH; -int windowHeight = SilkNetWindow.DEFAULT_HEIGHT; +int windowWidth = SilkNetHostApp.DEFAULT_WIDTH; +int windowHeight = SilkNetHostApp.DEFAULT_HEIGHT; var windowOptions = WindowOptions.Default; // Update frequency, in hertz. -windowOptions.UpdatesPerSecond = SilkNetWindow.DEFAULT_RENDER_HZ; +windowOptions.UpdatesPerSecond = SilkNetHostApp.DEFAULT_RENDER_HZ; // Render frequency, in hertz. windowOptions.FramesPerSecond = 60.0f; // TODO: With Vsync=false the FramesPerSecond settings does not seem to matter. Measured in OnRender method it'll be same as UpdatesPerSecond setting. @@ -101,5 +101,7 @@ //windowOptions.PreferredDepthBufferBits = 24; // Depth buffer bits must be set explicitly on MacOS (tested on M1), otherwise there will be be no depth buffer (for OpenGL 3d). IWindow window = Window.Create(windowOptions); -var silkNetWindow = new SilkNetWindow(emulatorConfig, window, systemList, logStore, logConfig, loggerFactory, mapper); -silkNetWindow.Run(); + +var silkNetHostApp = new SilkNetHostApp(systemList, loggerFactory, emulatorConfig, window, logStore, logConfig, mapper); +silkNetHostApp.Run(); + diff --git a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetHostApp.cs b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetHostApp.cs new file mode 100644 index 00000000..ec24caab --- /dev/null +++ b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetHostApp.cs @@ -0,0 +1,513 @@ +using AutoMapper; +using Highbyte.DotNet6502.Impl.NAudio; +using Highbyte.DotNet6502.Impl.NAudio.NAudioOpenALProvider; +using Highbyte.DotNet6502.Impl.SilkNet; +using Highbyte.DotNet6502.Impl.Skia; +using Highbyte.DotNet6502.Instrumentation.Stats; +using Highbyte.DotNet6502.Logging; +using Highbyte.DotNet6502.Monitor; +using Highbyte.DotNet6502.Systems; +using Microsoft.Extensions.Logging; + +namespace Highbyte.DotNet6502.App.SilkNetNative +{ + public class SilkNetHostApp : HostApp, ISilkNetHostViewModel + { + // -------------------- + // Injected variables + // -------------------- + private readonly ILogger _logger; + private readonly IWindow _window; + private readonly EmulatorConfig _emulatorConfig; + public EmulatorConfig EmulatorConfig => _emulatorConfig; + + private readonly DotNet6502InMemLogStore _logStore; + private readonly DotNet6502InMemLoggerConfiguration _logConfig; + private readonly bool _defaultAudioEnabled; + private float _defaultAudioVolumePercent; + private readonly ILoggerFactory _loggerFactory; + private readonly IMapper _mapper; + + // -------------------- + // Other variables / constants + // -------------------- + private SilkNetRenderContextContainer _silkNetRenderContextContainer = default!; + private SilkNetInputHandlerContext _silkNetInputHandlerContext = default!; + private NAudioAudioHandlerContext _naudioAudioHandlerContext = default!; + + public float CanvasScale + { + get { return _emulatorConfig.CurrentDrawScale; } + set { _emulatorConfig.CurrentDrawScale = value; } + } + public const int DEFAULT_WIDTH = 1000; + public const int DEFAULT_HEIGHT = 700; + public const int DEFAULT_RENDER_HZ = 60; + + // Monitor + private SilkNetImGuiMonitor _monitor = default!; + public SilkNetImGuiMonitor Monitor => _monitor; + + // Instrumentations panel + private SilkNetImGuiStatsPanel _statsPanel = default!; + public SilkNetImGuiStatsPanel StatsPanel => _statsPanel; + + // Logs panel + private SilkNetImGuiLogsPanel _logsPanel = default!; + public SilkNetImGuiLogsPanel LogsPanel => _logsPanel; + + // Menu + private SilkNetImGuiMenu _menu = default!; + private bool _statsWasEnabled = false; + //private bool _logsWasEnabled = false; + + private readonly List _imGuiWindows = new List(); + private bool _atLeastOneImGuiWindowHasFocus => _imGuiWindows.Any(x => x.Visible && x.WindowIsFocused); + + + // GL and other ImGui resources + private GL _gl = default!; + private IInputContext _inputContext = default!; + private ImGuiController _imGuiController = default!; + + /// + /// Constructor + /// + /// + /// + /// + /// + /// + /// + /// + public SilkNetHostApp( + SystemList systemList, + ILoggerFactory loggerFactory, + + EmulatorConfig emulatorConfig, + IWindow window, + DotNet6502InMemLogStore logStore, + DotNet6502InMemLoggerConfiguration logConfig, + IMapper mapper + + ) : base("SilkNet", systemList, emulatorConfig.HostSystemConfigs, loggerFactory) + { + _emulatorConfig = emulatorConfig; + _emulatorConfig.CurrentDrawScale = _emulatorConfig.DefaultDrawScale; + _window = window; + _logStore = logStore; + _logConfig = logConfig; + _defaultAudioEnabled = true; + _defaultAudioVolumePercent = 20.0f; + + _loggerFactory = loggerFactory; + _mapper = mapper; + _logger = loggerFactory.CreateLogger(typeof(SilkNetHostApp).Name); + } + + + public void Run() + { + _window.Load += OnLoad; + _window.Closing += OnClosing; + _window.Update += OnUpdate; + _window.Render += OnRender; + _window.Resize += OnResize; + + _window.Run(); + // Cleanup SilNet window resources + _window?.Dispose(); + } + + protected void OnLoad() + { + SetUninitializedWindow(); + + InitRenderContext(); + InitInputContext(); + InitAudioContext(); + + base.InitContexts(() => _silkNetRenderContextContainer, () => _silkNetInputHandlerContext, () => _naudioAudioHandlerContext); + + InitImGui(); + + // Init main menu UI + _menu = new SilkNetImGuiMenu(this, _emulatorConfig.DefaultEmulator, _defaultAudioEnabled, _defaultAudioVolumePercent, _mapper, _loggerFactory); + + // Create other UI windows + _statsPanel = CreateStatsUI(); + _monitor = CreateMonitorUI(_statsPanel, _emulatorConfig.Monitor); + _logsPanel = CreateLogsUI(_logStore, _logConfig); + + // Add all ImGui windows to a list + _imGuiWindows.Add(_menu); + _imGuiWindows.Add(_statsPanel); + _imGuiWindows.Add(_monitor); + _imGuiWindows.Add(_logsPanel); + } + + protected void OnClosing() + { + base.Close(); + } + + public override void OnAfterSelectSystem() + { + } + + public override bool OnBeforeStart(ISystem systemAboutToBeStarted) + { + // Make sure to adjust window size and render frequency to match the system that is about to be started + if (EmulatorState == EmulatorState.Uninitialized) + { + var screen = systemAboutToBeStarted.Screen; + _window.Size = new Vector2D((int)(screen.VisibleWidth * CanvasScale), (int)(screen.VisibleHeight * CanvasScale)); + _window.UpdatesPerSecond = screen.RefreshFrequencyHz; + InitRenderContext(); + } + return true; + } + + public override void OnAfterStart() + { + _monitor.Init(CurrentSystemRunner!); + CurrentSystemRunner!.AudioHandler.StartPlaying(); + } + + public override void OnAfterClose() + { + // Dispose Monitor/Instrumentations panel + //_monitor.Cleanup(); + //_statsPanel.Cleanup(); + DestroyImGuiController(); + + // Cleanup contexts + _silkNetRenderContextContainer?.Cleanup(); + _silkNetInputHandlerContext?.Cleanup(); + _naudioAudioHandlerContext?.Cleanup(); + + } + + /// + /// Runs on every Update Frame event. + /// + /// Use this method to run logic. + /// + /// + /// + protected void OnUpdate(double deltaTime) + { + base.RunEmulatorOneFrame(); + } + + public override void OnBeforeRunEmulatorOneFrame(out bool shouldRun, out bool shouldReceiveInput) + { + shouldRun = false; + shouldReceiveInput = false; + // Don't update emulator state when monitor is visible + if (_monitor.Visible) + return; + // Don't update emulator state when app is quiting + if (_silkNetInputHandlerContext.Quit || _monitor.Quit) + { + _window.Close(); + return; + } + + shouldRun = true; + + // Only receive input to emulator when no ImGui window has focus + if (!_atLeastOneImGuiWindowHasFocus) + shouldReceiveInput = true; + } + + public override void OnAfterRunEmulatorOneFrame(ExecEvaluatorTriggerResult execEvaluatorTriggerResult) + { + // Show monitor if we encounter breakpoint or other break + if (execEvaluatorTriggerResult.Triggered) + _monitor.Enable(execEvaluatorTriggerResult); + } + + + /// + /// 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 OnRender(double deltaTime) + { + //RenderEmulator(deltaTime); + + // Make sure ImGui is up-to-date + _imGuiController.Update((float)deltaTime); + + // Draw emulator on screen + base.DrawFrame(); + } + public override void OnBeforeDrawFrame(bool emulatorWillBeRendered) + { + // If any ImGui window is visible, make sure to clear Gl buffer before rendering emulator + if (emulatorWillBeRendered) + { + if (_monitor.Visible || _statsPanel.Visible || _logsPanel.Visible) + _gl.Clear((uint)(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit)); + } + } + public override void OnAfterDrawFrame(bool emulatorRendered) + { + if (emulatorRendered) + { + // Flush the SkiaSharp Context + _silkNetRenderContextContainer.SkiaRenderContext.GetGRContext().Flush(); + + // Render monitor if enabled and emulator was rendered + if (_monitor.Visible) + _monitor.PostOnRender(); + + // Render stats if enabled and emulator was rendered + if (_statsPanel.Visible) + _statsPanel.PostOnRender(); + } + + // Render logs if enabled, regardless of if emulator was rendered or not + if (_logsPanel.Visible) + _logsPanel.PostOnRender(); + + // If emulator was not rendered, clear Gl buffer before rendering ImGui windows + if (!emulatorRendered) + { + if (_menu.Visible) + _gl.Clear((uint)(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit)); + // Seems the canvas has to be drawn & flushed for ImGui stuff to be visible on top + var canvas = _silkNetRenderContextContainer.SkiaRenderContext.GetCanvas(); + canvas.Clear(); + _silkNetRenderContextContainer.SkiaRenderContext.GetGRContext().Flush(); + } + + if (_menu.Visible) + _menu.PostOnRender(); + + // Render any ImGui UI rendered above emulator. + _imGuiController?.Render(); + } + + private void OnResize(Vector2D vec2) + { + } + + + private void InitRenderContext() + { + _silkNetRenderContextContainer?.Cleanup(); + + // Init SkipSharp resources (must be done in OnLoad, otherwise no OpenGL context will exist create by SilkNet.) + //_skiaRenderContext = new SkiaRenderContext(s_window.Size.X, s_window.Size.Y, _canvasScale); + GRGlGetProcedureAddressDelegate getProcAddress = (name) => + { + var addrFound = _window.GLContext!.TryGetProcAddress(name, out var addr); + return addrFound ? addr : 0; + }; + + var skiaRenderContext = new SkiaRenderContext( + getProcAddress, + _window.FramebufferSize.X, + _window.FramebufferSize.Y, + _emulatorConfig.CurrentDrawScale * (_window.FramebufferSize.X / _window.Size.X)); + + var silkNetOpenGlRenderContext = new SilkNetOpenGlRenderContext(_window, _emulatorConfig.CurrentDrawScale); + + _silkNetRenderContextContainer = new SilkNetRenderContextContainer(skiaRenderContext, silkNetOpenGlRenderContext); + } + + private void InitInputContext() + { + _silkNetInputHandlerContext = new SilkNetInputHandlerContext(_window, _loggerFactory); + + _inputContext = _window.CreateInput(); + // Listen to key to enable monitor + if (_inputContext.Keyboards == null || _inputContext.Keyboards.Count == 0) + throw new DotNet6502Exception("Keyboard not found"); + var primaryKeyboard = _inputContext.Keyboards[0]; + + // Listen to special key that will show/hide overlays for monitor/stats + primaryKeyboard.KeyDown += OnKeyDown; + } + + private void InitAudioContext() + { + // Output to NAudio built-in output (Windows only) + //var wavePlayer = new WaveOutEvent + //{ + // NumberOfBuffers = 2, + // DesiredLatency = 100, + //} + + // Output to OpenAL (cross platform) instead of via NAudio built-in output (Windows only) + var wavePlayer = new SilkNetOpenALWavePlayer() + { + NumberOfBuffers = 2, + DesiredLatency = 40 + }; + + _naudioAudioHandlerContext = new NAudioAudioHandlerContext( + wavePlayer, + initialVolumePercent: 20); + } + + public void SetVolumePercent(float volumePercent) + { + _defaultAudioVolumePercent = volumePercent; + _naudioAudioHandlerContext.SetMasterVolumePercent(masterVolumePercent: volumePercent); + } + + private void SetUninitializedWindow() + { + _window.Size = new Vector2D(DEFAULT_WIDTH, DEFAULT_HEIGHT); + _window.UpdatesPerSecond = DEFAULT_RENDER_HZ; + } + + private void InitImGui() + { + // Init ImGui resource + _gl = GL.GetApi(_window); + _imGuiController = new ImGuiController( + _gl, + _window, // pass in our window + _inputContext // input context + ); + } + + private SilkNetImGuiMonitor CreateMonitorUI(SilkNetImGuiStatsPanel statsPanel, MonitorConfig monitorConfig) + { + // Init Monitor ImGui resources + var monitor = new SilkNetImGuiMonitor(monitorConfig); + monitor.MonitorStateChange += (s, monitorEnabled) => _silkNetInputHandlerContext.ListenForKeyboardInput(enabled: !monitorEnabled); + monitor.MonitorStateChange += (s, monitorEnabled) => + { + if (monitorEnabled) + statsPanel.Disable(); + }; + return monitor; + } + + private SilkNetImGuiStatsPanel CreateStatsUI() + { + return new SilkNetImGuiStatsPanel(GetStats); + } + + private SilkNetImGuiLogsPanel CreateLogsUI(DotNet6502InMemLogStore logStore, DotNet6502InMemLoggerConfiguration logConfig) + { + return new SilkNetImGuiLogsPanel(logStore, logConfig); + } + + private void DestroyImGuiController() + { + _imGuiController?.Dispose(); + _inputContext?.Dispose(); + _gl?.Dispose(); + } + + + private void OnKeyDown(IKeyboard keyboard, Key key, int x) + { + if (key == Key.F6) + ToggleMainMenu(); + if (key == Key.F10) + ToggleLogsPanel(); + + if (EmulatorState == EmulatorState.Running || EmulatorState == EmulatorState.Paused) + { + if (key == Key.F11) + ToggleStatsPanel(); + if (key == Key.F12) + ToggleMonitor(); + } + } + private void ToggleMainMenu() + { + if (_menu.Visible) + _menu.Disable(); + else + _menu.Enable(); + } + + #region ISilkNetHostViewModel (members that are not part of base class) + + public float Scale + { + get { return _emulatorConfig.CurrentDrawScale; } + set { _emulatorConfig.CurrentDrawScale = value; } + } + + public void ToggleMonitor() + { + // Only be able to toggle monitor if emulator is running or paused + if (EmulatorState == EmulatorState.Uninitialized) + return; + + if (_statsPanel.Visible) + { + _statsWasEnabled = true; + _statsPanel.Disable(); + } + + if (_monitor.Visible) + { + _monitor.Disable(); + if (_statsWasEnabled) + { + CurrentRunningSystem!.InstrumentationEnabled = true; + _statsPanel.Enable(); + } + } + else + { + _monitor.Enable(); + } + } + + public void ToggleStatsPanel() + { + // Only be able to toggle stats if emulator is running or paused + if (EmulatorState == EmulatorState.Uninitialized) + return; + + if (_monitor.Visible) + return; + + if (_statsPanel.Visible) + { + _statsPanel.Disable(); + CurrentRunningSystem!.InstrumentationEnabled = false; + _statsWasEnabled = false; + } + else + { + CurrentRunningSystem!.InstrumentationEnabled = true; + _statsPanel.Enable(); + } + } + + public void ToggleLogsPanel() + { + if (_monitor.Visible) + return; + + if (_logsPanel.Visible) + { + //_logsWasEnabled = true; + _logsPanel.Disable(); + } + else + { + _logsPanel.Enable(); + } + } + + #endregion + + } +} diff --git a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetImGuiMenu.cs b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetImGuiMenu.cs index 15c0eac3..cc5689d8 100644 --- a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetImGuiMenu.cs +++ b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetImGuiMenu.cs @@ -13,8 +13,8 @@ namespace Highbyte.DotNet6502.App.SilkNetNative; public class SilkNetImGuiMenu : ISilkNetImGuiWindow { - private readonly SilkNetWindow _silkNetWindow; - private EmulatorState EmulatorState => _silkNetWindow.EmulatorState; + private readonly ISilkNetHostViewModel _hostViewModel; + private EmulatorState EmulatorState => _hostViewModel.EmulatorState; public bool Visible { get; private set; } = true; public bool WindowIsFocused { get; private set; } @@ -39,7 +39,7 @@ public class SilkNetImGuiMenu : ISilkNetImGuiWindow public int C64SelectedJoystick; public string[] C64AvailableJoysticks = []; - private string SelectedSystemName => _silkNetWindow.SystemList.Systems.ToArray()[_selectedSystemItem]; + private string SelectedSystemName => _hostViewModel.AvailableSystemNames.ToArray()[_selectedSystemItem]; private ISystemConfig _originalSystemConfig = default!; private IHostSystemConfig _originalHostSystemConfig = default!; @@ -49,12 +49,12 @@ public class SilkNetImGuiMenu : ISilkNetImGuiWindow private string _lastFileError = ""; - public SilkNetImGuiMenu(SilkNetWindow silkNetWindow, string defaultSystemName, bool defaultAudioEnabled, float defaultAudioVolumePercent, IMapper mapper, ILoggerFactory loggerFactory) + public SilkNetImGuiMenu(ISilkNetHostViewModel hostViewModel, string defaultSystemName, bool defaultAudioEnabled, float defaultAudioVolumePercent, IMapper mapper, ILoggerFactory loggerFactory) { - _silkNetWindow = silkNetWindow; - _screenScaleString = silkNetWindow.CanvasScale.ToString(); + _hostViewModel = hostViewModel; + _screenScaleString = _hostViewModel.Scale.ToString(); - _selectedSystemItem = _silkNetWindow.SystemList.Systems.ToList().IndexOf(defaultSystemName); + _selectedSystemItem = _hostViewModel.AvailableSystemNames.ToList().IndexOf(defaultSystemName); _audioEnabled = defaultAudioEnabled; _audioVolumePercent = defaultAudioVolumePercent; @@ -87,7 +87,10 @@ public void PostOnRender() ImGui.SameLine(); ImGui.BeginDisabled(disabled: !(EmulatorState == EmulatorState.Uninitialized)); ImGui.PushItemWidth(120); - ImGui.Combo("", ref _selectedSystemItem, _silkNetWindow.SystemList.Systems.ToArray(), _silkNetWindow.SystemList.Systems.Count); + if (ImGui.Combo("", ref _selectedSystemItem, _hostViewModel.AvailableSystemNames.ToArray(), _hostViewModel.AvailableSystemNames.Count)) + { + _hostViewModel.SelectSystem(SelectedSystemName); + }; ImGui.PopItemWidth(); ImGui.EndDisabled(); ImGui.PopStyleColor(); @@ -101,9 +104,7 @@ public void PostOnRender() ImGui.BeginDisabled(disabled: !(EmulatorState != EmulatorState.Running && SelectedSystemConfigIsValid())); if (ImGui.Button("Start")) { - if (_silkNetWindow.EmulatorState == EmulatorState.Uninitialized) - _silkNetWindow.SetCurrentSystem(SelectedSystemName); - _silkNetWindow.Start(); + _hostViewModel.Start(); return; } ImGui.EndDisabled(); @@ -112,7 +113,7 @@ public void PostOnRender() ImGui.SameLine(); if (ImGui.Button("Pause")) { - _silkNetWindow.Pause(); + _hostViewModel.Pause(); } ImGui.EndDisabled(); @@ -120,7 +121,7 @@ public void PostOnRender() ImGui.SameLine(); if (ImGui.Button("Reset")) { - _silkNetWindow.Reset(); + _hostViewModel.Reset(); return; } ImGui.EndDisabled(); @@ -129,7 +130,7 @@ public void PostOnRender() ImGui.SameLine(); if (ImGui.Button("Stop")) { - _silkNetWindow.Stop(); + _hostViewModel.Stop(); return; } ImGui.EndDisabled(); @@ -137,7 +138,7 @@ public void PostOnRender() ImGui.BeginDisabled(disabled: !(EmulatorState == EmulatorState.Running || EmulatorState == EmulatorState.Paused)); if (ImGui.Button("Monitor")) { - _silkNetWindow.ToggleMonitor(); + _hostViewModel.ToggleMonitor(); } ImGui.EndDisabled(); @@ -145,7 +146,7 @@ public void PostOnRender() ImGui.SameLine(); if (ImGui.Button("Stats")) { - _silkNetWindow.ToggleStatsPanel(); + _hostViewModel.ToggleStatsPanel(); } ImGui.EndDisabled(); @@ -153,7 +154,7 @@ public void PostOnRender() ImGui.SameLine(); if (ImGui.Button("Logs")) { - _silkNetWindow.ToggleLogsPanel(); + _hostViewModel.ToggleLogsPanel(); } //ImGui.EndDisabled(); @@ -164,7 +165,7 @@ public void PostOnRender() if (ImGui.InputText("Scale", ref _screenScaleString, 4)) { if (float.TryParse(_screenScaleString, out float scale)) - _silkNetWindow.CanvasScale = scale; + _hostViewModel.Scale = scale; } ImGui.PopItemWidth(); ImGui.PopStyleColor(); @@ -199,7 +200,7 @@ public void PostOnRender() { if (ImGui.SliderFloat("Volume", ref _audioVolumePercent, 0f, 100f, "")) { - _silkNetWindow.SetVolumePercent(_audioVolumePercent); + _hostViewModel.SetVolumePercent(_audioVolumePercent); } } ImGui.PopStyleColor(); @@ -211,10 +212,10 @@ public void PostOnRender() if (ImGui.Button("Load & start binary PRG file")) { bool wasRunning = false; - if (_silkNetWindow.EmulatorState == EmulatorState.Running) + if (_hostViewModel.EmulatorState == EmulatorState.Running) { wasRunning = true; - _silkNetWindow.Pause(); + _hostViewModel.Pause(); } _lastFileError = ""; @@ -225,14 +226,14 @@ public void PostOnRender() { var fileName = dialogResult.Path; BinaryLoader.Load( - _silkNetWindow.SystemRunner.System.Mem, + _hostViewModel.CurrentRunningSystem.Mem, fileName, out ushort loadedAtAddress, out ushort fileLength); - _silkNetWindow.SystemRunner.System.CPU.PC = loadedAtAddress; + _hostViewModel.CurrentRunningSystem.CPU.PC = loadedAtAddress; - _silkNetWindow.Start(); + _hostViewModel.Start(); } catch (Exception ex) { @@ -242,7 +243,7 @@ public void PostOnRender() else { if (wasRunning) - _silkNetWindow.Start(); + _hostViewModel.Start(); } } ImGui.EndDisabled(); @@ -308,7 +309,8 @@ private void DrawC64Config() } else { - C64 c64 = (C64)_silkNetWindow.SystemList.GetSystem(SelectedSystemName).Result; + //C64 c64 = (C64)_hostViewModel.SystemList.GetSystem(SelectedSystemName).Result; + C64 c64 = (C64)_hostViewModel.CurrentRunningSystem; c64.Cia.Joystick.KeyboardJoystickEnabled = C64KeyboardJoystickEnabled; } } @@ -323,7 +325,8 @@ private void DrawC64Config() } else { - C64 c64 = (C64)_silkNetWindow.SystemList.GetSystem(SelectedSystemName).Result; + //C64 c64 = (C64)_hostViewModel.SystemList.GetSystem(SelectedSystemName).Result; + C64 c64 = (C64)_hostViewModel.CurrentRunningSystem; c64.Cia.Joystick.KeyboardJoystick = C64KeyboardJoystick + 1; } } @@ -337,12 +340,12 @@ private void DrawC64Config() if (ImGui.Button("Load Basic PRG file")) { bool wasRunning = false; - if (_silkNetWindow.EmulatorState == EmulatorState.Running) + if (_hostViewModel.EmulatorState == EmulatorState.Running) { wasRunning = true; - _silkNetWindow.Pause(); + _hostViewModel.Pause(); } - _silkNetWindow.Pause(); + _hostViewModel.Pause(); _lastFileError = ""; var dialogResult = Dialog.FileOpen(@"prg;*"); if (dialogResult.IsOk) @@ -351,7 +354,7 @@ private void DrawC64Config() { var fileName = dialogResult.Path; BinaryLoader.Load( - _silkNetWindow.SystemRunner.System.Mem, + _hostViewModel.CurrentRunningSystem.Mem, fileName, out ushort loadedAtAddress, out ushort fileLength); @@ -364,7 +367,7 @@ private void DrawC64Config() else { // Init C64 BASIC memory variables - ((C64)_silkNetWindow.SystemRunner.System).InitBasicMemoryVariables(loadedAtAddress, fileLength); + ((C64)_hostViewModel.CurrentRunningSystem).InitBasicMemoryVariables(loadedAtAddress, fileLength); } } catch (Exception ex) @@ -374,7 +377,7 @@ private void DrawC64Config() } if (wasRunning) - _silkNetWindow.Start(); + _hostViewModel.Start(); } ImGui.EndDisabled(); @@ -382,12 +385,12 @@ private void DrawC64Config() if (ImGui.Button("Save Basic PRG file")) { bool wasRunning = false; - if (_silkNetWindow.EmulatorState == EmulatorState.Running) + if (_hostViewModel.EmulatorState == EmulatorState.Running) { wasRunning = true; - _silkNetWindow.Pause(); + _hostViewModel.Pause(); } - _silkNetWindow.Pause(); + _hostViewModel.Pause(); _lastFileError = ""; var dialogResult = Dialog.FileSave(@"prg;*"); if (dialogResult.IsOk) @@ -396,9 +399,9 @@ private void DrawC64Config() { var fileName = dialogResult.Path; ushort startAddressValue = C64.BASIC_LOAD_ADDRESS; - var endAddressValue = ((C64)_silkNetWindow.SystemRunner.System).GetBasicProgramEndAddress(); + var endAddressValue = ((C64)_hostViewModel.CurrentRunningSystem).GetBasicProgramEndAddress(); BinarySaver.Save( - _silkNetWindow.SystemRunner.System.Mem, + _hostViewModel.CurrentRunningSystem.Mem, fileName, startAddressValue, endAddressValue, @@ -411,7 +414,7 @@ private void DrawC64Config() } if (wasRunning) - _silkNetWindow.Start(); + _hostViewModel.Start(); } ImGui.EndDisabled(); @@ -467,15 +470,15 @@ private void DrawGenericComputerConfig() private bool SelectedSystemConfigIsValid() { - return _silkNetWindow.SystemList.IsValidConfig(SelectedSystemName).Result; + return _hostViewModel.IsSystemConfigValid().Result; } internal ISystemConfig GetSelectedSystemConfig() { - return _silkNetWindow.SystemList.GetCurrentSystemConfig(SelectedSystemName).Result; + return _hostViewModel.GetSystemConfig().Result; } internal IHostSystemConfig GetSelectedSystemHostConfig() { - return _silkNetWindow.EmulatorConfig.HostSystemConfigs[SelectedSystemName]; + return _hostViewModel.GetHostSystemConfig(); } internal void RememberOriginalConfigs() @@ -491,21 +494,12 @@ internal void RestoreOriginalConfigs() internal void UpdateCurrentSystemConfig(ISystemConfig config, IHostSystemConfig hostSystemConfig) { // Update the system config - _silkNetWindow.SystemList.ChangeCurrentSystemConfig(SelectedSystemName, config); + _hostViewModel.UpdateSystemConfig(config); // Update the existing host system config, it is referenced from different objects (thus we cannot replace it with a new one). - var orgHostSystemConfig = _silkNetWindow.EmulatorConfig.HostSystemConfigs[SelectedSystemName]; + var orgHostSystemConfig = _hostViewModel.GetHostSystemConfig(); _mapper.Map(hostSystemConfig, orgHostSystemConfig); } - public void Run() - { - _silkNetWindow.EmulatorState = EmulatorState.Running; - } - - public void Stop() - { - _silkNetWindow.EmulatorState = EmulatorState.Paused; - } public void Enable() { diff --git a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetWindow.cs b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetWindow.cs deleted file mode 100644 index 42dd553e..00000000 --- a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetWindow.cs +++ /dev/null @@ -1,622 +0,0 @@ -using AutoMapper; -using Highbyte.DotNet6502.Impl.NAudio; -using Highbyte.DotNet6502.Impl.NAudio.NAudioOpenALProvider; -using Highbyte.DotNet6502.Impl.SilkNet; -using Highbyte.DotNet6502.Impl.Skia; -using Highbyte.DotNet6502.Impl.Skia.Commodore64.Video.v2; -using Highbyte.DotNet6502.Instrumentation; -using Highbyte.DotNet6502.Instrumentation.Stats; -using Highbyte.DotNet6502.Logging; -using Highbyte.DotNet6502.Monitor; -using Highbyte.DotNet6502.Systems; -using Highbyte.DotNet6502.Systems.Commodore64.Config; -using Highbyte.DotNet6502.Systems.Commodore64; -using Microsoft.Extensions.Logging; -using Silk.NET.SDL; -using Highbyte.DotNet6502.App.SilkNetNative.SystemSetup; -using Highbyte.DotNet6502.Impl.SilkNet.Commodore64.Video; -using AutoMapper.Internal.Mappers; -using NAudio.Wave.SampleProviders; - -namespace Highbyte.DotNet6502.App.SilkNetNative; - -public enum EmulatorState -{ - Uninitialized, - Running, - Paused -} - -public class SilkNetWindow -{ - private readonly ILogger _logger; - private readonly IWindow _window; - - private readonly EmulatorConfig _emulatorConfig; - public EmulatorConfig EmulatorConfig => _emulatorConfig; - - private readonly SystemList _systemList; - public SystemList SystemList => _systemList; - - private readonly DotNet6502InMemLogStore _logStore; - private readonly DotNet6502InMemLoggerConfiguration _logConfig; - private string _currentSystemName = default!; - private readonly bool _defaultAudioEnabled; - private float _defaultAudioVolumePercent; - private readonly ILoggerFactory _loggerFactory; - private readonly IMapper _mapper; - - public float CanvasScale - { - get { return _emulatorConfig.CurrentDrawScale; } - set { _emulatorConfig.CurrentDrawScale = value; } - } - - public const int DEFAULT_WIDTH = 1000; - public const int DEFAULT_HEIGHT = 700; - public const int DEFAULT_RENDER_HZ = 60; - - public IWindow Window { get { return _window; } } - - public EmulatorState EmulatorState { get; set; } = EmulatorState.Uninitialized; - - private const string HostStatRootName = "SilkNet"; - private const string SystemTimeStatName = "Emulator-SystemTime"; - private const string RenderTimeStatName = "RenderTime"; - private const string InputTimeStatName = "InputTime"; - private const string AudioTimeStatName = "AudioTime"; - - private readonly Instrumentations _systemInstrumentations = new(); - private ElapsedMillisecondsTimedStatSystem _systemTime; - private ElapsedMillisecondsTimedStatSystem _renderTime; - private ElapsedMillisecondsTimedStatSystem _inputTime; - //private ElapsedMillisecondsTimedStatSystem _audioTime; - - private readonly PerSecondTimedStat _updateFps = InstrumentationBag.Add($"{HostStatRootName}-OnUpdateFPS"); - private readonly PerSecondTimedStat _renderFps = InstrumentationBag.Add($"{HostStatRootName}-OnRenderFPS"); - - - // Render context container for SkipSharp (surface/canvas) and SilkNetOpenGl - private SilkNetRenderContextContainer _silkNetRenderContextContainer = default!; - // SilkNet input handling - private SilkNetInputHandlerContext _silkNetInputHandlerContext = default!; - // NAudio audio handling - private NAudioAudioHandlerContext _naudioAudioHandlerContext = default!; - - // Emulator - private SystemRunner _systemRunner = default!; - public SystemRunner SystemRunner => _systemRunner!; - - // Monitor - private SilkNetImGuiMonitor _monitor = default!; - public SilkNetImGuiMonitor Monitor => _monitor; - - // Instrumentations panel - private SilkNetImGuiStatsPanel _statsPanel = default!; - public SilkNetImGuiStatsPanel StatsPanel => _statsPanel; - - // Logs panel - private SilkNetImGuiLogsPanel _logsPanel = default!; - public SilkNetImGuiLogsPanel LogsPanel => _logsPanel; - - // Menu - private SilkNetImGuiMenu _menu = default!; - private bool _statsWasEnabled = false; - //private bool _logsWasEnabled = false; - - private readonly List _imGuiWindows = new List(); - private bool _atLeastOneImGuiWindowHasFocus => _imGuiWindows.Any(x => x.Visible && x.WindowIsFocused); - - // GL and other ImGui resources - private GL _gl = default!; - private IInputContext _inputContext = default!; - private ImGuiController _imGuiController = default!; - - public SilkNetWindow( - EmulatorConfig emulatorConfig, - IWindow window, - SystemList systemList, - DotNet6502InMemLogStore logStore, - DotNet6502InMemLoggerConfiguration logConfig, - ILoggerFactory loggerFactory, - IMapper mapper - ) - { - _emulatorConfig = emulatorConfig; - _emulatorConfig.CurrentDrawScale = _emulatorConfig.DefaultDrawScale; - _window = window; - _systemList = systemList; - _logStore = logStore; - _logConfig = logConfig; - _defaultAudioEnabled = true; - _defaultAudioVolumePercent = 20.0f; - - _loggerFactory = loggerFactory; - _mapper = mapper; - _logger = loggerFactory.CreateLogger(typeof(SilkNetWindow).Name); - } - - public void Run() - { - _window.Load += OnLoad; - _window.Closing += OnClosing; - _window.Update += OnUpdate; - _window.Render += OnRender; - _window.Resize += OnResize; - - _window.Run(); - // Cleanup SilNet window resources - _window?.Dispose(); - } - - protected void OnLoad() - { - SetUninitializedWindow(); - - InitRendering(); - InitInput(); - InitAudio(); - - _systemList.InitContext(() => _silkNetRenderContextContainer, () => _silkNetInputHandlerContext, () => _naudioAudioHandlerContext); - - InitImGui(); - - // Init main menu UI - _menu = new SilkNetImGuiMenu(this, _emulatorConfig.DefaultEmulator, _defaultAudioEnabled, _defaultAudioVolumePercent, _mapper, _loggerFactory); - - // Create other UI windows - _statsPanel = CreateStatsUI(); - _monitor = CreateMonitorUI(_statsPanel, _emulatorConfig.Monitor); - _logsPanel = CreateLogsUI(_logStore, _logConfig); - - // Add all ImGui windows to a list - _imGuiWindows.Add(_menu); - _imGuiWindows.Add(_statsPanel); - _imGuiWindows.Add(_monitor); - _imGuiWindows.Add(_logsPanel); - } - - protected void OnClosing() - { - // Dispose Monitor/Instrumentations panel - // _monitor.Cleanup(); - // _statsPanel.Cleanup(); - DestroyImGuiController(); - - // Cleanup systemrunner (which also cleanup renderer, inputhandler, and audiohandler) - _systemRunner?.Cleanup(); - - // Cleanup contexts - _silkNetRenderContextContainer?.Cleanup(); - _silkNetInputHandlerContext?.Cleanup(); - _naudioAudioHandlerContext?.Cleanup(); - } - - /// - /// Runs on every Update Frame event. - /// - /// Use this method to run logic. - /// - /// - /// - protected void OnUpdate(double deltaTime) - { - if (EmulatorState != EmulatorState.Running) - return; - _updateFps.Update(); - RunEmulator(); - } - - private void SetUninitializedWindow() - { - Window.Size = new Vector2D(DEFAULT_WIDTH, DEFAULT_HEIGHT); - Window.UpdatesPerSecond = DEFAULT_RENDER_HZ; - EmulatorState = EmulatorState.Uninitialized; - } - - private void InitRendering() - { - _silkNetRenderContextContainer?.Cleanup(); - - // Init SkipSharp resources (must be done in OnLoad, otherwise no OpenGL context will exist create by SilkNet.) - //_skiaRenderContext = new SkiaRenderContext(s_window.Size.X, s_window.Size.Y, _canvasScale); - GRGlGetProcedureAddressDelegate getProcAddress = (name) => - { - var addrFound = _window.GLContext!.TryGetProcAddress(name, out var addr); - return addrFound ? addr : 0; - }; - - var skiaRenderContext = new SkiaRenderContext( - getProcAddress, - _window.FramebufferSize.X, - _window.FramebufferSize.Y, - _emulatorConfig.CurrentDrawScale * (_window.FramebufferSize.X / _window.Size.X)); - - var silkNetOpenGlRenderContext = new SilkNetOpenGlRenderContext(_window, _emulatorConfig.CurrentDrawScale); - - _silkNetRenderContextContainer = new SilkNetRenderContextContainer(skiaRenderContext, silkNetOpenGlRenderContext); - } - - public void SetCurrentSystem(string systemName) - { - if (EmulatorState != EmulatorState.Uninitialized) - throw new DotNet6502Exception("Internal error. Cannot change system while running"); - - _currentSystemName = systemName; - - _logger.LogInformation($"System selected: {_currentSystemName}"); - - if (_systemList.IsValidConfig(systemName).Result) - { - var system = _systemList.GetSystem(systemName).Result; - var screen = system.Screen; - Window.Size = new Vector2D((int)(screen.VisibleWidth * CanvasScale), (int)(screen.VisibleHeight * CanvasScale)); - Window.UpdatesPerSecond = screen.RefreshFrequencyHz; - - InitRendering(); - } - else - { - } - } - - private void InitInstrumentation(ISystem system) - { - _systemInstrumentations.Clear(); - _systemTime = _systemInstrumentations.Add($"{HostStatRootName}-{SystemTimeStatName}", new ElapsedMillisecondsTimedStatSystem(system)); - _renderTime = _systemInstrumentations.Add($"{HostStatRootName}-{RenderTimeStatName}", new ElapsedMillisecondsTimedStatSystem(system)); - _inputTime = _systemInstrumentations.Add($"{HostStatRootName}-{InputTimeStatName}", new ElapsedMillisecondsTimedStatSystem(system)); - //_audioTime = InstrumentationBag.Add($"{HostStatRootName}-{AudioTimeStatName}", new ElapsedMillisecondsTimedStatSystem(system)); - } - - public void SetVolumePercent(float volumePercent) - { - _defaultAudioVolumePercent = volumePercent; - _naudioAudioHandlerContext.SetMasterVolumePercent(masterVolumePercent: volumePercent); - } - - public void Start() - { - if (EmulatorState == EmulatorState.Running) - return; - - if (!_systemList.IsValidConfig(_currentSystemName).Result) - throw new DotNet6502Exception("Internal error. Cannot start emulator if current system config is invalid."); - - // Force a full GC to free up memory, so it won't risk accumulate memory usage if GC has not run for a while. - var m0 = GC.GetTotalMemory(forceFullCollection: true); - _logger.LogInformation("Allocated memory before starting emulator: " + m0); - - // Only create a new instance of SystemRunner if we previously has not started (so resume after pause works). - if (EmulatorState == EmulatorState.Uninitialized) - _systemRunner = _systemList.BuildSystemRunner(_currentSystemName).Result; - - InitInstrumentation(_systemRunner.System); - - _monitor.Init(_systemRunner!); - - _systemRunner.AudioHandler.StartPlaying(); - - EmulatorState = EmulatorState.Running; - - _logger.LogInformation($"System started: {_currentSystemName}"); - } - - public void Pause() - { - if (EmulatorState == EmulatorState.Paused || EmulatorState == EmulatorState.Uninitialized) - return; - - _systemRunner.AudioHandler.PausePlaying(); - EmulatorState = EmulatorState.Paused; - - _logger.LogInformation($"System paused: {_currentSystemName}"); - } - - public void Reset() - { - if (EmulatorState == EmulatorState.Uninitialized) - return; - - if (_statsPanel.Visible) - ToggleStatsPanel(); - - _systemRunner?.Cleanup(); - _systemRunner = default!; - EmulatorState = EmulatorState.Uninitialized; - Start(); - } - - public void Stop() - { - if (EmulatorState == EmulatorState.Running) - Pause(); - - if (_statsPanel.Visible) - ToggleStatsPanel(); - - _systemRunner.Cleanup(); - _systemRunner = default!; - SetUninitializedWindow(); - InitRendering(); - - _logger.LogInformation($"System stopped: {_currentSystemName}"); - } - - private void RunEmulator() - { - // Don't update emulator state when monitor is visible - if (_monitor.Visible) - return; - - if (_silkNetInputHandlerContext.Quit || _monitor.Quit) - { - _window.Close(); - return; - } - - // Handle input - if (!_atLeastOneImGuiWindowHasFocus) - { - _inputTime.Start(); - _systemRunner.ProcessInputBeforeFrame(); - _inputTime.Stop(); - } - - // Run emulator for one frame worth of emulated CPU cycles - ExecEvaluatorTriggerResult execEvaluatorTriggerResult; - _systemTime.Start(); - execEvaluatorTriggerResult = _systemRunner.RunEmulatorOneFrame(); - _systemTime.Stop(); - - // Show monitor if we encounter breakpoint or other break - if (execEvaluatorTriggerResult.Triggered) - _monitor.Enable(execEvaluatorTriggerResult); - } - - /// - /// 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 OnRender(double deltaTime) - { - _renderFps.Update(); - RenderEmulator(deltaTime); - } - - private void RenderEmulator(double deltaTime) - { - // Make sure ImGui is up-to-date - _imGuiController.Update((float)deltaTime); - - var emulatorRendered = false; - - if (EmulatorState == EmulatorState.Running) - { - if (_monitor.Visible || _statsPanel.Visible || _logsPanel.Visible) - _gl.Clear((uint)(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit)); - - _renderTime.Start(); - // Render emulator system screen - _systemRunner.Draw(); - // Flush the SkiaSharp Context - _silkNetRenderContextContainer.SkiaRenderContext.GetGRContext().Flush(); - _renderTime.Stop(); - - emulatorRendered = true; - - // SilkNet windows are what's known as "double-buffered". In essence, the window manages two buffers. - // One is rendered to while the other is currently displayed by the window. - // This avoids screen tearing, a visual artifact that can happen if the buffer is modified while being displayed. - // After drawing, call this function to swap the buffers. If you don't, it won't display what you've rendered. - - // NOTE: s_window.SwapBuffers() seem to have some problem. Window is darker, and some flickering. - // Use windowOptions.ShouldSwapAutomatically = true instead - //s_window.SwapBuffers(); - - // Render monitor if enabled - if (_monitor.Visible) - _monitor.PostOnRender(); - - // Render stats if enabled - if (_statsPanel.Visible) - _statsPanel.PostOnRender(); - } - - // Render logs if enabled - if (_logsPanel.Visible) - _logsPanel.PostOnRender(); - - if (!emulatorRendered) - { - if (_menu.Visible) - _gl.Clear((uint)(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit)); - // Seems the canvas has to be drawn & flushed for ImGui stuff to be visible on top - var canvas = _silkNetRenderContextContainer.SkiaRenderContext.GetCanvas(); - canvas.Clear(); - _silkNetRenderContextContainer.SkiaRenderContext.GetGRContext().Flush(); - } - - if (_menu.Visible) - _menu.PostOnRender(); - - // Render any ImGui UI rendered above emulator. - _imGuiController?.Render(); - } - - private void OnResize(Vector2D vec2) - { - } - - private void InitInput() - { - _silkNetInputHandlerContext = new SilkNetInputHandlerContext(_window, _loggerFactory); - - _inputContext = _window.CreateInput(); - // Listen to key to enable monitor - if (_inputContext.Keyboards == null || _inputContext.Keyboards.Count == 0) - throw new DotNet6502Exception("Keyboard not found"); - var primaryKeyboard = _inputContext.Keyboards[0]; - - // Listen to special key that will show/hide overlays for monitor/stats - primaryKeyboard.KeyDown += OnKeyDown; - } - - private void InitAudio() - { - // Output to NAudio built-in output (Windows only) - //var wavePlayer = new WaveOutEvent - //{ - // NumberOfBuffers = 2, - // DesiredLatency = 100, - //} - - // Output to OpenAL (cross platform) instead of via NAudio built-in output (Windows only) - var wavePlayer = new SilkNetOpenALWavePlayer() - { - NumberOfBuffers = 2, - DesiredLatency = 40 - }; - - _naudioAudioHandlerContext = new NAudioAudioHandlerContext( - wavePlayer, - initialVolumePercent: 20); - } - - private void InitImGui() - { - // Init ImGui resource - _gl = GL.GetApi(_window); - _imGuiController = new ImGuiController( - _gl, - _window, // pass in our window - _inputContext // input context - ); - } - - private SilkNetImGuiMonitor CreateMonitorUI(SilkNetImGuiStatsPanel statsPanel, MonitorConfig monitorConfig) - { - // Init Monitor ImGui resources - var monitor = new SilkNetImGuiMonitor(monitorConfig); - monitor.MonitorStateChange += (s, monitorEnabled) => _silkNetInputHandlerContext.ListenForKeyboardInput(enabled: !monitorEnabled); - monitor.MonitorStateChange += (s, monitorEnabled) => - { - if (monitorEnabled) - statsPanel.Disable(); - }; - return monitor; - } - - private SilkNetImGuiStatsPanel CreateStatsUI() - { - return new SilkNetImGuiStatsPanel(GetStats); - } - - private List<(string name, IStat stat)> GetStats() - { - return InstrumentationBag.Stats - .Union(_systemInstrumentations.Stats) - .Union(_systemRunner.System.Instrumentations.Stats.Select(x => (Name: $"{HostStatRootName}-{SystemTimeStatName}-{x.Name}", x.Stat))) - .Union(_systemRunner.Renderer.Instrumentations.Stats.Select(x => (Name: $"{HostStatRootName}-{RenderTimeStatName}-{x.Name}", x.Stat))) - .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(); - } - - private SilkNetImGuiLogsPanel CreateLogsUI(DotNet6502InMemLogStore logStore, DotNet6502InMemLoggerConfiguration logConfig) - { - return new SilkNetImGuiLogsPanel(logStore, logConfig); - } - - private void DestroyImGuiController() - { - _imGuiController?.Dispose(); - _inputContext?.Dispose(); - _gl?.Dispose(); - } - - private void OnKeyDown(IKeyboard keyboard, Key key, int x) - { - if (key == Key.F6) - ToggleMainMenu(); - if (key == Key.F10) - ToggleLogsPanel(); - - if (EmulatorState == EmulatorState.Running || EmulatorState == EmulatorState.Paused) - { - if (key == Key.F11) - ToggleStatsPanel(); - if (key == Key.F12) - ToggleMonitor(); - } - } - - public void ToggleMainMenu() - { - if (_menu.Visible) - _menu.Disable(); - else - _menu.Enable(); - } - - public void ToggleStatsPanel() - { - if (_monitor.Visible) - return; - - if (_statsPanel.Visible) - { - _statsPanel.Disable(); - _systemRunner.System.InstrumentationEnabled = false; - _statsWasEnabled = false; - } - else - { - _systemRunner.System.InstrumentationEnabled = true; - _statsPanel.Enable(); - } - } - - public void ToggleLogsPanel() - { - if (_monitor.Visible) - return; - - if (_logsPanel.Visible) - { - //_logsWasEnabled = true; - _logsPanel.Disable(); - } - else - { - _logsPanel.Enable(); - } - } - - public void ToggleMonitor() - { - if (_statsPanel.Visible) - { - _statsWasEnabled = true; - _statsPanel.Disable(); - } - - if (_monitor.Visible) - { - _monitor.Disable(); - if (_statsWasEnabled) - { - _systemRunner.System.InstrumentationEnabled = true; - _statsPanel.Enable(); - } - } - else - { - _monitor.Enable(); - } - } -} diff --git a/src/libraries/Highbyte.DotNet6502/Systems/HostApp.cs b/src/libraries/Highbyte.DotNet6502/Systems/HostApp.cs new file mode 100644 index 00000000..3776db34 --- /dev/null +++ b/src/libraries/Highbyte.DotNet6502/Systems/HostApp.cs @@ -0,0 +1,276 @@ +using Highbyte.DotNet6502.Instrumentation.Stats; +using Highbyte.DotNet6502.Instrumentation; +using Microsoft.Extensions.Logging; + +namespace Highbyte.DotNet6502.Systems; + +public enum EmulatorState { Uninitialized, Running, Paused } + +/// +/// A base class to be used as a common host application model for managing and running different emulators on a specific host platform. +/// The generic type parameters TRenderContext, TInputHandlerContext, and TAudioHandlerContext dictates the types of rendering, input handling, and audio handling available on a specific platform. +/// +/// The constructor must also provide a generic SystemList parameter (with the same generic context types) that provides the different emulators and their Renderers, InputHandlers, and AudioHandlers base on the context types. +/// +/// +/// +/// +public class HostApp + where TRenderContext : IRenderContext + where TInputHandlerContext : IInputHandlerContext + where TAudioHandlerContext : IAudioHandlerContext +{ + // Injected via constructor + private readonly ILoggerFactory _loggerFactory; + private readonly ILogger _logger; + private readonly SystemList _systemList; + private readonly Dictionary _hostSystemConfigs; + + // Other variables + private string _selectedSystemName; + public string SelectedSystemName => _selectedSystemName; + public HashSet AvailableSystemNames => _systemList.Systems; + + private SystemRunner? _systemRunner = null; + public SystemRunner? CurrentSystemRunner => _systemRunner; + public ISystem? CurrentRunningSystem => _systemRunner?.System; + public EmulatorState EmulatorState { get; private set; } = EmulatorState.Uninitialized; + + private readonly string _hostName; + private const string SystemTimeStatName = "Emulator-SystemTime"; + private const string RenderTimeStatName = "RenderTime"; + private const string InputTimeStatName = "InputTime"; + private const string AudioTimeStatName = "AudioTime"; + private readonly Instrumentations _systemInstrumentations = new(); + private ElapsedMillisecondsTimedStatSystem? _systemTime; + private ElapsedMillisecondsTimedStatSystem? _renderTime; + private ElapsedMillisecondsTimedStatSystem? _inputTime; + //private ElapsedMillisecondsTimedStatSystem _audioTime; + + private readonly PerSecondTimedStat _updateFps; + private readonly PerSecondTimedStat _renderFps; + + + public HostApp( + string hostName, + SystemList systemList, + Dictionary hostSystemConfigs, + ILoggerFactory loggerFactory + ) + { + _hostName = hostName; + _updateFps = InstrumentationBag.Add($"{_hostName}-OnUpdateFPS"); + _renderFps = InstrumentationBag.Add($"{_hostName}-OnRenderFPS"); + + _loggerFactory = loggerFactory; + _logger = loggerFactory.CreateLogger("HostApp"); + + if (systemList.Systems.Count == 0) + throw new DotNet6502Exception("No systems added to system list."); + _systemList = systemList; + _selectedSystemName = _systemList.Systems.First(); + + _hostSystemConfigs = hostSystemConfigs; + } + + public void InitContexts( + Func getRenderContext, + Func getInputHandlerContext, + Func getAudioHandlerContext + ) + { + _systemList.InitContext(getRenderContext, getInputHandlerContext, getAudioHandlerContext); + } + + public void SelectSystem(string systemName) + { + if (EmulatorState != EmulatorState.Uninitialized) + throw new DotNet6502Exception("Cannot change system while emulator is running."); + if (!_systemList.Systems.Contains(systemName)) + throw new DotNet6502Exception($"System not found: {systemName}"); + _selectedSystemName = systemName; + } + public virtual void OnAfterSelectSystem() { } + + public virtual bool OnBeforeStart(ISystem systemAboutToBeStarted) + { + return true; + } + + public async Task Start() + { + if (EmulatorState == EmulatorState.Running) + return; + + if (!_systemList.IsValidConfig(_selectedSystemName).Result) + throw new DotNet6502Exception("Internal error. Cannot start emulator if current system config is invalid."); + + // Force a full GC to free up memory, so it won't risk accumulate memory usage if GC has not run for a while. + var m0 = GC.GetTotalMemory(forceFullCollection: true); + _logger.LogInformation("Allocated memory before starting emulator: " + m0); + + var systemAboutToBeStarted = await _systemList.GetSystem(_selectedSystemName); + bool shouldStart = OnBeforeStart(systemAboutToBeStarted); + if (!shouldStart) + return; + + // Only create a new instance of SystemRunner if we previously has not started (so resume after pause works). + if (EmulatorState == EmulatorState.Uninitialized) + _systemRunner = _systemList.BuildSystemRunner(_selectedSystemName).Result; + + InitInstrumentation(_systemRunner!.System); + + _systemRunner.AudioHandler.StartPlaying(); + + OnAfterStart(); + + EmulatorState = EmulatorState.Running; + _logger.LogInformation($"System started: {_selectedSystemName}"); + + } + public virtual void OnAfterStart() { } + + public void Pause() + { + if (EmulatorState == EmulatorState.Paused || EmulatorState == EmulatorState.Uninitialized) + return; + + _systemRunner!.AudioHandler.PausePlaying(); + + OnAfterPause(); + + EmulatorState = EmulatorState.Paused; + _logger.LogInformation($"System paused: {_selectedSystemName}"); + } + + public virtual void OnAfterPause() { } + + public void Stop() + { + if (EmulatorState == EmulatorState.Running) + Pause(); + + // Cleanup systemrunner (which also cleanup renderer, inputhandler, and audiohandler) + _systemRunner!.Cleanup(); + _systemRunner = default!; + + OnAfterStop(); + + EmulatorState = EmulatorState.Uninitialized; + _logger.LogInformation($"System stopped: {_selectedSystemName}"); + } + public virtual void OnAfterStop() { } + + public void Reset() + { + if (EmulatorState == EmulatorState.Uninitialized) + return; + + Stop(); + Start(); + } + + public void Close() + { + if (EmulatorState != EmulatorState.Uninitialized) + Stop(); + + _logger.LogInformation($"Emulator closed"); + + OnAfterClose(); + } + public virtual void OnAfterClose() { } + + + public virtual void OnBeforeRunEmulatorOneFrame(out bool shouldRun, out bool shouldReceiveInput) + { + shouldRun = true; + shouldReceiveInput = true; + } + public void RunEmulatorOneFrame() + { + // Safety check to avoid running emulator if it's not in a running state. + if (EmulatorState != EmulatorState.Running) + return; + + OnBeforeRunEmulatorOneFrame(out bool shouldRun, out bool shouldReceiveInput); + if (!shouldRun) + return; + + _updateFps.Update(); + + if (shouldReceiveInput) + { + _inputTime!.Start(); + _systemRunner!.ProcessInputBeforeFrame(); + _inputTime!.Stop(); + } + + _systemTime!.Start(); + var execEvaluatorTriggerResult = _systemRunner!.RunEmulatorOneFrame(); + OnAfterRunEmulatorOneFrame(execEvaluatorTriggerResult); + _systemTime!.Stop(); + + } + public virtual void OnAfterRunEmulatorOneFrame(ExecEvaluatorTriggerResult execEvaluatorTriggerResult) { } + + public virtual void OnBeforeDrawFrame(bool emulatorWillBeRendered) { } + + public void DrawFrame() + { + if (EmulatorState != EmulatorState.Running) + { + OnBeforeDrawFrame(emulatorWillBeRendered: false); + OnAfterDrawFrame(emulatorRendered: false); + return; + } + + _renderTime!.Start(); + OnBeforeDrawFrame(emulatorWillBeRendered: true); + _systemRunner!.Draw(); + OnAfterDrawFrame(emulatorRendered: true); + _renderTime!.Stop(); + } + public virtual void OnAfterDrawFrame(bool emulatorRendered) { } + + public async Task IsSystemConfigValid() + { + return await _systemList.IsValidConfig(_selectedSystemName); + } + public async Task GetSystemConfig() + { + return await _systemList.GetCurrentSystemConfig(_selectedSystemName); + } + public IHostSystemConfig GetHostSystemConfig() + { + return _hostSystemConfigs[_selectedSystemName]; + } + + public void UpdateSystemConfig(ISystemConfig newConfig) + { + _systemList.ChangeCurrentSystemConfig(_selectedSystemName, newConfig); + } + + private void InitInstrumentation(ISystem system) + { + _systemInstrumentations.Clear(); + _systemTime = _systemInstrumentations.Add($"{_hostName}-{SystemTimeStatName}", new ElapsedMillisecondsTimedStatSystem(system)); + _renderTime = _systemInstrumentations.Add($"{_hostName}-{RenderTimeStatName}", new ElapsedMillisecondsTimedStatSystem(system)); + _inputTime = _systemInstrumentations.Add($"{_hostName}-{InputTimeStatName}", new ElapsedMillisecondsTimedStatSystem(system)); + //_audioTime = InstrumentationBag.Add($"{HostStatRootName}-{AudioTimeStatName}", new ElapsedMillisecondsTimedStatSystem(system)); + } + + public List<(string name, IStat stat)> GetStats() + { + if (_systemRunner == null) + return new List<(string name, IStat)>(); + + return InstrumentationBag.Stats + .Union(_systemInstrumentations.Stats) + .Union(_systemRunner.System.Instrumentations.Stats.Select(x => (Name: $"{_hostName}-{SystemTimeStatName}-{x.Name}", x.Stat))) + .Union(_systemRunner.Renderer.Instrumentations.Stats.Select(x => (Name: $"{_hostName}-{RenderTimeStatName}-{x.Name}", x.Stat))) + .Union(_systemRunner.AudioHandler.Instrumentations.Stats.Select(x => (Name: $"{_hostName}-{AudioTimeStatName}-{x.Name}", x.Stat))) + .Union(_systemRunner.InputHandler.Instrumentations.Stats.Select(x => (Name: $"{_hostName}-{InputTimeStatName}-{x.Name}", x.Stat))) + .ToList(); + } +} diff --git a/src/libraries/Highbyte.DotNet6502.Systems/SystemConfigurer.cs b/src/libraries/Highbyte.DotNet6502/Systems/SystemConfigurer.cs similarity index 100% rename from src/libraries/Highbyte.DotNet6502.Systems/SystemConfigurer.cs rename to src/libraries/Highbyte.DotNet6502/Systems/SystemConfigurer.cs diff --git a/src/libraries/Highbyte.DotNet6502.Systems/SystemList.cs b/src/libraries/Highbyte.DotNet6502/Systems/SystemList.cs similarity index 100% rename from src/libraries/Highbyte.DotNet6502.Systems/SystemList.cs rename to src/libraries/Highbyte.DotNet6502/Systems/SystemList.cs From dc7ff727c48f5a528db2dc6d467892bd14bf2406 Mon Sep 17 00:00:00 2001 From: Highbyte Date: Fri, 19 Jul 2024 23:15:05 +0200 Subject: [PATCH 02/40] Add missing method call. --- src/libraries/Highbyte.DotNet6502/Systems/HostApp.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libraries/Highbyte.DotNet6502/Systems/HostApp.cs b/src/libraries/Highbyte.DotNet6502/Systems/HostApp.cs index 3776db34..2f7205dd 100644 --- a/src/libraries/Highbyte.DotNet6502/Systems/HostApp.cs +++ b/src/libraries/Highbyte.DotNet6502/Systems/HostApp.cs @@ -218,6 +218,8 @@ public virtual void OnBeforeDrawFrame(bool emulatorWillBeRendered) { } public void DrawFrame() { + _renderFps.Update(); + if (EmulatorState != EmulatorState.Running) { OnBeforeDrawFrame(emulatorWillBeRendered: false); From c6d40a78a74c2145edf9deb8539f0a2e666b68b3 Mon Sep 17 00:00:00 2001 From: Highbyte Date: Sat, 20 Jul 2024 23:44:45 +0200 Subject: [PATCH 03/40] WIP (not working): WASM Host app refactor to use new HostApp base class. --- .../ISilkNetHostViewModel.cs | 2 +- .../SilkNetHostApp.cs | 47 +- .../{Skia => Emulator}/BrowserContext.cs | 2 +- .../{Skia => Emulator}/EmulatorConfig.cs | 2 +- .../{ => Emulator}/Skia/PeriodicAsyncTimer.cs | 2 +- .../Emulator/Skia/SkiaWASMHostApp.cs | 348 +++++++++ .../SystemSetup}/C64HostConfig.cs | 2 +- .../SystemSetup}/C64Setup.cs | 4 +- .../SystemSetup}/GenericComputerHostConfig.cs | 2 +- .../SystemSetup}/GenericComputerSetup.cs | 17 +- .../{Skia => Emulator}/WasmMonitor.cs | 14 +- .../Pages/Commodore64/C64ConfigUI.razor | 2 +- .../Pages/Commodore64/C64HelpUI.razor | 3 +- .../Pages/Commodore64/C64Menu.razor | 27 +- .../Pages/GeneralHelpUI.razor | 3 +- .../Pages/Generic/GenericConfigUI.razor | 3 +- .../Pages/Generic/GenericHelpUI.razor | 3 +- .../Pages/Generic/GenericMenu.razor | 11 +- .../Pages/Index.razor | 6 +- .../Pages/Index.razor.cs | 208 +++--- .../Skia/WasmHost.cs | 694 +++++++++--------- .../WASMAudioHandlerContext.cs | 8 +- .../Highbyte.DotNet6502/Systems/HostApp.cs | 27 +- 23 files changed, 887 insertions(+), 550 deletions(-) rename src/apps/Highbyte.DotNet6502.App.WASM/{Skia => Emulator}/BrowserContext.cs (82%) rename src/apps/Highbyte.DotNet6502.App.WASM/{Skia => Emulator}/EmulatorConfig.cs (95%) rename src/apps/Highbyte.DotNet6502.App.WASM/{ => Emulator}/Skia/PeriodicAsyncTimer.cs (95%) create mode 100644 src/apps/Highbyte.DotNet6502.App.WASM/Emulator/Skia/SkiaWASMHostApp.cs rename src/apps/Highbyte.DotNet6502.App.WASM/{Skia => Emulator/SystemSetup}/C64HostConfig.cs (93%) rename src/apps/Highbyte.DotNet6502.App.WASM/{Skia => Emulator/SystemSetup}/C64Setup.cs (98%) 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 (94%) rename src/apps/Highbyte.DotNet6502.App.WASM/{Skia => Emulator}/WasmMonitor.cs (96%) diff --git a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/ISilkNetHostViewModel.cs b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/ISilkNetHostViewModel.cs index b1bb407c..7ffec3de 100644 --- a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/ISilkNetHostViewModel.cs +++ b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/ISilkNetHostViewModel.cs @@ -8,7 +8,7 @@ public interface ISilkNetHostViewModel public Task Start(); public void Pause(); public void Stop(); - public void Reset(); + public Task Reset(); public void SetVolumePercent(float volumePercent); public float Scale { get; set; } diff --git a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetHostApp.cs b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetHostApp.cs index ec24caab..23730c72 100644 --- a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetHostApp.cs +++ b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetHostApp.cs @@ -3,7 +3,6 @@ using Highbyte.DotNet6502.Impl.NAudio.NAudioOpenALProvider; using Highbyte.DotNet6502.Impl.SilkNet; using Highbyte.DotNet6502.Impl.Skia; -using Highbyte.DotNet6502.Instrumentation.Stats; using Highbyte.DotNet6502.Logging; using Highbyte.DotNet6502.Monitor; using Highbyte.DotNet6502.Systems; @@ -31,9 +30,9 @@ public class SilkNetHostApp : HostApp _silkNetRenderContextContainer, () => _silkNetInputHandlerContext, () => _naudioAudioHandlerContext); + base.InitContexts(() => _renderContextContainer, () => _inputHandlerContext, () => _audioHandlerContext); InitImGui(); @@ -168,10 +167,11 @@ public override bool OnBeforeStart(ISystem systemAboutToBeStarted) return true; } - public override void OnAfterStart() + public override void OnAfterStart(EmulatorState emulatorStateBeforeStart) { - _monitor.Init(CurrentSystemRunner!); - CurrentSystemRunner!.AudioHandler.StartPlaying(); + // Init monitor for current system started if this system was not started before + if (emulatorStateBeforeStart == EmulatorState.Uninitialized) + _monitor.Init(CurrentSystemRunner!); } public override void OnAfterClose() @@ -182,9 +182,9 @@ public override void OnAfterClose() DestroyImGuiController(); // Cleanup contexts - _silkNetRenderContextContainer?.Cleanup(); - _silkNetInputHandlerContext?.Cleanup(); - _naudioAudioHandlerContext?.Cleanup(); + _renderContextContainer?.Cleanup(); + _inputHandlerContext?.Cleanup(); + _audioHandlerContext?.Cleanup(); } @@ -208,7 +208,7 @@ public override void OnBeforeRunEmulatorOneFrame(out bool shouldRun, out bool sh if (_monitor.Visible) return; // Don't update emulator state when app is quiting - if (_silkNetInputHandlerContext.Quit || _monitor.Quit) + if (_inputHandlerContext.Quit || _monitor.Quit) { _window.Close(); return; @@ -230,9 +230,7 @@ public override void OnAfterRunEmulatorOneFrame(ExecEvaluatorTriggerResult execE /// - /// Runs on every Render Frame event. - /// - /// Use this method to render the world. + /// Runs on every Render Frame event. Draws one emulator frame on screen. /// /// This method is called at a RenderFrequency set in the GameWindowSettings object. /// @@ -247,6 +245,7 @@ protected void OnRender(double deltaTime) // Draw emulator on screen base.DrawFrame(); } + public override void OnBeforeDrawFrame(bool emulatorWillBeRendered) { // If any ImGui window is visible, make sure to clear Gl buffer before rendering emulator @@ -261,7 +260,7 @@ public override void OnAfterDrawFrame(bool emulatorRendered) if (emulatorRendered) { // Flush the SkiaSharp Context - _silkNetRenderContextContainer.SkiaRenderContext.GetGRContext().Flush(); + _renderContextContainer.SkiaRenderContext.GetGRContext().Flush(); // Render monitor if enabled and emulator was rendered if (_monitor.Visible) @@ -282,9 +281,9 @@ public override void OnAfterDrawFrame(bool emulatorRendered) if (_menu.Visible) _gl.Clear((uint)(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit)); // Seems the canvas has to be drawn & flushed for ImGui stuff to be visible on top - var canvas = _silkNetRenderContextContainer.SkiaRenderContext.GetCanvas(); + var canvas = _renderContextContainer.SkiaRenderContext.GetCanvas(); canvas.Clear(); - _silkNetRenderContextContainer.SkiaRenderContext.GetGRContext().Flush(); + _renderContextContainer.SkiaRenderContext.GetGRContext().Flush(); } if (_menu.Visible) @@ -301,7 +300,7 @@ private void OnResize(Vector2D vec2) private void InitRenderContext() { - _silkNetRenderContextContainer?.Cleanup(); + _renderContextContainer?.Cleanup(); // Init SkipSharp resources (must be done in OnLoad, otherwise no OpenGL context will exist create by SilkNet.) //_skiaRenderContext = new SkiaRenderContext(s_window.Size.X, s_window.Size.Y, _canvasScale); @@ -319,12 +318,12 @@ private void InitRenderContext() var silkNetOpenGlRenderContext = new SilkNetOpenGlRenderContext(_window, _emulatorConfig.CurrentDrawScale); - _silkNetRenderContextContainer = new SilkNetRenderContextContainer(skiaRenderContext, silkNetOpenGlRenderContext); + _renderContextContainer = new SilkNetRenderContextContainer(skiaRenderContext, silkNetOpenGlRenderContext); } private void InitInputContext() { - _silkNetInputHandlerContext = new SilkNetInputHandlerContext(_window, _loggerFactory); + _inputHandlerContext = new SilkNetInputHandlerContext(_window, _loggerFactory); _inputContext = _window.CreateInput(); // Listen to key to enable monitor @@ -352,7 +351,7 @@ private void InitAudioContext() DesiredLatency = 40 }; - _naudioAudioHandlerContext = new NAudioAudioHandlerContext( + _audioHandlerContext = new NAudioAudioHandlerContext( wavePlayer, initialVolumePercent: 20); } @@ -360,7 +359,7 @@ private void InitAudioContext() public void SetVolumePercent(float volumePercent) { _defaultAudioVolumePercent = volumePercent; - _naudioAudioHandlerContext.SetMasterVolumePercent(masterVolumePercent: volumePercent); + _audioHandlerContext.SetMasterVolumePercent(masterVolumePercent: volumePercent); } private void SetUninitializedWindow() @@ -384,7 +383,7 @@ private SilkNetImGuiMonitor CreateMonitorUI(SilkNetImGuiStatsPanel statsPanel, M { // Init Monitor ImGui resources var monitor = new SilkNetImGuiMonitor(monitorConfig); - monitor.MonitorStateChange += (s, monitorEnabled) => _silkNetInputHandlerContext.ListenForKeyboardInput(enabled: !monitorEnabled); + monitor.MonitorStateChange += (s, monitorEnabled) => _inputHandlerContext.ListenForKeyboardInput(enabled: !monitorEnabled); monitor.MonitorStateChange += (s, monitorEnabled) => { if (monitorEnabled) diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Skia/BrowserContext.cs b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/BrowserContext.cs similarity index 82% rename from src/apps/Highbyte.DotNet6502.App.WASM/Skia/BrowserContext.cs rename to src/apps/Highbyte.DotNet6502.App.WASM/Emulator/BrowserContext.cs index fa81e6ce..e6e2e89f 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Skia/BrowserContext.cs +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/BrowserContext.cs @@ -1,6 +1,6 @@ using Blazored.LocalStorage; -namespace Highbyte.DotNet6502.App.WASM.Skia; +namespace Highbyte.DotNet6502.App.WASM.Emulator; public class BrowserContext { diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Skia/EmulatorConfig.cs b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/EmulatorConfig.cs similarity index 95% rename from src/apps/Highbyte.DotNet6502.App.WASM/Skia/EmulatorConfig.cs rename to src/apps/Highbyte.DotNet6502.App.WASM/Emulator/EmulatorConfig.cs index a337b303..739092e0 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 { 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 1367bc8a..e56d3bcf 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/SkiaWASMHostApp.cs b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/Skia/SkiaWASMHostApp.cs new file mode 100644 index 00000000..647514cd --- /dev/null +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/Skia/SkiaWASMHostApp.cs @@ -0,0 +1,348 @@ +using System.Data; +using Highbyte.DotNet6502.Impl.AspNet; +using Highbyte.DotNet6502.Impl.AspNet.JSInterop.BlazorWebAudioSync; +using Highbyte.DotNet6502.Impl.Skia; +using Highbyte.DotNet6502.Instrumentation.Stats; +using Highbyte.DotNet6502.Systems; +using Toolbelt.Blazor.Gamepad; + +namespace Highbyte.DotNet6502.App.WASM.Emulator.Skia +{ + public class SkiaWASMHostApp : HostApp + { + // -------------------- + // Injected variables + // -------------------- + private readonly ILogger _logger; + private readonly EmulatorConfig _emulatorConfig; + private readonly Func _getCanvas; + private readonly Func _getGrContext; + private readonly Func _getAudioContext; + + public EmulatorConfig EmulatorConfig => _emulatorConfig; + + private readonly bool _defaultAudioEnabled; + private readonly float _defaultAudioVolumePercent; + private readonly ILoggerFactory _loggerFactory; + + // -------------------- + // Other variables / constants + // -------------------- + private SkiaRenderContext _renderContext = default!; + private AspNetInputHandlerContext _inputHandlerContext = default!; + private WASMAudioHandlerContext _audioHandlerContext = default!; + + private readonly IJSRuntime _jsRuntime; + private PeriodicAsyncTimer? _updateTimer; + + private WasmMonitor _monitor = default!; + public WasmMonitor Monitor => _monitor; + + // Delegates for changing the state of Stats, Debug, and Monitor UI panels + private readonly Action _updateStats; + private readonly Action _updateDebug; + private readonly Func _setMonitorState; + private readonly Func _toggleDebugStatsState; + + private const int STATS_EVERY_X_FRAME = 60 * 1; + private int _statsFrameCount = 0; + + private const int DEBUGMESSAGE_EVERY_X_FRAME = 1; + private int _debugFrameCount = 0; + + public bool Initialized { get; private set; } = false; + + + public SkiaWASMHostApp( + SystemList systemList, + ILoggerFactory loggerFactory, + EmulatorConfig emulatorConfig, + + + Func getCanvas, + Func getGrContext, + Func getAudioContext, + Action updateStats, + Action updateDebug, + Func setMonitorState, + Func toggleDebugStatsState + ) : base("SilkNet", systemList, emulatorConfig.HostSystemConfigs, loggerFactory) + { + _loggerFactory = loggerFactory; + _logger = loggerFactory.CreateLogger(typeof(SkiaWASMHostApp).Name); + _emulatorConfig = emulatorConfig; + _emulatorConfig.CurrentDrawScale = _emulatorConfig.DefaultDrawScale; + + _getCanvas = getCanvas; + _getGrContext = getGrContext; + _getAudioContext = getAudioContext; + + _updateStats = updateStats; + _updateDebug = updateDebug; + _setMonitorState = setMonitorState; + _toggleDebugStatsState = toggleDebugStatsState; + + _defaultAudioEnabled = false; + _defaultAudioVolumePercent = 20.0f; + } + + /// + /// Call Init once from Blazor SKGLView "OnAfterRenderAsync" event. + /// + public void Init(GamepadList gamepadList, IJSRuntime jsRuntime) + { + if (Initialized) + throw new InvalidOperationException("Init can only be called once."); + + _renderContext = new SkiaRenderContext(_getCanvas, _getGrContext); + _inputHandlerContext = new AspNetInputHandlerContext(_loggerFactory, gamepadList); + _audioHandlerContext = new WASMAudioHandlerContext(_getAudioContext, jsRuntime, _defaultAudioVolumePercent); + + base.InitContexts(() => _renderContext, () => _inputHandlerContext, () => _audioHandlerContext); + + Initialized = true; + } + + public override void OnAfterSelectSystem() + { + } + + public override bool OnBeforeStart(ISystem systemAboutToBeStarted) + { + return true; + } + + public override void OnAfterStart(EmulatorState emulatorStateBeforeStart) + { + // Setup and start timer for current system started + if (_updateTimer != null) + { + _updateTimer.Stop(); + _updateTimer.Dispose(); + } + _updateTimer = CreateUpdateTimerForSystem(CurrentSystemRunner!.System); + _updateTimer!.Start(); + + // Init monitor for current system started if this system was not started before + if (emulatorStateBeforeStart == EmulatorState.Uninitialized) + _monitor = new WasmMonitor(_jsRuntime, CurrentSystemRunner, _emulatorConfig, _setMonitorState); + } + + public override void OnAfterStop() + { + // TODO: Disable debug window? + //_debugVisible = false; + + _monitor.Disable(); + } + + public override void OnAfterClose() + { + // Cleanup contexts + _renderContext?.Cleanup(); + _inputHandlerContext?.Cleanup(); + _audioHandlerContext?.Cleanup(); + } + + + private PeriodicAsyncTimer CreateUpdateTimerForSystem(ISystem system) + { + // Number of milliseconds between each invokation of the main loop. 60 fps -> (1/60) * 1000 -> approx 16.6667ms + double updateIntervalMS = (1 / system.Screen.RefreshFrequencyHz) * 1000; + var updateTimer = new PeriodicAsyncTimer(); + updateTimer.IntervalMilliseconds = updateIntervalMS; + updateTimer.Elapsed += UpdateTimerElapsed; + return updateTimer; + } + + private void UpdateTimerElapsed(object? sender, EventArgs e) => RunEmulatorOneFrame(); + + public override void OnBeforeRunEmulatorOneFrame(out bool shouldRun, out bool shouldReceiveInput) + { + shouldRun = false; + shouldReceiveInput = false; + // Don't update emulator state when monitor is visible + if (_monitor.Visible) + return; + + shouldRun = true; + shouldReceiveInput = true; + } + + public override void OnAfterRunEmulatorOneFrame(ExecEvaluatorTriggerResult execEvaluatorTriggerResult) + { + // Push debug info to debug UI + _debugFrameCount++; + if (_debugFrameCount >= DEBUGMESSAGE_EVERY_X_FRAME) + { + _debugFrameCount = 0; + var debugString = GetDebugMessagesHtmlString(); + _updateDebug(debugString); + } + + // Push stats to stats UI + if (CurrentRunningSystem!.InstrumentationEnabled) + { + _statsFrameCount++; + if (_statsFrameCount >= STATS_EVERY_X_FRAME) + { + _statsFrameCount = 0; + _updateStats(GetStatsHtmlString()); + } + } + + // Show monitor if we encounter breakpoint or other break + if (execEvaluatorTriggerResult.Triggered) + _monitor.Enable(execEvaluatorTriggerResult); + } + + /// + /// Called from ASP.NET Blazor SKGLView "OnPaintSurface" event to render one frame. + /// + /// + public void Render() + { + // Draw emulator on screen + base.DrawFrame(); + } + + public override void OnBeforeDrawFrame(bool emulatorWillBeRendered) + { + if (emulatorWillBeRendered) + { + // TODO: Shouldn't scale be able to set once we start the emulator (OnBeforeStart method?) instead of every frame? + _getCanvas().Scale((float)_emulatorConfig.CurrentDrawScale); + } + } + + public override void OnAfterDrawFrame(bool emulatorRendered) + { + if (emulatorRendered) + { + } + } + + public void SetVolumePercent(float volumePercent) + { + _audioHandlerContext.SetMasterVolumePercent(masterVolumePercent: volumePercent); + } + + private string GetStatsHtmlString() + { + string stats = ""; + + var allStats = GetStats(); + foreach ((string name, IStat stat) in allStats.OrderBy(i => i.name)) + { + if (stat.ShouldShow()) + { + if (stats != "") + stats += "
"; + stats += $"{BuildHtmlString(name, "header")}: {BuildHtmlString(stat.GetDescription(), "value")} "; + } + } + return stats; + } + + private string GetDebugMessagesHtmlString() + { + string debugMessages = ""; + + var inputDebugInfo = CurrentSystemRunner!.InputHandler.GetDebugInfo(); + var inputStatsOneString = string.Join(" # ", inputDebugInfo); + debugMessages += $"{BuildHtmlString("INPUT", "header")}: {BuildHtmlString(inputStatsOneString, "value")} "; + //foreach (var message in inputDebugInfo) + //{ + // if (debugMessages != "") + // debugMessages += "
"; + // debugMessages += $"{BuildHtmlString("DEBUG INPUT", "header")}: {BuildHtmlString(message, "value")} "; + //} + + var audioDebugInfo = CurrentSystemRunner!.AudioHandler.GetDebugInfo(); + foreach (var message in audioDebugInfo) + { + if (debugMessages != "") + debugMessages += "
"; + debugMessages += $"{BuildHtmlString("AUDIO", "header")}: {BuildHtmlString(message, "value")} "; + } + + return debugMessages; + } + + private string BuildHtmlString(string message, string cssClass, bool startNewLine = false) + { + string html = ""; + if (startNewLine) + html += "
"; + html += $@"{HttpUtility.HtmlEncode(message)}"; + return html; + } + + /// + /// Receive Key Down event in emulator canvas. + /// Also check for special non-emulator functions such as monitor and stats/debug + /// + /// + public void OnKeyDown(KeyboardEventArgs e) + { + // Send key press to emulator + _inputHandlerContext.KeyDown(e); + + // Check for other emulator functions + var key = e.Key; + if (key == "F11") + { + _toggleDebugStatsState(); + + } + else if (key == "F12") + { + ToggleMonitor(); + } + } + + /// + /// Receive Key Up event in emulator canvas. + /// Also check for special non-emulator functions such as monitor and stats/debug + /// + /// + public void OnKeyUp(KeyboardEventArgs e) + { + // Send key press to emulator + _inputHandlerContext.KeyUp(e); + + // Check for other emulator functions + var key = e.Key; + if (key == "F11") + { + _toggleDebugStatsState(); + + } + else if (key == "F12") + { + ToggleMonitor(); + } + } + + /// + /// Receive Focus on emulator canvas. + /// + /// + public void OnFocus(FocusEventArgs e) + { + _inputHandlerContext.OnFocus(e); + } + + public void ToggleMonitor() + { + if (Monitor.Visible) + { + Monitor.Disable(); + } + else + { + Monitor.Enable(); + } + } + } +} 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 93% rename from src/apps/Highbyte.DotNet6502.App.WASM/Skia/C64HostConfig.cs rename to src/apps/Highbyte.DotNet6502.App.WASM/Emulator/SystemSetup/C64HostConfig.cs index 4b8a17b8..ca43411b 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Skia/C64HostConfig.cs +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/SystemSetup/C64HostConfig.cs @@ -1,7 +1,7 @@ using Highbyte.DotNet6502.Impl.AspNet.Commodore64.Input; using Highbyte.DotNet6502.Systems; -namespace Highbyte.DotNet6502.App.WASM.Skia +namespace Highbyte.DotNet6502.App.WASM.Emulator.SystemSetup { public enum C64HostRenderer { 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 98% rename from src/apps/Highbyte.DotNet6502.App.WASM/Skia/C64Setup.cs rename to src/apps/Highbyte.DotNet6502.App.WASM/Emulator/SystemSetup/C64Setup.cs index 99afd6fd..cf145bf7 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Skia/C64Setup.cs +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/SystemSetup/C64Setup.cs @@ -1,3 +1,4 @@ +using Highbyte.DotNet6502.App.WASM.Emulator; using Highbyte.DotNet6502.Impl.AspNet; using Highbyte.DotNet6502.Impl.AspNet.Commodore64.Audio; using Highbyte.DotNet6502.Impl.AspNet.Commodore64.Input; @@ -8,7 +9,7 @@ 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 { @@ -77,6 +78,7 @@ public SystemRunner BuildSystemRunner( WASMAudioHandlerContext audioHandlerContext ) { + var c64 = (C64)system; var c64HostConfig = (C64HostConfig)hostSystemConfig; 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 8d768403..946355b1 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 94% rename from src/apps/Highbyte.DotNet6502.App.WASM/Skia/GenericComputerSetup.cs rename to src/apps/Highbyte.DotNet6502.App.WASM/Emulator/SystemSetup/GenericComputerSetup.cs index 68c69ba9..bec42f6f 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Skia/GenericComputerSetup.cs +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/SystemSetup/GenericComputerSetup.cs @@ -1,3 +1,4 @@ +using Highbyte.DotNet6502.App.WASM.Emulator; using Highbyte.DotNet6502.Impl.AspNet; using Highbyte.DotNet6502.Impl.AspNet.Generic.Input; using Highbyte.DotNet6502.Impl.Skia; @@ -8,7 +9,7 @@ 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 { @@ -37,7 +38,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; @@ -125,28 +126,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; @@ -160,7 +161,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: @@ -178,7 +178,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()); @@ -204,7 +203,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/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 abb48ed7..973ceb24 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/Pages/Commodore64/C64ConfigUI.razor b/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Commodore64/C64ConfigUI.razor index af6ad502..b1741656 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Commodore64/C64ConfigUI.razor +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Commodore64/C64ConfigUI.razor @@ -1,4 +1,4 @@ -@using Highbyte.DotNet6502.App.WASM.Skia +@using Highbyte.DotNet6502.App.WASM.Emulator.SystemSetup @using Highbyte.DotNet6502.Impl.AspNet.Commodore64.Input; @using Highbyte.DotNet6502.Systems; @using Highbyte.DotNet6502.Systems.Commodore64.Config 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 2209435e..091f760b 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 440f3d1d..10ceeb47 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Commodore64/C64Menu.razor +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Commodore64/C64Menu.razor @@ -1,8 +1,9 @@ -@using Highbyte.DotNet6502.App.WASM.Skia; +@using Highbyte.DotNet6502.Systems @using Highbyte.DotNet6502.Systems.Commodore64.Config; @using Highbyte.DotNet6502.Systems.Commodore64; @using Highbyte.DotNet6502.Systems.Commodore64.Video; @using static Highbyte.DotNet6502.App.WASM.Pages.Index; +@using Highbyte.DotNet6502.App.WASM.Emulator.SystemSetup; @if(Parent.Initialized) { @@ -122,7 +123,7 @@ _c64Config.KeyboardJoystickEnabled = value; if (Parent.CurrentEmulatorState != EmulatorState.Uninitialized) { - C64 c64 = (C64)Parent.WasmHost.SystemList.GetSystem(SYSTEM_NAME).Result; + C64 c64 = (C64)Parent.WasmHost.CurrentRunningSystem!; c64.Cia.Joystick.KeyboardJoystickEnabled = value; } } @@ -151,7 +152,7 @@ _c64Config.KeyboardJoystick = value; if (Parent.CurrentEmulatorState != EmulatorState.Uninitialized) { - C64 c64 = (C64)Parent.WasmHost.SystemList.GetSystem(SYSTEM_NAME).Result; + C64 c64 = (C64)Parent.WasmHost.CurrentRunningSystem!; c64.Cia.Joystick.KeyboardJoystick = value; } } @@ -228,7 +229,7 @@ // Load file into memory, assume starting at address specified in two first bytes of .prg file BinaryLoader.Load( - Parent.WasmHost.SystemRunner.System.Mem, + Parent.WasmHost.CurrentRunningSystem!.Mem, fileBuffer, out ushort loadedAtAddress, out ushort fileLength); @@ -236,7 +237,7 @@ _logger.LogInformation($"File loaded at {loadedAtAddress.ToHex()}, length {fileLength.ToHex()}"); _logger.LogInformation($"Starting loaded program by changing Program Counter to {loadedAtAddress.ToHex()}"); - Parent.WasmHost.SystemRunner.System.CPU.PC = loadedAtAddress; + Parent.WasmHost.CurrentRunningSystem!.CPU.PC = loadedAtAddress; await Parent.OnStart(new()); @@ -287,7 +288,7 @@ // Load file into memory, assume starting at address specified in two first bytes of .prg file BinaryLoader.Load( - Parent.WasmHost.SystemRunner.System.Mem, + Parent.WasmHost.CurrentRunningSystem!.Mem, fileBuffer, out ushort loadedAtAddress, out ushort fileLength); @@ -300,7 +301,7 @@ else { // Init C64 BASIC memory variables - ((C64)Parent.WasmHost.SystemRunner.System).InitBasicMemoryVariables(loadedAtAddress, fileLength); + ((C64)Parent.WasmHost.CurrentRunningSystem!).InitBasicMemoryVariables(loadedAtAddress, fileLength); } _logger.LogInformation($"Basic program loaded at {loadedAtAddress.ToHex()}, length {fileLength.ToHex()}"); @@ -338,8 +339,8 @@ { _latestFileError = ""; var startAddress = C64.BASIC_LOAD_ADDRESS; - var endAddress = ((C64)Parent.WasmHost.SystemRunner.System).GetBasicProgramEndAddress(); - var saveData = BinarySaver.BuildSaveData(Parent.WasmHost.SystemRunner.System.Mem, startAddress, endAddress, addFileHeaderWithLoadAddress: true); + var endAddress = ((C64)Parent.WasmHost.CurrentRunningSystem!).GetBasicProgramEndAddress(); + var saveData = BinarySaver.BuildSaveData(Parent.WasmHost.CurrentRunningSystem!.Mem, startAddress, endAddress, addFileHeaderWithLoadAddress: true); var fileStream = new MemoryStream(saveData); using var streamRef = new DotNetStreamReference(stream: fileStream); // Invoke JS helper script to trigger save dialog to users browser downloads folder @@ -373,7 +374,7 @@ var prgBytes = await HttpClient!.GetByteArrayAsync(url); // Load file into memory, assume starting at address specified in two first bytes of .prg file BinaryLoader.Load( - Parent.WasmHost.SystemRunner.System.Mem, + Parent.WasmHost.CurrentRunningSystem!.Mem, prgBytes, out ushort loadedAtAddress, out ushort fileLength); @@ -381,7 +382,7 @@ _logger.LogInformation($"File loaded at {loadedAtAddress.ToHex()}, length {fileLength.ToHex()}"); _logger.LogInformation($"Starting loaded program by changing Program Counter to {loadedAtAddress.ToHex()}"); - Parent.WasmHost.SystemRunner.System.CPU.PC = loadedAtAddress; + Parent.WasmHost.CurrentRunningSystem!.CPU.PC = loadedAtAddress; } catch (Exception ex) { @@ -412,12 +413,12 @@ // Load file into memory, assume starting at address specified in two first bytes of .prg file BinaryLoader.Load( - Parent.WasmHost.SystemRunner.System.Mem, + Parent.WasmHost.CurrentRunningSystem!.Mem, prgBytes, out ushort loadedAtAddress, out ushort fileLength); - var c64 = (C64)Parent.WasmHost.SystemRunner.System; + var c64 = (C64)Parent.WasmHost.CurrentRunningSystem!; if (loadedAtAddress != C64.BASIC_LOAD_ADDRESS) { // Probably not a Basic program that was loaded. Don't init BASIC memory variables. diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Pages/GeneralHelpUI.razor b/src/apps/Highbyte.DotNet6502.App.WASM/Pages/GeneralHelpUI.razor index ba8137a0..4ebb6d75 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 60872a31..e1643aab 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 4a2e2f5d..c29ef0dc 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 12146f9c..ce8a0354 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.Systems +@using Highbyte.DotNet6502.Systems.Generic; @using static Highbyte.DotNet6502.App.WASM.Pages.Index; @if(Parent.Initialized) @@ -92,7 +93,7 @@ // Load file into memory, assume starting at address specified in two first bytes of .prg file BinaryLoader.Load( - Parent.WasmHost.SystemRunner.System.Mem, + Parent.WasmHost.CurrentRunningSystem!.Mem, fileBuffer, out ushort loadedAtAddress, out ushort fileLength); @@ -100,7 +101,7 @@ System.Diagnostics.Debug.WriteLine($"File loaded at {loadedAtAddress.ToHex()}, length {fileLength.ToHex()}"); System.Diagnostics.Debug.WriteLine($"Starting loaded program by changing Program Counter to {loadedAtAddress.ToHex()}"); - Parent.WasmHost.SystemRunner.System.CPU.PC = loadedAtAddress; + Parent.WasmHost.CurrentRunningSystem!.CPU.PC = loadedAtAddress; await Parent.OnStart(new()); } @@ -121,7 +122,7 @@ var prgBytes = await HttpClient!.GetByteArrayAsync(url); // Load file into memory, assume starting at address specified in two first bytes of .prg file BinaryLoader.Load( - Parent.WasmHost.SystemRunner.System.Mem, + Parent.WasmHost.CurrentRunningSystem!.Mem, prgBytes, out ushort loadedAtAddress, out ushort fileLength); @@ -129,7 +130,7 @@ System.Diagnostics.Debug.WriteLine($"File loaded at {loadedAtAddress.ToHex()}, length {fileLength.ToHex()}"); System.Diagnostics.Debug.WriteLine($"Starting loaded program by changing Program Counter to {loadedAtAddress.ToHex()}"); - Parent.WasmHost.SystemRunner.System.CPU.PC = loadedAtAddress; + Parent.WasmHost.CurrentRunningSystem!.CPU.PC = loadedAtAddress; await Parent.OnStart(new()); } diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Index.razor b/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Index.razor index 11300356..69d98480 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Index.razor +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Index.razor @@ -21,8 +21,8 @@

System: @* - @foreach (var systemName in _systemList.Systems) + @@ -17,6 +16,7 @@ } +

@@ -59,25 +59,24 @@ @_latestFileError
- -
+
-
+
} @@ -90,7 +89,7 @@ #pragma warning disable CS8669 } @code { -@inject IJSRuntime Js + @inject IJSRuntime Js @inject HttpClient HttpClient @inject ILoggerFactory LoggerFactory 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 ce8a0354..590489e2 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Generic/GenericMenu.razor +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Generic/GenericMenu.razor @@ -2,9 +2,9 @@ @using Highbyte.DotNet6502.Systems.Generic; @using static Highbyte.DotNet6502.App.WASM.Pages.Index; -@if(Parent.Initialized) +@if(Parent.Initialized && Parent.WasmHost.SelectedSystemName == SYSTEM_NAME) { -
+
diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Index.razor b/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Index.razor index 69d98480..5eb0f91d 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Index.razor +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Index.razor @@ -20,13 +20,21 @@
System: - @* +@* + + *@ + + @foreach (var systemName in _wasmHost.AvailableSystemNames) + { + + } + +
Status: @(CurrentEmulatorState) @@ -125,7 +133,7 @@
-
+
@( (MarkupString)_statsString @@ -187,6 +195,10 @@ height: 320px; } + .statsStyle { + display: @GetDisplayStyle("Stats"); + } + .debugStyle { display: @GetDisplayStyle("Debug"); } 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 ea906233..f465a77c 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Index.razor.cs +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Index.razor.cs @@ -15,10 +15,11 @@ using Microsoft.AspNetCore.Components.Forms; using Microsoft.AspNetCore.WebUtilities; using Toolbelt.Blazor.Gamepad; +using Highbyte.DotNet6502.Systems.Generic; namespace Highbyte.DotNet6502.App.WASM.Pages; -public partial class Index +public partial class Index : IWASMHostUIViewModel { //private string Version => typeof(Program).Assembly.GetName().Version!.ToString(); private string Version => Assembly.GetEntryAssembly()!.GetCustomAttribute()!.InformationalVersion; @@ -104,10 +105,11 @@ private double Scale private string _windowHeightStyle = "0px"; private bool _debugVisible = false; + private bool _statsVisible = false; private bool _monitorVisible = false; [Inject] - public IJSRuntime? Js { get; set; } + public IJSRuntime Js { get; set; } = default!; [Inject] public HttpClient HttpClient { get; set; } = default!; @@ -171,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); @@ -185,12 +187,11 @@ protected override async Task OnInitializedAsync() () => _canvas, () => _grContext, () => _audioContext, - UpdateStats, - UpdateDebug, - SetMonitorState, - ToggleDebugStatsState); + GamepadList, + Js, + this); - _wasmHost.Init(GamepadList, Js!); + _wasmHost.InitInputHandlerContext(); // Set the default system await SelectSystem(_emulatorConfig.DefaultEmulator); @@ -207,7 +208,6 @@ protected override async Task OnInitializedAsync() ); _mapper = mapperConfiguration.CreateMapper(); - Initialized = true; } @@ -233,31 +233,23 @@ private async Task SetDefaultsFromQueryParams(Uri uri) protected override async Task OnAfterRenderAsync(bool firstRender) { - if (firstRender) + if (firstRender && !_wasmHost.IsAudioHandlerContextInitialized) { _audioContext = await AudioContextSync.CreateAsync(Js!); + _wasmHost.InitAudioHandlerContext(); } } - //protected override async void OnAfterRender(bool firstRender) - //{ - // if (firstRender) - // { - // //await FocusEmulator(); - // } - //} - - private async Task OnSelectedEmulatorChanged(ChangeEventArgs e) - { - var systemName = e.Value?.ToString() ?? ""; - if (systemName != "") - await SelectSystem(systemName); - } - private async Task SelectSystem(string systemName) { + //Initialized = false; + //this.StateHasChanged(); + _wasmHost.SelectSystem(systemName); + _currentHostSystemConfig = _wasmHost.GetHostSystemConfig(); + _currentSystemConfig = await _wasmHost.GetSystemConfig(); + (bool isOk, List validationErrors) = await _wasmHost.IsValidConfigWithDetails(); if (!isOk) @@ -265,14 +257,13 @@ private async Task SelectSystem(string systemName) else _selectedSystemConfigValidationMessage = ""; - _currentSystemConfig = await _wasmHost.GetSystemConfig(); - _currentHostSystemConfig = _wasmHost.GetHostSystemConfig(); + await UpdateCanvasSize(); - UpdateCanvasSize(); + //Initialized = true; this.StateHasChanged(); } - private async void UpdateCanvasSize() + private async Task UpdateCanvasSize() { bool isOk = await _wasmHost.IsSystemConfigValid(); if (!isOk) @@ -292,26 +283,28 @@ private async void UpdateCanvasSize() this.StateHasChanged(); } - protected async void OnPaintSurface(SKPaintGLSurfaceEventArgs e) + protected void OnPaintSurface(SKPaintGLSurfaceEventArgs e) { if (CurrentEmulatorState != EmulatorState.Running) return; - if (!(e.Surface.Context is GRContext grContext && grContext != null)) - return; - - if (_wasmHost == null) - return; + // Assume e.Surface.Canvas is not null and type GRContext + var grContext = e.Surface.Context as GRContext; - _canvas = e.Surface.Canvas; - _grContext = grContext; - - //_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); - //} + if (_canvas != e.Surface.Canvas || _grContext != grContext) + { + if (_grContext != grContext) + { + _grContext?.Dispose(); + _grContext = grContext!; + } + if (_canvas != e.Surface.Canvas) + { + _canvas?.Dispose(); + _canvas = e.Surface.Canvas; + } + _wasmHost.InitRenderContext(); + } _wasmHost.Render(); } @@ -376,7 +369,7 @@ public async Task ShowConfigUI() where T : IComponent _selectedSystemConfigValidationMessage = string.Join(",", validationErrors); } - UpdateCanvasSize(); + await UpdateCanvasSize(); this.StateHasChanged(); } @@ -409,28 +402,46 @@ public async Task ShowGeneralSettingsUI() where T : IComponent } } - private void UpdateStats(string stats) + + public async Task SetDebugState(bool visible) { - _statsString = stats; + _debugVisible = visible; + await FocusEmulator(); this.StateHasChanged(); } - - private void UpdateDebug(string debug) + public async Task ToggleDebugState() + { + _debugVisible = !_debugVisible; + await FocusEmulator(); + this.StateHasChanged(); + } + public void UpdateDebug(string debug) { _debugString = debug; this.StateHasChanged(); } - private async Task ToggleDebugStatsState() + public async Task SetStatsState(bool visible) { - _debugVisible = !_debugVisible; + _statsVisible = visible; + await FocusEmulator(); + this.StateHasChanged(); + } + public async Task ToggleStatsState() + { + _statsVisible = !_statsVisible; // Assume to only run when emulator is running - _wasmHost.CurrentRunningSystem!.InstrumentationEnabled = _debugVisible; + _wasmHost.CurrentRunningSystem!.InstrumentationEnabled = _statsVisible; await FocusEmulator(); this.StateHasChanged(); } + public void UpdateStats(string stats) + { + _statsString = stats; + this.StateHasChanged(); + } - private async Task SetMonitorState(bool visible) + public async Task SetMonitorState(bool visible) { _monitorVisible = visible; if (visible) @@ -512,6 +523,10 @@ private string GetDisplayStyle(string displayData) { return CurrentEmulatorState == EmulatorState.Uninitialized ? VISIBLE_BLOCK : HIDDEN; } + case "Stats": + { + return _statsVisible ? VISIBLE : HIDDEN; + } case "Debug": { return _debugVisible ? VISIBLE : HIDDEN; @@ -593,7 +608,8 @@ private void OnMonitorToggle(MouseEventArgs mouseEventArgs) private async Task OnStatsToggle(MouseEventArgs mouseEventArgs) { - await ToggleDebugStatsState(); + await ToggleStatsState(); + await ToggleDebugState(); } /// diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Skia/WasmHost.cs b/src/apps/Highbyte.DotNet6502.App.WASM/Skia/WasmHost.cs deleted file mode 100644 index 312fa648..00000000 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Skia/WasmHost.cs +++ /dev/null @@ -1,348 +0,0 @@ -//using Highbyte.DotNet6502.App.WASM.Emulator; -//using Highbyte.DotNet6502.App.WASM.Emulator.Skia; -//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; - -//public class WasmHost : IDisposable -//{ -// public bool Initialized { get; private set; } - -// private readonly IJSRuntime _jsRuntime; - -// private PeriodicAsyncTimer? _updateTimer; - -// private SystemRunner _systemRunner = default!; -// public SystemRunner SystemRunner => _systemRunner; - -// private SKCanvas _skCanvas = default!; -// private GRContext _grContext = default!; - -// 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; -// private readonly Func _toggleDebugStatsState; -// private readonly ILoggerFactory _loggerFactory; -// private readonly float _initialMasterVolume; -// private readonly ILogger _logger; - -// public WasmMonitor Monitor { get; private set; } = default!; - - -// private readonly Instrumentations _systemInstrumentations = new(); -// private const string HostStatRootName = "WASM"; -// private const string SystemTimeStatName = "Emulator-SystemTime"; -// private const string RenderTimeStatName = "RenderTime"; -// private const string InputTimeStatName = "InputTime"; -// private const string AudioTimeStatName = "AudioTime"; -// private ElapsedMillisecondsTimedStatSystem _systemTime; -// private ElapsedMillisecondsTimedStatSystem _renderTime; -// private ElapsedMillisecondsTimedStatSystem _inputTime; -// private readonly PerSecondTimedStat _updateFps; -// private readonly PerSecondTimedStat _renderFps; - -// private const int STATS_EVERY_X_FRAME = 60 * 1; -// private int _statsFrameCount = 0; - -// private const int DEBUGMESSAGE_EVERY_X_FRAME = 1; -// private int _debugFrameCount = 0; - -// public WasmHost( -// IJSRuntime jsRuntime, -// string systemName, -// SystemList systemList, -// Action updateStats, -// Action updateDebug, -// Func setMonitorState, -// EmulatorConfig emulatorConfig, -// Func toggleDebugStatsState, -// ILoggerFactory loggerFactory, -// float scale = 1.0f, -// float initialMasterVolume = 50.0f) -// { -// _jsRuntime = jsRuntime; -// _systemName = systemName; -// _systemList = systemList; -// _updateStats = updateStats; -// _updateDebug = updateDebug; -// _setMonitorState = setMonitorState; -// _emulatorConfig = emulatorConfig; -// _toggleDebugStatsState = toggleDebugStatsState; -// _initialMasterVolume = initialMasterVolume; -// _loggerFactory = loggerFactory; -// _logger = loggerFactory.CreateLogger(typeof(WasmHost).Name); - -// // Init stats -// InstrumentationBag.Clear(); -// _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) -// { -// _skCanvas = canvas; -// _grContext = grContext; - -// _skiaRenderContext = new SkiaRenderContext(GetCanvas, GetGRContext); -// InputHandlerContext = new AspNetInputHandlerContext(_loggerFactory, gamepadList); -// AudioHandlerContext = new WASMAudioHandlerContext(audioContext, jsRuntime, _initialMasterVolume); - -// _systemList.InitContext(() => _skiaRenderContext, () => InputHandlerContext, () => AudioHandlerContext); - -// _systemRunner = await _systemList.BuildSystemRunner(_systemName); - -// Monitor = new WasmMonitor(_jsRuntime, _systemRunner, _emulatorConfig, _setMonitorState); - -// // Init instrumentation -// _systemInstrumentations.Clear(); -// _systemTime = _systemInstrumentations.Add($"{HostStatRootName}-{SystemTimeStatName}", new ElapsedMillisecondsTimedStatSystem(_systemRunner.System)); -// _inputTime = _systemInstrumentations.Add($"{HostStatRootName}-{InputTimeStatName}", new ElapsedMillisecondsTimedStatSystem(_systemRunner.System)); -// //_audioTime = _systemInstrumentations.Add($"{HostStatRootName}-{AudioTimeStatName}", new ElapsedMillisecondsTimedStatSystem(_systemRunner.System)); -// _renderTime = _systemInstrumentations.Add($"{HostStatRootName}-{RenderTimeStatName}", new ElapsedMillisecondsTimedStatSystem(_systemRunner.System)); - -// Initialized = true; -// } - -// public void Stop() -// { -// _updateTimer?.Stop(); - -// _systemRunner.AudioHandler.PausePlaying(); - -// _logger.LogInformation($"System stopped: {_systemName}"); -// } - -// public void Start() -// { -// if (_systemRunner != null && _systemRunner.AudioHandler != null) -// _systemRunner.AudioHandler.StartPlaying(); - -// if (_updateTimer != null) -// { -// } -// else -// { -// 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; -// } -// _updateTimer!.Start(); - -// _logger.LogInformation($"System started: {_systemName}"); -// } - -// public void Cleanup() -// { -// if (_updateTimer != null) -// { -// _updateTimer.Stop(); -// _updateTimer = null; -// } - -// // Cleanup systemrunner (which also cleanup renderer, inputhandler, and audiohandler) -// _systemRunner.Cleanup(); - -// // Clean up contexts -// _skiaRenderContext?.Cleanup(); -// InputHandlerContext?.Cleanup(); -// AudioHandlerContext?.Cleanup(); -// } - -// private void UpdateTimerElapsed(object? sender, EventArgs e) => EmulatorRunOneFrame(); - -// [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "")] -// private void EmulatorRunOneFrame() -// { - -// if (!Initialized) -// return; - -// if (Monitor.Visible) -// return; - -// _updateFps.Update(); - -// _debugFrameCount++; -// if (_debugFrameCount >= DEBUGMESSAGE_EVERY_X_FRAME) -// { -// _debugFrameCount = 0; -// var debugString = GetDebugMessages(); -// _updateDebug(debugString); -// } - -// //_emulatorHelper.GenerateRandomNumber(); -// _inputTime.Start(); -// _systemRunner.ProcessInputBeforeFrame(); -// _inputTime.Stop(); - -// ExecEvaluatorTriggerResult execEvaluatorTriggerResult; -// _systemTime.Start(); -// execEvaluatorTriggerResult = _systemRunner.RunEmulatorOneFrame(); -// _systemTime.Stop(); - -// if (_systemRunner.System.InstrumentationEnabled) -// { -// _statsFrameCount++; -// if (_statsFrameCount >= STATS_EVERY_X_FRAME) -// { -// _statsFrameCount = 0; -// var statsString = GetStats(); -// _updateStats(statsString); -// } -// } - -// // 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); - -// _renderTime.Start(); -// _systemRunner.Draw(); -// //using (new SKAutoCanvasRestore(skCanvas)) -// //{ -// // _systemRunner.Draw(skCanvas); -// //} -// _renderTime.Stop(); -// } - -// private SKCanvas GetCanvas() -// { -// return _skCanvas; -// } - -// private GRContext GetGRContext() -// { -// return _grContext; -// } - -// private string GetStats() -// { -// string stats = ""; - -// var allStats = InstrumentationBag.Stats -// .Union(_systemInstrumentations.Stats) -// .Union(_systemRunner.System.Instrumentations.Stats.Select(x => (Name: $"{HostStatRootName}-{SystemTimeStatName}-{x.Name}", x.Stat))) -// .Union(_systemRunner.Renderer.Instrumentations.Stats.Select(x => (Name: $"{HostStatRootName}-{RenderTimeStatName}-{x.Name}", x.Stat))) -// .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)) -// { -// if (stat.ShouldShow()) -// { -// if (stats != "") -// stats += "
"; -// stats += $"{BuildHtmlString(name, "header")}: {BuildHtmlString(stat.GetDescription(), "value")} "; -// } -// } -// return stats; -// } - -// private string GetDebugMessages() -// { -// string debugMessages = ""; - -// var inputDebugInfo = _systemRunner.InputHandler.GetDebugInfo(); -// var inputStatsOneString = string.Join(" # ", inputDebugInfo); -// debugMessages += $"{BuildHtmlString("INPUT", "header")}: {BuildHtmlString(inputStatsOneString, "value")} "; -// //foreach (var message in inputDebugInfo) -// //{ -// // if (debugMessages != "") -// // debugMessages += "
"; -// // debugMessages += $"{BuildHtmlString("DEBUG INPUT", "header")}: {BuildHtmlString(message, "value")} "; -// //} - -// var audioDebugInfo = _systemRunner.AudioHandler.GetDebugInfo(); -// foreach (var message in audioDebugInfo) -// { -// if (debugMessages != "") -// debugMessages += "
"; -// debugMessages += $"{BuildHtmlString("AUDIO", "header")}: {BuildHtmlString(message, "value")} "; -// } - -// return debugMessages; -// } - -// private string BuildHtmlString(string message, string cssClass, bool startNewLine = false) -// { -// string html = ""; -// if (startNewLine) -// html += "
"; -// html += $@"{HttpUtility.HtmlEncode(message)}"; -// return html; -// } - -// public void Dispose() -// { -// } - -// /// -// /// Enable / Disable emulator functions such as monitor and stats/debug -// /// -// /// -// public void OnKeyDown(KeyboardEventArgs e) -// { -// var key = e.Key; - -// if (key == "F11") -// { -// _toggleDebugStatsState(); - -// } -// else if (key == "F12") -// { -// ToggleMonitor(); -// } -// } - -// public void ToggleMonitor() -// { -// if (Monitor.Visible) -// { -// Monitor.Disable(); -// } -// else -// { -// Monitor.Enable(); -// } -// } - -// /// -// /// -// /// -// public void OnKeyPress(KeyboardEventArgs e) -// { -// } -//} diff --git a/src/libraries/Highbyte.DotNet6502.Impl.AspNet/AspNetInputHandlerContext.cs b/src/libraries/Highbyte.DotNet6502.Impl.AspNet/AspNetInputHandlerContext.cs index 7560d277..47fc2700 100644 --- a/src/libraries/Highbyte.DotNet6502.Impl.AspNet/AspNetInputHandlerContext.cs +++ b/src/libraries/Highbyte.DotNet6502.Impl.AspNet/AspNetInputHandlerContext.cs @@ -19,6 +19,7 @@ public class AspNetInputHandlerContext : IInputHandlerContext private readonly System.Timers.Timer _gamepadConnectCheckTimer = new System.Timers.Timer(1000) { Enabled = true }; private Gamepad? _currentGamepad; public HashSet GamepadButtonsDown = new(); + public bool IsInitialized { get; private set; } public AspNetInputHandlerContext(ILoggerFactory loggerFactory, GamepadList gamepadList) { @@ -30,6 +31,7 @@ public void Init() { _gamepadUpdateTimer.Elapsed += GamepadUpdateTimer_Elapsed; _gamepadConnectCheckTimer.Elapsed += GamepadConectCheckTimer_Elapsed; + IsInitialized = true; } private async void GamepadConectCheckTimer_Elapsed(object? sender, EventArgs args) diff --git a/src/libraries/Highbyte.DotNet6502.Impl.AspNet/WASMAudioHandlerContext.cs b/src/libraries/Highbyte.DotNet6502.Impl.AspNet/WASMAudioHandlerContext.cs index d2c6d66f..8f4cfcc4 100644 --- a/src/libraries/Highbyte.DotNet6502.Impl.AspNet/WASMAudioHandlerContext.cs +++ b/src/libraries/Highbyte.DotNet6502.Impl.AspNet/WASMAudioHandlerContext.cs @@ -17,6 +17,8 @@ public class WASMAudioHandlerContext : IAudioHandlerContext private GainNodeSync _masterVolumeGainNode = default!; internal GainNodeSync MasterVolumeGainNode => _masterVolumeGainNode; + public bool IsInitialized { get; private set; } + public WASMAudioHandlerContext( Func getAudioContext, IJSRuntime jsRuntime, @@ -35,6 +37,8 @@ public void Init() // Set initial master volume % SetMasterVolumePercent(_initialVolumePercent); + + IsInitialized = true; } public void Cleanup() diff --git a/src/libraries/Highbyte.DotNet6502.Impl.NAudio/NAudioAudioHandlerContext.cs b/src/libraries/Highbyte.DotNet6502.Impl.NAudio/NAudioAudioHandlerContext.cs index 1c5c117d..46b6f3d2 100644 --- a/src/libraries/Highbyte.DotNet6502.Impl.NAudio/NAudioAudioHandlerContext.cs +++ b/src/libraries/Highbyte.DotNet6502.Impl.NAudio/NAudioAudioHandlerContext.cs @@ -11,6 +11,8 @@ public class NAudioAudioHandlerContext : IAudioHandlerContext private float _initialVolumePercent; + public bool IsInitialized { get; private set; } + public NAudioAudioHandlerContext( IWavePlayer wavePlayer, float initialVolumePercent @@ -22,6 +24,7 @@ float initialVolumePercent public void Init() { + IsInitialized = true; } public void ConfigureWavePlayer(ISampleProvider sampleProvider) diff --git a/src/libraries/Highbyte.DotNet6502.Impl.SadConsole/SadConsoleInputHandlerContext.cs b/src/libraries/Highbyte.DotNet6502.Impl.SadConsole/SadConsoleInputHandlerContext.cs index 04e96e24..67e8bcff 100644 --- a/src/libraries/Highbyte.DotNet6502.Impl.SadConsole/SadConsoleInputHandlerContext.cs +++ b/src/libraries/Highbyte.DotNet6502.Impl.SadConsole/SadConsoleInputHandlerContext.cs @@ -10,6 +10,8 @@ public class SadConsoleInputHandlerContext : IInputHandlerContext private Keyboard _sadConsoleKeyboard => GameHost.Instance.Keyboard; private readonly ILogger _logger; + public bool IsInitialized { get; private set; } + public List KeysDown { get @@ -33,6 +35,7 @@ public SadConsoleInputHandlerContext(ILoggerFactory loggerFactory) public void Init() { //_sadConsoleKeyboard = keyboard; + IsInitialized = true; } public void Cleanup() diff --git a/src/libraries/Highbyte.DotNet6502.Impl.SadConsole/SadConsoleRenderContext.cs b/src/libraries/Highbyte.DotNet6502.Impl.SadConsole/SadConsoleRenderContext.cs index 4f3f496c..c28cff8c 100644 --- a/src/libraries/Highbyte.DotNet6502.Impl.SadConsole/SadConsoleRenderContext.cs +++ b/src/libraries/Highbyte.DotNet6502.Impl.SadConsole/SadConsoleRenderContext.cs @@ -6,6 +6,8 @@ public class SadConsoleRenderContext : IRenderContext private readonly Func _getSadConsoleScreen; public SadConsoleScreenObject Screen => _getSadConsoleScreen(); + public bool IsInitialized { get; private set; } = false; + public SadConsoleRenderContext(Func getSadConsoleScreen) { _getSadConsoleScreen = getSadConsoleScreen; @@ -13,6 +15,7 @@ public SadConsoleRenderContext(Func getSadConsoleScreen) public void Init() { + IsInitialized = true; } public void Cleanup() diff --git a/src/libraries/Highbyte.DotNet6502.Impl.SilkNet/SilkNetInputHandlerContext.cs b/src/libraries/Highbyte.DotNet6502.Impl.SilkNet/SilkNetInputHandlerContext.cs index 9bc99d4a..6b9dd613 100644 --- a/src/libraries/Highbyte.DotNet6502.Impl.SilkNet/SilkNetInputHandlerContext.cs +++ b/src/libraries/Highbyte.DotNet6502.Impl.SilkNet/SilkNetInputHandlerContext.cs @@ -28,6 +28,8 @@ public class SilkNetInputHandlerContext : IInputHandlerContext public bool IsKeyPressed(Key key) => _primaryKeyboard.IsKeyPressed(key); + public bool IsInitialized { get; private set; } + public SilkNetInputHandlerContext(IWindow silkNetWindow, ILoggerFactory loggerFactory) { _silkNetWindow = silkNetWindow; @@ -62,6 +64,8 @@ public void Init() { _logger.LogInformation("No gamepads found."); } + + IsInitialized = true; } private void ConnectionChanged(IInputDevice device, bool isConnected) diff --git a/src/libraries/Highbyte.DotNet6502.Impl.SilkNet/SilkNetOpenGlRenderContext.cs b/src/libraries/Highbyte.DotNet6502.Impl.SilkNet/SilkNetOpenGlRenderContext.cs index fbe520e4..303ff634 100644 --- a/src/libraries/Highbyte.DotNet6502.Impl.SilkNet/SilkNetOpenGlRenderContext.cs +++ b/src/libraries/Highbyte.DotNet6502.Impl.SilkNet/SilkNetOpenGlRenderContext.cs @@ -14,6 +14,8 @@ public class SilkNetOpenGlRenderContext : IRenderContext private readonly float _drawScale; public float DrawScale => _drawScale; + public bool IsInitialized { get; private set; } = false; + public SilkNetOpenGlRenderContext(IWindow window, float drawScale) { @@ -24,6 +26,7 @@ public SilkNetOpenGlRenderContext(IWindow window, float drawScale) public void Init() { + IsInitialized = true; } public void Cleanup() diff --git a/src/libraries/Highbyte.DotNet6502.Impl.Skia/SkiaRenderContext.cs b/src/libraries/Highbyte.DotNet6502.Impl.Skia/SkiaRenderContext.cs index 836dafde..b5a89890 100644 --- a/src/libraries/Highbyte.DotNet6502.Impl.Skia/SkiaRenderContext.cs +++ b/src/libraries/Highbyte.DotNet6502.Impl.Skia/SkiaRenderContext.cs @@ -17,6 +17,7 @@ public class SkiaRenderContext : IRenderContext private readonly Func? _getGrContextExternal; private GRGlInterface? _glInterface; + public bool IsInitialized { get; private set; } = false; private SKCanvas GetCanvasInternal() { @@ -76,6 +77,7 @@ public SkiaRenderContext(GRGlGetProcedureAddressDelegate getProcAddress, int siz public void Init() { + IsInitialized = true; } public void Cleanup() diff --git a/src/libraries/Highbyte.DotNet6502/Systems/HostApp.cs b/src/libraries/Highbyte.DotNet6502/Systems/HostApp.cs index 915cad03..39069ea3 100644 --- a/src/libraries/Highbyte.DotNet6502/Systems/HostApp.cs +++ b/src/libraries/Highbyte.DotNet6502/Systems/HostApp.cs @@ -73,15 +73,36 @@ ILoggerFactory loggerFactory _hostSystemConfigs = hostSystemConfigs; } - public void InitContexts( - Func getRenderContext, - Func getInputHandlerContext, - Func getAudioHandlerContext + public void SetAndInitContexts( + Func? getRenderContext = null, + Func? getInputHandlerContext = null, + Func? getAudioHandlerContext = null ) { - _systemList.InitContext(getRenderContext, getInputHandlerContext, getAudioHandlerContext); + _systemList.SetContext(getRenderContext, getInputHandlerContext, getAudioHandlerContext); + _systemList.InitRenderContext(); + _systemList.InitInputHandlerContext(); + _systemList.InitAudioHandlerContext(); } + public void SetContexts( + Func? getRenderContext = null, + Func? getInputHandlerContext = null, + Func? getAudioHandlerContext = null + ) + { + _systemList.SetContext(getRenderContext, getInputHandlerContext, getAudioHandlerContext); + } + + public void InitRenderContext() => _systemList.InitRenderContext(); + public void InitInputHandlerContext() => _systemList.InitInputHandlerContext(); + public void InitAudioHandlerContext() => _systemList.InitAudioHandlerContext(); + + public bool IsRenderContextInitialized => _systemList.IsRenderContextInitialized; + public bool IsInputHandlerContextInitialized => _systemList.IsInputHandlerContextInitialized; + public bool IsAudioHandlerContextInitialized => _systemList.IsAudioHandlerContextInitialized; + + public void SelectSystem(string systemName) { if (EmulatorState != EmulatorState.Uninitialized) diff --git a/src/libraries/Highbyte.DotNet6502/Systems/IAudioHandlerContext.cs b/src/libraries/Highbyte.DotNet6502/Systems/IAudioHandlerContext.cs index 7a6e1cbc..4bd0988b 100644 --- a/src/libraries/Highbyte.DotNet6502/Systems/IAudioHandlerContext.cs +++ b/src/libraries/Highbyte.DotNet6502/Systems/IAudioHandlerContext.cs @@ -4,14 +4,20 @@ public interface IAudioHandlerContext { void Init(); void Cleanup(); + + public bool IsInitialized { get; } + } public class NullAudioHandlerContext : IAudioHandlerContext { + public bool IsInitialized { get; private set; } + public void Cleanup() { } public void Init() { + IsInitialized = true; } } diff --git a/src/libraries/Highbyte.DotNet6502/Systems/IInputHandlerContext.cs b/src/libraries/Highbyte.DotNet6502/Systems/IInputHandlerContext.cs index 19b08cb7..7c9121cc 100644 --- a/src/libraries/Highbyte.DotNet6502/Systems/IInputHandlerContext.cs +++ b/src/libraries/Highbyte.DotNet6502/Systems/IInputHandlerContext.cs @@ -4,14 +4,18 @@ public interface IInputHandlerContext { void Init(); void Cleanup(); + public bool IsInitialized { get; } } public class NullInputHandlerContext : IInputHandlerContext { + public bool IsInitialized { get; private set; } = false; + public void Cleanup() { } public void Init() { + IsInitialized = true; } } diff --git a/src/libraries/Highbyte.DotNet6502/Systems/IRenderContext.cs b/src/libraries/Highbyte.DotNet6502/Systems/IRenderContext.cs index 6f94debc..77ec573f 100644 --- a/src/libraries/Highbyte.DotNet6502/Systems/IRenderContext.cs +++ b/src/libraries/Highbyte.DotNet6502/Systems/IRenderContext.cs @@ -4,14 +4,18 @@ public interface IRenderContext { void Init(); void Cleanup(); + + public bool IsInitialized { get; } } public class NullRenderContext : IRenderContext { + public bool IsInitialized { get; private set; } = false; public void Cleanup() { } public void Init() { + IsInitialized = true; } } diff --git a/src/libraries/Highbyte.DotNet6502/Systems/SystemList.cs b/src/libraries/Highbyte.DotNet6502/Systems/SystemList.cs index a5cb28d8..0838a5e3 100644 --- a/src/libraries/Highbyte.DotNet6502/Systems/SystemList.cs +++ b/src/libraries/Highbyte.DotNet6502/Systems/SystemList.cs @@ -21,19 +21,59 @@ public SystemList() { } - public void InitContext( - Func getRenderContext, - Func getInputHandlerContext, - Func getAudioHandlerContext) + public void SetContext( + Func? getRenderContext = null, + Func? getInputHandlerContext = null, + Func? getAudioHandlerContext = null) { - getRenderContext().Init(); - getInputHandlerContext().Init(); - getAudioHandlerContext().Init(); + if (getRenderContext != null) + { + if (_getRenderContext != null) + throw new DotNet6502Exception("RenderContext has already been set. Call SetContext only once."); + _getRenderContext = getRenderContext; + } + if (getInputHandlerContext != null) + { + if (_getInputHandlerContext != null) + throw new DotNet6502Exception("InputHandlerContext has already been set. Call SetContext only once."); + _getInputHandlerContext = getInputHandlerContext; + } + if (getAudioHandlerContext != null) + { + if (_getAudioHandlerContext != null) + throw new DotNet6502Exception("AudioHandlerContext has already been set. Call SetContext only once."); + _getAudioHandlerContext = getAudioHandlerContext; + } + } - _getRenderContext = getRenderContext; - _getInputHandlerContext = getInputHandlerContext; - _getAudioHandlerContext = getAudioHandlerContext; + public void InitRenderContext() + { + if (_getRenderContext == null) + throw new DotNet6502Exception("RenderContext has not been set. Call SetContext first."); + if (_getRenderContext().IsInitialized) + _getRenderContext().Cleanup(); + _getRenderContext().Init(); + } + public void InitInputHandlerContext() + { + if (_getInputHandlerContext == null) + throw new DotNet6502Exception("InputHandlerContext has not been set. Call SetContext first."); + if (_getInputHandlerContext().IsInitialized) + _getInputHandlerContext().Cleanup(); + _getInputHandlerContext().Init(); } + public void InitAudioHandlerContext() + { + if (_getAudioHandlerContext == null) + throw new DotNet6502Exception("AudioHandlerContext has not been set. Call SetContext first."); + if (_getAudioHandlerContext().IsInitialized) + _getAudioHandlerContext().Cleanup(); + _getAudioHandlerContext().Init(); + } + + public bool IsRenderContextInitialized => _getRenderContext != null ? _getRenderContext().IsInitialized : false; + public bool IsInputHandlerContextInitialized => _getInputHandlerContext != null ? _getInputHandlerContext().IsInitialized : false; + public bool IsAudioHandlerContextInitialized => _getAudioHandlerContext != null ? _getAudioHandlerContext().IsInitialized : false; /// /// Add a system to the list of available systems. From 76371ac4375cd8989b68fd33c4a8e29739e5fb73 Mon Sep 17 00:00:00 2001 From: Highbyte Date: Sun, 21 Jul 2024 22:23:32 +0200 Subject: [PATCH 05/40] Rename interface --- .../{ISilkNetHostViewModel.cs => ISilkNetHostUIViewModel.cs} | 2 +- .../Highbyte.DotNet6502.App.SilkNetNative/SilkNetHostApp.cs | 2 +- .../Highbyte.DotNet6502.App.SilkNetNative/SilkNetImGuiMenu.cs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) rename src/apps/Highbyte.DotNet6502.App.SilkNetNative/{ISilkNetHostViewModel.cs => ISilkNetHostUIViewModel.cs} (95%) diff --git a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/ISilkNetHostViewModel.cs b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/ISilkNetHostUIViewModel.cs similarity index 95% rename from src/apps/Highbyte.DotNet6502.App.SilkNetNative/ISilkNetHostViewModel.cs rename to src/apps/Highbyte.DotNet6502.App.SilkNetNative/ISilkNetHostUIViewModel.cs index 7ffec3de..694da43d 100644 --- a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/ISilkNetHostViewModel.cs +++ b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/ISilkNetHostUIViewModel.cs @@ -2,7 +2,7 @@ namespace Highbyte.DotNet6502.App.SilkNetNative { - public interface ISilkNetHostViewModel + public interface ISilkNetHostUIViewModel { public EmulatorState EmulatorState { get; } public Task Start(); diff --git a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetHostApp.cs b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetHostApp.cs index 4c3b8cb3..0c0bbc90 100644 --- a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetHostApp.cs +++ b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetHostApp.cs @@ -10,7 +10,7 @@ namespace Highbyte.DotNet6502.App.SilkNetNative { - public class SilkNetHostApp : HostApp, ISilkNetHostViewModel + public class SilkNetHostApp : HostApp, ISilkNetHostUIViewModel { // -------------------- // Injected variables diff --git a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetImGuiMenu.cs b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetImGuiMenu.cs index cc5689d8..09b296ad 100644 --- a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetImGuiMenu.cs +++ b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetImGuiMenu.cs @@ -13,7 +13,7 @@ namespace Highbyte.DotNet6502.App.SilkNetNative; public class SilkNetImGuiMenu : ISilkNetImGuiWindow { - private readonly ISilkNetHostViewModel _hostViewModel; + private readonly ISilkNetHostUIViewModel _hostViewModel; private EmulatorState EmulatorState => _hostViewModel.EmulatorState; public bool Visible { get; private set; } = true; @@ -49,7 +49,7 @@ public class SilkNetImGuiMenu : ISilkNetImGuiWindow private string _lastFileError = ""; - public SilkNetImGuiMenu(ISilkNetHostViewModel hostViewModel, string defaultSystemName, bool defaultAudioEnabled, float defaultAudioVolumePercent, IMapper mapper, ILoggerFactory loggerFactory) + public SilkNetImGuiMenu(ISilkNetHostUIViewModel hostViewModel, string defaultSystemName, bool defaultAudioEnabled, float defaultAudioVolumePercent, IMapper mapper, ILoggerFactory loggerFactory) { _hostViewModel = hostViewModel; _screenScaleString = _hostViewModel.Scale.ToString(); From 01ebcd84ce666d778272a7146b08ba1d12a6d610 Mon Sep 17 00:00:00 2001 From: Highbyte Date: Sun, 21 Jul 2024 23:37:17 +0200 Subject: [PATCH 06/40] Change to file-scoped namespaces. --- .../Commodore64/Data/C64CharGenerator.cs | 81 +- .../Commodore64/Data/C64SpriteGenerator.cs | 283 +++--- .../ISilkNetHostUIViewModel.cs | 43 +- .../SilkNetHostApp.cs | 833 ++++++++-------- .../SystemSetup/C64HostConfig.cs | 39 +- .../SystemSetup/GenericComputerHostConfig.cs | 15 +- .../Emulator/IWASMHostUIViewModel.cs | 21 +- .../Emulator/Skia/SkiaWASMHostApp.cs | 511 +++++----- .../Emulator/SystemSetup/C64HostConfig.cs | 33 +- .../SystemSetup/GenericComputerHostConfig.cs | 13 +- .../SystemSetup/GenericComputerSetup.cs | 2 - .../Audio/C64WASMNoiseOscillator.cs | 205 ++-- .../Audio/C64WASMPulseOscillator.cs | 235 +++-- .../Audio/C64WASMSawToothOscillator.cs | 115 ++- .../Audio/C64WASMTriangleOscillator.cs | 115 ++- .../Commodore64/Audio/C64WASMVoiceContext.cs | 895 +++++++++--------- .../Commodore64/Input/C64AspNetInputConfig.cs | 59 +- .../BlazorDOMSync/EventTargetSync.cs | 53 +- .../WASMAudioHandlerContext.cs | 97 +- .../Audio/C64NAudioVoiceContext.cs | 565 ++++++----- .../NAudioAudioHandlerContext.cs | 105 +- .../Synth/SquareWaveHelper.cs | 73 +- .../Synth/SynthEnvelopeProvider.cs | 227 +++-- .../Synth/SynthSignalProvider.cs | 309 +++--- .../Input/C64SilkNetInputConfig.cs | 59 +- .../Video/C64SilkNetOpenGlRendererConfig.cs | 23 +- .../OpenGLHelpers/BufferHelper.cs | 63 +- .../OpenGLHelpers/BufferInfo.cs | 21 +- .../OpenGLHelpers/BufferObject.cs | 55 +- .../OpenGLHelpers/Shader.cs | 285 +++--- .../OpenGLHelpers/VertexArrayObject.cs | 110 ++- .../Commodore64/Audio/AudioGlobalCommand.cs | 49 +- .../Commodore64/Audio/AudioGlobalParameter.cs | 43 +- .../Commodore64/Audio/AudioVoiceCommand.cs | 121 ++- .../Commodore64/Audio/AudioVoiceParameter.cs | 127 ++- .../Commodore64/Audio/AudioVoiceStatus.cs | 43 +- .../Commodore64/Audio/SidVoiceWaveForm.cs | 17 +- .../Commodore64/Video/IVic2SpriteManager.cs | 47 +- .../Logging/InMem/DotNet6502InMemLogStore.cs | 69 +- .../Instrumentation/AveragedStatTest.cs | 49 +- .../ElapsedMillisecondsTimedStatSystemTest.cs | 63 +- .../ElapsedMillisecondsTimedStatTest.cs | 165 ++-- .../Instrumentation/InstrumentationBagTest.cs | 111 ++- .../Instrumentation/InstrumentationsTest.cs | 105 +- .../Instrumentation/PerSecondTimedStatTest.cs | 112 ++- 45 files changed, 3308 insertions(+), 3356 deletions(-) diff --git a/benchmarks/Highbyte.DotNet6502.Benchmarks/Commodore64/Data/C64CharGenerator.cs b/benchmarks/Highbyte.DotNet6502.Benchmarks/Commodore64/Data/C64CharGenerator.cs index 7e2dcd70..bdff0ca0 100644 --- a/benchmarks/Highbyte.DotNet6502.Benchmarks/Commodore64/Data/C64CharGenerator.cs +++ b/benchmarks/Highbyte.DotNet6502.Benchmarks/Commodore64/Data/C64CharGenerator.cs @@ -1,58 +1,57 @@ using Highbyte.DotNet6502.Systems.Commodore64; using Highbyte.DotNet6502.Systems.Commodore64.Video; -namespace Highbyte.DotNet6502.Benchmarks.Commodore64.Data +namespace Highbyte.DotNet6502.Benchmarks.Commodore64.Data; + +public class C64CharGenerator { - public class C64CharGenerator - { - private readonly C64 _c64; - private readonly Vic2 _vic2; - private readonly Memory _vic2Mem; + private readonly C64 _c64; + private readonly Vic2 _vic2; + private readonly Memory _vic2Mem; - public C64CharGenerator(C64 c64) - { - _c64 = c64; - _vic2 = c64.Vic2; - _vic2Mem = c64.Mem; - } + public C64CharGenerator(C64 c64) + { + _c64 = c64; + _vic2 = c64.Vic2; + _vic2Mem = c64.Mem; + } - public void WriteToScreen(byte characterCode, byte col, byte row) - { - var screenAddress = (ushort)(_vic2.VideoMatrixBaseAddress + (row * _vic2.Vic2Screen.TextCols) + col); - _vic2Mem[screenAddress] = characterCode; - } - public void CreateCharData() + public void WriteToScreen(byte characterCode, byte col, byte row) + { + var screenAddress = (ushort)(_vic2.VideoMatrixBaseAddress + (row * _vic2.Vic2Screen.TextCols) + col); + _vic2Mem[screenAddress] = characterCode; + } + public void CreateCharData() + { + foreach (var characterCode in s_chars.Keys) { - foreach (var characterCode in s_chars.Keys) + var characterSetLineAddress = (ushort)(_vic2.CharsetManager.CharacterSetAddressInVIC2Bank + + (characterCode * _vic2.Vic2Screen.CharacterHeight)); + for (int i = 0; i < s_chars[characterCode].Length; i++) { - var characterSetLineAddress = (ushort)(_vic2.CharsetManager.CharacterSetAddressInVIC2Bank - + (characterCode * _vic2.Vic2Screen.CharacterHeight)); - for (int i = 0; i < s_chars[characterCode].Length; i++) - { - _vic2Mem[(ushort)(characterSetLineAddress + i)] = s_chars[characterCode][i]; - } + _vic2Mem[(ushort)(characterSetLineAddress + i)] = s_chars[characterCode][i]; } } + } - private static Dictionary s_chars + private static Dictionary s_chars + { + get { - get + return new Dictionary { - return new Dictionary - { - // A - {1, new byte[] { - 0b00011000, - 0b01100110, - 0b01111110, - 0b01100110, - 0b01100110, - 0b01100110, - 0b00000000 - } + // A + {1, new byte[] { + 0b00011000, + 0b01100110, + 0b01111110, + 0b01100110, + 0b01100110, + 0b01100110, + 0b00000000 } - }; - } + } + }; } } } diff --git a/benchmarks/Highbyte.DotNet6502.Benchmarks/Commodore64/Data/C64SpriteGenerator.cs b/benchmarks/Highbyte.DotNet6502.Benchmarks/Commodore64/Data/C64SpriteGenerator.cs index 2ff3635f..6f948619 100644 --- a/benchmarks/Highbyte.DotNet6502.Benchmarks/Commodore64/Data/C64SpriteGenerator.cs +++ b/benchmarks/Highbyte.DotNet6502.Benchmarks/Commodore64/Data/C64SpriteGenerator.cs @@ -1,164 +1,163 @@ using Highbyte.DotNet6502.Systems.Commodore64; using Highbyte.DotNet6502.Systems.Commodore64.Video; -namespace Highbyte.DotNet6502.Benchmarks.Commodore64.Data +namespace Highbyte.DotNet6502.Benchmarks.Commodore64.Data; + +public class C64SpriteGenerator { - public class C64SpriteGenerator - { - private readonly C64 _c64; - private readonly Memory _vic2Mem; - private readonly IVic2SpriteManager _vic2SpriteManager; + private readonly C64 _c64; + private readonly Memory _vic2Mem; + private readonly IVic2SpriteManager _vic2SpriteManager; - public C64SpriteGenerator(C64 c64) - { - _c64 = c64; - _vic2Mem = c64.Mem; - _vic2SpriteManager = c64.Vic2.SpriteManager; - } + public C64SpriteGenerator(C64 c64) + { + _c64 = c64; + _vic2Mem = c64.Mem; + _vic2SpriteManager = c64.Vic2.SpriteManager; + } - public Vic2Sprite CreateSprite(int spriteNumber, byte x, byte y, bool doubleWidth, bool doubleHeight, bool multiColor, byte[] spriteShape, byte spritePointer = 192) - { - SetSpriteProperties( - spriteNumber, - x, - y, - doubleWidth: doubleWidth, - doubleHeight: doubleHeight, - multiColor: multiColor); - - // Create sprite shape - FillSpriteShape(spriteNumber, spriteShape, spritePointer); - - return _vic2SpriteManager.Sprites[spriteNumber]; - } + public Vic2Sprite CreateSprite(int spriteNumber, byte x, byte y, bool doubleWidth, bool doubleHeight, bool multiColor, byte[] spriteShape, byte spritePointer = 192) + { + SetSpriteProperties( + spriteNumber, + x, + y, + doubleWidth: doubleWidth, + doubleHeight: doubleHeight, + multiColor: multiColor); + + // Create sprite shape + FillSpriteShape(spriteNumber, spriteShape, spritePointer); + + return _vic2SpriteManager.Sprites[spriteNumber]; + } - private void SetSpriteProperties(int spriteNumber, byte x, byte y, bool doubleWidth, bool doubleHeight, bool multiColor) - { - _c64.WriteIOStorage((ushort)(Vic2Addr.SPRITE_0_X + spriteNumber * 2), x); - _c64.WriteIOStorage((ushort)(Vic2Addr.SPRITE_0_Y + spriteNumber * 2), y); + private void SetSpriteProperties(int spriteNumber, byte x, byte y, bool doubleWidth, bool doubleHeight, bool multiColor) + { + _c64.WriteIOStorage((ushort)(Vic2Addr.SPRITE_0_X + spriteNumber * 2), x); + _c64.WriteIOStorage((ushort)(Vic2Addr.SPRITE_0_Y + spriteNumber * 2), y); - var spriteXEnable = _c64.ReadIOStorage(Vic2Addr.SPRITE_ENABLE); - spriteXEnable.ChangeBit(spriteNumber, true); - _c64.WriteIOStorage(Vic2Addr.SPRITE_ENABLE, spriteXEnable); + var spriteXEnable = _c64.ReadIOStorage(Vic2Addr.SPRITE_ENABLE); + spriteXEnable.ChangeBit(spriteNumber, true); + _c64.WriteIOStorage(Vic2Addr.SPRITE_ENABLE, spriteXEnable); - var spriteXExpand = _c64.ReadIOStorage(Vic2Addr.SPRITE_X_EXPAND); - spriteXExpand.ChangeBit(spriteNumber, doubleWidth); - _c64.WriteIOStorage(Vic2Addr.SPRITE_X_EXPAND, spriteXExpand); + var spriteXExpand = _c64.ReadIOStorage(Vic2Addr.SPRITE_X_EXPAND); + spriteXExpand.ChangeBit(spriteNumber, doubleWidth); + _c64.WriteIOStorage(Vic2Addr.SPRITE_X_EXPAND, spriteXExpand); - var spriteYExpand = _c64.ReadIOStorage(Vic2Addr.SPRITE_Y_EXPAND); - spriteYExpand.ChangeBit(spriteNumber, doubleHeight); - _c64.WriteIOStorage(Vic2Addr.SPRITE_Y_EXPAND, spriteYExpand); + var spriteYExpand = _c64.ReadIOStorage(Vic2Addr.SPRITE_Y_EXPAND); + spriteYExpand.ChangeBit(spriteNumber, doubleHeight); + _c64.WriteIOStorage(Vic2Addr.SPRITE_Y_EXPAND, spriteYExpand); - var multiColorEnable = _c64.ReadIOStorage(Vic2Addr.SPRITE_MULTICOLOR_ENABLE); - multiColorEnable.ChangeBit(spriteNumber, multiColor); - _c64.WriteIOStorage(Vic2Addr.SPRITE_MULTICOLOR_ENABLE, multiColorEnable); - } + var multiColorEnable = _c64.ReadIOStorage(Vic2Addr.SPRITE_MULTICOLOR_ENABLE); + multiColorEnable.ChangeBit(spriteNumber, multiColor); + _c64.WriteIOStorage(Vic2Addr.SPRITE_MULTICOLOR_ENABLE, multiColorEnable); + } - private void FillSpriteShape(int spriteNumber, byte[] shape, byte spritePointer) + private void FillSpriteShape(int spriteNumber, byte[] shape, byte spritePointer) + { + _vic2Mem[(ushort)(_vic2SpriteManager.SpritePointerStartAddress + spriteNumber)] = spritePointer; + //var spritePointer = vic2Mem[(ushort)(Vic2.SPRITE_POINTERS_START_ADDRESS + spriteNumber)]; + var spritePointerAddress = (ushort)(spritePointer * 64); + for (int i = 0; i < shape.Length; i++) { - _vic2Mem[(ushort)(_vic2SpriteManager.SpritePointerStartAddress + spriteNumber)] = spritePointer; - //var spritePointer = vic2Mem[(ushort)(Vic2.SPRITE_POINTERS_START_ADDRESS + spriteNumber)]; - var spritePointerAddress = (ushort)(spritePointer * 64); - for (int i = 0; i < shape.Length; i++) - { - _vic2Mem[(ushort)(spritePointerAddress + i)] = shape[i]; - } + _vic2Mem[(ushort)(spritePointerAddress + i)] = shape[i]; } + } - public byte[] CreateTestSingleColorSpriteImage() + public byte[] CreateTestSingleColorSpriteImage() + { + // 24 x 21 pixels = 3 * 21 bytes = 63 bytes. 3 bytes per row. + return new byte[] { - // 24 x 21 pixels = 3 * 21 bytes = 63 bytes. 3 bytes per row. - return new byte[] - { - 0b11110000, 0b11110000, 0b11110000, - 0b00000000, 0b00000000, 0b00000000, - 0b00000000, 0b00000000, 0b00000000, - 0b00000000, 0b00000000, 0b00000000, - 0b00000000, 0b00000000, 0b00000000, - 0b00000000, 0b00000000, 0b00000000, - 0b00000000, 0b00000000, 0b00000000, - 0b00000000, 0b00000000, 0b00000000, - 0b00000000, 0b00000000, 0b00000000, - 0b00000000, 0b00000000, 0b00000000, - - 0b00000000, 0b00000000, 0b00000000, - 0b00000000, 0b00000000, 0b00000000, - 0b00000000, 0b00000000, 0b00000000, - 0b00000000, 0b00000000, 0b00000000, - 0b00000000, 0b00000000, 0b00000000, - 0b00000000, 0b00000000, 0b00000000, - 0b00000000, 0b00000000, 0b00000000, - 0b00000000, 0b00000000, 0b00000000, - 0b00000000, 0b00000000, 0b00000000, - 0b00000000, 0b00000000, 0b00000000, - - 0b00000000, 0b00000000, 0b00000000, - }; - } + 0b11110000, 0b11110000, 0b11110000, + 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, + + 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, + + 0b00000000, 0b00000000, 0b00000000, + }; + } - public byte[] CreateTestSingleColorSpriteImage2() + public byte[] CreateTestSingleColorSpriteImage2() + { + // 24 x 21 pixels = 3 * 21 bytes = 63 bytes. 3 bytes per row. + return new byte[] { - // 24 x 21 pixels = 3 * 21 bytes = 63 bytes. 3 bytes per row. - return new byte[] - { - 0b00011000, 0b00011000, 0b00011000, - 0b00111100, 0b00111100, 0b00111100, - 0b01111110, 0b01111110, 0b01111110, - 0b11111111, 0b11111111, 0b11111111, - 0b01111110, 0b01111110, 0b01111110, - 0b00111100, 0b00111100, 0b00111100, - 0b00011000, 0b00011000, 0b00011000, - 0b00000000, 0b00000000, 0b00000000, - 0b00000000, 0b00000000, 0b00000000, - 0b00000000, 0b00000000, 0b00000000, - - 0b00000000, 0b00000000, 0b00000000, - 0b00000000, 0b00000000, 0b00000000, - 0b00000000, 0b00000000, 0b00000000, - 0b00000000, 0b00000000, 0b00000000, - 0b00000000, 0b00000000, 0b00000000, - 0b00000000, 0b00000000, 0b00000000, - 0b00000000, 0b00000000, 0b00000000, - 0b00000000, 0b00000000, 0b00000000, - 0b00000000, 0b00000000, 0b00000000, - 0b00000000, 0b00000000, 0b00000000, - - 0b00000000, 0b00000000, 0b00000000, - }; - } + 0b00011000, 0b00011000, 0b00011000, + 0b00111100, 0b00111100, 0b00111100, + 0b01111110, 0b01111110, 0b01111110, + 0b11111111, 0b11111111, 0b11111111, + 0b01111110, 0b01111110, 0b01111110, + 0b00111100, 0b00111100, 0b00111100, + 0b00011000, 0b00011000, 0b00011000, + 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, + + 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, + + 0b00000000, 0b00000000, 0b00000000, + }; + } - public byte[] CreateTestMultiColorSpriteImage() + public byte[] CreateTestMultiColorSpriteImage() + { + // 24 x 21 pixels = 3 * 21 bytes = 63 bytes. 3 bytes per row. + // Each byte contains 4 pixels. + // Bit pairs 01,10, and 11 are used to 3 different select color. + // Bit pair 00 is used to select background color. + return new byte[] { - // 24 x 21 pixels = 3 * 21 bytes = 63 bytes. 3 bytes per row. - // Each byte contains 4 pixels. - // Bit pairs 01,10, and 11 are used to 3 different select color. - // Bit pair 00 is used to select background color. - return new byte[] - { - 0b01010000, 0b10100000, 0b11110000, - 0b00000000, 0b00000000, 0b00000000, - 0b00000000, 0b00000000, 0b00000000, - 0b00000000, 0b00000000, 0b00000000, - 0b00000000, 0b00000000, 0b00000000, - 0b00000000, 0b00000000, 0b00000000, - 0b00000000, 0b00000000, 0b00000000, - 0b00000000, 0b00000000, 0b00000000, - 0b00000000, 0b00000000, 0b00000000, - 0b00000000, 0b00000000, 0b00000000, - - 0b00000000, 0b00000000, 0b00000000, - 0b00000000, 0b00000000, 0b00000000, - 0b00000000, 0b00000000, 0b00000000, - 0b00000000, 0b00000000, 0b00000000, - 0b00000000, 0b00000000, 0b00000000, - 0b00000000, 0b00000000, 0b00000000, - 0b00000000, 0b00000000, 0b00000000, - 0b00000000, 0b00000000, 0b00000000, - 0b00000000, 0b00000000, 0b00000000, - 0b00000000, 0b00000000, 0b00000000, - - 0b00000000, 0b00000000, 0b00000000, - }; - } + 0b01010000, 0b10100000, 0b11110000, + 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, + + 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, + + 0b00000000, 0b00000000, 0b00000000, + }; } } diff --git a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/ISilkNetHostUIViewModel.cs b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/ISilkNetHostUIViewModel.cs index 694da43d..ff2f68e8 100644 --- a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/ISilkNetHostUIViewModel.cs +++ b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/ISilkNetHostUIViewModel.cs @@ -1,31 +1,30 @@ using Highbyte.DotNet6502.Systems; -namespace Highbyte.DotNet6502.App.SilkNetNative +namespace Highbyte.DotNet6502.App.SilkNetNative; + +public interface ISilkNetHostUIViewModel { - public interface ISilkNetHostUIViewModel - { - public EmulatorState EmulatorState { get; } - public Task Start(); - public void Pause(); - public void Stop(); - public Task Reset(); + public EmulatorState EmulatorState { get; } + public Task Start(); + public void Pause(); + public void Stop(); + public Task Reset(); - public void SetVolumePercent(float volumePercent); - public float Scale { get; set; } + public void SetVolumePercent(float volumePercent); + public float Scale { get; set; } - public void ToggleMonitor(); - public void ToggleStatsPanel(); - public void ToggleLogsPanel(); + public void ToggleMonitor(); + public void ToggleStatsPanel(); + public void ToggleLogsPanel(); - public HashSet AvailableSystemNames { get; } - public string SelectedSystemName { get; } - public void SelectSystem(string systemName); + public HashSet AvailableSystemNames { get; } + public string SelectedSystemName { get; } + public void SelectSystem(string systemName); - public Task IsSystemConfigValid(); - public Task GetSystemConfig(); - public IHostSystemConfig GetHostSystemConfig(); - public void UpdateSystemConfig(ISystemConfig newConfig); + public Task IsSystemConfigValid(); + public Task GetSystemConfig(); + public IHostSystemConfig GetHostSystemConfig(); + public void UpdateSystemConfig(ISystemConfig newConfig); - public ISystem? CurrentRunningSystem { get; } - } + public ISystem? CurrentRunningSystem { get; } } diff --git a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetHostApp.cs b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetHostApp.cs index 0c0bbc90..c052b110 100644 --- a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetHostApp.cs +++ b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetHostApp.cs @@ -8,505 +8,504 @@ using Highbyte.DotNet6502.Systems; using Microsoft.Extensions.Logging; -namespace Highbyte.DotNet6502.App.SilkNetNative +namespace Highbyte.DotNet6502.App.SilkNetNative; + +public class SilkNetHostApp : HostApp, ISilkNetHostUIViewModel { - public class SilkNetHostApp : HostApp, ISilkNetHostUIViewModel - { - // -------------------- - // Injected variables - // -------------------- - private readonly ILogger _logger; - private readonly IWindow _window; - private readonly EmulatorConfig _emulatorConfig; - public EmulatorConfig EmulatorConfig => _emulatorConfig; - - private readonly DotNet6502InMemLogStore _logStore; - private readonly DotNet6502InMemLoggerConfiguration _logConfig; - private readonly bool _defaultAudioEnabled; - private float _defaultAudioVolumePercent; - private readonly ILoggerFactory _loggerFactory; - private readonly IMapper _mapper; - - // -------------------- - // Other variables / constants - // -------------------- - private SilkNetRenderContextContainer _renderContextContainer = default!; - private SilkNetInputHandlerContext _inputHandlerContext = default!; - private NAudioAudioHandlerContext _audioHandlerContext = default!; - - public float CanvasScale - { - get { return _emulatorConfig.CurrentDrawScale; } - set { _emulatorConfig.CurrentDrawScale = value; } - } - public const int DEFAULT_WIDTH = 1000; - public const int DEFAULT_HEIGHT = 700; - public const int DEFAULT_RENDER_HZ = 60; - - // Monitor - private SilkNetImGuiMonitor _monitor = default!; - public SilkNetImGuiMonitor Monitor => _monitor; - - // Instrumentations panel - private SilkNetImGuiStatsPanel _statsPanel = default!; - public SilkNetImGuiStatsPanel StatsPanel => _statsPanel; - - // Logs panel - private SilkNetImGuiLogsPanel _logsPanel = default!; - public SilkNetImGuiLogsPanel LogsPanel => _logsPanel; - - // Menu - private SilkNetImGuiMenu _menu = default!; - private bool _statsWasEnabled = false; - //private bool _logsWasEnabled = false; - - private readonly List _imGuiWindows = new List(); - private bool _atLeastOneImGuiWindowHasFocus => _imGuiWindows.Any(x => x.Visible && x.WindowIsFocused); - - - // GL and other ImGui resources - private GL _gl = default!; - private IInputContext _inputContext = default!; - private ImGuiController _imGuiController = default!; - - /// - /// Constructor - /// - /// - /// - /// - /// - /// - /// - /// - public SilkNetHostApp( - SystemList systemList, - ILoggerFactory loggerFactory, - - EmulatorConfig emulatorConfig, - IWindow window, - DotNet6502InMemLogStore logStore, - DotNet6502InMemLoggerConfiguration logConfig, - IMapper mapper - - ) : base("SilkNet", systemList, emulatorConfig.HostSystemConfigs, loggerFactory) - { - _emulatorConfig = emulatorConfig; - _emulatorConfig.CurrentDrawScale = _emulatorConfig.DefaultDrawScale; - _window = window; - _logStore = logStore; - _logConfig = logConfig; - _defaultAudioEnabled = true; - _defaultAudioVolumePercent = 20.0f; - - _loggerFactory = loggerFactory; - _mapper = mapper; - _logger = loggerFactory.CreateLogger(typeof(SilkNetHostApp).Name); - } + // -------------------- + // Injected variables + // -------------------- + private readonly ILogger _logger; + private readonly IWindow _window; + private readonly EmulatorConfig _emulatorConfig; + public EmulatorConfig EmulatorConfig => _emulatorConfig; + + private readonly DotNet6502InMemLogStore _logStore; + private readonly DotNet6502InMemLoggerConfiguration _logConfig; + private readonly bool _defaultAudioEnabled; + private float _defaultAudioVolumePercent; + private readonly ILoggerFactory _loggerFactory; + private readonly IMapper _mapper; + + // -------------------- + // Other variables / constants + // -------------------- + private SilkNetRenderContextContainer _renderContextContainer = default!; + private SilkNetInputHandlerContext _inputHandlerContext = default!; + private NAudioAudioHandlerContext _audioHandlerContext = default!; + + public float CanvasScale + { + get { return _emulatorConfig.CurrentDrawScale; } + set { _emulatorConfig.CurrentDrawScale = value; } + } + public const int DEFAULT_WIDTH = 1000; + public const int DEFAULT_HEIGHT = 700; + public const int DEFAULT_RENDER_HZ = 60; + + // Monitor + private SilkNetImGuiMonitor _monitor = default!; + public SilkNetImGuiMonitor Monitor => _monitor; + + // Instrumentations panel + private SilkNetImGuiStatsPanel _statsPanel = default!; + public SilkNetImGuiStatsPanel StatsPanel => _statsPanel; + + // Logs panel + private SilkNetImGuiLogsPanel _logsPanel = default!; + public SilkNetImGuiLogsPanel LogsPanel => _logsPanel; + + // Menu + private SilkNetImGuiMenu _menu = default!; + private bool _statsWasEnabled = false; + //private bool _logsWasEnabled = false; + + private readonly List _imGuiWindows = new List(); + private bool _atLeastOneImGuiWindowHasFocus => _imGuiWindows.Any(x => x.Visible && x.WindowIsFocused); + + + // GL and other ImGui resources + private GL _gl = default!; + private IInputContext _inputContext = default!; + private ImGuiController _imGuiController = default!; + + /// + /// Constructor + /// + /// + /// + /// + /// + /// + /// + /// + public SilkNetHostApp( + SystemList systemList, + ILoggerFactory loggerFactory, + + EmulatorConfig emulatorConfig, + IWindow window, + DotNet6502InMemLogStore logStore, + DotNet6502InMemLoggerConfiguration logConfig, + IMapper mapper + + ) : base("SilkNet", systemList, emulatorConfig.HostSystemConfigs, loggerFactory) + { + _emulatorConfig = emulatorConfig; + _emulatorConfig.CurrentDrawScale = _emulatorConfig.DefaultDrawScale; + _window = window; + _logStore = logStore; + _logConfig = logConfig; + _defaultAudioEnabled = true; + _defaultAudioVolumePercent = 20.0f; + + _loggerFactory = loggerFactory; + _mapper = mapper; + _logger = loggerFactory.CreateLogger(typeof(SilkNetHostApp).Name); + } - public void Run() - { - _window.Load += OnLoad; - _window.Closing += OnClosing; - _window.Update += OnUpdate; - _window.Render += OnRender; - _window.Resize += OnResize; - - _window.Run(); - // Cleanup SilNet window resources - _window?.Dispose(); - } + public void Run() + { + _window.Load += OnLoad; + _window.Closing += OnClosing; + _window.Update += OnUpdate; + _window.Render += OnRender; + _window.Resize += OnResize; + + _window.Run(); + // Cleanup SilNet window resources + _window?.Dispose(); + } - protected void OnLoad() - { - SetUninitializedWindow(); + protected void OnLoad() + { + SetUninitializedWindow(); - InitRenderContext(); - InitInputContext(); - InitAudioContext(); + InitRenderContext(); + InitInputContext(); + InitAudioContext(); - base.SetAndInitContexts(() => _renderContextContainer, () => _inputHandlerContext, () => _audioHandlerContext); + base.SetAndInitContexts(() => _renderContextContainer, () => _inputHandlerContext, () => _audioHandlerContext); - InitImGui(); + InitImGui(); - // Init main menu UI - _menu = new SilkNetImGuiMenu(this, _emulatorConfig.DefaultEmulator, _defaultAudioEnabled, _defaultAudioVolumePercent, _mapper, _loggerFactory); + // Init main menu UI + _menu = new SilkNetImGuiMenu(this, _emulatorConfig.DefaultEmulator, _defaultAudioEnabled, _defaultAudioVolumePercent, _mapper, _loggerFactory); - // Create other UI windows - _statsPanel = CreateStatsUI(); - _monitor = CreateMonitorUI(_statsPanel, _emulatorConfig.Monitor); - _logsPanel = CreateLogsUI(_logStore, _logConfig); + // Create other UI windows + _statsPanel = CreateStatsUI(); + _monitor = CreateMonitorUI(_statsPanel, _emulatorConfig.Monitor); + _logsPanel = CreateLogsUI(_logStore, _logConfig); - // Add all ImGui windows to a list - _imGuiWindows.Add(_menu); - _imGuiWindows.Add(_statsPanel); - _imGuiWindows.Add(_monitor); - _imGuiWindows.Add(_logsPanel); - } + // Add all ImGui windows to a list + _imGuiWindows.Add(_menu); + _imGuiWindows.Add(_statsPanel); + _imGuiWindows.Add(_monitor); + _imGuiWindows.Add(_logsPanel); + } - protected void OnClosing() - { - base.Close(); - } + protected void OnClosing() + { + base.Close(); + } - public override void OnAfterSelectSystem() - { - } + public override void OnAfterSelectSystem() + { + } - public override bool OnBeforeStart(ISystem systemAboutToBeStarted) + public override bool OnBeforeStart(ISystem systemAboutToBeStarted) + { + // Make sure to adjust window size and render frequency to match the system that is about to be started + if (EmulatorState == EmulatorState.Uninitialized) { - // Make sure to adjust window size and render frequency to match the system that is about to be started - if (EmulatorState == EmulatorState.Uninitialized) - { - var screen = systemAboutToBeStarted.Screen; - _window.Size = new Vector2D((int)(screen.VisibleWidth * CanvasScale), (int)(screen.VisibleHeight * CanvasScale)); - _window.UpdatesPerSecond = screen.RefreshFrequencyHz; - InitRenderContext(); - } - return true; + var screen = systemAboutToBeStarted.Screen; + _window.Size = new Vector2D((int)(screen.VisibleWidth * CanvasScale), (int)(screen.VisibleHeight * CanvasScale)); + _window.UpdatesPerSecond = screen.RefreshFrequencyHz; + InitRenderContext(); } + return true; + } - public override void OnAfterStart(EmulatorState emulatorStateBeforeStart) - { - // Init monitor for current system started if this system was not started before - if (emulatorStateBeforeStart == EmulatorState.Uninitialized) - _monitor.Init(CurrentSystemRunner!); - } + public override void OnAfterStart(EmulatorState emulatorStateBeforeStart) + { + // Init monitor for current system started if this system was not started before + if (emulatorStateBeforeStart == EmulatorState.Uninitialized) + _monitor.Init(CurrentSystemRunner!); + } - public override void OnAfterClose() - { - // Dispose Monitor/Instrumentations panel - //_monitor.Cleanup(); - //_statsPanel.Cleanup(); - DestroyImGuiController(); + public override void OnAfterClose() + { + // Dispose Monitor/Instrumentations panel + //_monitor.Cleanup(); + //_statsPanel.Cleanup(); + DestroyImGuiController(); - // Cleanup contexts - _renderContextContainer?.Cleanup(); - _inputHandlerContext?.Cleanup(); - _audioHandlerContext?.Cleanup(); + // Cleanup contexts + _renderContextContainer?.Cleanup(); + _inputHandlerContext?.Cleanup(); + _audioHandlerContext?.Cleanup(); - } + } - /// - /// Runs on every Update Frame event. - /// - /// Use this method to run logic. - /// - /// - /// - protected void OnUpdate(double deltaTime) + /// + /// Runs on every Update Frame event. + /// + /// Use this method to run logic. + /// + /// + /// + protected void OnUpdate(double deltaTime) + { + base.RunEmulatorOneFrame(); + } + + public override void OnBeforeRunEmulatorOneFrame(out bool shouldRun, out bool shouldReceiveInput) + { + shouldRun = false; + shouldReceiveInput = false; + // Don't update emulator state when monitor is visible + if (_monitor.Visible) + return; + // Don't update emulator state when app is quiting + if (_inputHandlerContext.Quit || _monitor.Quit) { - base.RunEmulatorOneFrame(); + _window.Close(); + return; } - public override void OnBeforeRunEmulatorOneFrame(out bool shouldRun, out bool shouldReceiveInput) - { - shouldRun = false; - shouldReceiveInput = false; - // Don't update emulator state when monitor is visible - if (_monitor.Visible) - return; - // Don't update emulator state when app is quiting - if (_inputHandlerContext.Quit || _monitor.Quit) - { - _window.Close(); - return; - } + shouldRun = true; - shouldRun = true; + // Only receive input to emulator when no ImGui window has focus + if (!_atLeastOneImGuiWindowHasFocus) + shouldReceiveInput = true; + } - // Only receive input to emulator when no ImGui window has focus - if (!_atLeastOneImGuiWindowHasFocus) - shouldReceiveInput = true; - } + public override void OnAfterRunEmulatorOneFrame(ExecEvaluatorTriggerResult execEvaluatorTriggerResult) + { + // Show monitor if we encounter breakpoint or other break + if (execEvaluatorTriggerResult.Triggered) + _monitor.Enable(execEvaluatorTriggerResult); + } - public override void OnAfterRunEmulatorOneFrame(ExecEvaluatorTriggerResult execEvaluatorTriggerResult) - { - // Show monitor if we encounter breakpoint or other break - if (execEvaluatorTriggerResult.Triggered) - _monitor.Enable(execEvaluatorTriggerResult); - } + /// + /// Runs on every Render Frame event. Draws one emulator frame on screen. + /// + /// This method is called at a RenderFrequency set in the GameWindowSettings object. + /// + /// + protected void OnRender(double deltaTime) + { + //RenderEmulator(deltaTime); - /// - /// Runs on every Render Frame event. Draws one emulator frame on screen. - /// - /// This method is called at a RenderFrequency set in the GameWindowSettings object. - /// - /// - protected void OnRender(double deltaTime) + // Make sure ImGui is up-to-date + _imGuiController.Update((float)deltaTime); + + // Draw emulator on screen + base.DrawFrame(); + } + + public override void OnBeforeDrawFrame(bool emulatorWillBeRendered) + { + // If any ImGui window is visible, make sure to clear Gl buffer before rendering emulator + if (emulatorWillBeRendered) + { + if (_monitor.Visible || _statsPanel.Visible || _logsPanel.Visible) + _gl.Clear((uint)(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit)); + } + } + public override void OnAfterDrawFrame(bool emulatorRendered) + { + if (emulatorRendered) { - //RenderEmulator(deltaTime); + // Flush the SkiaSharp Context + _renderContextContainer.SkiaRenderContext.GetGRContext().Flush(); - // Make sure ImGui is up-to-date - _imGuiController.Update((float)deltaTime); + // Render monitor if enabled and emulator was rendered + if (_monitor.Visible) + _monitor.PostOnRender(); - // Draw emulator on screen - base.DrawFrame(); + // Render stats if enabled and emulator was rendered + if (_statsPanel.Visible) + _statsPanel.PostOnRender(); } - public override void OnBeforeDrawFrame(bool emulatorWillBeRendered) + // Render logs if enabled, regardless of if emulator was rendered or not + if (_logsPanel.Visible) + _logsPanel.PostOnRender(); + + // If emulator was not rendered, clear Gl buffer before rendering ImGui windows + if (!emulatorRendered) { - // If any ImGui window is visible, make sure to clear Gl buffer before rendering emulator - if (emulatorWillBeRendered) - { - if (_monitor.Visible || _statsPanel.Visible || _logsPanel.Visible) - _gl.Clear((uint)(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit)); - } + if (_menu.Visible) + _gl.Clear((uint)(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit)); + // Seems the canvas has to be drawn & flushed for ImGui stuff to be visible on top + var canvas = _renderContextContainer.SkiaRenderContext.GetCanvas(); + canvas.Clear(); + _renderContextContainer.SkiaRenderContext.GetGRContext().Flush(); } - public override void OnAfterDrawFrame(bool emulatorRendered) - { - if (emulatorRendered) - { - // Flush the SkiaSharp Context - _renderContextContainer.SkiaRenderContext.GetGRContext().Flush(); - // Render monitor if enabled and emulator was rendered - if (_monitor.Visible) - _monitor.PostOnRender(); + if (_menu.Visible) + _menu.PostOnRender(); - // Render stats if enabled and emulator was rendered - if (_statsPanel.Visible) - _statsPanel.PostOnRender(); - } - - // Render logs if enabled, regardless of if emulator was rendered or not - if (_logsPanel.Visible) - _logsPanel.PostOnRender(); + // Render any ImGui UI rendered above emulator. + _imGuiController?.Render(); + } - // If emulator was not rendered, clear Gl buffer before rendering ImGui windows - if (!emulatorRendered) - { - if (_menu.Visible) - _gl.Clear((uint)(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit)); - // Seems the canvas has to be drawn & flushed for ImGui stuff to be visible on top - var canvas = _renderContextContainer.SkiaRenderContext.GetCanvas(); - canvas.Clear(); - _renderContextContainer.SkiaRenderContext.GetGRContext().Flush(); - } + private void OnResize(Vector2D vec2) + { + } - if (_menu.Visible) - _menu.PostOnRender(); - // Render any ImGui UI rendered above emulator. - _imGuiController?.Render(); - } + private void InitRenderContext() + { + _renderContextContainer?.Cleanup(); - private void OnResize(Vector2D vec2) + // Init SkipSharp resources (must be done in OnLoad, otherwise no OpenGL context will exist create by SilkNet.) + //_skiaRenderContext = new SkiaRenderContext(s_window.Size.X, s_window.Size.Y, _canvasScale); + GRGlGetProcedureAddressDelegate getProcAddress = (name) => { - } + var addrFound = _window.GLContext!.TryGetProcAddress(name, out var addr); + return addrFound ? addr : 0; + }; + var skiaRenderContext = new SkiaRenderContext( + getProcAddress, + _window.FramebufferSize.X, + _window.FramebufferSize.Y, + _emulatorConfig.CurrentDrawScale * (_window.FramebufferSize.X / _window.Size.X)); - private void InitRenderContext() - { - _renderContextContainer?.Cleanup(); + var silkNetOpenGlRenderContext = new SilkNetOpenGlRenderContext(_window, _emulatorConfig.CurrentDrawScale); - // Init SkipSharp resources (must be done in OnLoad, otherwise no OpenGL context will exist create by SilkNet.) - //_skiaRenderContext = new SkiaRenderContext(s_window.Size.X, s_window.Size.Y, _canvasScale); - GRGlGetProcedureAddressDelegate getProcAddress = (name) => - { - var addrFound = _window.GLContext!.TryGetProcAddress(name, out var addr); - return addrFound ? addr : 0; - }; + _renderContextContainer = new SilkNetRenderContextContainer(skiaRenderContext, silkNetOpenGlRenderContext); + } + + private void InitInputContext() + { + _inputHandlerContext = new SilkNetInputHandlerContext(_window, _loggerFactory); - var skiaRenderContext = new SkiaRenderContext( - getProcAddress, - _window.FramebufferSize.X, - _window.FramebufferSize.Y, - _emulatorConfig.CurrentDrawScale * (_window.FramebufferSize.X / _window.Size.X)); + _inputContext = _window.CreateInput(); + // Listen to key to enable monitor + if (_inputContext.Keyboards == null || _inputContext.Keyboards.Count == 0) + throw new DotNet6502Exception("Keyboard not found"); + var primaryKeyboard = _inputContext.Keyboards[0]; - var silkNetOpenGlRenderContext = new SilkNetOpenGlRenderContext(_window, _emulatorConfig.CurrentDrawScale); + // Listen to special key that will show/hide overlays for monitor/stats + primaryKeyboard.KeyDown += OnKeyDown; + } - _renderContextContainer = new SilkNetRenderContextContainer(skiaRenderContext, silkNetOpenGlRenderContext); - } + private void InitAudioContext() + { + // Output to NAudio built-in output (Windows only) + //var wavePlayer = new WaveOutEvent + //{ + // NumberOfBuffers = 2, + // DesiredLatency = 100, + //} + + // Output to OpenAL (cross platform) instead of via NAudio built-in output (Windows only) + var wavePlayer = new SilkNetOpenALWavePlayer() + { + NumberOfBuffers = 2, + DesiredLatency = 40 + }; + + _audioHandlerContext = new NAudioAudioHandlerContext( + wavePlayer, + initialVolumePercent: 20); + } - private void InitInputContext() - { - _inputHandlerContext = new SilkNetInputHandlerContext(_window, _loggerFactory); + public void SetVolumePercent(float volumePercent) + { + _defaultAudioVolumePercent = volumePercent; + _audioHandlerContext.SetMasterVolumePercent(masterVolumePercent: volumePercent); + } - _inputContext = _window.CreateInput(); - // Listen to key to enable monitor - if (_inputContext.Keyboards == null || _inputContext.Keyboards.Count == 0) - throw new DotNet6502Exception("Keyboard not found"); - var primaryKeyboard = _inputContext.Keyboards[0]; + private void SetUninitializedWindow() + { + _window.Size = new Vector2D(DEFAULT_WIDTH, DEFAULT_HEIGHT); + _window.UpdatesPerSecond = DEFAULT_RENDER_HZ; + } - // Listen to special key that will show/hide overlays for monitor/stats - primaryKeyboard.KeyDown += OnKeyDown; - } + private void InitImGui() + { + // Init ImGui resource + _gl = GL.GetApi(_window); + _imGuiController = new ImGuiController( + _gl, + _window, // pass in our window + _inputContext // input context + ); + } - private void InitAudioContext() - { - // Output to NAudio built-in output (Windows only) - //var wavePlayer = new WaveOutEvent - //{ - // NumberOfBuffers = 2, - // DesiredLatency = 100, - //} - - // Output to OpenAL (cross platform) instead of via NAudio built-in output (Windows only) - var wavePlayer = new SilkNetOpenALWavePlayer() - { - NumberOfBuffers = 2, - DesiredLatency = 40 - }; + private SilkNetImGuiMonitor CreateMonitorUI(SilkNetImGuiStatsPanel statsPanel, MonitorConfig monitorConfig) + { + // Init Monitor ImGui resources + var monitor = new SilkNetImGuiMonitor(monitorConfig); + monitor.MonitorStateChange += (s, monitorEnabled) => _inputHandlerContext.ListenForKeyboardInput(enabled: !monitorEnabled); + monitor.MonitorStateChange += (s, monitorEnabled) => + { + if (monitorEnabled) + statsPanel.Disable(); + }; + return monitor; + } - _audioHandlerContext = new NAudioAudioHandlerContext( - wavePlayer, - initialVolumePercent: 20); - } + private SilkNetImGuiStatsPanel CreateStatsUI() + { + return new SilkNetImGuiStatsPanel(GetStats); + } - public void SetVolumePercent(float volumePercent) - { - _defaultAudioVolumePercent = volumePercent; - _audioHandlerContext.SetMasterVolumePercent(masterVolumePercent: volumePercent); - } + private SilkNetImGuiLogsPanel CreateLogsUI(DotNet6502InMemLogStore logStore, DotNet6502InMemLoggerConfiguration logConfig) + { + return new SilkNetImGuiLogsPanel(logStore, logConfig); + } - private void SetUninitializedWindow() - { - _window.Size = new Vector2D(DEFAULT_WIDTH, DEFAULT_HEIGHT); - _window.UpdatesPerSecond = DEFAULT_RENDER_HZ; - } + private void DestroyImGuiController() + { + _imGuiController?.Dispose(); + _inputContext?.Dispose(); + _gl?.Dispose(); + } - private void InitImGui() - { - // Init ImGui resource - _gl = GL.GetApi(_window); - _imGuiController = new ImGuiController( - _gl, - _window, // pass in our window - _inputContext // input context - ); - } - private SilkNetImGuiMonitor CreateMonitorUI(SilkNetImGuiStatsPanel statsPanel, MonitorConfig monitorConfig) - { - // Init Monitor ImGui resources - var monitor = new SilkNetImGuiMonitor(monitorConfig); - monitor.MonitorStateChange += (s, monitorEnabled) => _inputHandlerContext.ListenForKeyboardInput(enabled: !monitorEnabled); - monitor.MonitorStateChange += (s, monitorEnabled) => - { - if (monitorEnabled) - statsPanel.Disable(); - }; - return monitor; - } + private void OnKeyDown(IKeyboard keyboard, Key key, int x) + { + if (key == Key.F6) + ToggleMainMenu(); + if (key == Key.F10) + ToggleLogsPanel(); - private SilkNetImGuiStatsPanel CreateStatsUI() + if (EmulatorState == EmulatorState.Running || EmulatorState == EmulatorState.Paused) { - return new SilkNetImGuiStatsPanel(GetStats); + if (key == Key.F11) + ToggleStatsPanel(); + if (key == Key.F12) + ToggleMonitor(); } + } + private void ToggleMainMenu() + { + if (_menu.Visible) + _menu.Disable(); + else + _menu.Enable(); + } - private SilkNetImGuiLogsPanel CreateLogsUI(DotNet6502InMemLogStore logStore, DotNet6502InMemLoggerConfiguration logConfig) - { - return new SilkNetImGuiLogsPanel(logStore, logConfig); - } + #region ISilkNetHostViewModel (members that are not part of base class) - private void DestroyImGuiController() - { - _imGuiController?.Dispose(); - _inputContext?.Dispose(); - _gl?.Dispose(); - } + public float Scale + { + get { return _emulatorConfig.CurrentDrawScale; } + set { _emulatorConfig.CurrentDrawScale = value; } + } + public void ToggleMonitor() + { + // Only be able to toggle monitor if emulator is running or paused + if (EmulatorState == EmulatorState.Uninitialized) + return; - private void OnKeyDown(IKeyboard keyboard, Key key, int x) + if (_statsPanel.Visible) { - if (key == Key.F6) - ToggleMainMenu(); - if (key == Key.F10) - ToggleLogsPanel(); + _statsWasEnabled = true; + _statsPanel.Disable(); + } - if (EmulatorState == EmulatorState.Running || EmulatorState == EmulatorState.Paused) + if (_monitor.Visible) + { + _monitor.Disable(); + if (_statsWasEnabled) { - if (key == Key.F11) - ToggleStatsPanel(); - if (key == Key.F12) - ToggleMonitor(); + CurrentRunningSystem!.InstrumentationEnabled = true; + _statsPanel.Enable(); } } - private void ToggleMainMenu() + else { - if (_menu.Visible) - _menu.Disable(); - else - _menu.Enable(); + _monitor.Enable(); } + } - #region ISilkNetHostViewModel (members that are not part of base class) + public void ToggleStatsPanel() + { + // Only be able to toggle stats if emulator is running or paused + if (EmulatorState == EmulatorState.Uninitialized) + return; - public float Scale - { - get { return _emulatorConfig.CurrentDrawScale; } - set { _emulatorConfig.CurrentDrawScale = value; } - } + if (_monitor.Visible) + return; - public void ToggleMonitor() + if (_statsPanel.Visible) { - // Only be able to toggle monitor if emulator is running or paused - if (EmulatorState == EmulatorState.Uninitialized) - return; - - if (_statsPanel.Visible) - { - _statsWasEnabled = true; - _statsPanel.Disable(); - } - - if (_monitor.Visible) - { - _monitor.Disable(); - if (_statsWasEnabled) - { - CurrentRunningSystem!.InstrumentationEnabled = true; - _statsPanel.Enable(); - } - } - else - { - _monitor.Enable(); - } + _statsPanel.Disable(); + CurrentRunningSystem!.InstrumentationEnabled = false; + _statsWasEnabled = false; } - - public void ToggleStatsPanel() + else { - // Only be able to toggle stats if emulator is running or paused - if (EmulatorState == EmulatorState.Uninitialized) - return; + CurrentRunningSystem!.InstrumentationEnabled = true; + _statsPanel.Enable(); + } + } - if (_monitor.Visible) - return; + public void ToggleLogsPanel() + { + if (_monitor.Visible) + return; - if (_statsPanel.Visible) - { - _statsPanel.Disable(); - CurrentRunningSystem!.InstrumentationEnabled = false; - _statsWasEnabled = false; - } - else - { - CurrentRunningSystem!.InstrumentationEnabled = true; - _statsPanel.Enable(); - } + if (_logsPanel.Visible) + { + //_logsWasEnabled = true; + _logsPanel.Disable(); } - - public void ToggleLogsPanel() + else { - if (_monitor.Visible) - return; - - if (_logsPanel.Visible) - { - //_logsWasEnabled = true; - _logsPanel.Disable(); - } - else - { - _logsPanel.Enable(); - } + _logsPanel.Enable(); } + } - #endregion + #endregion - } } diff --git a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SystemSetup/C64HostConfig.cs b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SystemSetup/C64HostConfig.cs index 07a68d44..d7dee82f 100644 --- a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SystemSetup/C64HostConfig.cs +++ b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SystemSetup/C64HostConfig.cs @@ -2,28 +2,27 @@ using Highbyte.DotNet6502.Impl.SilkNet.Commodore64.Video; using Highbyte.DotNet6502.Systems; -namespace Highbyte.DotNet6502.App.SilkNetNative.SystemSetup +namespace Highbyte.DotNet6502.App.SilkNetNative.SystemSetup; + +public enum C64HostRenderer { - public enum C64HostRenderer - { - SkiaSharp, - SkiaSharp2, // Experimental render directly to pixel buffer backed by a SKBitmap + Skia shader (SKSL) - SkiaSharp2b, // Experimental render after each instruction directly to pixel buffer backed by a SKBitmap + Skia shader (SKSL) - SilkNetOpenGl - } + SkiaSharp, + SkiaSharp2, // Experimental render directly to pixel buffer backed by a SKBitmap + Skia shader (SKSL) + SkiaSharp2b, // Experimental render after each instruction directly to pixel buffer backed by a SKBitmap + Skia shader (SKSL) + SilkNetOpenGl +} - public class C64HostConfig : IHostSystemConfig, ICloneable - { - public C64HostRenderer Renderer { get; set; } = C64HostRenderer.SkiaSharp; - public C64SilkNetOpenGlRendererConfig SilkNetOpenGlRendererConfig { get; set; } = new C64SilkNetOpenGlRendererConfig(); - public C64SilkNetInputConfig InputConfig { get; set; } = new C64SilkNetInputConfig(); +public class C64HostConfig : IHostSystemConfig, ICloneable +{ + public C64HostRenderer Renderer { get; set; } = C64HostRenderer.SkiaSharp; + public C64SilkNetOpenGlRendererConfig SilkNetOpenGlRendererConfig { get; set; } = new C64SilkNetOpenGlRendererConfig(); + public C64SilkNetInputConfig InputConfig { get; set; } = new C64SilkNetInputConfig(); - public object Clone() - { - var clone = (C64HostConfig)this.MemberwiseClone(); - clone.InputConfig = (C64SilkNetInputConfig)InputConfig.Clone(); - clone.SilkNetOpenGlRendererConfig = (C64SilkNetOpenGlRendererConfig)SilkNetOpenGlRendererConfig.Clone(); - return clone; - } + public object Clone() + { + var clone = (C64HostConfig)this.MemberwiseClone(); + clone.InputConfig = (C64SilkNetInputConfig)InputConfig.Clone(); + clone.SilkNetOpenGlRendererConfig = (C64SilkNetOpenGlRendererConfig)SilkNetOpenGlRendererConfig.Clone(); + return clone; } } diff --git a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SystemSetup/GenericComputerHostConfig.cs b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SystemSetup/GenericComputerHostConfig.cs index 1e4411ea..400445bf 100644 --- a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SystemSetup/GenericComputerHostConfig.cs +++ b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SystemSetup/GenericComputerHostConfig.cs @@ -1,14 +1,13 @@ using Highbyte.DotNet6502.Systems; -namespace Highbyte.DotNet6502.App.SilkNetNative.SystemSetup +namespace Highbyte.DotNet6502.App.SilkNetNative.SystemSetup; + +public class GenericComputerHostConfig : IHostSystemConfig, ICloneable { - public class GenericComputerHostConfig : IHostSystemConfig, ICloneable - { - public object Clone() - { - var clone = (GenericComputerHostConfig)this.MemberwiseClone(); - return clone; - } + public object Clone() + { + var clone = (GenericComputerHostConfig)this.MemberwiseClone(); + return clone; } } diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/IWASMHostUIViewModel.cs b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/IWASMHostUIViewModel.cs index f85d39b8..63a04292 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/IWASMHostUIViewModel.cs +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/IWASMHostUIViewModel.cs @@ -1,15 +1,14 @@ -namespace Highbyte.DotNet6502.App.WASM.Emulator +namespace Highbyte.DotNet6502.App.WASM.Emulator; + +public interface IWASMHostUIViewModel { - public interface IWASMHostUIViewModel - { - Task SetDebugState(bool visible); - Task ToggleDebugState(); - void UpdateDebug(string debug); + Task SetDebugState(bool visible); + Task ToggleDebugState(); + void UpdateDebug(string debug); - void UpdateStats(string stats); - Task SetStatsState(bool visible); - Task ToggleStatsState(); + void UpdateStats(string stats); + Task SetStatsState(bool visible); + Task ToggleStatsState(); - Task SetMonitorState(bool visible); - } + Task SetMonitorState(bool visible); } diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/Skia/SkiaWASMHostApp.cs b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/Skia/SkiaWASMHostApp.cs index 22d32ce9..7d77dac6 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/Skia/SkiaWASMHostApp.cs +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/Skia/SkiaWASMHostApp.cs @@ -6,318 +6,317 @@ using Highbyte.DotNet6502.Systems; using Toolbelt.Blazor.Gamepad; -namespace Highbyte.DotNet6502.App.WASM.Emulator.Skia +namespace Highbyte.DotNet6502.App.WASM.Emulator.Skia; + +public class SkiaWASMHostApp : HostApp { - public class SkiaWASMHostApp : HostApp + // -------------------- + // Injected variables + // -------------------- + private readonly ILogger _logger; + private readonly EmulatorConfig _emulatorConfig; + private readonly Func _getCanvas; + private readonly Func _getGrContext; + private readonly Func _getAudioContext; + private readonly GamepadList _gamepadList; + + public EmulatorConfig EmulatorConfig => _emulatorConfig; + + private readonly bool _defaultAudioEnabled; + private readonly float _defaultAudioVolumePercent; + private readonly ILoggerFactory _loggerFactory; + + // -------------------- + // Other variables / constants + // -------------------- + private SkiaRenderContext _renderContext = default!; + private AspNetInputHandlerContext _inputHandlerContext = default!; + private WASMAudioHandlerContext _audioHandlerContext = default!; + + private readonly IJSRuntime _jsRuntime; + private PeriodicAsyncTimer? _updateTimer; + + private WasmMonitor _monitor = default!; + public WasmMonitor Monitor => _monitor; + + // Operations to update UI + private readonly IWASMHostUIViewModel _wasmHostUIViewModel; + + private const int STATS_EVERY_X_FRAME = 60 * 1; + private int _statsFrameCount = 0; + + private const int DEBUGMESSAGE_EVERY_X_FRAME = 1; + private int _debugFrameCount = 0; + + public SkiaWASMHostApp( + SystemList systemList, + ILoggerFactory loggerFactory, + EmulatorConfig emulatorConfig, + + Func getCanvas, + Func getGrContext, + Func getAudioContext, + GamepadList gamepadList, + IJSRuntime jsRuntime, + IWASMHostUIViewModel wasmHostUIViewModel + ) : base("SilkNet", systemList, emulatorConfig.HostSystemConfigs, loggerFactory) { - // -------------------- - // Injected variables - // -------------------- - private readonly ILogger _logger; - private readonly EmulatorConfig _emulatorConfig; - private readonly Func _getCanvas; - private readonly Func _getGrContext; - private readonly Func _getAudioContext; - private readonly GamepadList _gamepadList; - - public EmulatorConfig EmulatorConfig => _emulatorConfig; - - private readonly bool _defaultAudioEnabled; - private readonly float _defaultAudioVolumePercent; - private readonly ILoggerFactory _loggerFactory; - - // -------------------- - // Other variables / constants - // -------------------- - private SkiaRenderContext _renderContext = default!; - private AspNetInputHandlerContext _inputHandlerContext = default!; - private WASMAudioHandlerContext _audioHandlerContext = default!; - - private readonly IJSRuntime _jsRuntime; - private PeriodicAsyncTimer? _updateTimer; - - private WasmMonitor _monitor = default!; - public WasmMonitor Monitor => _monitor; - - // Operations to update UI - private readonly IWASMHostUIViewModel _wasmHostUIViewModel; - - private const int STATS_EVERY_X_FRAME = 60 * 1; - private int _statsFrameCount = 0; - - private const int DEBUGMESSAGE_EVERY_X_FRAME = 1; - private int _debugFrameCount = 0; - - public SkiaWASMHostApp( - SystemList systemList, - ILoggerFactory loggerFactory, - EmulatorConfig emulatorConfig, - - Func getCanvas, - Func getGrContext, - Func getAudioContext, - GamepadList gamepadList, - IJSRuntime jsRuntime, - IWASMHostUIViewModel wasmHostUIViewModel - ) : base("SilkNet", systemList, emulatorConfig.HostSystemConfigs, loggerFactory) - { - _loggerFactory = loggerFactory; - _logger = loggerFactory.CreateLogger(typeof(SkiaWASMHostApp).Name); - _emulatorConfig = emulatorConfig; - _emulatorConfig.CurrentDrawScale = _emulatorConfig.DefaultDrawScale; + _loggerFactory = loggerFactory; + _logger = loggerFactory.CreateLogger(typeof(SkiaWASMHostApp).Name); + _emulatorConfig = emulatorConfig; + _emulatorConfig.CurrentDrawScale = _emulatorConfig.DefaultDrawScale; - _getCanvas = getCanvas; - _getGrContext = getGrContext; - _getAudioContext = getAudioContext; - _gamepadList = gamepadList; - _jsRuntime = jsRuntime; - _wasmHostUIViewModel = wasmHostUIViewModel; + _getCanvas = getCanvas; + _getGrContext = getGrContext; + _getAudioContext = getAudioContext; + _gamepadList = gamepadList; + _jsRuntime = jsRuntime; + _wasmHostUIViewModel = wasmHostUIViewModel; - _defaultAudioEnabled = false; - _defaultAudioVolumePercent = 20.0f; + _defaultAudioEnabled = false; + _defaultAudioVolumePercent = 20.0f; - _renderContext = new SkiaRenderContext(_getCanvas, _getGrContext); - _inputHandlerContext = new AspNetInputHandlerContext(_loggerFactory, _gamepadList); - _audioHandlerContext = new WASMAudioHandlerContext(_getAudioContext, _jsRuntime, _defaultAudioVolumePercent); + _renderContext = new SkiaRenderContext(_getCanvas, _getGrContext); + _inputHandlerContext = new AspNetInputHandlerContext(_loggerFactory, _gamepadList); + _audioHandlerContext = new WASMAudioHandlerContext(_getAudioContext, _jsRuntime, _defaultAudioVolumePercent); - base.SetContexts(getRenderContext: () => _renderContext, getInputHandlerContext: () => _inputHandlerContext, getAudioHandlerContext: () => _audioHandlerContext); - } + base.SetContexts(getRenderContext: () => _renderContext, getInputHandlerContext: () => _inputHandlerContext, getAudioHandlerContext: () => _audioHandlerContext); + } - public override void OnAfterSelectSystem() - { - } + public override void OnAfterSelectSystem() + { + } + + public override bool OnBeforeStart(ISystem systemAboutToBeStarted) + { + return true; + } - public override bool OnBeforeStart(ISystem systemAboutToBeStarted) + public override void OnAfterStart(EmulatorState emulatorStateBeforeStart) + { + // Create timer for current system on initial start. Assume Stop() sets _updateTimer to null. + if (_updateTimer == null) { - return true; + _updateTimer = CreateUpdateTimerForSystem(CurrentSystemRunner!.System); } + _updateTimer!.Start(); - public override void OnAfterStart(EmulatorState emulatorStateBeforeStart) - { - // Create timer for current system on initial start. Assume Stop() sets _updateTimer to null. - if (_updateTimer == null) - { - _updateTimer = CreateUpdateTimerForSystem(CurrentSystemRunner!.System); - } - _updateTimer!.Start(); + // Init monitor for current system started if this system was not started before + if (emulatorStateBeforeStart == EmulatorState.Uninitialized) + _monitor = new WasmMonitor(_jsRuntime, CurrentSystemRunner!, _emulatorConfig, _wasmHostUIViewModel); + } - // Init monitor for current system started if this system was not started before - if (emulatorStateBeforeStart == EmulatorState.Uninitialized) - _monitor = new WasmMonitor(_jsRuntime, CurrentSystemRunner!, _emulatorConfig, _wasmHostUIViewModel); - } + public override void OnAfterPause() + { + _updateTimer!.Stop(); + } - public override void OnAfterPause() - { - _updateTimer!.Stop(); - } + public override void OnAfterStop() + { + _wasmHostUIViewModel.SetDebugState(visible: false); + _wasmHostUIViewModel.SetStatsState(visible: false); + _monitor.Disable(); - public override void OnAfterStop() - { - _wasmHostUIViewModel.SetDebugState(visible: false); - _wasmHostUIViewModel.SetStatsState(visible: false); - _monitor.Disable(); + _updateTimer!.Stop(); + _updateTimer!.Dispose(); + _updateTimer = null; + } - _updateTimer!.Stop(); - _updateTimer!.Dispose(); - _updateTimer = null; - } + public override void OnAfterClose() + { + // Cleanup contexts + _renderContext?.Cleanup(); + _inputHandlerContext?.Cleanup(); + _audioHandlerContext?.Cleanup(); + } - public override void OnAfterClose() - { - // Cleanup contexts - _renderContext?.Cleanup(); - _inputHandlerContext?.Cleanup(); - _audioHandlerContext?.Cleanup(); - } + private PeriodicAsyncTimer CreateUpdateTimerForSystem(ISystem system) + { + // Number of milliseconds between each invokation of the main loop. 60 fps -> (1/60) * 1000 -> approx 16.6667ms + double updateIntervalMS = (1 / system.Screen.RefreshFrequencyHz) * 1000; + var updateTimer = new PeriodicAsyncTimer(); + updateTimer.IntervalMilliseconds = updateIntervalMS; + updateTimer.Elapsed += UpdateTimerElapsed; + return updateTimer; + } - private PeriodicAsyncTimer CreateUpdateTimerForSystem(ISystem system) - { - // Number of milliseconds between each invokation of the main loop. 60 fps -> (1/60) * 1000 -> approx 16.6667ms - double updateIntervalMS = (1 / system.Screen.RefreshFrequencyHz) * 1000; - var updateTimer = new PeriodicAsyncTimer(); - updateTimer.IntervalMilliseconds = updateIntervalMS; - updateTimer.Elapsed += UpdateTimerElapsed; - return updateTimer; - } + private void UpdateTimerElapsed(object? sender, EventArgs e) => RunEmulatorOneFrame(); - private void UpdateTimerElapsed(object? sender, EventArgs e) => RunEmulatorOneFrame(); + public override void OnBeforeRunEmulatorOneFrame(out bool shouldRun, out bool shouldReceiveInput) + { + shouldRun = false; + shouldReceiveInput = false; + // Don't update emulator state when monitor is visible + if (_monitor.Visible) + return; + + shouldRun = true; + shouldReceiveInput = true; + } - public override void OnBeforeRunEmulatorOneFrame(out bool shouldRun, out bool shouldReceiveInput) + public override void OnAfterRunEmulatorOneFrame(ExecEvaluatorTriggerResult execEvaluatorTriggerResult) + { + // Push debug info to debug UI + _debugFrameCount++; + if (_debugFrameCount >= DEBUGMESSAGE_EVERY_X_FRAME) { - shouldRun = false; - shouldReceiveInput = false; - // Don't update emulator state when monitor is visible - if (_monitor.Visible) - return; - - shouldRun = true; - shouldReceiveInput = true; + _debugFrameCount = 0; + var debugString = GetDebugMessagesHtmlString(); + _wasmHostUIViewModel.UpdateDebug(debugString); } - public override void OnAfterRunEmulatorOneFrame(ExecEvaluatorTriggerResult execEvaluatorTriggerResult) + // Push stats to stats UI + if (CurrentRunningSystem!.InstrumentationEnabled) { - // Push debug info to debug UI - _debugFrameCount++; - if (_debugFrameCount >= DEBUGMESSAGE_EVERY_X_FRAME) + _statsFrameCount++; + if (_statsFrameCount >= STATS_EVERY_X_FRAME) { - _debugFrameCount = 0; - var debugString = GetDebugMessagesHtmlString(); - _wasmHostUIViewModel.UpdateDebug(debugString); + _statsFrameCount = 0; + _wasmHostUIViewModel.UpdateStats(GetStatsHtmlString()); } + } - // Push stats to stats UI - if (CurrentRunningSystem!.InstrumentationEnabled) - { - _statsFrameCount++; - if (_statsFrameCount >= STATS_EVERY_X_FRAME) - { - _statsFrameCount = 0; - _wasmHostUIViewModel.UpdateStats(GetStatsHtmlString()); - } - } + // Show monitor if we encounter breakpoint or other break + if (execEvaluatorTriggerResult.Triggered) + _monitor.Enable(execEvaluatorTriggerResult); + } - // Show monitor if we encounter breakpoint or other break - if (execEvaluatorTriggerResult.Triggered) - _monitor.Enable(execEvaluatorTriggerResult); - } + /// + /// Called from ASP.NET Blazor SKGLView "OnPaintSurface" event to render one frame. + /// + /// + public void Render() + { + // Draw emulator on screen + base.DrawFrame(); + } - /// - /// Called from ASP.NET Blazor SKGLView "OnPaintSurface" event to render one frame. - /// - /// - public void Render() + public override void OnBeforeDrawFrame(bool emulatorWillBeRendered) + { + if (emulatorWillBeRendered) { - // Draw emulator on screen - base.DrawFrame(); + // TODO: Shouldn't scale be able to set once we start the emulator (OnBeforeStart method?) instead of every frame? + _getCanvas().Scale((float)_emulatorConfig.CurrentDrawScale); } + } - public override void OnBeforeDrawFrame(bool emulatorWillBeRendered) + public override void OnAfterDrawFrame(bool emulatorRendered) + { + if (emulatorRendered) { - if (emulatorWillBeRendered) - { - // TODO: Shouldn't scale be able to set once we start the emulator (OnBeforeStart method?) instead of every frame? - _getCanvas().Scale((float)_emulatorConfig.CurrentDrawScale); - } } + } - public override void OnAfterDrawFrame(bool emulatorRendered) + public void SetVolumePercent(float volumePercent) + { + _audioHandlerContext.SetMasterVolumePercent(masterVolumePercent: volumePercent); + } + + private string GetStatsHtmlString() + { + string stats = ""; + + var allStats = GetStats(); + foreach ((string name, IStat stat) in allStats.OrderBy(i => i.name)) { - if (emulatorRendered) + if (stat.ShouldShow()) { + if (stats != "") + stats += "
"; + stats += $"{BuildHtmlString(name, "header")}: {BuildHtmlString(stat.GetDescription(), "value")} "; } } + return stats; + } - public void SetVolumePercent(float volumePercent) + private string GetDebugMessagesHtmlString() + { + string debugMessages = ""; + + var inputDebugInfo = CurrentSystemRunner!.InputHandler.GetDebugInfo(); + var inputStatsOneString = string.Join(" # ", inputDebugInfo); + debugMessages += $"{BuildHtmlString("INPUT", "header")}: {BuildHtmlString(inputStatsOneString, "value")} "; + //foreach (var message in inputDebugInfo) + //{ + // if (debugMessages != "") + // debugMessages += "
"; + // debugMessages += $"{BuildHtmlString("DEBUG INPUT", "header")}: {BuildHtmlString(message, "value")} "; + //} + + var audioDebugInfo = CurrentSystemRunner!.AudioHandler.GetDebugInfo(); + foreach (var message in audioDebugInfo) { - _audioHandlerContext.SetMasterVolumePercent(masterVolumePercent: volumePercent); + if (debugMessages != "") + debugMessages += "
"; + debugMessages += $"{BuildHtmlString("AUDIO", "header")}: {BuildHtmlString(message, "value")} "; } - private string GetStatsHtmlString() - { - string stats = ""; - - var allStats = GetStats(); - foreach ((string name, IStat stat) in allStats.OrderBy(i => i.name)) - { - if (stat.ShouldShow()) - { - if (stats != "") - stats += "
"; - stats += $"{BuildHtmlString(name, "header")}: {BuildHtmlString(stat.GetDescription(), "value")} "; - } - } - return stats; - } + return debugMessages; + } - private string GetDebugMessagesHtmlString() - { - string debugMessages = ""; - - var inputDebugInfo = CurrentSystemRunner!.InputHandler.GetDebugInfo(); - var inputStatsOneString = string.Join(" # ", inputDebugInfo); - debugMessages += $"{BuildHtmlString("INPUT", "header")}: {BuildHtmlString(inputStatsOneString, "value")} "; - //foreach (var message in inputDebugInfo) - //{ - // if (debugMessages != "") - // debugMessages += "
"; - // debugMessages += $"{BuildHtmlString("DEBUG INPUT", "header")}: {BuildHtmlString(message, "value")} "; - //} - - var audioDebugInfo = CurrentSystemRunner!.AudioHandler.GetDebugInfo(); - foreach (var message in audioDebugInfo) - { - if (debugMessages != "") - debugMessages += "
"; - debugMessages += $"{BuildHtmlString("AUDIO", "header")}: {BuildHtmlString(message, "value")} "; - } + private string BuildHtmlString(string message, string cssClass, bool startNewLine = false) + { + string html = ""; + if (startNewLine) + html += "
"; + html += $@"{HttpUtility.HtmlEncode(message)}"; + return html; + } - return debugMessages; - } + /// + /// Receive Key Down event in emulator canvas. + /// Also check for special non-emulator functions such as monitor and stats/debug + /// + /// + public void OnKeyDown(KeyboardEventArgs e) + { + // Send event to emulator + _inputHandlerContext.KeyDown(e); - private string BuildHtmlString(string message, string cssClass, bool startNewLine = false) + // Check for other emulator functions + var key = e.Key; + if (key == "F11") { - string html = ""; - if (startNewLine) - html += "
"; - html += $@"{HttpUtility.HtmlEncode(message)}"; - return html; + _wasmHostUIViewModel.ToggleDebugState(); + _wasmHostUIViewModel.ToggleStatsState(); } - - /// - /// Receive Key Down event in emulator canvas. - /// Also check for special non-emulator functions such as monitor and stats/debug - /// - /// - public void OnKeyDown(KeyboardEventArgs e) + else if (key == "F12") { - // Send event to emulator - _inputHandlerContext.KeyDown(e); - - // Check for other emulator functions - var key = e.Key; - if (key == "F11") - { - _wasmHostUIViewModel.ToggleDebugState(); - _wasmHostUIViewModel.ToggleStatsState(); - } - else if (key == "F12") - { - ToggleMonitor(); - } + ToggleMonitor(); } + } - /// - /// Receive Key Up event in emulator canvas. - /// Also check for special non-emulator functions such as monitor and stats/debug - /// - /// - public void OnKeyUp(KeyboardEventArgs e) - { - // Send event to emulator - _inputHandlerContext.KeyUp(e); - } + /// + /// Receive Key Up event in emulator canvas. + /// Also check for special non-emulator functions such as monitor and stats/debug + /// + /// + public void OnKeyUp(KeyboardEventArgs e) + { + // Send event to emulator + _inputHandlerContext.KeyUp(e); + } + + /// + /// Receive Focus on emulator canvas. + /// + /// + public void OnFocus(FocusEventArgs e) + { + _inputHandlerContext.OnFocus(e); + } - /// - /// Receive Focus on emulator canvas. - /// - /// - public void OnFocus(FocusEventArgs e) + public void ToggleMonitor() + { + if (Monitor.Visible) { - _inputHandlerContext.OnFocus(e); + Monitor.Disable(); } - - public void ToggleMonitor() + else { - if (Monitor.Visible) - { - Monitor.Disable(); - } - else - { - Monitor.Enable(); - } + Monitor.Enable(); } } } 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 ca43411b..e826d454 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/SystemSetup/C64HostConfig.cs +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/SystemSetup/C64HostConfig.cs @@ -1,26 +1,25 @@ using Highbyte.DotNet6502.Impl.AspNet.Commodore64.Input; using Highbyte.DotNet6502.Systems; -namespace Highbyte.DotNet6502.App.WASM.Emulator.SystemSetup +namespace Highbyte.DotNet6502.App.WASM.Emulator.SystemSetup; + +public enum C64HostRenderer +{ + SkiaSharp, + SkiaSharp2, // Experimental render directly to pixel buffer backed by a SKBitmap + Skia shader (SKSL) + SkiaSharp2b, // Experimental render after each instruction directly to pixel buffer backed by a SKBitmap + Skia shader (SKSL) +} +public class C64HostConfig : IHostSystemConfig, ICloneable { - public enum C64HostRenderer - { - SkiaSharp, - SkiaSharp2, // Experimental render directly to pixel buffer backed by a SKBitmap + Skia shader (SKSL) - SkiaSharp2b, // Experimental render after each instruction directly to pixel buffer backed by a SKBitmap + Skia shader (SKSL) - } - public class C64HostConfig : IHostSystemConfig, ICloneable - { - public C64HostRenderer Renderer { get; set; } = C64HostRenderer.SkiaSharp; + public C64HostRenderer Renderer { get; set; } = C64HostRenderer.SkiaSharp; - public C64AspNetInputConfig InputConfig { get; set; } = new C64AspNetInputConfig(); + public C64AspNetInputConfig InputConfig { get; set; } = new C64AspNetInputConfig(); - public object Clone() - { - var clone = (C64HostConfig)MemberwiseClone(); - clone.InputConfig = (C64AspNetInputConfig)InputConfig.Clone(); - return clone; - } + public object Clone() + { + var clone = (C64HostConfig)MemberwiseClone(); + clone.InputConfig = (C64AspNetInputConfig)InputConfig.Clone(); + return clone; } } diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/SystemSetup/GenericComputerHostConfig.cs b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/SystemSetup/GenericComputerHostConfig.cs index 946355b1..304ca121 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/SystemSetup/GenericComputerHostConfig.cs +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/SystemSetup/GenericComputerHostConfig.cs @@ -1,13 +1,12 @@ using Highbyte.DotNet6502.Systems; -namespace Highbyte.DotNet6502.App.WASM.Emulator.SystemSetup +namespace Highbyte.DotNet6502.App.WASM.Emulator.SystemSetup; + +public class GenericComputerHostConfig : IHostSystemConfig, ICloneable { - public class GenericComputerHostConfig : IHostSystemConfig, ICloneable + public object Clone() { - public object Clone() - { - var clone = (GenericComputerHostConfig)MemberwiseClone(); - return clone; - } + var clone = (GenericComputerHostConfig)MemberwiseClone(); + return clone; } } diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/SystemSetup/GenericComputerSetup.cs b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/SystemSetup/GenericComputerSetup.cs index bec42f6f..421d5c6f 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/SystemSetup/GenericComputerSetup.cs +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/SystemSetup/GenericComputerSetup.cs @@ -1,10 +1,8 @@ -using Highbyte.DotNet6502.App.WASM.Emulator; using Highbyte.DotNet6502.Impl.AspNet; using Highbyte.DotNet6502.Impl.AspNet.Generic.Input; using Highbyte.DotNet6502.Impl.Skia; using Highbyte.DotNet6502.Impl.Skia.Generic.Video; using Highbyte.DotNet6502.Systems; -using Highbyte.DotNet6502.Systems.Commodore64; using Highbyte.DotNet6502.Systems.Generic; using Highbyte.DotNet6502.Systems.Generic.Config; using Microsoft.AspNetCore.WebUtilities; diff --git a/src/libraries/Highbyte.DotNet6502.Impl.AspNet/Commodore64/Audio/C64WASMNoiseOscillator.cs b/src/libraries/Highbyte.DotNet6502.Impl.AspNet/Commodore64/Audio/C64WASMNoiseOscillator.cs index 90a6fcb8..15475c2a 100644 --- a/src/libraries/Highbyte.DotNet6502.Impl.AspNet/Commodore64/Audio/C64WASMNoiseOscillator.cs +++ b/src/libraries/Highbyte.DotNet6502.Impl.AspNet/Commodore64/Audio/C64WASMNoiseOscillator.cs @@ -2,123 +2,122 @@ using Highbyte.DotNet6502.Impl.AspNet.JSInterop.BlazorWebAudioSync; using Highbyte.DotNet6502.Impl.AspNet.JSInterop.BlazorWebAudioSync.Options; -namespace Highbyte.DotNet6502.Impl.AspNet.Commodore64.Audio +namespace Highbyte.DotNet6502.Impl.AspNet.Commodore64.Audio; + +public class C64WASMNoiseOscillator { - public class C64WASMNoiseOscillator - { - private readonly C64WASMVoiceContext _c64WASMVoiceContext; - private WASMAudioHandlerContext _audioHandlerContext => _c64WASMVoiceContext.AudioHandlerContext; + private readonly C64WASMVoiceContext _c64WASMVoiceContext; + private WASMAudioHandlerContext _audioHandlerContext => _c64WASMVoiceContext.AudioHandlerContext; - private Action _addDebugMessage => _c64WASMVoiceContext.AddDebugMessage; + private Action _addDebugMessage => _c64WASMVoiceContext.AddDebugMessage; - // SID noise oscillator - private AudioBufferSync _noiseBuffer = default!; - internal AudioBufferSourceNodeSync? NoiseGenerator; + // SID noise oscillator + private AudioBufferSync _noiseBuffer = default!; + internal AudioBufferSourceNodeSync? NoiseGenerator; - public C64WASMNoiseOscillator(C64WASMVoiceContext c64WASMVoiceContext) - { - _c64WASMVoiceContext = c64WASMVoiceContext; - } + public C64WASMNoiseOscillator(C64WASMVoiceContext c64WASMVoiceContext) + { + _c64WASMVoiceContext = c64WASMVoiceContext; + } - internal void Create(float playbackRate) - { - if (_noiseBuffer == null) - PrepareNoiseGenerator(); - _addDebugMessage($"Creating NoiseGenerator"); - NoiseGenerator = AudioBufferSourceNodeSync.Create( - _audioHandlerContext.JSRuntime, - _audioHandlerContext.AudioContext, - new AudioBufferSourceNodeOptions - { - PlaybackRate = playbackRate, // Factor of sample rate. 1.0 = same speed as original. - Loop = true, - Buffer = _noiseBuffer - }); - } + internal void Create(float playbackRate) + { + if (_noiseBuffer == null) + PrepareNoiseGenerator(); + _addDebugMessage($"Creating NoiseGenerator"); + NoiseGenerator = AudioBufferSourceNodeSync.Create( + _audioHandlerContext.JSRuntime, + _audioHandlerContext.AudioContext, + new AudioBufferSourceNodeOptions + { + PlaybackRate = playbackRate, // Factor of sample rate. 1.0 = same speed as original. + Loop = true, + Buffer = _noiseBuffer + }); + } - internal void Start() - { - if (NoiseGenerator == null) - throw new DotNet6502Exception($"NoiseGenerator is null. Call Create() first."); - _addDebugMessage($"Starting NoiseGenerator"); - NoiseGenerator!.Start(); - //voiceContext!.NoiseGenerator.Start(0, 0, currentTime + wasmVoiceParameter.AttackDurationSeconds + wasmVoiceParameter.ReleaseDurationSeconds); - } + internal void Start() + { + if (NoiseGenerator == null) + throw new DotNet6502Exception($"NoiseGenerator is null. Call Create() first."); + _addDebugMessage($"Starting NoiseGenerator"); + NoiseGenerator!.Start(); + //voiceContext!.NoiseGenerator.Start(0, 0, currentTime + wasmVoiceParameter.AttackDurationSeconds + wasmVoiceParameter.ReleaseDurationSeconds); + } - internal void StopNow() - { - if (NoiseGenerator == null) - return; - NoiseGenerator!.Stop(); - NoiseGenerator = null; // Make sure the NoiseGenerator is not reused. After .Stop() it isn't designed be used anymore. - _addDebugMessage($"Stopped and removed NoiseGenerator."); - } + internal void StopNow() + { + if (NoiseGenerator == null) + return; + NoiseGenerator!.Stop(); + NoiseGenerator = null; // Make sure the NoiseGenerator is not reused. After .Stop() it isn't designed be used anymore. + _addDebugMessage($"Stopped and removed NoiseGenerator."); + } - internal void StopLater(double when) - { - if (NoiseGenerator == null) - throw new DotNet6502Exception($"NoiseGenerator is null. Call Create() first."); - _addDebugMessage($"Planning stopp of NoiseGenerator: {when}"); - NoiseGenerator!.Stop(when); - } + internal void StopLater(double when) + { + if (NoiseGenerator == null) + throw new DotNet6502Exception($"NoiseGenerator is null. Call Create() first."); + _addDebugMessage($"Planning stopp of NoiseGenerator: {when}"); + NoiseGenerator!.Stop(when); + } - internal void Connect() - { - if (NoiseGenerator == null) - throw new DotNet6502Exception($"NoiseGenerator is null. Call Create() first."); - NoiseGenerator!.Connect(_c64WASMVoiceContext.GainNode!); - } + internal void Connect() + { + if (NoiseGenerator == null) + throw new DotNet6502Exception($"NoiseGenerator is null. Call Create() first."); + NoiseGenerator!.Connect(_c64WASMVoiceContext.GainNode!); + } - internal void Disconnect() - { - if (NoiseGenerator == null) - return; - NoiseGenerator!.Disconnect(); - } + internal void Disconnect() + { + if (NoiseGenerator == null) + return; + NoiseGenerator!.Disconnect(); + } - private void PrepareNoiseGenerator() - { - var noiseDuration = 1.0f; // Seconds - - var sampleRate = _audioHandlerContext.AudioContext.GetSampleRate(); - var bufferSize = (int)(sampleRate * noiseDuration); - // Create an empty buffer - _noiseBuffer = AudioBufferSync.Create( - _audioHandlerContext.AudioContext.WebAudioHelper, - _audioHandlerContext.JSRuntime, - new AudioBufferOptions - { - Length = bufferSize, - SampleRate = sampleRate, - }); - - // Note: Too slow to call Float32Array index in a loop - //var data = noiseBuffer.GetChannelData(0); - //var random = new Random(); - //for (var i = 0; i < bufferSize; i++) - //{ - // data[i] = ((float)random.NextDouble()) * 2 - 1; - //} - - // Optimized by filling a .NET array, and then creating a Float32Array from that array in one call. - var values = new float[bufferSize]; - var random = new Random(); - for (var i = 0; i < bufferSize; i++) + private void PrepareNoiseGenerator() + { + var noiseDuration = 1.0f; // Seconds + + var sampleRate = _audioHandlerContext.AudioContext.GetSampleRate(); + var bufferSize = (int)(sampleRate * noiseDuration); + // Create an empty buffer + _noiseBuffer = AudioBufferSync.Create( + _audioHandlerContext.AudioContext.WebAudioHelper, + _audioHandlerContext.JSRuntime, + new AudioBufferOptions { - values[i] = (float)random.NextDouble() * 2 - 1; - } - var data = Float32ArraySync.Create(_audioHandlerContext.AudioContext.WebAudioHelper, _audioHandlerContext.JSRuntime, values); - _noiseBuffer.CopyToChannel(data, 0); - } + Length = bufferSize, + SampleRate = sampleRate, + }); - internal float GetPlaybackRateFromFrequency(float frequency) + // Note: Too slow to call Float32Array index in a loop + //var data = noiseBuffer.GetChannelData(0); + //var random = new Random(); + //for (var i = 0; i < bufferSize; i++) + //{ + // data[i] = ((float)random.NextDouble()) * 2 - 1; + //} + + // Optimized by filling a .NET array, and then creating a Float32Array from that array in one call. + var values = new float[bufferSize]; + var random = new Random(); + for (var i = 0; i < bufferSize; i++) { - const float playbackRateMin = 0.0f; // Should be used for the minimum SID frequency ( 0 Hz) - const float playbackRateMax = 1.0f; // Should be used for the maximum SID frequency ( ca 4000 Hz) - const float sidFreqMin = 0; - const float sidFreqMax = 4000; - var playbackRate = playbackRateMin + (float)(frequency - sidFreqMin) / (sidFreqMax - sidFreqMin) * (playbackRateMax - playbackRateMin); - return playbackRate; + values[i] = (float)random.NextDouble() * 2 - 1; } + var data = Float32ArraySync.Create(_audioHandlerContext.AudioContext.WebAudioHelper, _audioHandlerContext.JSRuntime, values); + _noiseBuffer.CopyToChannel(data, 0); + } + + internal float GetPlaybackRateFromFrequency(float frequency) + { + const float playbackRateMin = 0.0f; // Should be used for the minimum SID frequency ( 0 Hz) + const float playbackRateMax = 1.0f; // Should be used for the maximum SID frequency ( ca 4000 Hz) + const float sidFreqMin = 0; + const float sidFreqMax = 4000; + var playbackRate = playbackRateMin + (float)(frequency - sidFreqMin) / (sidFreqMax - sidFreqMin) * (playbackRateMax - playbackRateMin); + return playbackRate; } } diff --git a/src/libraries/Highbyte.DotNet6502.Impl.AspNet/Commodore64/Audio/C64WASMPulseOscillator.cs b/src/libraries/Highbyte.DotNet6502.Impl.AspNet/Commodore64/Audio/C64WASMPulseOscillator.cs index e47d3908..19616764 100644 --- a/src/libraries/Highbyte.DotNet6502.Impl.AspNet/Commodore64/Audio/C64WASMPulseOscillator.cs +++ b/src/libraries/Highbyte.DotNet6502.Impl.AspNet/Commodore64/Audio/C64WASMPulseOscillator.cs @@ -1,144 +1,143 @@ using Highbyte.DotNet6502.Impl.AspNet.JSInterop.BlazorWebAudioSync; using Highbyte.DotNet6502.Impl.AspNet.JSInterop.BlazorWebAudioSync.Options; -namespace Highbyte.DotNet6502.Impl.AspNet.Commodore64.Audio -{ - public class C64WASMPulseOscillator - { - private readonly C64WASMVoiceContext _c64WASMVoiceContext; - private WASMAudioHandlerContext _audioHandlerContext => _c64WASMVoiceContext.AudioHandlerContext; +namespace Highbyte.DotNet6502.Impl.AspNet.Commodore64.Audio; - private Action _addDebugMessage => _c64WASMVoiceContext.AddDebugMessage; +public class C64WASMPulseOscillator +{ + private readonly C64WASMVoiceContext _c64WASMVoiceContext; + private WASMAudioHandlerContext _audioHandlerContext => _c64WASMVoiceContext.AudioHandlerContext; - // SID pulse oscillator - internal CustomPulseOscillatorNodeSync? PulseOscillator; - internal GainNodeSync? PulseWidthGainNode; - internal OscillatorNodeSync? LFOOscillator; + private Action _addDebugMessage => _c64WASMVoiceContext.AddDebugMessage; - public C64WASMPulseOscillator(C64WASMVoiceContext c64WASMVoiceContext) - { - _c64WASMVoiceContext = c64WASMVoiceContext; - } + // SID pulse oscillator + internal CustomPulseOscillatorNodeSync? PulseOscillator; + internal GainNodeSync? PulseWidthGainNode; + internal OscillatorNodeSync? LFOOscillator; - internal void Create(float frequency, float defaultPulseWidth) - { - // Create Pulse Oscillator - PulseOscillator = CustomPulseOscillatorNodeSync.Create( - _audioHandlerContext!.JSRuntime, - _audioHandlerContext.AudioContext, - new() - { - Frequency = frequency, - - //Pulse width - 1 to + 1 = ratio of the waveform's duty (power) cycle /mark-space - //DefaultWidth = -1.0 // 0% duty cycle - silent - //DefaultWidth = -0.5f // 25% duty cycle - //DefaultWidth = 0 // 50% duty cycle - //DefaultWidth = 0.5f // 75% duty cycle - //DefaultWidth = 1.0f // 100% duty cycle - DefaultWidth = defaultPulseWidth - }); + public C64WASMPulseOscillator(C64WASMVoiceContext c64WASMVoiceContext) + { + _c64WASMVoiceContext = c64WASMVoiceContext; + } - // Create Pulse Width GainNode for pulse width modulation - PulseWidthGainNode = GainNodeSync.Create( - _audioHandlerContext!.JSRuntime, - _audioHandlerContext.AudioContext, - new() + internal void Create(float frequency, float defaultPulseWidth) + { + // Create Pulse Oscillator + PulseOscillator = CustomPulseOscillatorNodeSync.Create( + _audioHandlerContext!.JSRuntime, + _audioHandlerContext.AudioContext, + new() + { + Frequency = frequency, + + //Pulse width - 1 to + 1 = ratio of the waveform's duty (power) cycle /mark-space + //DefaultWidth = -1.0 // 0% duty cycle - silent + //DefaultWidth = -0.5f // 25% duty cycle + //DefaultWidth = 0 // 50% duty cycle + //DefaultWidth = 0.5f // 75% duty cycle + //DefaultWidth = 1.0f // 100% duty cycle + DefaultWidth = defaultPulseWidth + }); + + // Create Pulse Width GainNode for pulse width modulation + PulseWidthGainNode = GainNodeSync.Create( + _audioHandlerContext!.JSRuntime, + _audioHandlerContext.AudioContext, + new() + { + Gain = 0 + }); + PulseWidthGainNode.Connect(PulseOscillator.WidthGainNode); + + // Create low frequency oscillator, use as base for Pulse Oscillator. + LFOOscillator = OscillatorNodeSync.Create( + _audioHandlerContext!.JSRuntime, + _audioHandlerContext.AudioContext, + new OscillatorOptions { - Gain = 0 + Type = OscillatorType.Triangle, + Frequency = 10 }); - PulseWidthGainNode.Connect(PulseOscillator.WidthGainNode); - // Create low frequency oscillator, use as base for Pulse Oscillator. - LFOOscillator = OscillatorNodeSync.Create( - _audioHandlerContext!.JSRuntime, - _audioHandlerContext.AudioContext, - new OscillatorOptions - { - Type = OscillatorType.Triangle, - Frequency = 10 - }); + //LFOOscillator.Connect(detuneDepth); + LFOOscillator.Connect(PulseWidthGainNode); - //LFOOscillator.Connect(detuneDepth); - LFOOscillator.Connect(PulseWidthGainNode); + } - } + internal void Start() + { + if (PulseOscillator == null) + throw new DotNet6502Exception($"PulseOscillator is null. Call CreatePulseOscillator() first."); + _addDebugMessage($"Starting PulseOscillator and LFOOscillator"); + PulseOscillator!.Start(); + LFOOscillator!.Start(); + } - internal void Start() - { - if (PulseOscillator == null) - throw new DotNet6502Exception($"PulseOscillator is null. Call CreatePulseOscillator() first."); - _addDebugMessage($"Starting PulseOscillator and LFOOscillator"); - PulseOscillator!.Start(); - LFOOscillator!.Start(); - } + internal void StopNow() + { + if (PulseOscillator == null) + return; + PulseOscillator!.Stop(); + LFOOscillator!.Stop(); + PulseWidthGainNode!.Disconnect(); + PulseOscillator = null; // Make sure the oscillator is not reused. After .Stop() it isn't designed be used anymore. + LFOOscillator = null; + PulseWidthGainNode = null; + _addDebugMessage($"Stopped and removed PulseOscillator, LFOOscillator, and related resources."); + } - internal void StopNow() - { - if (PulseOscillator == null) - return; - PulseOscillator!.Stop(); - LFOOscillator!.Stop(); - PulseWidthGainNode!.Disconnect(); - PulseOscillator = null; // Make sure the oscillator is not reused. After .Stop() it isn't designed be used anymore. - LFOOscillator = null; - PulseWidthGainNode = null; - _addDebugMessage($"Stopped and removed PulseOscillator, LFOOscillator, and related resources."); - } + internal void StopLater(double when) + { + if (PulseOscillator == null) + throw new DotNet6502Exception($"PulseOscillator is null. Call Create() first."); + _addDebugMessage($"Planning stopp of PulseOscillator and LFOOscillator: {when}"); + PulseOscillator!.Stop(when); + LFOOscillator!.Stop(when); + } - internal void StopLater(double when) - { - if (PulseOscillator == null) - throw new DotNet6502Exception($"PulseOscillator is null. Call Create() first."); - _addDebugMessage($"Planning stopp of PulseOscillator and LFOOscillator: {when}"); - PulseOscillator!.Stop(when); - LFOOscillator!.Stop(when); - } + internal void Connect() + { + if (PulseOscillator == null) + throw new DotNet6502Exception($"PulseOscillator is null. Call Create() first."); + PulseOscillator!.Connect(_c64WASMVoiceContext.GainNode!); + } - internal void Connect() - { - if (PulseOscillator == null) - throw new DotNet6502Exception($"PulseOscillator is null. Call Create() first."); - PulseOscillator!.Connect(_c64WASMVoiceContext.GainNode!); - } + internal void Disconnect() + { + if (PulseOscillator == null) + return; + PulseOscillator!.Disconnect(); + } - internal void Disconnect() - { - if (PulseOscillator == null) - return; - PulseOscillator!.Disconnect(); - } + internal void SetPulseWidthDepthADSR(double currentTime) + { + // Set Pulse Width ADSR (will start playing immediately if oscillator is already started) + var widthDepthGainNodeAudioParam = PulseWidthGainNode!.GetGain(); + var oscWidthDepth = 0.5f; // LFO depth - Pulse modulation depth (percent) // TODO: Configurable? + var oscWidthAttack = 0.05f; // TODO: Configurable? + //var oscWidthDecay = 0.4f; // TODO: Configurable? + var oscWidthSustain = 0.4f; // TODO: Configurable? + var oscWidthRelease = 0.4f; // TODO: Configurable? + var widthDepthSustainTime = currentTime + oscWidthAttack + oscWidthRelease; + widthDepthGainNodeAudioParam.CancelScheduledValues(currentTime); + widthDepthGainNodeAudioParam.LinearRampToValueAtTime(0.5f * oscWidthDepth, currentTime + oscWidthAttack); + widthDepthGainNodeAudioParam.LinearRampToValueAtTime(0.5f * oscWidthDepth * oscWidthSustain, widthDepthSustainTime); + widthDepthGainNodeAudioParam.LinearRampToValueAtTime(0, oscWidthSustain + oscWidthRelease); - internal void SetPulseWidthDepthADSR(double currentTime) - { - // Set Pulse Width ADSR (will start playing immediately if oscillator is already started) - var widthDepthGainNodeAudioParam = PulseWidthGainNode!.GetGain(); - var oscWidthDepth = 0.5f; // LFO depth - Pulse modulation depth (percent) // TODO: Configurable? - var oscWidthAttack = 0.05f; // TODO: Configurable? - //var oscWidthDecay = 0.4f; // TODO: Configurable? - var oscWidthSustain = 0.4f; // TODO: Configurable? - var oscWidthRelease = 0.4f; // TODO: Configurable? - var widthDepthSustainTime = currentTime + oscWidthAttack + oscWidthRelease; - widthDepthGainNodeAudioParam.CancelScheduledValues(currentTime); - widthDepthGainNodeAudioParam.LinearRampToValueAtTime(0.5f * oscWidthDepth, currentTime + oscWidthAttack); - widthDepthGainNodeAudioParam.LinearRampToValueAtTime(0.5f * oscWidthDepth * oscWidthSustain, widthDepthSustainTime); - widthDepthGainNodeAudioParam.LinearRampToValueAtTime(0, oscWidthSustain + oscWidthRelease); + } - } + internal void SetPulseWidth(float pulseWidth, double changeTime) + { + var widthDepthGainNodeAudioParam = PulseWidthGainNode!.GetGain(); - internal void SetPulseWidth(float pulseWidth, double changeTime) + // Check if the pulse width of the actual oscillator is different from the new frequency + // TODO: Is this necessary to check? Could the pulse width have been changed in other way? + var currentPulseWidthValue = widthDepthGainNodeAudioParam.GetCurrentValue(); + if (currentPulseWidthValue != pulseWidth) { - var widthDepthGainNodeAudioParam = PulseWidthGainNode!.GetGain(); - - // Check if the pulse width of the actual oscillator is different from the new frequency - // TODO: Is this necessary to check? Could the pulse width have been changed in other way? - var currentPulseWidthValue = widthDepthGainNodeAudioParam.GetCurrentValue(); - if (currentPulseWidthValue != pulseWidth) - { - _addDebugMessage($"Changing pulse width to {pulseWidth}."); - widthDepthGainNodeAudioParam.SetValueAtTime(pulseWidth, changeTime); - } + _addDebugMessage($"Changing pulse width to {pulseWidth}."); + widthDepthGainNodeAudioParam.SetValueAtTime(pulseWidth, changeTime); } } } diff --git a/src/libraries/Highbyte.DotNet6502.Impl.AspNet/Commodore64/Audio/C64WASMSawToothOscillator.cs b/src/libraries/Highbyte.DotNet6502.Impl.AspNet/Commodore64/Audio/C64WASMSawToothOscillator.cs index 4c7152c4..f1973d0c 100644 --- a/src/libraries/Highbyte.DotNet6502.Impl.AspNet/Commodore64/Audio/C64WASMSawToothOscillator.cs +++ b/src/libraries/Highbyte.DotNet6502.Impl.AspNet/Commodore64/Audio/C64WASMSawToothOscillator.cs @@ -2,73 +2,72 @@ using Highbyte.DotNet6502.Impl.AspNet.JSInterop.BlazorWebAudioSync; using Highbyte.DotNet6502.Impl.AspNet.JSInterop.BlazorWebAudioSync.Options; -namespace Highbyte.DotNet6502.Impl.AspNet.Commodore64.Audio +namespace Highbyte.DotNet6502.Impl.AspNet.Commodore64.Audio; + +public class C64WASMSawToothOscillator { - public class C64WASMSawToothOscillator - { - private readonly C64WASMVoiceContext _c64WASMVoiceContext; - private WASMAudioHandlerContext _audioHandlerContext => _c64WASMVoiceContext.AudioHandlerContext; + private readonly C64WASMVoiceContext _c64WASMVoiceContext; + private WASMAudioHandlerContext _audioHandlerContext => _c64WASMVoiceContext.AudioHandlerContext; - private Action _addDebugMessage => _c64WASMVoiceContext.AddDebugMessage; + private Action _addDebugMessage => _c64WASMVoiceContext.AddDebugMessage; - // SID SawTooth Oscillator - internal OscillatorNodeSync? SawToothOscillator; + // SID SawTooth Oscillator + internal OscillatorNodeSync? SawToothOscillator; - public C64WASMSawToothOscillator(C64WASMVoiceContext c64WASMVoiceContext) - { - _c64WASMVoiceContext = c64WASMVoiceContext; - } + public C64WASMSawToothOscillator(C64WASMVoiceContext c64WASMVoiceContext) + { + _c64WASMVoiceContext = c64WASMVoiceContext; + } - internal void Create(float frequency) - { - // Create SawTooth Oscillator - SawToothOscillator = OscillatorNodeSync.Create( - _audioHandlerContext!.JSRuntime, - _audioHandlerContext.AudioContext, - new() - { - Type = OscillatorType.Sawtooth, - Frequency = frequency, - }); - } + internal void Create(float frequency) + { + // Create SawTooth Oscillator + SawToothOscillator = OscillatorNodeSync.Create( + _audioHandlerContext!.JSRuntime, + _audioHandlerContext.AudioContext, + new() + { + Type = OscillatorType.Sawtooth, + Frequency = frequency, + }); + } - internal void Start() - { - if (SawToothOscillator == null) - throw new DotNet6502Exception($"SawToothOscillator is null. Call Create() first."); - _addDebugMessage($"Starting SawToothOscillator"); - SawToothOscillator!.Start(); - } + internal void Start() + { + if (SawToothOscillator == null) + throw new DotNet6502Exception($"SawToothOscillator is null. Call Create() first."); + _addDebugMessage($"Starting SawToothOscillator"); + SawToothOscillator!.Start(); + } - internal void StopNow() - { - if (SawToothOscillator == null) - return; - SawToothOscillator!.Stop(); - SawToothOscillator = null; // Make sure the oscillator is not reused. After .Stop() it isn't designed be used anymore. - _addDebugMessage($"Stopped and removed SawToothOscillator"); - } + internal void StopNow() + { + if (SawToothOscillator == null) + return; + SawToothOscillator!.Stop(); + SawToothOscillator = null; // Make sure the oscillator is not reused. After .Stop() it isn't designed be used anymore. + _addDebugMessage($"Stopped and removed SawToothOscillator"); + } - internal void StopLater(double when) - { - if (SawToothOscillator == null) - throw new DotNet6502Exception($"SawToothOscillator is null. Call Create() first."); - _addDebugMessage($"Planning stopp of SawToothOscillator: {when}"); - SawToothOscillator!.Stop(when); - } + internal void StopLater(double when) + { + if (SawToothOscillator == null) + throw new DotNet6502Exception($"SawToothOscillator is null. Call Create() first."); + _addDebugMessage($"Planning stopp of SawToothOscillator: {when}"); + SawToothOscillator!.Stop(when); + } - internal void Connect() - { - if (SawToothOscillator == null) - throw new DotNet6502Exception($"SawToothOscillator is null. Call Create() first."); - SawToothOscillator!.Connect(_c64WASMVoiceContext.GainNode!); - } + internal void Connect() + { + if (SawToothOscillator == null) + throw new DotNet6502Exception($"SawToothOscillator is null. Call Create() first."); + SawToothOscillator!.Connect(_c64WASMVoiceContext.GainNode!); + } - internal void Disconnect() - { - if (SawToothOscillator == null) - return; - SawToothOscillator!.Disconnect(); - } + internal void Disconnect() + { + if (SawToothOscillator == null) + return; + SawToothOscillator!.Disconnect(); } } diff --git a/src/libraries/Highbyte.DotNet6502.Impl.AspNet/Commodore64/Audio/C64WASMTriangleOscillator.cs b/src/libraries/Highbyte.DotNet6502.Impl.AspNet/Commodore64/Audio/C64WASMTriangleOscillator.cs index 11169172..aeceaabb 100644 --- a/src/libraries/Highbyte.DotNet6502.Impl.AspNet/Commodore64/Audio/C64WASMTriangleOscillator.cs +++ b/src/libraries/Highbyte.DotNet6502.Impl.AspNet/Commodore64/Audio/C64WASMTriangleOscillator.cs @@ -1,73 +1,72 @@ using Highbyte.DotNet6502.Impl.AspNet.JSInterop.BlazorWebAudioSync; using Highbyte.DotNet6502.Impl.AspNet.JSInterop.BlazorWebAudioSync.Options; -namespace Highbyte.DotNet6502.Impl.AspNet.Commodore64.Audio +namespace Highbyte.DotNet6502.Impl.AspNet.Commodore64.Audio; + +public class C64WASMTriangleOscillator { - public class C64WASMTriangleOscillator - { - private readonly C64WASMVoiceContext _c64WASMVoiceContext; - private WASMAudioHandlerContext _audioHandlerContext => _c64WASMVoiceContext.AudioHandlerContext; + private readonly C64WASMVoiceContext _c64WASMVoiceContext; + private WASMAudioHandlerContext _audioHandlerContext => _c64WASMVoiceContext.AudioHandlerContext; - private Action _addDebugMessage => _c64WASMVoiceContext.AddDebugMessage; + private Action _addDebugMessage => _c64WASMVoiceContext.AddDebugMessage; - // SID Triangle Oscillator - internal OscillatorNodeSync? TriangleOscillator; + // SID Triangle Oscillator + internal OscillatorNodeSync? TriangleOscillator; - public C64WASMTriangleOscillator(C64WASMVoiceContext c64WASMVoiceContext) - { - _c64WASMVoiceContext = c64WASMVoiceContext; - } + public C64WASMTriangleOscillator(C64WASMVoiceContext c64WASMVoiceContext) + { + _c64WASMVoiceContext = c64WASMVoiceContext; + } - internal void Create(float frequency) - { - // Create Triangle Oscillator - TriangleOscillator = OscillatorNodeSync.Create( - _audioHandlerContext!.JSRuntime, - _audioHandlerContext.AudioContext, - new() - { - Type = OscillatorType.Triangle, - Frequency = frequency, - }); - } + internal void Create(float frequency) + { + // Create Triangle Oscillator + TriangleOscillator = OscillatorNodeSync.Create( + _audioHandlerContext!.JSRuntime, + _audioHandlerContext.AudioContext, + new() + { + Type = OscillatorType.Triangle, + Frequency = frequency, + }); + } - internal void StopNow() - { - if (TriangleOscillator == null) - return; - TriangleOscillator!.Stop(); - TriangleOscillator = null; // Make sure the oscillator is not reused. After .Stop() it isn't designed be used anymore. - _addDebugMessage($"Stopped and removed TriangleOscillator"); - } + internal void StopNow() + { + if (TriangleOscillator == null) + return; + TriangleOscillator!.Stop(); + TriangleOscillator = null; // Make sure the oscillator is not reused. After .Stop() it isn't designed be used anymore. + _addDebugMessage($"Stopped and removed TriangleOscillator"); + } - internal void StopLater(double when) - { - if (TriangleOscillator == null) - throw new DotNet6502Exception($"TriangleOscillator is null. Call Create() first."); - _addDebugMessage($"Planning stopp of TriangleOscillator: {when}"); - TriangleOscillator!.Stop(when); - } + internal void StopLater(double when) + { + if (TriangleOscillator == null) + throw new DotNet6502Exception($"TriangleOscillator is null. Call Create() first."); + _addDebugMessage($"Planning stopp of TriangleOscillator: {when}"); + TriangleOscillator!.Stop(when); + } - internal void Start() - { - if (TriangleOscillator == null) - throw new DotNet6502Exception($"TriangleOscillator is null. Call Create() first."); - _addDebugMessage($"Starting TriangleOscillator"); - TriangleOscillator!.Start(); - } + internal void Start() + { + if (TriangleOscillator == null) + throw new DotNet6502Exception($"TriangleOscillator is null. Call Create() first."); + _addDebugMessage($"Starting TriangleOscillator"); + TriangleOscillator!.Start(); + } - internal void Connect() - { - if (TriangleOscillator == null) - throw new DotNet6502Exception($"TriangleOscillator is null. Call Create() first."); - TriangleOscillator!.Connect(_c64WASMVoiceContext.GainNode!); - } + internal void Connect() + { + if (TriangleOscillator == null) + throw new DotNet6502Exception($"TriangleOscillator is null. Call Create() first."); + TriangleOscillator!.Connect(_c64WASMVoiceContext.GainNode!); + } - internal void Disconnect() - { - if (TriangleOscillator == null) - return; - TriangleOscillator!.Disconnect(); - } + internal void Disconnect() + { + if (TriangleOscillator == null) + return; + TriangleOscillator!.Disconnect(); } } diff --git a/src/libraries/Highbyte.DotNet6502.Impl.AspNet/Commodore64/Audio/C64WASMVoiceContext.cs b/src/libraries/Highbyte.DotNet6502.Impl.AspNet/Commodore64/Audio/C64WASMVoiceContext.cs index fbcfcfe7..0cd0b46c 100644 --- a/src/libraries/Highbyte.DotNet6502.Impl.AspNet/Commodore64/Audio/C64WASMVoiceContext.cs +++ b/src/libraries/Highbyte.DotNet6502.Impl.AspNet/Commodore64/Audio/C64WASMVoiceContext.cs @@ -2,544 +2,543 @@ using Highbyte.DotNet6502.Impl.AspNet.JSInterop.BlazorWebAudioSync; using Highbyte.DotNet6502.Systems.Commodore64.Audio; -namespace Highbyte.DotNet6502.Impl.AspNet.Commodore64.Audio +namespace Highbyte.DotNet6502.Impl.AspNet.Commodore64.Audio; + +public class C64WASMVoiceContext { - public class C64WASMVoiceContext - { - private C64WASMAudioHandler _audioHandler = default!; - internal GainNodeSync? GainNode { get; private set; } + private C64WASMAudioHandler _audioHandler = default!; + internal GainNodeSync? GainNode { get; private set; } - internal WASMAudioHandlerContext AudioHandlerContext => _audioHandler.AudioHandlerContext!; - private AudioContextSync _audioContext => AudioHandlerContext.AudioContext; + internal WASMAudioHandlerContext AudioHandlerContext => _audioHandler.AudioHandlerContext!; + private AudioContextSync _audioContext => AudioHandlerContext.AudioContext; - private Action _addDebugMessage = default!; + private Action _addDebugMessage = default!; - internal void AddDebugMessage(string msg) - { - _addDebugMessage(msg, _voice, CurrentSidVoiceWaveForm, Status); - } + internal void AddDebugMessage(string msg) + { + _addDebugMessage(msg, _voice, CurrentSidVoiceWaveForm, Status); + } - private readonly byte _voice; - public byte Voice => _voice; - public AudioVoiceStatus Status = AudioVoiceStatus.Stopped; - public SidVoiceWaveForm CurrentSidVoiceWaveForm = SidVoiceWaveForm.None; + private readonly byte _voice; + public byte Voice => _voice; + public AudioVoiceStatus Status = AudioVoiceStatus.Stopped; + public SidVoiceWaveForm CurrentSidVoiceWaveForm = SidVoiceWaveForm.None; - // SID Triangle Oscillator - public C64WASMTriangleOscillator C64WASMTriangleOscillator { get; private set; } = default!; + // SID Triangle Oscillator + public C64WASMTriangleOscillator C64WASMTriangleOscillator { get; private set; } = default!; - // SID Sawtooth Oscillator - public C64WASMSawToothOscillator C64WASMSawToothOscillator { get; private set; } = default!; + // SID Sawtooth Oscillator + public C64WASMSawToothOscillator C64WASMSawToothOscillator { get; private set; } = default!; - // SID pulse oscillator - public C64WASMPulseOscillator C64WASMPulseOscillator { get; private set; } = default!; + // SID pulse oscillator + public C64WASMPulseOscillator C64WASMPulseOscillator { get; private set; } = default!; - // SID noise oscillator - public C64WASMNoiseOscillator C64WASMNoiseOscillator { get; private set; } = default!; + // SID noise oscillator + public C64WASMNoiseOscillator C64WASMNoiseOscillator { get; private set; } = default!; - private EventListener _audioStoppedCallback = default!; + private EventListener _audioStoppedCallback = default!; #pragma warning disable IDE0052 // Remove unread private members - private Timer _adsCycleCompleteTimer = default!; - private Timer _releaseCycleCompleteTimer = default!; + private Timer _adsCycleCompleteTimer = default!; + private Timer _releaseCycleCompleteTimer = default!; #pragma warning restore IDE0052 // Remove unread private members - //private readonly SemaphoreSlim _semaphoreSlim = new(1); - //public SemaphoreSlim SemaphoreSlim => _semaphoreSlim; + //private readonly SemaphoreSlim _semaphoreSlim = new(1); + //public SemaphoreSlim SemaphoreSlim => _semaphoreSlim; - public C64WASMVoiceContext(byte voice) - { - _voice = voice; - } + public C64WASMVoiceContext(byte voice) + { + _voice = voice; + } - internal void Init( - C64WASMAudioHandler audioHandler, - Action addDebugMessage) - { - Status = AudioVoiceStatus.Stopped; + internal void Init( + C64WASMAudioHandler audioHandler, + Action addDebugMessage) + { + Status = AudioVoiceStatus.Stopped; - _audioHandler = audioHandler; - _addDebugMessage = addDebugMessage; + _audioHandler = audioHandler; + _addDebugMessage = addDebugMessage; - // Create gain node to use for a specfic voice. Used internally to be able to turn off audio without stopping the oscillator. - GainNode = GainNodeSync.Create(_audioContext.JSRuntime, _audioContext); - // Connect the gain node to the common SID volume gain node - GainNode.Connect(_audioHandler.CommonSIDGainNode); + // Create gain node to use for a specfic voice. Used internally to be able to turn off audio without stopping the oscillator. + GainNode = GainNodeSync.Create(_audioContext.JSRuntime, _audioContext); + // Connect the gain node to the common SID volume gain node + GainNode.Connect(_audioHandler.CommonSIDGainNode); - // Create implementations of the different oscillators - C64WASMTriangleOscillator = new C64WASMTriangleOscillator(this); - C64WASMSawToothOscillator = new C64WASMSawToothOscillator(this); - C64WASMPulseOscillator = new C64WASMPulseOscillator(this); - C64WASMNoiseOscillator = new C64WASMNoiseOscillator(this); + // Create implementations of the different oscillators + C64WASMTriangleOscillator = new C64WASMTriangleOscillator(this); + C64WASMSawToothOscillator = new C64WASMSawToothOscillator(this); + C64WASMPulseOscillator = new C64WASMPulseOscillator(this); + C64WASMNoiseOscillator = new C64WASMNoiseOscillator(this); - if (_audioHandler.StopAndRecreateOscillator) + if (_audioHandler.StopAndRecreateOscillator) + { + // Define callback handler to know when an oscillator has stopped playing. Only used if creating + starting oscillators before each audio. + _audioStoppedCallback = EventListener.Create(_audioContext.WebAudioHelper, _audioContext.JSRuntime, (e) => { - // Define callback handler to know when an oscillator has stopped playing. Only used if creating + starting oscillators before each audio. - _audioStoppedCallback = EventListener.Create(_audioContext.WebAudioHelper, _audioContext.JSRuntime, (e) => - { - AddDebugMessage($"Oscillator Stop Callback triggered."); - Stop(); - }); - } - else + AddDebugMessage($"Oscillator Stop Callback triggered."); + Stop(); + }); + } + else + { + // Unless we won't recreate/start the oscillator before each audio, create and start oscillators in advance + foreach (var sidWaveFormType in Enum.GetValues()) { - // Unless we won't recreate/start the oscillator before each audio, create and start oscillators in advance - foreach (var sidWaveFormType in Enum.GetValues()) + var audioVoiceParameter = new AudioVoiceParameter { - var audioVoiceParameter = new AudioVoiceParameter - { - SIDOscillatorType = sidWaveFormType, - Frequency = 300f, - PulseWidth = -0.22f, - }; - CreateOscillator(audioVoiceParameter); - //ConnectOscillator(audioVoiceParameter.SIDOscillatorType); - - // In this scenario the WebAudio oscilltor has to be running all the time (it can only be started/stopped once). - // As the gain is 0, no sound will play. When a SID sound started to play, a ADS envelope (gain variation over time) is scheduled on the current time. - StartOscillator(audioVoiceParameter.SIDOscillatorType); - } - } + SIDOscillatorType = sidWaveFormType, + Frequency = 300f, + PulseWidth = -0.22f, + }; + CreateOscillator(audioVoiceParameter); + //ConnectOscillator(audioVoiceParameter.SIDOscillatorType); - CurrentSidVoiceWaveForm = SidVoiceWaveForm.None; + // In this scenario the WebAudio oscilltor has to be running all the time (it can only be started/stopped once). + // As the gain is 0, no sound will play. When a SID sound started to play, a ADS envelope (gain variation over time) is scheduled on the current time. + StartOscillator(audioVoiceParameter.SIDOscillatorType); + } } - private void ScheduleAudioStopAfterDecay(int waitMs) - { - // Set timer to stop audio after a while via a .NET timer - _adsCycleCompleteTimer = new Timer((_) => - { - AddDebugMessage($"Scheduled Stop after Decay triggered."); - Stop(); - }, null, waitMs, Timeout.Infinite); - } + CurrentSidVoiceWaveForm = SidVoiceWaveForm.None; + } - private void ScheduleAudioStopAfterRelease(double releaseDurationSeconds) + private void ScheduleAudioStopAfterDecay(int waitMs) + { + // Set timer to stop audio after a while via a .NET timer + _adsCycleCompleteTimer = new Timer((_) => { - AddDebugMessage($"Scheduling voice stop at now + {releaseDurationSeconds} seconds."); + AddDebugMessage($"Scheduled Stop after Decay triggered."); + Stop(); + }, null, waitMs, Timeout.Infinite); + } - // Schedule Stop for oscillator and other audio sources) when the Release period if over - //voiceContext.Oscillator?.Stop(currentTime + audioVoiceParameter.ReleaseDurationSeconds); - //voiceContext.PulseOscillator?.Stop(currentTime + audioVoiceParameter.ReleaseDurationSeconds); - //voiceContext.NoiseGenerator?.Stop(currentTime + audioVoiceParameter.ReleaseDurationSeconds); + private void ScheduleAudioStopAfterRelease(double releaseDurationSeconds) + { + AddDebugMessage($"Scheduling voice stop at now + {releaseDurationSeconds} seconds."); - var waitMs = (int)(releaseDurationSeconds * 1000.0d); - // Set timer to stop audio after a while via a .NET timer - _releaseCycleCompleteTimer = new Timer((_) => - { - AddDebugMessage($"Scheduled Stop after Release triggered."); - Stop(); - }, null, waitMs, Timeout.Infinite); - } + // Schedule Stop for oscillator and other audio sources) when the Release period if over + //voiceContext.Oscillator?.Stop(currentTime + audioVoiceParameter.ReleaseDurationSeconds); + //voiceContext.PulseOscillator?.Stop(currentTime + audioVoiceParameter.ReleaseDurationSeconds); + //voiceContext.NoiseGenerator?.Stop(currentTime + audioVoiceParameter.ReleaseDurationSeconds); - internal void Stop() + var waitMs = (int)(releaseDurationSeconds * 1000.0d); + // Set timer to stop audio after a while via a .NET timer + _releaseCycleCompleteTimer = new Timer((_) => { - AddDebugMessage($"Stop issued"); + AddDebugMessage($"Scheduled Stop after Release triggered."); + Stop(); + }, null, waitMs, Timeout.Infinite); + } - if (_audioHandler.StopAndRecreateOscillator) - { - // This is called either via callback when oscillator sent "ended" event, or manually stopped via turning off SID gate. - if (Status != AudioVoiceStatus.Stopped) - StopOscillatorNow(CurrentSidVoiceWaveForm); - CurrentSidVoiceWaveForm = SidVoiceWaveForm.None; - } - else - { - // In this scenario, the oscillator is still running. Set volume to 0 in the CommonSIDGainNode to ensure no audio is playing. - AddDebugMessage($"Cancelling current CommonSIDGainNode schedule"); - var gainAudioParam = GainNode!.GetGain(); - var currentTime = _audioContext.GetCurrentTime(); - gainAudioParam.CancelScheduledValues(currentTime); - gainAudioParam.SetValueAtTime(0, currentTime); - - // If configured, disconnect the oscillator when stopping - if (_audioHandler.DisconnectOscillatorOnStop) - { - DisconnectOscillator(CurrentSidVoiceWaveForm); - CurrentSidVoiceWaveForm = SidVoiceWaveForm.None; - } - } + internal void Stop() + { + AddDebugMessage($"Stop issued"); + if (_audioHandler.StopAndRecreateOscillator) + { + // This is called either via callback when oscillator sent "ended" event, or manually stopped via turning off SID gate. if (Status != AudioVoiceStatus.Stopped) - { - Status = AudioVoiceStatus.Stopped; - AddDebugMessage($"Status changed."); - } - else - { - AddDebugMessage($"Status already was Stopped"); - } + StopOscillatorNow(CurrentSidVoiceWaveForm); + CurrentSidVoiceWaveForm = SidVoiceWaveForm.None; } - - internal void StopAllOscillatorsNow() + else { - foreach (var sidWaveFormType in Enum.GetValues()) + // In this scenario, the oscillator is still running. Set volume to 0 in the CommonSIDGainNode to ensure no audio is playing. + AddDebugMessage($"Cancelling current CommonSIDGainNode schedule"); + var gainAudioParam = GainNode!.GetGain(); + var currentTime = _audioContext.GetCurrentTime(); + gainAudioParam.CancelScheduledValues(currentTime); + gainAudioParam.SetValueAtTime(0, currentTime); + + // If configured, disconnect the oscillator when stopping + if (_audioHandler.DisconnectOscillatorOnStop) { - StopOscillatorNow(sidWaveFormType); + DisconnectOscillator(CurrentSidVoiceWaveForm); + CurrentSidVoiceWaveForm = SidVoiceWaveForm.None; } } - private void StopOscillatorNow(SidVoiceWaveForm sidVoiceWaveForm) + if (Status != AudioVoiceStatus.Stopped) { - AddDebugMessage($"Stopping oscillator: {sidVoiceWaveForm}"); - - // Switch on sidVoiceForm - switch (sidVoiceWaveForm) - { - case SidVoiceWaveForm.None: - break; - case SidVoiceWaveForm.Triangle: - C64WASMTriangleOscillator?.StopNow(); - break; - case SidVoiceWaveForm.Sawtooth: - C64WASMSawToothOscillator?.StopNow(); - break; - case SidVoiceWaveForm.Pulse: - C64WASMPulseOscillator?.StopNow(); - break; - case SidVoiceWaveForm.RandomNoise: - C64WASMNoiseOscillator?.StopNow(); - break; - default: - break; - } + Status = AudioVoiceStatus.Stopped; + AddDebugMessage($"Status changed."); } - - private void StopOscillatorLater(SidVoiceWaveForm sidVoiceWaveForm, double when) + else { - AddDebugMessage($"Stopping oscillator: {sidVoiceWaveForm} later"); - - // Switch on sidVoiceForm - switch (sidVoiceWaveForm) - { - case SidVoiceWaveForm.None: - break; - case SidVoiceWaveForm.Triangle: - C64WASMTriangleOscillator?.StopLater(when); - break; - case SidVoiceWaveForm.Sawtooth: - C64WASMSawToothOscillator?.StopLater(when); - break; - case SidVoiceWaveForm.Pulse: - C64WASMPulseOscillator?.StopLater(when); - break; - case SidVoiceWaveForm.RandomNoise: - C64WASMNoiseOscillator?.StopLater(when); - break; - default: - break; - } + AddDebugMessage($"Status already was Stopped"); } + } - private void ConnectOscillator(SidVoiceWaveForm sidVoiceWaveForm) + internal void StopAllOscillatorsNow() + { + foreach (var sidWaveFormType in Enum.GetValues()) { - AddDebugMessage($"Connecting oscillator: {sidVoiceWaveForm}"); - switch (sidVoiceWaveForm) - { - case SidVoiceWaveForm.None: - break; - case SidVoiceWaveForm.Triangle: - C64WASMTriangleOscillator?.Connect(); - break; - case SidVoiceWaveForm.Sawtooth: - C64WASMSawToothOscillator?.Connect(); - break; - case SidVoiceWaveForm.Pulse: - C64WASMPulseOscillator?.Connect(); - break; - case SidVoiceWaveForm.RandomNoise: - C64WASMNoiseOscillator?.Connect(); - break; - default: - break; - } + StopOscillatorNow(sidWaveFormType); } + } - private void DisconnectOscillator(SidVoiceWaveForm sidVoiceWaveForm) - { - AddDebugMessage($"Disconnecting oscillator: {sidVoiceWaveForm}"); + private void StopOscillatorNow(SidVoiceWaveForm sidVoiceWaveForm) + { + AddDebugMessage($"Stopping oscillator: {sidVoiceWaveForm}"); - // Switch on sidVoiceForm - switch (sidVoiceWaveForm) - { - case SidVoiceWaveForm.None: - break; - case SidVoiceWaveForm.Triangle: - C64WASMTriangleOscillator?.Disconnect(); - break; - case SidVoiceWaveForm.Sawtooth: - C64WASMSawToothOscillator?.Disconnect(); - break; - case SidVoiceWaveForm.Pulse: - C64WASMPulseOscillator?.Disconnect(); - break; - case SidVoiceWaveForm.RandomNoise: - C64WASMNoiseOscillator?.Disconnect(); - break; - default: - break; - } + // Switch on sidVoiceForm + switch (sidVoiceWaveForm) + { + case SidVoiceWaveForm.None: + break; + case SidVoiceWaveForm.Triangle: + C64WASMTriangleOscillator?.StopNow(); + break; + case SidVoiceWaveForm.Sawtooth: + C64WASMSawToothOscillator?.StopNow(); + break; + case SidVoiceWaveForm.Pulse: + C64WASMPulseOscillator?.StopNow(); + break; + case SidVoiceWaveForm.RandomNoise: + C64WASMNoiseOscillator?.StopNow(); + break; + default: + break; } + } - private void CreateOscillator(AudioVoiceParameter audioVoiceParameter) - { - AddDebugMessage($"Creating oscillator: {audioVoiceParameter.SIDOscillatorType}"); + private void StopOscillatorLater(SidVoiceWaveForm sidVoiceWaveForm, double when) + { + AddDebugMessage($"Stopping oscillator: {sidVoiceWaveForm} later"); - switch (audioVoiceParameter.SIDOscillatorType) - { - case SidVoiceWaveForm.None: - break; - case SidVoiceWaveForm.Triangle: - C64WASMTriangleOscillator?.Create(audioVoiceParameter.Frequency); - if (_audioHandler.StopAndRecreateOscillator) - C64WASMTriangleOscillator!.TriangleOscillator!.AddEndedEventListsner(_audioStoppedCallback); - break; - case SidVoiceWaveForm.Sawtooth: - C64WASMSawToothOscillator?.Create(audioVoiceParameter.Frequency); - if (_audioHandler.StopAndRecreateOscillator) - C64WASMSawToothOscillator!.SawToothOscillator!.AddEndedEventListsner(_audioStoppedCallback); - break; - case SidVoiceWaveForm.Pulse: - C64WASMPulseOscillator?.Create(audioVoiceParameter.Frequency, audioVoiceParameter.PulseWidth); - if (_audioHandler.StopAndRecreateOscillator) - C64WASMPulseOscillator!.PulseOscillator!.AddEndedEventListsner(_audioStoppedCallback); - break; - case SidVoiceWaveForm.RandomNoise: - var playbackRate = C64WASMNoiseOscillator.GetPlaybackRateFromFrequency(audioVoiceParameter.Frequency); - C64WASMNoiseOscillator?.Create(playbackRate); - if (_audioHandler.StopAndRecreateOscillator) - C64WASMNoiseOscillator!.NoiseGenerator!.AddEndedEventListsner(_audioStoppedCallback); - break; - default: - break; - } + // Switch on sidVoiceForm + switch (sidVoiceWaveForm) + { + case SidVoiceWaveForm.None: + break; + case SidVoiceWaveForm.Triangle: + C64WASMTriangleOscillator?.StopLater(when); + break; + case SidVoiceWaveForm.Sawtooth: + C64WASMSawToothOscillator?.StopLater(when); + break; + case SidVoiceWaveForm.Pulse: + C64WASMPulseOscillator?.StopLater(when); + break; + case SidVoiceWaveForm.RandomNoise: + C64WASMNoiseOscillator?.StopLater(when); + break; + default: + break; } + } - private void StartOscillator(SidVoiceWaveForm sidVoiceWaveForm) + private void ConnectOscillator(SidVoiceWaveForm sidVoiceWaveForm) + { + AddDebugMessage($"Connecting oscillator: {sidVoiceWaveForm}"); + switch (sidVoiceWaveForm) { - AddDebugMessage($"Starting oscillator: {sidVoiceWaveForm}"); + case SidVoiceWaveForm.None: + break; + case SidVoiceWaveForm.Triangle: + C64WASMTriangleOscillator?.Connect(); + break; + case SidVoiceWaveForm.Sawtooth: + C64WASMSawToothOscillator?.Connect(); + break; + case SidVoiceWaveForm.Pulse: + C64WASMPulseOscillator?.Connect(); + break; + case SidVoiceWaveForm.RandomNoise: + C64WASMNoiseOscillator?.Connect(); + break; + default: + break; + } + } - switch (sidVoiceWaveForm) - { - case SidVoiceWaveForm.None: - break; - case SidVoiceWaveForm.Triangle: - C64WASMTriangleOscillator?.Start(); - break; - case SidVoiceWaveForm.Sawtooth: - C64WASMSawToothOscillator?.Start(); - break; - case SidVoiceWaveForm.Pulse: - C64WASMPulseOscillator?.Start(); - break; - case SidVoiceWaveForm.RandomNoise: - C64WASMNoiseOscillator?.Start(); - break; - default: - break; - } + private void DisconnectOscillator(SidVoiceWaveForm sidVoiceWaveForm) + { + AddDebugMessage($"Disconnecting oscillator: {sidVoiceWaveForm}"); + + // Switch on sidVoiceForm + switch (sidVoiceWaveForm) + { + case SidVoiceWaveForm.None: + break; + case SidVoiceWaveForm.Triangle: + C64WASMTriangleOscillator?.Disconnect(); + break; + case SidVoiceWaveForm.Sawtooth: + C64WASMSawToothOscillator?.Disconnect(); + break; + case SidVoiceWaveForm.Pulse: + C64WASMPulseOscillator?.Disconnect(); + break; + case SidVoiceWaveForm.RandomNoise: + C64WASMNoiseOscillator?.Disconnect(); + break; + default: + break; } + } + + private void CreateOscillator(AudioVoiceParameter audioVoiceParameter) + { + AddDebugMessage($"Creating oscillator: {audioVoiceParameter.SIDOscillatorType}"); - private void SetOscillatorParameters(AudioVoiceParameter audioVoiceParameter, double currentTime) + switch (audioVoiceParameter.SIDOscillatorType) { - AddDebugMessage($"Setting oscillator parameters: {audioVoiceParameter.SIDOscillatorType}"); + case SidVoiceWaveForm.None: + break; + case SidVoiceWaveForm.Triangle: + C64WASMTriangleOscillator?.Create(audioVoiceParameter.Frequency); + if (_audioHandler.StopAndRecreateOscillator) + C64WASMTriangleOscillator!.TriangleOscillator!.AddEndedEventListsner(_audioStoppedCallback); + break; + case SidVoiceWaveForm.Sawtooth: + C64WASMSawToothOscillator?.Create(audioVoiceParameter.Frequency); + if (_audioHandler.StopAndRecreateOscillator) + C64WASMSawToothOscillator!.SawToothOscillator!.AddEndedEventListsner(_audioStoppedCallback); + break; + case SidVoiceWaveForm.Pulse: + C64WASMPulseOscillator?.Create(audioVoiceParameter.Frequency, audioVoiceParameter.PulseWidth); + if (_audioHandler.StopAndRecreateOscillator) + C64WASMPulseOscillator!.PulseOscillator!.AddEndedEventListsner(_audioStoppedCallback); + break; + case SidVoiceWaveForm.RandomNoise: + var playbackRate = C64WASMNoiseOscillator.GetPlaybackRateFromFrequency(audioVoiceParameter.Frequency); + C64WASMNoiseOscillator?.Create(playbackRate); + if (_audioHandler.StopAndRecreateOscillator) + C64WASMNoiseOscillator!.NoiseGenerator!.AddEndedEventListsner(_audioStoppedCallback); + break; + default: + break; + } + } - switch (audioVoiceParameter.SIDOscillatorType) - { - case SidVoiceWaveForm.None: - // Set frequency - SetFrequencyOnCurrentOscillator(audioVoiceParameter.Frequency, currentTime); - break; - case SidVoiceWaveForm.Triangle: - // Set frequency - SetFrequencyOnCurrentOscillator(audioVoiceParameter.Frequency, currentTime); - break; - case SidVoiceWaveForm.Sawtooth: - // Set frequency - SetFrequencyOnCurrentOscillator(audioVoiceParameter.Frequency, currentTime); - break; - case SidVoiceWaveForm.Pulse: - // Set frequency - SetFrequencyOnCurrentOscillator(audioVoiceParameter.Frequency, currentTime); - // Set pulsewidth - C64WASMPulseOscillator.SetPulseWidth(audioVoiceParameter.PulseWidth, currentTime); - // Set Pulse Width ADSR - C64WASMPulseOscillator.SetPulseWidthDepthADSR(currentTime); - break; - case SidVoiceWaveForm.RandomNoise: - // Set frequency (playback rate) on current NoiseGenerator - SetFrequencyOnCurrentOscillator(audioVoiceParameter.Frequency, currentTime); - break; - default: - break; - } + private void StartOscillator(SidVoiceWaveForm sidVoiceWaveForm) + { + AddDebugMessage($"Starting oscillator: {sidVoiceWaveForm}"); + + switch (sidVoiceWaveForm) + { + case SidVoiceWaveForm.None: + break; + case SidVoiceWaveForm.Triangle: + C64WASMTriangleOscillator?.Start(); + break; + case SidVoiceWaveForm.Sawtooth: + C64WASMSawToothOscillator?.Start(); + break; + case SidVoiceWaveForm.Pulse: + C64WASMPulseOscillator?.Start(); + break; + case SidVoiceWaveForm.RandomNoise: + C64WASMNoiseOscillator?.Start(); + break; + default: + break; } + } + + private void SetOscillatorParameters(AudioVoiceParameter audioVoiceParameter, double currentTime) + { + AddDebugMessage($"Setting oscillator parameters: {audioVoiceParameter.SIDOscillatorType}"); - private void SwitchOscillatorConnection(SidVoiceWaveForm newSidVoiceWaveForm) + switch (audioVoiceParameter.SIDOscillatorType) { - // If current oscillator is the same as the requested one, do nothing (assume it's already connected) - if (newSidVoiceWaveForm == CurrentSidVoiceWaveForm) - return; + case SidVoiceWaveForm.None: + // Set frequency + SetFrequencyOnCurrentOscillator(audioVoiceParameter.Frequency, currentTime); + break; + case SidVoiceWaveForm.Triangle: + // Set frequency + SetFrequencyOnCurrentOscillator(audioVoiceParameter.Frequency, currentTime); + break; + case SidVoiceWaveForm.Sawtooth: + // Set frequency + SetFrequencyOnCurrentOscillator(audioVoiceParameter.Frequency, currentTime); + break; + case SidVoiceWaveForm.Pulse: + // Set frequency + SetFrequencyOnCurrentOscillator(audioVoiceParameter.Frequency, currentTime); + // Set pulsewidth + C64WASMPulseOscillator.SetPulseWidth(audioVoiceParameter.PulseWidth, currentTime); + // Set Pulse Width ADSR + C64WASMPulseOscillator.SetPulseWidthDepthADSR(currentTime); + break; + case SidVoiceWaveForm.RandomNoise: + // Set frequency (playback rate) on current NoiseGenerator + SetFrequencyOnCurrentOscillator(audioVoiceParameter.Frequency, currentTime); + break; + default: + break; + } + } - // If any other oscillator is currently connected - if (CurrentSidVoiceWaveForm != SidVoiceWaveForm.None) - // Stop any existing playing audio will also disconnect it's oscillator - Stop(); + private void SwitchOscillatorConnection(SidVoiceWaveForm newSidVoiceWaveForm) + { + // If current oscillator is the same as the requested one, do nothing (assume it's already connected) + if (newSidVoiceWaveForm == CurrentSidVoiceWaveForm) + return; - // Then connect the specified oscillator - ConnectOscillator(newSidVoiceWaveForm); + // If any other oscillator is currently connected + if (CurrentSidVoiceWaveForm != SidVoiceWaveForm.None) + // Stop any existing playing audio will also disconnect it's oscillator + Stop(); - // Remember the new current oscillator - CurrentSidVoiceWaveForm = newSidVoiceWaveForm; - } + // Then connect the specified oscillator + ConnectOscillator(newSidVoiceWaveForm); - internal void StartAudioADSPhase(AudioVoiceParameter audioVoiceParameter) - { - var currentTime = _audioContext.GetCurrentTime(); + // Remember the new current oscillator + CurrentSidVoiceWaveForm = newSidVoiceWaveForm; + } - if (_audioHandler.StopAndRecreateOscillator) - { - // 1. Stop current oscillator (if any) and release it's resoruces. - // 2. Create new oscillator (even if same as before) - // With parameters such as Frequency, PulseWidth, etc. - // With Callback when ADSR envelope is finished to stop audio by stopping the oscillator (which then cannot be used anymore) - // 3. Connect oscillator to gain node - // 4. Set Gain ADSR envelope - // 5. Start oscillator -> This will start the audio + internal void StartAudioADSPhase(AudioVoiceParameter audioVoiceParameter) + { + var currentTime = _audioContext.GetCurrentTime(); - StopOscillatorNow(CurrentSidVoiceWaveForm); - CurrentSidVoiceWaveForm = audioVoiceParameter.SIDOscillatorType; - CreateOscillator(audioVoiceParameter); - ConnectOscillator(CurrentSidVoiceWaveForm); - SetGainADS(audioVoiceParameter, currentTime); - StartOscillator(CurrentSidVoiceWaveForm); - } - else + if (_audioHandler.StopAndRecreateOscillator) + { + // 1. Stop current oscillator (if any) and release it's resoruces. + // 2. Create new oscillator (even if same as before) + // With parameters such as Frequency, PulseWidth, etc. + // With Callback when ADSR envelope is finished to stop audio by stopping the oscillator (which then cannot be used anymore) + // 3. Connect oscillator to gain node + // 4. Set Gain ADSR envelope + // 5. Start oscillator -> This will start the audio + + StopOscillatorNow(CurrentSidVoiceWaveForm); + CurrentSidVoiceWaveForm = audioVoiceParameter.SIDOscillatorType; + CreateOscillator(audioVoiceParameter); + ConnectOscillator(CurrentSidVoiceWaveForm); + SetGainADS(audioVoiceParameter, currentTime); + StartOscillator(CurrentSidVoiceWaveForm); + } + else + { + // Assume oscillator is already created and started + // 1. Connect oscillator to gain node (and disconnect previous oscillator if different) + // 2. Set parameters on existing oscillator such as Frequency, PulseWidth, etc. + // 3. Set Gain ADSR envelope -> This will start the audio + // 4. Set Callback to stop audio by setting Gain to 0 when envelope is finished + + SwitchOscillatorConnection(audioVoiceParameter.SIDOscillatorType); + SetOscillatorParameters(audioVoiceParameter, currentTime); + SetGainADS(audioVoiceParameter, currentTime); + + // If SustainGain is 0, then we need to schedule a stop of the audio + // when the attack + decay period is over. + if (audioVoiceParameter.SustainGain == 0) { - // Assume oscillator is already created and started - // 1. Connect oscillator to gain node (and disconnect previous oscillator if different) - // 2. Set parameters on existing oscillator such as Frequency, PulseWidth, etc. - // 3. Set Gain ADSR envelope -> This will start the audio - // 4. Set Callback to stop audio by setting Gain to 0 when envelope is finished - - SwitchOscillatorConnection(audioVoiceParameter.SIDOscillatorType); - SetOscillatorParameters(audioVoiceParameter, currentTime); - SetGainADS(audioVoiceParameter, currentTime); - - // If SustainGain is 0, then we need to schedule a stop of the audio - // when the attack + decay period is over. - if (audioVoiceParameter.SustainGain == 0) - { - var waitSeconds = audioVoiceParameter.AttackDurationSeconds + audioVoiceParameter.DecayDurationSeconds; - AddDebugMessage($"Scheduling voice stop now + {waitSeconds} seconds."); - ScheduleAudioStopAfterDecay(waitMs: (int)(waitSeconds * 1000.0d)); - } + var waitSeconds = audioVoiceParameter.AttackDurationSeconds + audioVoiceParameter.DecayDurationSeconds; + AddDebugMessage($"Scheduling voice stop now + {waitSeconds} seconds."); + ScheduleAudioStopAfterDecay(waitMs: (int)(waitSeconds * 1000.0d)); } - - Status = AudioVoiceStatus.ADSCycleStarted; - AddDebugMessage($"Status changed"); } - internal void StartAudioReleasePhase(AudioVoiceParameter audioVoiceParameter) - { - var currentTime = _audioContext.GetCurrentTime(); - SetGainRelease(audioVoiceParameter, currentTime); + Status = AudioVoiceStatus.ADSCycleStarted; + AddDebugMessage($"Status changed"); + } - if (_audioHandler.StopAndRecreateOscillator) - { - // Plan oscillator built-in delayed stop with callback - StopOscillatorLater(CurrentSidVoiceWaveForm, currentTime + audioVoiceParameter.ReleaseDurationSeconds); - } - else - { - // Plan manual callback after release duration (as we don't stop the oscillator in this scenario, as it cannot be started again) - ScheduleAudioStopAfterRelease(audioVoiceParameter.ReleaseDurationSeconds); - } + internal void StartAudioReleasePhase(AudioVoiceParameter audioVoiceParameter) + { + var currentTime = _audioContext.GetCurrentTime(); + SetGainRelease(audioVoiceParameter, currentTime); - Status = AudioVoiceStatus.ReleaseCycleStarted; - AddDebugMessage($"Status changed"); + if (_audioHandler.StopAndRecreateOscillator) + { + // Plan oscillator built-in delayed stop with callback + StopOscillatorLater(CurrentSidVoiceWaveForm, currentTime + audioVoiceParameter.ReleaseDurationSeconds); } - - private void SetGainADS(AudioVoiceParameter audioVoiceParameter, double currentTime) + else { - AddDebugMessage($"Setting Attack ({audioVoiceParameter.AttackDurationSeconds}) Decay ({audioVoiceParameter.DecayDurationSeconds}) Sustain ({audioVoiceParameter.SustainGain})"); - - // Set Attack/Decay/Sustain gain envelope - var gainAudioParam = GainNode!.GetGain(); - gainAudioParam.CancelScheduledValues(currentTime); - gainAudioParam.SetValueAtTime(0, currentTime); - gainAudioParam.LinearRampToValueAtTime(1.0f, currentTime + audioVoiceParameter.AttackDurationSeconds); - gainAudioParam.LinearRampToValueAtTime(audioVoiceParameter.SustainGain, currentTime + audioVoiceParameter.AttackDurationSeconds + audioVoiceParameter.DecayDurationSeconds); - //gainAudioParam.SetTargetAtTime(audioVoiceParameter.SustainGain, currentTime + audioVoiceParameter.AttackDurationSeconds, audioVoiceParameter.DecayDurationSeconds); + // Plan manual callback after release duration (as we don't stop the oscillator in this scenario, as it cannot be started again) + ScheduleAudioStopAfterRelease(audioVoiceParameter.ReleaseDurationSeconds); } - private void SetGainRelease(AudioVoiceParameter audioVoiceParameter, double currentTime) - { - AddDebugMessage($"Setting Gain Release ({audioVoiceParameter.ReleaseDurationSeconds})"); + Status = AudioVoiceStatus.ReleaseCycleStarted; + AddDebugMessage($"Status changed"); + } - // Schedule a volume change from current gain level down to 0 during specified Release time - var gainAudioParam = GainNode!.GetGain(); - var currentGainValue = gainAudioParam.GetCurrentValue(); - gainAudioParam.CancelScheduledValues(currentTime); - gainAudioParam.SetValueAtTime(currentGainValue, currentTime); - gainAudioParam.LinearRampToValueAtTime(0, currentTime + audioVoiceParameter.ReleaseDurationSeconds); - } + private void SetGainADS(AudioVoiceParameter audioVoiceParameter, double currentTime) + { + AddDebugMessage($"Setting Attack ({audioVoiceParameter.AttackDurationSeconds}) Decay ({audioVoiceParameter.DecayDurationSeconds}) Sustain ({audioVoiceParameter.SustainGain})"); + + // Set Attack/Decay/Sustain gain envelope + var gainAudioParam = GainNode!.GetGain(); + gainAudioParam.CancelScheduledValues(currentTime); + gainAudioParam.SetValueAtTime(0, currentTime); + gainAudioParam.LinearRampToValueAtTime(1.0f, currentTime + audioVoiceParameter.AttackDurationSeconds); + gainAudioParam.LinearRampToValueAtTime(audioVoiceParameter.SustainGain, currentTime + audioVoiceParameter.AttackDurationSeconds + audioVoiceParameter.DecayDurationSeconds); + //gainAudioParam.SetTargetAtTime(audioVoiceParameter.SustainGain, currentTime + audioVoiceParameter.AttackDurationSeconds, audioVoiceParameter.DecayDurationSeconds); + } - internal void SetFrequencyOnCurrentOscillator(float frequency, double changeTime) - { - // Noise sample generator - if (CurrentSidVoiceWaveForm == SidVoiceWaveForm.RandomNoise) - { - var playbackRate = C64WASMNoiseOscillator.GetPlaybackRateFromFrequency(frequency); - var playbackRateAudioParam = C64WASMNoiseOscillator.NoiseGenerator!.GetPlaybackRate(); + private void SetGainRelease(AudioVoiceParameter audioVoiceParameter, double currentTime) + { + AddDebugMessage($"Setting Gain Release ({audioVoiceParameter.ReleaseDurationSeconds})"); + + // Schedule a volume change from current gain level down to 0 during specified Release time + var gainAudioParam = GainNode!.GetGain(); + var currentGainValue = gainAudioParam.GetCurrentValue(); + gainAudioParam.CancelScheduledValues(currentTime); + gainAudioParam.SetValueAtTime(currentGainValue, currentTime); + gainAudioParam.LinearRampToValueAtTime(0, currentTime + audioVoiceParameter.ReleaseDurationSeconds); + } - // Check if the playback rate of the actual audio buffer source is different from the new rate - // TODO: Is this necessary to check? Could the rate have been changed in other way? - var currentPlaybackRateValue = playbackRateAudioParam.GetCurrentValue(); - if (currentPlaybackRateValue != playbackRate) - { - AddDebugMessage($"Changing playback rate to {playbackRate} based on freq {frequency}"); - playbackRateAudioParam.SetValueAtTime(playbackRate, changeTime); - } - return; - } + internal void SetFrequencyOnCurrentOscillator(float frequency, double changeTime) + { + // Noise sample generator + if (CurrentSidVoiceWaveForm == SidVoiceWaveForm.RandomNoise) + { + var playbackRate = C64WASMNoiseOscillator.GetPlaybackRateFromFrequency(frequency); + var playbackRateAudioParam = C64WASMNoiseOscillator.NoiseGenerator!.GetPlaybackRate(); - // Normal oscillators - AudioParamSync frequencyAudioParam; - switch (CurrentSidVoiceWaveForm) + // Check if the playback rate of the actual audio buffer source is different from the new rate + // TODO: Is this necessary to check? Could the rate have been changed in other way? + var currentPlaybackRateValue = playbackRateAudioParam.GetCurrentValue(); + if (currentPlaybackRateValue != playbackRate) { - case SidVoiceWaveForm.None: - return; - case SidVoiceWaveForm.Triangle: - frequencyAudioParam = C64WASMTriangleOscillator!.TriangleOscillator!.GetFrequency(); - break; - case SidVoiceWaveForm.Sawtooth: - frequencyAudioParam = C64WASMSawToothOscillator!.SawToothOscillator!.GetFrequency(); - break; - case SidVoiceWaveForm.Pulse: - frequencyAudioParam = C64WASMPulseOscillator!.PulseOscillator!.GetFrequency(); - break; - default: - throw new NotImplementedException(); + AddDebugMessage($"Changing playback rate to {playbackRate} based on freq {frequency}"); + playbackRateAudioParam.SetValueAtTime(playbackRate, changeTime); } + return; + } - // Check if the frequency of the actual oscillator is different from the new frequency - // TODO: Is this necessary to check? Could the frequency have been changed in other way? - var currentFrequencyValue = frequencyAudioParam.GetCurrentValue(); - if (currentFrequencyValue != frequency) - { - // DEBUG START - //var gainAudioParam = CommonSIDGainNode!.GetGain(); - //var currentGainValue = gainAudioParam.GetCurrentValue(); - // END DEBUG + // Normal oscillators + AudioParamSync frequencyAudioParam; + switch (CurrentSidVoiceWaveForm) + { + case SidVoiceWaveForm.None: + return; + case SidVoiceWaveForm.Triangle: + frequencyAudioParam = C64WASMTriangleOscillator!.TriangleOscillator!.GetFrequency(); + break; + case SidVoiceWaveForm.Sawtooth: + frequencyAudioParam = C64WASMSawToothOscillator!.SawToothOscillator!.GetFrequency(); + break; + case SidVoiceWaveForm.Pulse: + frequencyAudioParam = C64WASMPulseOscillator!.PulseOscillator!.GetFrequency(); + break; + default: + throw new NotImplementedException(); + } - AddDebugMessage($"Changing freq to {frequency}."); - frequencyAudioParam.SetValueAtTime(frequency, changeTime); + // Check if the frequency of the actual oscillator is different from the new frequency + // TODO: Is this necessary to check? Could the frequency have been changed in other way? + var currentFrequencyValue = frequencyAudioParam.GetCurrentValue(); + if (currentFrequencyValue != frequency) + { + // DEBUG START + //var gainAudioParam = CommonSIDGainNode!.GetGain(); + //var currentGainValue = gainAudioParam.GetCurrentValue(); + // END DEBUG - // DEBUG START - // currentGainValue = gainAudioParam.GetCurrentValue(); - // END DEBUG - } + AddDebugMessage($"Changing freq to {frequency}."); + frequencyAudioParam.SetValueAtTime(frequency, changeTime); + + // DEBUG START + // currentGainValue = gainAudioParam.GetCurrentValue(); + // END DEBUG } } } diff --git a/src/libraries/Highbyte.DotNet6502.Impl.AspNet/Commodore64/Input/C64AspNetInputConfig.cs b/src/libraries/Highbyte.DotNet6502.Impl.AspNet/Commodore64/Input/C64AspNetInputConfig.cs index d71bf00b..7092ee84 100644 --- a/src/libraries/Highbyte.DotNet6502.Impl.AspNet/Commodore64/Input/C64AspNetInputConfig.cs +++ b/src/libraries/Highbyte.DotNet6502.Impl.AspNet/Commodore64/Input/C64AspNetInputConfig.cs @@ -1,42 +1,41 @@ using Highbyte.DotNet6502.Systems.Commodore64.TimerAndPeripheral; -namespace Highbyte.DotNet6502.Impl.AspNet.Commodore64.Input +namespace Highbyte.DotNet6502.Impl.AspNet.Commodore64.Input; + +public class C64AspNetInputConfig : ICloneable { - public class C64AspNetInputConfig : ICloneable - { - public int CurrentJoystick = 2; + public int CurrentJoystick = 2; - public List AvailableJoysticks = new() { 1, 2 }; + public List AvailableJoysticks = new() { 1, 2 }; - public Dictionary> GamePadToC64JoystickMap = new() + public Dictionary> GamePadToC64JoystickMap = new() + { { + 1, + new Dictionary { - 1, - new Dictionary - { - { new[] { 0 }, new[] { C64JoystickAction.Fire } }, - { new[] { 12 }, new[] { C64JoystickAction.Up} }, - { new[] { 13 }, new[] { C64JoystickAction.Down } }, - { new[] { 14 }, new[] { C64JoystickAction.Left } }, - { new[] { 15 }, new[] { C64JoystickAction.Right } }, - } - }, - { - 2, - new Dictionary - { - { new[] { 0 }, new[] { C64JoystickAction.Fire } }, - { new[] { 12 }, new[] { C64JoystickAction.Up} }, - { new[] { 13 }, new[] { C64JoystickAction.Down } }, - { new[] { 14 }, new[] { C64JoystickAction.Left } }, - { new[] { 15 }, new[] { C64JoystickAction.Right } }, - } + { new[] { 0 }, new[] { C64JoystickAction.Fire } }, + { new[] { 12 }, new[] { C64JoystickAction.Up} }, + { new[] { 13 }, new[] { C64JoystickAction.Down } }, + { new[] { 14 }, new[] { C64JoystickAction.Left } }, + { new[] { 15 }, new[] { C64JoystickAction.Right } }, } - }; - - public object Clone() + }, { - return MemberwiseClone(); + 2, + new Dictionary + { + { new[] { 0 }, new[] { C64JoystickAction.Fire } }, + { new[] { 12 }, new[] { C64JoystickAction.Up} }, + { new[] { 13 }, new[] { C64JoystickAction.Down } }, + { new[] { 14 }, new[] { C64JoystickAction.Left } }, + { new[] { 15 }, new[] { C64JoystickAction.Right } }, + } } + }; + + public object Clone() + { + return MemberwiseClone(); } } diff --git a/src/libraries/Highbyte.DotNet6502.Impl.AspNet/JSInterop/BlazorDOMSync/EventTargetSync.cs b/src/libraries/Highbyte.DotNet6502.Impl.AspNet/JSInterop/BlazorDOMSync/EventTargetSync.cs index 0eb1e77e..ffb4725c 100644 --- a/src/libraries/Highbyte.DotNet6502.Impl.AspNet/JSInterop/BlazorDOMSync/EventTargetSync.cs +++ b/src/libraries/Highbyte.DotNet6502.Impl.AspNet/JSInterop/BlazorDOMSync/EventTargetSync.cs @@ -6,38 +6,37 @@ using Highbyte.DotNet6502.Impl.AspNet.JSInterop.BlazorWebIDLSync; using Microsoft.JSInterop; -namespace Highbyte.DotNet6502.Impl.AspNet.JSInterop.BlazorDOMSync +namespace Highbyte.DotNet6502.Impl.AspNet.JSInterop.BlazorDOMSync; + +public class EventTargetSync : BaseJSWrapperSync { - public class EventTargetSync : BaseJSWrapperSync + public static EventTargetSync Create(IJSInProcessObjectReference helper, IJSRuntime jSRuntime, IJSInProcessObjectReference jSReference) + { + return new EventTargetSync(helper, jSRuntime, jSReference); + } + protected EventTargetSync(IJSInProcessObjectReference helper, IJSRuntime jSRuntime, IJSInProcessObjectReference jSReference) + : base(helper, jSRuntime, jSReference) { - public static EventTargetSync Create(IJSInProcessObjectReference helper, IJSRuntime jSRuntime, IJSInProcessObjectReference jSReference) - { - return new EventTargetSync(helper, jSRuntime, jSReference); - } - protected EventTargetSync(IJSInProcessObjectReference helper, IJSRuntime jSRuntime, IJSInProcessObjectReference jSReference) - : base(helper, jSRuntime, jSReference) - { - } + } - public void AddEventListener(string type, EventListener? callback, AddEventListenerOptions? options = null) where TEvent : EventSync, IJSWrapperSync - { - _helper.InvokeVoid("addEventListener", JSReference, type, callback?.JSReference, options); - } + public void AddEventListener(string type, EventListener? callback, AddEventListenerOptions? options = null) where TEvent : EventSync, IJSWrapperSync + { + _helper.InvokeVoid("addEventListener", JSReference, type, callback?.JSReference, options); + } - public void RemoveEventListener(string type, EventListener? callback, EventListenerOptions? options = null) where TEvent : EventSync, IJSWrapperSync - { - _helper.InvokeVoid("removeEventListener", JSReference, type, callback?.JSReference, options); - } + public void RemoveEventListener(string type, EventListener? callback, EventListenerOptions? options = null) where TEvent : EventSync, IJSWrapperSync + { + _helper.InvokeVoid("removeEventListener", JSReference, type, callback?.JSReference, options); + } - public void RemoveEventListener(EventListener? callback, EventListenerOptions? options = null) where TEvent : EventSync, IJSWrapperSync - { - _helper.InvokeVoid("removeEventListener", JSReference, typeof(TEvent)!.Name, callback?.JSReference, options); - } + public void RemoveEventListener(EventListener? callback, EventListenerOptions? options = null) where TEvent : EventSync, IJSWrapperSync + { + _helper.InvokeVoid("removeEventListener", JSReference, typeof(TEvent)!.Name, callback?.JSReference, options); + } - public bool DispatchEvent(EventSync eventInstance) - { - //return JSObjectReferenceExtensions.InvokeAsync(base.JSReference, "dispatchEvent", new object[1] { eventInstance.JSReference }).Result; - return JSReference.Invoke("dispatchEvent", new object[1] { eventInstance.JSReference }); - } + public bool DispatchEvent(EventSync eventInstance) + { + //return JSObjectReferenceExtensions.InvokeAsync(base.JSReference, "dispatchEvent", new object[1] { eventInstance.JSReference }).Result; + return JSReference.Invoke("dispatchEvent", new object[1] { eventInstance.JSReference }); } } diff --git a/src/libraries/Highbyte.DotNet6502.Impl.AspNet/WASMAudioHandlerContext.cs b/src/libraries/Highbyte.DotNet6502.Impl.AspNet/WASMAudioHandlerContext.cs index 8f4cfcc4..b713a149 100644 --- a/src/libraries/Highbyte.DotNet6502.Impl.AspNet/WASMAudioHandlerContext.cs +++ b/src/libraries/Highbyte.DotNet6502.Impl.AspNet/WASMAudioHandlerContext.cs @@ -2,56 +2,55 @@ using Highbyte.DotNet6502.Systems; using Microsoft.JSInterop; -namespace Highbyte.DotNet6502.Impl.AspNet +namespace Highbyte.DotNet6502.Impl.AspNet; + +public class WASMAudioHandlerContext : IAudioHandlerContext { - public class WASMAudioHandlerContext : IAudioHandlerContext + private readonly Func _getAudioContext; + public AudioContextSync AudioContext => _getAudioContext(); + + private readonly IJSRuntime _jsRuntime; + private readonly float _initialVolumePercent; + + public IJSRuntime JSRuntime => _jsRuntime; + + private GainNodeSync _masterVolumeGainNode = default!; + internal GainNodeSync MasterVolumeGainNode => _masterVolumeGainNode; + + public bool IsInitialized { get; private set; } + + public WASMAudioHandlerContext( + Func getAudioContext, + IJSRuntime jsRuntime, + float initialVolumePercent + ) + { + _getAudioContext = getAudioContext; + _jsRuntime = jsRuntime; + _initialVolumePercent = initialVolumePercent; + } + + public void Init() + { + // Create GainNode for master volume + _masterVolumeGainNode = GainNodeSync.Create(JSRuntime, AudioContext); + + // Set initial master volume % + SetMasterVolumePercent(_initialVolumePercent); + + IsInitialized = true; + } + + public void Cleanup() + { + } + + public void SetMasterVolumePercent(float masterVolumePercent) { - private readonly Func _getAudioContext; - public AudioContextSync AudioContext => _getAudioContext(); - - private readonly IJSRuntime _jsRuntime; - private readonly float _initialVolumePercent; - - public IJSRuntime JSRuntime => _jsRuntime; - - private GainNodeSync _masterVolumeGainNode = default!; - internal GainNodeSync MasterVolumeGainNode => _masterVolumeGainNode; - - public bool IsInitialized { get; private set; } - - public WASMAudioHandlerContext( - Func getAudioContext, - IJSRuntime jsRuntime, - float initialVolumePercent - ) - { - _getAudioContext = getAudioContext; - _jsRuntime = jsRuntime; - _initialVolumePercent = initialVolumePercent; - } - - public void Init() - { - // Create GainNode for master volume - _masterVolumeGainNode = GainNodeSync.Create(JSRuntime, AudioContext); - - // Set initial master volume % - SetMasterVolumePercent(_initialVolumePercent); - - IsInitialized = true; - } - - public void Cleanup() - { - } - - public void SetMasterVolumePercent(float masterVolumePercent) - { - var currentTime = AudioContext.GetCurrentTime(); - var gain = MasterVolumeGainNode.GetGain(); - gain.CancelScheduledValues(currentTime); - float newGain = Math.Clamp(masterVolumePercent, 0f, 100f) / 100f; - gain.SetValueAtTime(newGain, currentTime); - } + var currentTime = AudioContext.GetCurrentTime(); + var gain = MasterVolumeGainNode.GetGain(); + gain.CancelScheduledValues(currentTime); + float newGain = Math.Clamp(masterVolumePercent, 0f, 100f) / 100f; + gain.SetValueAtTime(newGain, currentTime); } } diff --git a/src/libraries/Highbyte.DotNet6502.Impl.NAudio/Commodore64/Audio/C64NAudioVoiceContext.cs b/src/libraries/Highbyte.DotNet6502.Impl.NAudio/Commodore64/Audio/C64NAudioVoiceContext.cs index eca10633..17798627 100644 --- a/src/libraries/Highbyte.DotNet6502.Impl.NAudio/Commodore64/Audio/C64NAudioVoiceContext.cs +++ b/src/libraries/Highbyte.DotNet6502.Impl.NAudio/Commodore64/Audio/C64NAudioVoiceContext.cs @@ -2,354 +2,353 @@ using Highbyte.DotNet6502.Systems.Commodore64.Audio; using NAudio.Wave.SampleProviders; -namespace Highbyte.DotNet6502.Impl.NAudio.Commodore64.Audio +namespace Highbyte.DotNet6502.Impl.NAudio.Commodore64.Audio; + +public class C64NAudioVoiceContext { - public class C64NAudioVoiceContext - { - private readonly bool _disconnectOscillatorOnStop = true; + private readonly bool _disconnectOscillatorOnStop = true; - private C64NAudioAudioHandler _audioHandler = default!; - internal C64NAudioAudioHandler AudioHandler => _audioHandler; + private C64NAudioAudioHandler _audioHandler = default!; + internal C64NAudioAudioHandler AudioHandler => _audioHandler; - private Action _addDebugMessage = default!; + private Action _addDebugMessage = default!; - internal void AddDebugMessage(string msg) - { - _addDebugMessage(msg, _voice, CurrentSidVoiceWaveForm, Status); - } + internal void AddDebugMessage(string msg) + { + _addDebugMessage(msg, _voice, CurrentSidVoiceWaveForm, Status); + } - private readonly byte _voice; - public byte Voice => _voice; - public AudioVoiceStatus Status = AudioVoiceStatus.Stopped; - public SidVoiceWaveForm CurrentSidVoiceWaveForm = SidVoiceWaveForm.None; + private readonly byte _voice; + public byte Voice => _voice; + public AudioVoiceStatus Status = AudioVoiceStatus.Stopped; + public SidVoiceWaveForm CurrentSidVoiceWaveForm = SidVoiceWaveForm.None; - public SynthEnvelopeProvider? GetOscillator(SidVoiceWaveForm sidVoiceWaveForm) => sidVoiceWaveForm switch - { - SidVoiceWaveForm.None => null, - SidVoiceWaveForm.Triangle => TriangleOscillator, - SidVoiceWaveForm.Sawtooth => SawToothOscillator, - SidVoiceWaveForm.Pulse => PulseOscillator, - SidVoiceWaveForm.RandomNoise => NoiseOscillator, - _ => null - }; - public SynthEnvelopeProvider? CurrentOscillator => GetOscillator(CurrentSidVoiceWaveForm); + public SynthEnvelopeProvider? GetOscillator(SidVoiceWaveForm sidVoiceWaveForm) => sidVoiceWaveForm switch + { + SidVoiceWaveForm.None => null, + SidVoiceWaveForm.Triangle => TriangleOscillator, + SidVoiceWaveForm.Sawtooth => SawToothOscillator, + SidVoiceWaveForm.Pulse => PulseOscillator, + SidVoiceWaveForm.RandomNoise => NoiseOscillator, + _ => null + }; + public SynthEnvelopeProvider? CurrentOscillator => GetOscillator(CurrentSidVoiceWaveForm); - // SID Triangle Oscillator - public SynthEnvelopeProvider TriangleOscillator { get; private set; } = default!; + // SID Triangle Oscillator + public SynthEnvelopeProvider TriangleOscillator { get; private set; } = default!; - // SID Sawtooth Oscillator - public SynthEnvelopeProvider SawToothOscillator { get; private set; } = default!; + // SID Sawtooth Oscillator + public SynthEnvelopeProvider SawToothOscillator { get; private set; } = default!; - // SID pulse oscillator - public SynthEnvelopeProvider PulseOscillator { get; private set; } = default!; + // SID pulse oscillator + public SynthEnvelopeProvider PulseOscillator { get; private set; } = default!; - // SID noise oscillator - public SynthEnvelopeProvider NoiseOscillator { get; private set; } = default!; + // SID noise oscillator + public SynthEnvelopeProvider NoiseOscillator { get; private set; } = default!; - //private EventListener _audioStoppedCallback; + //private EventListener _audioStoppedCallback; - //private readonly Timer _adsCycleCompleteTimer = default!; - //private readonly Timer _releaseCycleCompleteTimer = default!; + //private readonly Timer _adsCycleCompleteTimer = default!; + //private readonly Timer _releaseCycleCompleteTimer = default!; - //private readonly SemaphoreSlim _semaphoreSlim = new(1); - //public SemaphoreSlim SemaphoreSlim => _semaphoreSlim; + //private readonly SemaphoreSlim _semaphoreSlim = new(1); + //public SemaphoreSlim SemaphoreSlim => _semaphoreSlim; - public C64NAudioVoiceContext(byte voice) - { - _voice = voice; - } + public C64NAudioVoiceContext(byte voice) + { + _voice = voice; + } - internal void Init( - C64NAudioAudioHandler audioHandler, - Action addDebugMessage) - { - Status = AudioVoiceStatus.Stopped; + internal void Init( + C64NAudioAudioHandler audioHandler, + Action addDebugMessage) + { + Status = AudioVoiceStatus.Stopped; - _audioHandler = audioHandler; - _addDebugMessage = addDebugMessage; + _audioHandler = audioHandler; + _addDebugMessage = addDebugMessage; - // Create oscillators in advance - foreach (var sidWaveFormType in Enum.GetValues()) + // Create oscillators in advance + foreach (var sidWaveFormType in Enum.GetValues()) + { + var audioVoiceParameter = new AudioVoiceParameter { - var audioVoiceParameter = new AudioVoiceParameter - { - SIDOscillatorType = sidWaveFormType, - Frequency = 300f, - PulseWidth = -0.22f, - }; - CreateOscillator(audioVoiceParameter); - //ConnectOscillator(audioVoiceParameter.SIDOscillatorType); - } - - CurrentSidVoiceWaveForm = SidVoiceWaveForm.None; + SIDOscillatorType = sidWaveFormType, + Frequency = 300f, + PulseWidth = -0.22f, + }; + CreateOscillator(audioVoiceParameter); + //ConnectOscillator(audioVoiceParameter.SIDOscillatorType); } - //private void ScheduleAudioStopAfterDecay(int waitMs) - //{ - // // Set timer to stop audio after a while via a .NET timer - // _adsCycleCompleteTimer = new Timer((_) => - // { - // AddDebugMessage($"Scheduled StopWavePlayer after Decay triggered."); - // StopWavePlayer(); - // }, null, waitMs, Timeout.Infinite); - //} - - //private void ScheduleAudioStopAfterRelease(double releaseDurationSeconds) - //{ - // AddDebugMessage($"Scheduling voice stop at now + {releaseDurationSeconds} seconds."); - - // // Schedule StopWavePlayer for oscillator and other audio sources) when the Release period if over - // //voiceContext.Oscillator?.StopWavePlayer(currentTime + audioVoiceParameter.ReleaseDurationSeconds); - // //voiceContext.PulseOscillator?.StopWavePlayer(currentTime + audioVoiceParameter.ReleaseDurationSeconds); - // //voiceContext.NoiseGenerator?.StopWavePlayer(currentTime + audioVoiceParameter.ReleaseDurationSeconds); - - // var waitMs = (int)(releaseDurationSeconds * 1000.0d); - // // Set timer to stop audio after a while via a .NET timer - // _releaseCycleCompleteTimer = new Timer((_) => - // { - // AddDebugMessage($"Scheduled StopWavePlayer after Release triggered."); - // StopWavePlayer(); - // }, null, waitMs, Timeout.Infinite); - //} - - internal void Stop() - { - AddDebugMessage($"StopWavePlayer issued"); + CurrentSidVoiceWaveForm = SidVoiceWaveForm.None; + } - // In this scenario, the oscillator is still running. Set volume to 0 - AddDebugMessage($"Mute oscillator"); + //private void ScheduleAudioStopAfterDecay(int waitMs) + //{ + // // Set timer to stop audio after a while via a .NET timer + // _adsCycleCompleteTimer = new Timer((_) => + // { + // AddDebugMessage($"Scheduled StopWavePlayer after Decay triggered."); + // StopWavePlayer(); + // }, null, waitMs, Timeout.Infinite); + //} + + //private void ScheduleAudioStopAfterRelease(double releaseDurationSeconds) + //{ + // AddDebugMessage($"Scheduling voice stop at now + {releaseDurationSeconds} seconds."); + + // // Schedule StopWavePlayer for oscillator and other audio sources) when the Release period if over + // //voiceContext.Oscillator?.StopWavePlayer(currentTime + audioVoiceParameter.ReleaseDurationSeconds); + // //voiceContext.PulseOscillator?.StopWavePlayer(currentTime + audioVoiceParameter.ReleaseDurationSeconds); + // //voiceContext.NoiseGenerator?.StopWavePlayer(currentTime + audioVoiceParameter.ReleaseDurationSeconds); + + // var waitMs = (int)(releaseDurationSeconds * 1000.0d); + // // Set timer to stop audio after a while via a .NET timer + // _releaseCycleCompleteTimer = new Timer((_) => + // { + // AddDebugMessage($"Scheduled StopWavePlayer after Release triggered."); + // StopWavePlayer(); + // }, null, waitMs, Timeout.Infinite); + //} + + internal void Stop() + { + AddDebugMessage($"StopWavePlayer issued"); - // Set ADSR state to idle - ResetOscillatorADSR(CurrentSidVoiceWaveForm); + // In this scenario, the oscillator is still running. Set volume to 0 + AddDebugMessage($"Mute oscillator"); - // If configured, disconnect the oscillator when stopping - if (_disconnectOscillatorOnStop) - { - DisconnectOscillator(CurrentSidVoiceWaveForm); - CurrentSidVoiceWaveForm = SidVoiceWaveForm.None; - } + // Set ADSR state to idle + ResetOscillatorADSR(CurrentSidVoiceWaveForm); - if (Status != AudioVoiceStatus.Stopped) - { - Status = AudioVoiceStatus.Stopped; - AddDebugMessage($"Status changed."); - } - else - { - AddDebugMessage($"Status already was Stopped"); - } - } - - internal void StopAllOscillatorsNow() + // If configured, disconnect the oscillator when stopping + if (_disconnectOscillatorOnStop) { - foreach (var sidWaveFormType in Enum.GetValues()) - { - StopOscillatorNow(sidWaveFormType); - } + DisconnectOscillator(CurrentSidVoiceWaveForm); + CurrentSidVoiceWaveForm = SidVoiceWaveForm.None; } - private void StopOscillatorNow(SidVoiceWaveForm sidVoiceWaveForm) + if (Status != AudioVoiceStatus.Stopped) { - AddDebugMessage($"Stopping oscillator: {sidVoiceWaveForm}"); - var oscillator = GetOscillator(sidVoiceWaveForm); - if (oscillator != null) - { - ResetOscillatorADSR(sidVoiceWaveForm); - DisconnectOscillator(sidVoiceWaveForm); - } + Status = AudioVoiceStatus.Stopped; + AddDebugMessage($"Status changed."); } - - private void StopOscillatorLater(SidVoiceWaveForm sidVoiceWaveForm) + else { - AddDebugMessage($"Stopping oscillator: {sidVoiceWaveForm} later"); - var oscillator = GetOscillator(sidVoiceWaveForm); - oscillator?.StartRelease(); + AddDebugMessage($"Status already was Stopped"); } + } - private void ConnectOscillator(SidVoiceWaveForm sidVoiceWaveForm) + internal void StopAllOscillatorsNow() + { + foreach (var sidWaveFormType in Enum.GetValues()) { - AddDebugMessage($"Connecting oscillator: {sidVoiceWaveForm}"); - - var oscillator = GetOscillator(sidVoiceWaveForm); - if (oscillator != null) - _audioHandler.Mixer.AddMixerInput(oscillator); + StopOscillatorNow(sidWaveFormType); } + } - private void DisconnectOscillator(SidVoiceWaveForm sidVoiceWaveForm) + private void StopOscillatorNow(SidVoiceWaveForm sidVoiceWaveForm) + { + AddDebugMessage($"Stopping oscillator: {sidVoiceWaveForm}"); + var oscillator = GetOscillator(sidVoiceWaveForm); + if (oscillator != null) { - AddDebugMessage($"Disconnecting oscillator: {sidVoiceWaveForm}"); - var oscillator = GetOscillator(sidVoiceWaveForm); - if (oscillator != null) - _audioHandler.Mixer.RemoveMixerInput(oscillator); + ResetOscillatorADSR(sidVoiceWaveForm); + DisconnectOscillator(sidVoiceWaveForm); } + } - private void ResetOscillatorADSR(SidVoiceWaveForm sidVoiceWaveForm) - { - AddDebugMessage($"Reseting oscillator ADSR: {sidVoiceWaveForm}"); - var oscillator = GetOscillator(sidVoiceWaveForm); - oscillator?.ResetADSR(); - } + private void StopOscillatorLater(SidVoiceWaveForm sidVoiceWaveForm) + { + AddDebugMessage($"Stopping oscillator: {sidVoiceWaveForm} later"); + var oscillator = GetOscillator(sidVoiceWaveForm); + oscillator?.StartRelease(); + } - private void CreateOscillator(AudioVoiceParameter audioVoiceParameter) - { - AddDebugMessage($"Creating oscillator: {audioVoiceParameter.SIDOscillatorType}"); + private void ConnectOscillator(SidVoiceWaveForm sidVoiceWaveForm) + { + AddDebugMessage($"Connecting oscillator: {sidVoiceWaveForm}"); - switch (audioVoiceParameter.SIDOscillatorType) - { - case SidVoiceWaveForm.None: - break; - case SidVoiceWaveForm.Triangle: - TriangleOscillator = new SynthEnvelopeProvider(SignalGeneratorType.Triangle); - break; - case SidVoiceWaveForm.Sawtooth: - SawToothOscillator = new SynthEnvelopeProvider(SignalGeneratorType.SawTooth); - break; - case SidVoiceWaveForm.Pulse: - PulseOscillator = new SynthEnvelopeProvider(SignalGeneratorType.Square); - break; - case SidVoiceWaveForm.RandomNoise: - NoiseOscillator = new SynthEnvelopeProvider(SignalGeneratorType.White); - break; - default: - break; - } - } + var oscillator = GetOscillator(sidVoiceWaveForm); + if (oscillator != null) + _audioHandler.Mixer.AddMixerInput(oscillator); + } - private void StartAttackPhase(SidVoiceWaveForm sidVoiceWaveForm) - { - AddDebugMessage($"Starting oscillator: {sidVoiceWaveForm}"); + private void DisconnectOscillator(SidVoiceWaveForm sidVoiceWaveForm) + { + AddDebugMessage($"Disconnecting oscillator: {sidVoiceWaveForm}"); + var oscillator = GetOscillator(sidVoiceWaveForm); + if (oscillator != null) + _audioHandler.Mixer.RemoveMixerInput(oscillator); + } - var oscillator = GetOscillator(sidVoiceWaveForm); - oscillator?.StartAttack(); - } + private void ResetOscillatorADSR(SidVoiceWaveForm sidVoiceWaveForm) + { + AddDebugMessage($"Reseting oscillator ADSR: {sidVoiceWaveForm}"); + var oscillator = GetOscillator(sidVoiceWaveForm); + oscillator?.ResetADSR(); + } - private void SetOscillatorParameters(AudioVoiceParameter audioVoiceParameter) - { - AddDebugMessage($"Setting oscillator parameters: {audioVoiceParameter.SIDOscillatorType}"); + private void CreateOscillator(AudioVoiceParameter audioVoiceParameter) + { + AddDebugMessage($"Creating oscillator: {audioVoiceParameter.SIDOscillatorType}"); - switch (audioVoiceParameter.SIDOscillatorType) - { - case SidVoiceWaveForm.None: - // Set frequency - SetFrequencyOnCurrentOscillator(audioVoiceParameter.Frequency); - break; - case SidVoiceWaveForm.Triangle: - // Set frequency - SetFrequencyOnCurrentOscillator(audioVoiceParameter.Frequency); - break; - case SidVoiceWaveForm.Sawtooth: - // Set frequency - SetFrequencyOnCurrentOscillator(audioVoiceParameter.Frequency); - break; - case SidVoiceWaveForm.Pulse: - // Set frequency - SetFrequencyOnCurrentOscillator(audioVoiceParameter.Frequency); - // Set pulsewidth - SetPulseWidthOnCurrentOscillator(audioVoiceParameter.PulseWidth); - break; - case SidVoiceWaveForm.RandomNoise: - // Set frequency (playback rate) on current NoiseGenerator - SetFrequencyOnCurrentOscillator(audioVoiceParameter.Frequency); - break; - default: - break; - } + switch (audioVoiceParameter.SIDOscillatorType) + { + case SidVoiceWaveForm.None: + break; + case SidVoiceWaveForm.Triangle: + TriangleOscillator = new SynthEnvelopeProvider(SignalGeneratorType.Triangle); + break; + case SidVoiceWaveForm.Sawtooth: + SawToothOscillator = new SynthEnvelopeProvider(SignalGeneratorType.SawTooth); + break; + case SidVoiceWaveForm.Pulse: + PulseOscillator = new SynthEnvelopeProvider(SignalGeneratorType.Square); + break; + case SidVoiceWaveForm.RandomNoise: + NoiseOscillator = new SynthEnvelopeProvider(SignalGeneratorType.White); + break; + default: + break; } + } - private void SwitchOscillatorConnection(SidVoiceWaveForm newSidVoiceWaveForm, bool forceSwitch = true) - { - // If current oscillator is the same as the requested one, do nothing (assume it's already connected) - if (!forceSwitch && newSidVoiceWaveForm == CurrentSidVoiceWaveForm) - return; + private void StartAttackPhase(SidVoiceWaveForm sidVoiceWaveForm) + { + AddDebugMessage($"Starting oscillator: {sidVoiceWaveForm}"); - // If any other oscillator is currently connected - if (CurrentSidVoiceWaveForm != SidVoiceWaveForm.None) - // StopWavePlayer any existing playing audio will also disconnect it's oscillator - Stop(); + var oscillator = GetOscillator(sidVoiceWaveForm); + oscillator?.StartAttack(); + } - // Then connect the specified oscillator - ConnectOscillator(newSidVoiceWaveForm); + private void SetOscillatorParameters(AudioVoiceParameter audioVoiceParameter) + { + AddDebugMessage($"Setting oscillator parameters: {audioVoiceParameter.SIDOscillatorType}"); - // Remember the new current oscillator - CurrentSidVoiceWaveForm = newSidVoiceWaveForm; + switch (audioVoiceParameter.SIDOscillatorType) + { + case SidVoiceWaveForm.None: + // Set frequency + SetFrequencyOnCurrentOscillator(audioVoiceParameter.Frequency); + break; + case SidVoiceWaveForm.Triangle: + // Set frequency + SetFrequencyOnCurrentOscillator(audioVoiceParameter.Frequency); + break; + case SidVoiceWaveForm.Sawtooth: + // Set frequency + SetFrequencyOnCurrentOscillator(audioVoiceParameter.Frequency); + break; + case SidVoiceWaveForm.Pulse: + // Set frequency + SetFrequencyOnCurrentOscillator(audioVoiceParameter.Frequency); + // Set pulsewidth + SetPulseWidthOnCurrentOscillator(audioVoiceParameter.PulseWidth); + break; + case SidVoiceWaveForm.RandomNoise: + // Set frequency (playback rate) on current NoiseGenerator + SetFrequencyOnCurrentOscillator(audioVoiceParameter.Frequency); + break; + default: + break; } + } - internal void StartAudioADSPhase(AudioVoiceParameter audioVoiceParameter) - { - // Assume oscillator is already created and started - // 1. Add oscillator to Mixer (and remove previous oscillator if different) - // 2. Set parameters on existing oscillator such as Frequency, PulseWidth, etc. - // 3. Set Gain ADSR envelope -> This will start the audio - // 4. ? Set Callback to stop audio by setting Gain to 0 when envelope is finished - - SwitchOscillatorConnection(audioVoiceParameter.SIDOscillatorType); - SetOscillatorParameters(audioVoiceParameter); - SetGainADS(audioVoiceParameter); - - // If SustainGain is 0, then we need to schedule a stop of the audio - // when the attack + decay period is over. - if (audioVoiceParameter.SustainGain == 0) - { - //var waitSeconds = audioVoiceParameter.AttackDurationSeconds + audioVoiceParameter.DecayDurationSeconds; - //AddDebugMessage($"Scheduling voice stop now + {waitSeconds} seconds."); - //ScheduleAudioStopAfterDecay(waitMs: (int)(waitSeconds * 1000.0d)); - } + private void SwitchOscillatorConnection(SidVoiceWaveForm newSidVoiceWaveForm, bool forceSwitch = true) + { + // If current oscillator is the same as the requested one, do nothing (assume it's already connected) + if (!forceSwitch && newSidVoiceWaveForm == CurrentSidVoiceWaveForm) + return; - StartAttackPhase(CurrentSidVoiceWaveForm); + // If any other oscillator is currently connected + if (CurrentSidVoiceWaveForm != SidVoiceWaveForm.None) + // StopWavePlayer any existing playing audio will also disconnect it's oscillator + Stop(); - Status = AudioVoiceStatus.ADSCycleStarted; - AddDebugMessage($"Status changed"); - } + // Then connect the specified oscillator + ConnectOscillator(newSidVoiceWaveForm); + + // Remember the new current oscillator + CurrentSidVoiceWaveForm = newSidVoiceWaveForm; + } - internal void StartAudioReleasePhase(AudioVoiceParameter audioVoiceParameter) + internal void StartAudioADSPhase(AudioVoiceParameter audioVoiceParameter) + { + // Assume oscillator is already created and started + // 1. Add oscillator to Mixer (and remove previous oscillator if different) + // 2. Set parameters on existing oscillator such as Frequency, PulseWidth, etc. + // 3. Set Gain ADSR envelope -> This will start the audio + // 4. ? Set Callback to stop audio by setting Gain to 0 when envelope is finished + + SwitchOscillatorConnection(audioVoiceParameter.SIDOscillatorType); + SetOscillatorParameters(audioVoiceParameter); + SetGainADS(audioVoiceParameter); + + // If SustainGain is 0, then we need to schedule a stop of the audio + // when the attack + decay period is over. + if (audioVoiceParameter.SustainGain == 0) { - SetGainRelease(audioVoiceParameter); + //var waitSeconds = audioVoiceParameter.AttackDurationSeconds + audioVoiceParameter.DecayDurationSeconds; + //AddDebugMessage($"Scheduling voice stop now + {waitSeconds} seconds."); + //ScheduleAudioStopAfterDecay(waitMs: (int)(waitSeconds * 1000.0d)); + } - StopOscillatorLater(CurrentSidVoiceWaveForm); + StartAttackPhase(CurrentSidVoiceWaveForm); - Status = AudioVoiceStatus.ReleaseCycleStarted; - AddDebugMessage($"Status changed"); - } + Status = AudioVoiceStatus.ADSCycleStarted; + AddDebugMessage($"Status changed"); + } - private void SetGainADS(AudioVoiceParameter audioVoiceParameter) - { - AddDebugMessage($"Setting Attack ({audioVoiceParameter.AttackDurationSeconds}) Decay ({audioVoiceParameter.DecayDurationSeconds}) Sustain ({audioVoiceParameter.SustainGain})"); + internal void StartAudioReleasePhase(AudioVoiceParameter audioVoiceParameter) + { + SetGainRelease(audioVoiceParameter); - // TODO: Set Attack/Decay/Sustain values - var oscillator = CurrentOscillator; - if (oscillator != null) - { - oscillator.AttackSeconds = (float)audioVoiceParameter.AttackDurationSeconds; - oscillator.DecaySeconds = (float)audioVoiceParameter.DecayDurationSeconds; - oscillator.SustainLevel = (float)audioVoiceParameter.SustainGain; - } - } + StopOscillatorLater(CurrentSidVoiceWaveForm); - private void SetGainRelease(AudioVoiceParameter audioVoiceParameter) - { - AddDebugMessage($"Setting Gain Release ({audioVoiceParameter.ReleaseDurationSeconds})"); + Status = AudioVoiceStatus.ReleaseCycleStarted; + AddDebugMessage($"Status changed"); + } - var oscillator = CurrentOscillator; - if (oscillator != null) - oscillator.ReleaseSeconds = (float)audioVoiceParameter.ReleaseDurationSeconds; - } + private void SetGainADS(AudioVoiceParameter audioVoiceParameter) + { + AddDebugMessage($"Setting Attack ({audioVoiceParameter.AttackDurationSeconds}) Decay ({audioVoiceParameter.DecayDurationSeconds}) Sustain ({audioVoiceParameter.SustainGain})"); - internal void SetFrequencyOnCurrentOscillator(float frequency) + // TODO: Set Attack/Decay/Sustain values + var oscillator = CurrentOscillator; + if (oscillator != null) { - AddDebugMessage($"Changing freq to {frequency}."); - - var oscillator = CurrentOscillator; - if (oscillator != null) - oscillator.Frequency = (double)frequency; + oscillator.AttackSeconds = (float)audioVoiceParameter.AttackDurationSeconds; + oscillator.DecaySeconds = (float)audioVoiceParameter.DecayDurationSeconds; + oscillator.SustainLevel = (float)audioVoiceParameter.SustainGain; } + } - internal void SetPulseWidthOnCurrentOscillator(float pulseWidth) - { - AddDebugMessage($"Changing pulsewidth to {pulseWidth}."); + private void SetGainRelease(AudioVoiceParameter audioVoiceParameter) + { + AddDebugMessage($"Setting Gain Release ({audioVoiceParameter.ReleaseDurationSeconds})"); - var oscillator = CurrentOscillator; - if (oscillator != null) - oscillator.Duty = (double)pulseWidth; - } + var oscillator = CurrentOscillator; + if (oscillator != null) + oscillator.ReleaseSeconds = (float)audioVoiceParameter.ReleaseDurationSeconds; + } + + internal void SetFrequencyOnCurrentOscillator(float frequency) + { + AddDebugMessage($"Changing freq to {frequency}."); + + var oscillator = CurrentOscillator; + if (oscillator != null) + oscillator.Frequency = (double)frequency; + } + + internal void SetPulseWidthOnCurrentOscillator(float pulseWidth) + { + AddDebugMessage($"Changing pulsewidth to {pulseWidth}."); + + var oscillator = CurrentOscillator; + if (oscillator != null) + oscillator.Duty = (double)pulseWidth; } } diff --git a/src/libraries/Highbyte.DotNet6502.Impl.NAudio/NAudioAudioHandlerContext.cs b/src/libraries/Highbyte.DotNet6502.Impl.NAudio/NAudioAudioHandlerContext.cs index 46b6f3d2..b758a1d2 100644 --- a/src/libraries/Highbyte.DotNet6502.Impl.NAudio/NAudioAudioHandlerContext.cs +++ b/src/libraries/Highbyte.DotNet6502.Impl.NAudio/NAudioAudioHandlerContext.cs @@ -2,70 +2,69 @@ using NAudio.Wave; using NAudio.Wave.SampleProviders; -namespace Highbyte.DotNet6502.Impl.NAudio +namespace Highbyte.DotNet6502.Impl.NAudio; + +public class NAudioAudioHandlerContext : IAudioHandlerContext { - public class NAudioAudioHandlerContext : IAudioHandlerContext - { - private readonly IWavePlayer _wavePlayer; - private VolumeSampleProvider _masterVolumeControl = default!; + private readonly IWavePlayer _wavePlayer; + private VolumeSampleProvider _masterVolumeControl = default!; - private float _initialVolumePercent; + private float _initialVolumePercent; - public bool IsInitialized { get; private set; } + public bool IsInitialized { get; private set; } - public NAudioAudioHandlerContext( - IWavePlayer wavePlayer, - float initialVolumePercent - ) - { - _wavePlayer = wavePlayer; - _initialVolumePercent = initialVolumePercent; - } + public NAudioAudioHandlerContext( + IWavePlayer wavePlayer, + float initialVolumePercent + ) + { + _wavePlayer = wavePlayer; + _initialVolumePercent = initialVolumePercent; + } - public void Init() - { - IsInitialized = true; - } + public void Init() + { + IsInitialized = true; + } - public void ConfigureWavePlayer(ISampleProvider sampleProvider) + public void ConfigureWavePlayer(ISampleProvider sampleProvider) + { + // Route all audio through a maste volume control + _masterVolumeControl = new VolumeSampleProvider(sampleProvider) { - // Route all audio through a maste volume control - _masterVolumeControl = new VolumeSampleProvider(sampleProvider) - { - Volume = _initialVolumePercent / 100f - }; - _wavePlayer.Init(_masterVolumeControl); - } + Volume = _initialVolumePercent / 100f + }; + _wavePlayer.Init(_masterVolumeControl); + } - public void SetMasterVolumePercent(float masterVolumePercent) - { - _initialVolumePercent = masterVolumePercent; - if (_masterVolumeControl != null) - _masterVolumeControl.Volume = masterVolumePercent / 100f; - } + public void SetMasterVolumePercent(float masterVolumePercent) + { + _initialVolumePercent = masterVolumePercent; + if (_masterVolumeControl != null) + _masterVolumeControl.Volume = masterVolumePercent / 100f; + } - public void StartWavePlayer() - { - if (_wavePlayer.PlaybackState != PlaybackState.Playing) - _wavePlayer.Play(); - } + public void StartWavePlayer() + { + if (_wavePlayer.PlaybackState != PlaybackState.Playing) + _wavePlayer.Play(); + } - public void StopWavePlayer() - { - if (_wavePlayer.PlaybackState != PlaybackState.Stopped) - _wavePlayer.Stop(); - } + public void StopWavePlayer() + { + if (_wavePlayer.PlaybackState != PlaybackState.Stopped) + _wavePlayer.Stop(); + } - public void PauseWavePlayer() - { - if (_wavePlayer.PlaybackState != PlaybackState.Paused) - _wavePlayer.Pause(); - } + public void PauseWavePlayer() + { + if (_wavePlayer.PlaybackState != PlaybackState.Paused) + _wavePlayer.Pause(); + } - public void Cleanup() - { - StopWavePlayer(); - _wavePlayer.Dispose(); - } + public void Cleanup() + { + StopWavePlayer(); + _wavePlayer.Dispose(); } } diff --git a/src/libraries/Highbyte.DotNet6502.Impl.NAudio/Synth/SquareWaveHelper.cs b/src/libraries/Highbyte.DotNet6502.Impl.NAudio/Synth/SquareWaveHelper.cs index 0f9ba8ff..42689b94 100644 --- a/src/libraries/Highbyte.DotNet6502.Impl.NAudio/Synth/SquareWaveHelper.cs +++ b/src/libraries/Highbyte.DotNet6502.Impl.NAudio/Synth/SquareWaveHelper.cs @@ -1,53 +1,52 @@ -namespace Highbyte.DotNet6502.Impl.NAudio.Synth +namespace Highbyte.DotNet6502.Impl.NAudio.Synth; + +/// +/// Square wave generator with configurable duty cycle. +/// Based on code from https://github.com/BertyBasset/C-Analog-Synthesiser +/// + +public class SquareWaveHelper { - /// - /// Square wave generator with configurable duty cycle. - /// Based on code from https://github.com/BertyBasset/C-Analog-Synthesiser - /// + // _phase is the oscillator's 360 degree modulo phase accumulator + private double _phase = 0f; - public class SquareWaveHelper + public SquareWaveHelper() { - // _phase is the oscillator's 360 degree modulo phase accumulator - private double _phase = 0f; - public SquareWaveHelper() - { - - } + } - public double Read(int sampleRate, double frequency, double gain, double duty) - { - var timeIncrement = 1f / (double)sampleRate; + public double Read(int sampleRate, double frequency, double gain, double duty) + { + var timeIncrement = 1f / (double)sampleRate; - // Advance Phase Accumulator acording to timeIncrement and current frequency - var delta = timeIncrement * frequency * 360; - _phase += delta; + // Advance Phase Accumulator acording to timeIncrement and current frequency + var delta = timeIncrement * frequency * 360; + _phase += delta; - var originalPhase = _phase; - _phase %= 360; + var originalPhase = _phase; + _phase %= 360; - //if (_phase < originalPhase) // If % takes us back for a new cycle we've completed a cycle and can sync other ocs if needed - // TriggerSync(); + //if (_phase < originalPhase) // If % takes us back for a new cycle we've completed a cycle and can sync other ocs if needed + // TriggerSync(); - // Use Generator to return wave value for current state of the Phase Accumulator + // Use Generator to return wave value for current state of the Phase Accumulator - var sample = GenerateSquare(_phase, duty); - //Value = _Generator.GenerateSample(_phase, Duty.GetDuty(), delta); + var sample = GenerateSquare(_phase, duty); + //Value = _Generator.GenerateSample(_phase, Duty.GetDuty(), delta); - return sample * gain; - } + return sample * gain; + } - private double GenerateSquare(double phase, double duty) - { + private double GenerateSquare(double phase, double duty) + { - double sample; - if (phase > 360 * ((duty + 1.0) / 2.0)) - sample = 1; - else - sample = 0; + double sample; + if (phase > 360 * ((duty + 1.0) / 2.0)) + sample = 1; + else + sample = 0; - const double AMPLITUDE_NORMALISATION = 0.7; - return sample * AMPLITUDE_NORMALISATION; - } + const double AMPLITUDE_NORMALISATION = 0.7; + return sample * AMPLITUDE_NORMALISATION; } } diff --git a/src/libraries/Highbyte.DotNet6502.Impl.NAudio/Synth/SynthEnvelopeProvider.cs b/src/libraries/Highbyte.DotNet6502.Impl.NAudio/Synth/SynthEnvelopeProvider.cs index b946f9c2..6fabce41 100644 --- a/src/libraries/Highbyte.DotNet6502.Impl.NAudio/Synth/SynthEnvelopeProvider.cs +++ b/src/libraries/Highbyte.DotNet6502.Impl.NAudio/Synth/SynthEnvelopeProvider.cs @@ -3,147 +3,146 @@ using NAudio.Wave.SampleProviders; using static NAudio.Dsp.EnvelopeGenerator; -namespace Highbyte.DotNet6502.Impl.NAudio.Synth +namespace Highbyte.DotNet6502.Impl.NAudio.Synth; + +/// +/// NAudio Sample Provider that applies an ADSR envelope to a specifed synth signal type. +/// Based on code from https://github.com/essenbee/synthesizer. +/// +public class SynthEnvelopeProvider : ISampleProvider { - /// - /// NAudio Sample Provider that applies an ADSR envelope to a specifed synth signal type. - /// Based on code from https://github.com/essenbee/synthesizer. - /// - public class SynthEnvelopeProvider : ISampleProvider + private readonly int _sampleRate; + private readonly SynthSignalProvider _source; + private readonly EnvelopeGenerator _adsr; + public WaveFormat WaveFormat { get; } + public bool EnablbooleSubOsc { - private readonly int _sampleRate; - private readonly SynthSignalProvider _source; - private readonly EnvelopeGenerator _adsr; - public WaveFormat WaveFormat { get; } - public bool EnablbooleSubOsc - { - get => _source.EnableSubOsc; - set { _source.EnableSubOsc = value; } - } - - public EnvelopeState ADSRState => _adsr.State; + get => _source.EnableSubOsc; + set { _source.EnableSubOsc = value; } + } - private float _attackSeconds; - public float AttackSeconds - { - get => _attackSeconds; - set - { - _attackSeconds = value; - _adsr.AttackRate = _attackSeconds * WaveFormat.SampleRate; - } - } + public EnvelopeState ADSRState => _adsr.State; - private float _decaySeconds; - public float DecaySeconds + private float _attackSeconds; + public float AttackSeconds + { + get => _attackSeconds; + set { - get => _decaySeconds; - set - { - _decaySeconds = value; - _adsr.DecayRate = _decaySeconds * WaveFormat.SampleRate; - } + _attackSeconds = value; + _adsr.AttackRate = _attackSeconds * WaveFormat.SampleRate; } + } - public float SustainLevel + private float _decaySeconds; + public float DecaySeconds + { + get => _decaySeconds; + set { - get => _adsr.SustainLevel; - set { _adsr.SustainLevel = value; } + _decaySeconds = value; + _adsr.DecayRate = _decaySeconds * WaveFormat.SampleRate; } + } - private float _releaseSeconds; - public float ReleaseSeconds - { - get => _releaseSeconds; + public float SustainLevel + { + get => _adsr.SustainLevel; + set { _adsr.SustainLevel = value; } + } - set - { - _releaseSeconds = value; - _adsr.ReleaseRate = _releaseSeconds * WaveFormat.SampleRate; - } - } + private float _releaseSeconds; + public float ReleaseSeconds + { + get => _releaseSeconds; - public double Frequency + set { - get => _source.Frequency; - set { _source.Frequency = value; } + _releaseSeconds = value; + _adsr.ReleaseRate = _releaseSeconds * WaveFormat.SampleRate; } + } - public double LfoFrequency - { - get => _source.LfoFrequency; - set { _source.LfoFrequency = value; } - } - public double LfoGain - { - get => _source.LfoGain; - set { _source.LfoGain = value; } - } + public double Frequency + { + get => _source.Frequency; + set { _source.Frequency = value; } + } - public double Duty - { - get => _source.Duty; - set { _source.Duty = value; } - } + public double LfoFrequency + { + get => _source.LfoFrequency; + set { _source.LfoFrequency = value; } + } + public double LfoGain + { + get => _source.LfoGain; + set { _source.LfoGain = value; } + } - public SignalGeneratorType WaveType => _source.Type; + public double Duty + { + get => _source.Duty; + set { _source.Duty = value; } + } - public SynthEnvelopeProvider( - SignalGeneratorType waveType, - int sampleRate = 44100, - float gain = 1.0f) - { - _sampleRate = sampleRate; - var channels = 1; // Mono - WaveFormat = WaveFormat.CreateIeeeFloatWaveFormat(_sampleRate, channels); - _adsr = new EnvelopeGenerator(); - - //Defaults - AttackSeconds = 0.01f; - DecaySeconds = 0.0f; - SustainLevel = 1.0f; - ReleaseSeconds = 0.3f; - - _source = new SynthSignalProvider(_sampleRate, channels) - { - Frequency = 110.0, - Type = waveType, - Gain = gain, - }; - - // Uncomment to start attack phase immediately when this object is created - //_adsr.Gate(true); - } + public SignalGeneratorType WaveType => _source.Type; - public void StartRelease() + public SynthEnvelopeProvider( + SignalGeneratorType waveType, + int sampleRate = 44100, + float gain = 1.0f) + { + _sampleRate = sampleRate; + var channels = 1; // Mono + WaveFormat = WaveFormat.CreateIeeeFloatWaveFormat(_sampleRate, channels); + _adsr = new EnvelopeGenerator(); + + //Defaults + AttackSeconds = 0.01f; + DecaySeconds = 0.0f; + SustainLevel = 1.0f; + ReleaseSeconds = 0.3f; + + _source = new SynthSignalProvider(_sampleRate, channels) { - _adsr.Gate(false); - } + Frequency = 110.0, + Type = waveType, + Gain = gain, + }; - public void StartAttack() - { - _adsr.Gate(true); - } + // Uncomment to start attack phase immediately when this object is created + //_adsr.Gate(true); + } - public void ResetADSR() - { - _adsr.Reset(); - } + public void StartRelease() + { + _adsr.Gate(false); + } + public void StartAttack() + { + _adsr.Gate(true); + } + + public void ResetADSR() + { + _adsr.Reset(); + } - public int Read(float[] buffer, int offset, int count) - { - if (_adsr.State == EnvelopeState.Idle) - return 0; // we've finished - var samples = _source.Read(buffer, offset, count); + public int Read(float[] buffer, int offset, int count) + { + if (_adsr.State == EnvelopeState.Idle) + return 0; // we've finished - for (var i = 0; i < samples; i++) - { - buffer[offset++] *= _adsr.Process(); - } + var samples = _source.Read(buffer, offset, count); - return samples; + for (var i = 0; i < samples; i++) + { + buffer[offset++] *= _adsr.Process(); } + + return samples; } } diff --git a/src/libraries/Highbyte.DotNet6502.Impl.NAudio/Synth/SynthSignalProvider.cs b/src/libraries/Highbyte.DotNet6502.Impl.NAudio/Synth/SynthSignalProvider.cs index 154ace7f..b5e9c467 100644 --- a/src/libraries/Highbyte.DotNet6502.Impl.NAudio/Synth/SynthSignalProvider.cs +++ b/src/libraries/Highbyte.DotNet6502.Impl.NAudio/Synth/SynthSignalProvider.cs @@ -1,202 +1,201 @@ using NAudio.Wave; using NAudio.Wave.SampleProviders; -namespace Highbyte.DotNet6502.Impl.NAudio.Synth +namespace Highbyte.DotNet6502.Impl.NAudio.Synth; + +/// +/// NAudio Sample Provider that generates synth wave forms. +/// Based on code from https://github.com/essenbee/synthesizer. +/// +public class SynthSignalProvider : ISampleProvider { - /// - /// NAudio Sample Provider that generates synth wave forms. - /// Based on code from https://github.com/essenbee/synthesizer. - /// - public class SynthSignalProvider : ISampleProvider + private readonly WaveFormat _waveFormat; + private readonly Random _random = new Random(); + private readonly double[] _pinkNoiseBuffer = new double[7]; + private const double TwoPi = 2 * Math.PI; + private int _nSample; + private double _phi; + + public WaveFormat WaveFormat => _waveFormat; + public double Frequency { get; set; } + public double FrequencyLog => Math.Log(Frequency); + public double FrequencyEnd { get; set; } + public double FrequencyEndLog => Math.Log(FrequencyEnd); + public double Gain { get; set; } + public bool[] PhaseReverse { get; } + public SignalGeneratorType Type { get; set; } + public double SweepLengthSecs { get; set; } + public double LfoFrequency { get; set; } + public double LfoGain { get; set; } + public bool EnableSubOsc { get; set; } + + public double SubOscillatorFrequency => Frequency / 2.0; + + private SquareWaveHelper _squareWaveHelper; + public double Duty { get; set; } // Duty cycle for square wave. 0.5 (50%) is a square wave. + + public SynthSignalProvider() : this(44100, 2) { - private readonly WaveFormat _waveFormat; - private readonly Random _random = new Random(); - private readonly double[] _pinkNoiseBuffer = new double[7]; - private const double TwoPi = 2 * Math.PI; - private int _nSample; - private double _phi; - - public WaveFormat WaveFormat => _waveFormat; - public double Frequency { get; set; } - public double FrequencyLog => Math.Log(Frequency); - public double FrequencyEnd { get; set; } - public double FrequencyEndLog => Math.Log(FrequencyEnd); - public double Gain { get; set; } - public bool[] PhaseReverse { get; } - public SignalGeneratorType Type { get; set; } - public double SweepLengthSecs { get; set; } - public double LfoFrequency { get; set; } - public double LfoGain { get; set; } - public bool EnableSubOsc { get; set; } - - public double SubOscillatorFrequency => Frequency / 2.0; - - private SquareWaveHelper _squareWaveHelper; - public double Duty { get; set; } // Duty cycle for square wave. 0.5 (50%) is a square wave. - - public SynthSignalProvider() : this(44100, 2) - { - } + } - public SynthSignalProvider(int sampleRate, int channel, - double lfoFrequency = 0.0, double lfoGain = 0.0) - { - _phi = 0; - _waveFormat = WaveFormat.CreateIeeeFloatWaveFormat(sampleRate, channel); + public SynthSignalProvider(int sampleRate, int channel, + double lfoFrequency = 0.0, double lfoGain = 0.0) + { + _phi = 0; + _waveFormat = WaveFormat.CreateIeeeFloatWaveFormat(sampleRate, channel); - // Default - Type = SignalGeneratorType.Sin; - Frequency = 440.0; - Gain = 1; - PhaseReverse = new bool[channel]; - SweepLengthSecs = 2; - LfoFrequency = lfoFrequency; - LfoGain = lfoGain; + // Default + Type = SignalGeneratorType.Sin; + Frequency = 440.0; + Gain = 1; + PhaseReverse = new bool[channel]; + SweepLengthSecs = 2; + LfoFrequency = lfoFrequency; + LfoGain = lfoGain; - Duty = 0.5; - _squareWaveHelper = new SquareWaveHelper(); + Duty = 0.5; + _squareWaveHelper = new SquareWaveHelper(); - } + } - private double lfoSample(int n) - { - //` - //` - //` - if (LfoGain == 0.0) - return 0.0; - - var multiple = TwoPi * LfoFrequency / _waveFormat.SampleRate; - return LfoGain * Math.Sin(n * multiple); - } + private double lfoSample(int n) + { + //` + //` + //` + if (LfoGain == 0.0) + return 0.0; + + var multiple = TwoPi * LfoFrequency / _waveFormat.SampleRate; + return LfoGain * Math.Sin(n * multiple); + } - public int Read(float[] buffer, int offset, int count) - { - var outIndex = offset; + public int Read(float[] buffer, int offset, int count) + { + var outIndex = offset; - // Generator current value - double sampleValue; + // Generator current value + double sampleValue; - // Complete Buffer - for (var sampleCount = 0; sampleCount < count / _waveFormat.Channels; sampleCount++) + // Complete Buffer + for (var sampleCount = 0; sampleCount < count / _waveFormat.Channels; sampleCount++) + { + switch (Type) { - switch (Type) - { - case SignalGeneratorType.Sin: + case SignalGeneratorType.Sin: - // Sinus Generator + // Sinus Generator - var multiple = TwoPi * Frequency / _waveFormat.SampleRate; + var multiple = TwoPi * Frequency / _waveFormat.SampleRate; - if (EnableSubOsc) - { - var subOsc = Math.Sin(_nSample * (TwoPi * SubOscillatorFrequency / _waveFormat.SampleRate) - + lfoSample(_nSample)); - sampleValue = Gain * Math.Sin(_nSample * multiple + lfoSample(_nSample) - + 0.5 * Gain * subOsc); - } - else - { - sampleValue = Gain * Math.Sin(_nSample * multiple + lfoSample(_nSample)); - } + if (EnableSubOsc) + { + var subOsc = Math.Sin(_nSample * (TwoPi * SubOscillatorFrequency / _waveFormat.SampleRate) + + lfoSample(_nSample)); + sampleValue = Gain * Math.Sin(_nSample * multiple + lfoSample(_nSample) + + 0.5 * Gain * subOsc); + } + else + { + sampleValue = Gain * Math.Sin(_nSample * multiple + lfoSample(_nSample)); + } - _nSample++; + _nSample++; - break; + break; - case SignalGeneratorType.Square: + case SignalGeneratorType.Square: - // Square Generator - //multiple = TwoPi * Frequency / waveFormat.SampleRate; - //var sampleSaw = Math.Sin(nSample * multiple + lfoSample(nSample)); - //sampleValue = sampleSaw > 0 ? Gain : -Gain; + // Square Generator + //multiple = TwoPi * Frequency / waveFormat.SampleRate; + //var sampleSaw = Math.Sin(nSample * multiple + lfoSample(nSample)); + //sampleValue = sampleSaw > 0 ? Gain : -Gain; - sampleValue = _squareWaveHelper.Read(WaveFormat.SampleRate, Frequency, Gain, Duty); + sampleValue = _squareWaveHelper.Read(WaveFormat.SampleRate, Frequency, Gain, Duty); - _nSample++; - break; + _nSample++; + break; - case SignalGeneratorType.Triangle: + case SignalGeneratorType.Triangle: - // Triangle Generator + // Triangle Generator - multiple = TwoPi * Frequency / _waveFormat.SampleRate; - sampleValue = Math.Asin(Math.Sin(_nSample * multiple + lfoSample(_nSample))) * (2.0 / Math.PI); - sampleValue *= Gain; + multiple = TwoPi * Frequency / _waveFormat.SampleRate; + sampleValue = Math.Asin(Math.Sin(_nSample * multiple + lfoSample(_nSample))) * (2.0 / Math.PI); + sampleValue *= Gain; - _nSample++; - break; + _nSample++; + break; - case SignalGeneratorType.SawTooth: + case SignalGeneratorType.SawTooth: - // SawTooth Generator + // SawTooth Generator - multiple = 2 * Frequency / _waveFormat.SampleRate; - var sampleSaw = (_nSample * multiple + lfoSample(_nSample)) % 2 - 1; - sampleValue = Gain * sampleSaw; + multiple = 2 * Frequency / _waveFormat.SampleRate; + var sampleSaw = (_nSample * multiple + lfoSample(_nSample)) % 2 - 1; + sampleValue = Gain * sampleSaw; - _nSample++; - break; + _nSample++; + break; - case SignalGeneratorType.White: + case SignalGeneratorType.White: - // White Noise Generator - sampleValue = Gain * NextRandomTwo(); - break; + // White Noise Generator + sampleValue = Gain * NextRandomTwo(); + break; - case SignalGeneratorType.Pink: + case SignalGeneratorType.Pink: - // Pink Noise Generator + // Pink Noise Generator - var white = NextRandomTwo(); - _pinkNoiseBuffer[0] = 0.99886 * _pinkNoiseBuffer[0] + white * 0.0555179; - _pinkNoiseBuffer[1] = 0.99332 * _pinkNoiseBuffer[1] + white * 0.0750759; - _pinkNoiseBuffer[2] = 0.96900 * _pinkNoiseBuffer[2] + white * 0.1538520; - _pinkNoiseBuffer[3] = 0.86650 * _pinkNoiseBuffer[3] + white * 0.3104856; - _pinkNoiseBuffer[4] = 0.55000 * _pinkNoiseBuffer[4] + white * 0.5329522; - _pinkNoiseBuffer[5] = -0.7616 * _pinkNoiseBuffer[5] - white * 0.0168980; - var pink = _pinkNoiseBuffer[0] + _pinkNoiseBuffer[1] + _pinkNoiseBuffer[2] + _pinkNoiseBuffer[3] + _pinkNoiseBuffer[4] + _pinkNoiseBuffer[5] + _pinkNoiseBuffer[6] + white * 0.5362; - _pinkNoiseBuffer[6] = white * 0.115926; - sampleValue = Gain * (pink / 5); - break; + var white = NextRandomTwo(); + _pinkNoiseBuffer[0] = 0.99886 * _pinkNoiseBuffer[0] + white * 0.0555179; + _pinkNoiseBuffer[1] = 0.99332 * _pinkNoiseBuffer[1] + white * 0.0750759; + _pinkNoiseBuffer[2] = 0.96900 * _pinkNoiseBuffer[2] + white * 0.1538520; + _pinkNoiseBuffer[3] = 0.86650 * _pinkNoiseBuffer[3] + white * 0.3104856; + _pinkNoiseBuffer[4] = 0.55000 * _pinkNoiseBuffer[4] + white * 0.5329522; + _pinkNoiseBuffer[5] = -0.7616 * _pinkNoiseBuffer[5] - white * 0.0168980; + var pink = _pinkNoiseBuffer[0] + _pinkNoiseBuffer[1] + _pinkNoiseBuffer[2] + _pinkNoiseBuffer[3] + _pinkNoiseBuffer[4] + _pinkNoiseBuffer[5] + _pinkNoiseBuffer[6] + white * 0.5362; + _pinkNoiseBuffer[6] = white * 0.115926; + sampleValue = Gain * (pink / 5); + break; - case SignalGeneratorType.Sweep: + case SignalGeneratorType.Sweep: - // Sweep Generator - var f = Math.Exp(FrequencyLog + _nSample * (FrequencyEndLog - FrequencyLog) / (SweepLengthSecs * _waveFormat.SampleRate)); + // Sweep Generator + var f = Math.Exp(FrequencyLog + _nSample * (FrequencyEndLog - FrequencyLog) / (SweepLengthSecs * _waveFormat.SampleRate)); - multiple = TwoPi * f / _waveFormat.SampleRate; - _phi += multiple; - sampleValue = Gain * Math.Sin(_phi); - _nSample++; - if (_nSample > SweepLengthSecs * _waveFormat.SampleRate) - { - _nSample = 0; - _phi = 0; - } - break; + multiple = TwoPi * f / _waveFormat.SampleRate; + _phi += multiple; + sampleValue = Gain * Math.Sin(_phi); + _nSample++; + if (_nSample > SweepLengthSecs * _waveFormat.SampleRate) + { + _nSample = 0; + _phi = 0; + } + break; - default: - sampleValue = 0.0; - break; - } + default: + sampleValue = 0.0; + break; + } - // phase Reverse Per Channel - for (var i = 0; i < _waveFormat.Channels; i++) - { - if (PhaseReverse[i]) - buffer[outIndex++] = (float)-sampleValue; - else - buffer[outIndex++] = (float)sampleValue; - } + // phase Reverse Per Channel + for (var i = 0; i < _waveFormat.Channels; i++) + { + if (PhaseReverse[i]) + buffer[outIndex++] = (float)-sampleValue; + else + buffer[outIndex++] = (float)sampleValue; } - return count; } + return count; + } - private double NextRandomTwo() - { - return 2 * _random.NextDouble() - 1; - } + private double NextRandomTwo() + { + return 2 * _random.NextDouble() - 1; } } diff --git a/src/libraries/Highbyte.DotNet6502.Impl.SilkNet/Commodore64/Input/C64SilkNetInputConfig.cs b/src/libraries/Highbyte.DotNet6502.Impl.SilkNet/Commodore64/Input/C64SilkNetInputConfig.cs index 840e4111..e52bc393 100644 --- a/src/libraries/Highbyte.DotNet6502.Impl.SilkNet/Commodore64/Input/C64SilkNetInputConfig.cs +++ b/src/libraries/Highbyte.DotNet6502.Impl.SilkNet/Commodore64/Input/C64SilkNetInputConfig.cs @@ -1,42 +1,41 @@ using Highbyte.DotNet6502.Systems.Commodore64.TimerAndPeripheral; -namespace Highbyte.DotNet6502.Impl.SilkNet.Commodore64.Input +namespace Highbyte.DotNet6502.Impl.SilkNet.Commodore64.Input; + +public class C64SilkNetInputConfig : ICloneable { - public class C64SilkNetInputConfig : ICloneable - { - public int CurrentJoystick = 2; + public int CurrentJoystick = 2; - public List AvailableJoysticks = new() { 1, 2 }; + public List AvailableJoysticks = new() { 1, 2 }; - public Dictionary> GamePadToC64JoystickMap = new() + public Dictionary> GamePadToC64JoystickMap = new() + { { + 1, + new Dictionary { - 1, - new Dictionary - { - { new[] { ButtonName.A }, new[] { C64JoystickAction.Fire } }, - { new[] { ButtonName.DPadUp }, new[] { C64JoystickAction.Up} }, - { new[] { ButtonName.DPadDown }, new[] { C64JoystickAction.Down } }, - { new[] { ButtonName.DPadLeft }, new[] { C64JoystickAction.Left } }, - { new[] { ButtonName.DPadRight }, new[] { C64JoystickAction.Right } }, - } - }, - { - 2, - new Dictionary - { - { new[] { ButtonName.A }, new[] { C64JoystickAction.Fire } }, - { new[] { ButtonName.DPadUp }, new[] { C64JoystickAction.Up} }, - { new[] { ButtonName.DPadDown }, new[] { C64JoystickAction.Down } }, - { new[] { ButtonName.DPadLeft }, new[] { C64JoystickAction.Left } }, - { new[] { ButtonName.DPadRight }, new[] { C64JoystickAction.Right } }, - } + { new[] { ButtonName.A }, new[] { C64JoystickAction.Fire } }, + { new[] { ButtonName.DPadUp }, new[] { C64JoystickAction.Up} }, + { new[] { ButtonName.DPadDown }, new[] { C64JoystickAction.Down } }, + { new[] { ButtonName.DPadLeft }, new[] { C64JoystickAction.Left } }, + { new[] { ButtonName.DPadRight }, new[] { C64JoystickAction.Right } }, } - }; - - public object Clone() + }, { - return MemberwiseClone(); + 2, + new Dictionary + { + { new[] { ButtonName.A }, new[] { C64JoystickAction.Fire } }, + { new[] { ButtonName.DPadUp }, new[] { C64JoystickAction.Up} }, + { new[] { ButtonName.DPadDown }, new[] { C64JoystickAction.Down } }, + { new[] { ButtonName.DPadLeft }, new[] { C64JoystickAction.Left } }, + { new[] { ButtonName.DPadRight }, new[] { C64JoystickAction.Right } }, + } } + }; + + public object Clone() + { + return MemberwiseClone(); } } diff --git a/src/libraries/Highbyte.DotNet6502.Impl.SilkNet/Commodore64/Video/C64SilkNetOpenGlRendererConfig.cs b/src/libraries/Highbyte.DotNet6502.Impl.SilkNet/Commodore64/Video/C64SilkNetOpenGlRendererConfig.cs index 1e7cf206..8de9af5b 100644 --- a/src/libraries/Highbyte.DotNet6502.Impl.SilkNet/Commodore64/Video/C64SilkNetOpenGlRendererConfig.cs +++ b/src/libraries/Highbyte.DotNet6502.Impl.SilkNet/Commodore64/Video/C64SilkNetOpenGlRendererConfig.cs @@ -1,16 +1,15 @@ -namespace Highbyte.DotNet6502.Impl.SilkNet.Commodore64.Video +namespace Highbyte.DotNet6502.Impl.SilkNet.Commodore64.Video; + +public class C64SilkNetOpenGlRendererConfig : ICloneable { - public class C64SilkNetOpenGlRendererConfig : ICloneable + public bool UseFineScrollPerRasterLine { get; set; } + public C64SilkNetOpenGlRendererConfig() { - public bool UseFineScrollPerRasterLine { get; set; } - public C64SilkNetOpenGlRendererConfig() - { - UseFineScrollPerRasterLine = false; - } - public object Clone() - { - var clone = (C64SilkNetOpenGlRendererConfig)MemberwiseClone(); - return clone; - } + UseFineScrollPerRasterLine = false; + } + public object Clone() + { + var clone = (C64SilkNetOpenGlRendererConfig)MemberwiseClone(); + return clone; } } diff --git a/src/libraries/Highbyte.DotNet6502.Impl.SilkNet/OpenGLHelpers/BufferHelper.cs b/src/libraries/Highbyte.DotNet6502.Impl.SilkNet/OpenGLHelpers/BufferHelper.cs index 3e7c5970..af3fc69e 100644 --- a/src/libraries/Highbyte.DotNet6502.Impl.SilkNet/OpenGLHelpers/BufferHelper.cs +++ b/src/libraries/Highbyte.DotNet6502.Impl.SilkNet/OpenGLHelpers/BufferHelper.cs @@ -1,42 +1,41 @@ using Silk.NET.OpenGL; using System.Runtime.CompilerServices; -namespace Highbyte.DotNet6502.Impl.SilkNet.OpenGLHelpers +namespace Highbyte.DotNet6502.Impl.SilkNet.OpenGLHelpers; + +public static class BufferHelper { - public static class BufferHelper + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe BufferInfo CreateBuffer( + GL gl, + BufferTargetARB bufferTarget, + BufferUsageARB bufferUsage, + ReadOnlySpan data + ) + where TData : unmanaged { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe BufferInfo CreateBuffer( - GL gl, - BufferTargetARB bufferTarget, - BufferUsageARB bufferUsage, - ReadOnlySpan data - ) - where TData : unmanaged - { - var handle = gl.GenBuffer(); + var handle = gl.GenBuffer(); - gl.BindBuffer(bufferTarget, handle); - gl.BufferData(bufferTarget, data, bufferUsage); - gl.BindBuffer(bufferTarget, 0); - return new BufferInfo(bufferTarget, handle, (uint)(sizeof(TData) * data.Length)); - } + gl.BindBuffer(bufferTarget, handle); + gl.BufferData(bufferTarget, data, bufferUsage); + gl.BindBuffer(bufferTarget, 0); + return new BufferInfo(bufferTarget, handle, (uint)(sizeof(TData) * data.Length)); + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe void UpdateBuffer( - GL gl, - BufferInfo uniformBufferInfo, - ReadOnlySpan data, - int offset = 0 - ) - where TData : unmanaged - { - if (uniformBufferInfo.Size < sizeof(TData) * (data.Length + offset)) - throw new ArgumentException($"{nameof(data)} doesn't fit in provided {nameof(BufferInfo)}"); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe void UpdateBuffer( + GL gl, + BufferInfo uniformBufferInfo, + ReadOnlySpan data, + int offset = 0 + ) + where TData : unmanaged + { + if (uniformBufferInfo.Size < sizeof(TData) * (data.Length + offset)) + throw new ArgumentException($"{nameof(data)} doesn't fit in provided {nameof(BufferInfo)}"); - gl.BindBuffer(uniformBufferInfo.BufferTarget, uniformBufferInfo.Handle); - gl.BufferSubData(uniformBufferInfo.BufferTarget, offset * sizeof(TData), data); - gl.BindBuffer(uniformBufferInfo.BufferTarget, 0); - } + gl.BindBuffer(uniformBufferInfo.BufferTarget, uniformBufferInfo.Handle); + gl.BufferSubData(uniformBufferInfo.BufferTarget, offset * sizeof(TData), data); + gl.BindBuffer(uniformBufferInfo.BufferTarget, 0); } } diff --git a/src/libraries/Highbyte.DotNet6502.Impl.SilkNet/OpenGLHelpers/BufferInfo.cs b/src/libraries/Highbyte.DotNet6502.Impl.SilkNet/OpenGLHelpers/BufferInfo.cs index 74e7b33c..639cdbd9 100644 --- a/src/libraries/Highbyte.DotNet6502.Impl.SilkNet/OpenGLHelpers/BufferInfo.cs +++ b/src/libraries/Highbyte.DotNet6502.Impl.SilkNet/OpenGLHelpers/BufferInfo.cs @@ -1,17 +1,16 @@ using Silk.NET.OpenGL; -namespace Highbyte.DotNet6502.Impl.SilkNet.OpenGLHelpers +namespace Highbyte.DotNet6502.Impl.SilkNet.OpenGLHelpers; + +public class BufferInfo { - public class BufferInfo + public BufferTargetARB BufferTarget { get; private set; } + public uint Handle { get; private set; } + public uint Size { get; private set; } + public BufferInfo(BufferTargetARB bufferTarget, uint handle, uint size) { - public BufferTargetARB BufferTarget { get; private set; } - public uint Handle { get; private set; } - public uint Size { get; private set; } - public BufferInfo(BufferTargetARB bufferTarget, uint handle, uint size) - { - BufferTarget = bufferTarget; - Handle = handle; - Size = size; - } + BufferTarget = bufferTarget; + Handle = handle; + Size = size; } } diff --git a/src/libraries/Highbyte.DotNet6502.Impl.SilkNet/OpenGLHelpers/BufferObject.cs b/src/libraries/Highbyte.DotNet6502.Impl.SilkNet/OpenGLHelpers/BufferObject.cs index 8991b03e..1e12d13c 100644 --- a/src/libraries/Highbyte.DotNet6502.Impl.SilkNet/OpenGLHelpers/BufferObject.cs +++ b/src/libraries/Highbyte.DotNet6502.Impl.SilkNet/OpenGLHelpers/BufferObject.cs @@ -1,36 +1,35 @@ using Silk.NET.OpenGL; -namespace Highbyte.DotNet6502.Impl.SilkNet.OpenGLHelpers +namespace Highbyte.DotNet6502.Impl.SilkNet.OpenGLHelpers; + +public class BufferObject : IDisposable + where TData : unmanaged { - public class BufferObject : IDisposable - where TData : unmanaged - { - private GL _gl; - private BufferInfo _bufferInfo; - public BufferInfo BufferInfo => _bufferInfo; + private GL _gl; + private BufferInfo _bufferInfo; + public BufferInfo BufferInfo => _bufferInfo; - public unsafe BufferObject(GL gl, ReadOnlySpan data, BufferTargetARB bufferTarget, BufferUsageARB bufferUsage = BufferUsageARB.StaticDraw) - { - _gl = gl; - _bufferInfo = BufferHelper.CreateBuffer(gl, bufferTarget, bufferUsage, data); - } + public unsafe BufferObject(GL gl, ReadOnlySpan data, BufferTargetARB bufferTarget, BufferUsageARB bufferUsage = BufferUsageARB.StaticDraw) + { + _gl = gl; + _bufferInfo = BufferHelper.CreateBuffer(gl, bufferTarget, bufferUsage, data); + } - public unsafe void Update(ReadOnlySpan data, int offset = 0) - { - BufferHelper.UpdateBuffer(_gl, _bufferInfo, data, offset); - } + public unsafe void Update(ReadOnlySpan data, int offset = 0) + { + BufferHelper.UpdateBuffer(_gl, _bufferInfo, data, offset); + } - public void Bind() - { - _gl.BindBuffer(_bufferInfo.BufferTarget, _bufferInfo.Handle); - } - public void Unbind() - { - _gl.BindBuffer(_bufferInfo.BufferTarget, 0); - } - public void Dispose() - { - _gl.DeleteBuffer(_bufferInfo.Handle); - } + public void Bind() + { + _gl.BindBuffer(_bufferInfo.BufferTarget, _bufferInfo.Handle); + } + public void Unbind() + { + _gl.BindBuffer(_bufferInfo.BufferTarget, 0); + } + public void Dispose() + { + _gl.DeleteBuffer(_bufferInfo.Handle); } } diff --git a/src/libraries/Highbyte.DotNet6502.Impl.SilkNet/OpenGLHelpers/Shader.cs b/src/libraries/Highbyte.DotNet6502.Impl.SilkNet/OpenGLHelpers/Shader.cs index ecfbde74..459bdaad 100644 --- a/src/libraries/Highbyte.DotNet6502.Impl.SilkNet/OpenGLHelpers/Shader.cs +++ b/src/libraries/Highbyte.DotNet6502.Impl.SilkNet/OpenGLHelpers/Shader.cs @@ -1,176 +1,175 @@ using System.Numerics; using Silk.NET.OpenGL; -namespace Highbyte.DotNet6502.Impl.SilkNet.OpenGLHelpers +namespace Highbyte.DotNet6502.Impl.SilkNet.OpenGLHelpers; + +public class Shader : IDisposable { - public class Shader : IDisposable + private uint _programHandle; + public uint Handle => _programHandle; + private GL _gl; + + public Shader(GL gl, string? vertexShaderPath = null, string? geometryShaderPath = null, string? fragmentShaderPath = null) { - private uint _programHandle; - public uint Handle => _programHandle; - private GL _gl; + _gl = gl; + _programHandle = _gl.CreateProgram(); - public Shader(GL gl, string? vertexShaderPath = null, string? geometryShaderPath = null, string? fragmentShaderPath = null) + uint? vertexShaderHandle = null; + if (!string.IsNullOrEmpty(vertexShaderPath)) { - _gl = gl; - _programHandle = _gl.CreateProgram(); - - uint? vertexShaderHandle = null; - if (!string.IsNullOrEmpty(vertexShaderPath)) - { - vertexShaderHandle = LoadShader(ShaderType.VertexShader, vertexShaderPath); - _gl.AttachShader(_programHandle, vertexShaderHandle.Value); - } - - uint? geometryShaderHandle = null; - if (!string.IsNullOrEmpty(geometryShaderPath)) - { - geometryShaderHandle = LoadShader(ShaderType.GeometryShader, geometryShaderPath); - _gl.AttachShader(_programHandle, geometryShaderHandle.Value); - } - - uint? fragmentShaderHandle = null; - if (!string.IsNullOrEmpty(fragmentShaderPath)) - { - fragmentShaderHandle = LoadShader(ShaderType.FragmentShader, fragmentShaderPath); - _gl.AttachShader(_programHandle, fragmentShaderHandle.Value); - } - - - _gl.LinkProgram(_programHandle); - _gl.GetProgram(_programHandle, GLEnum.LinkStatus, out var status); - if (status == 0) - throw new DotNet6502Exception($"Program failed to link with error: {_gl.GetProgramInfoLog(_programHandle)}"); - - if (vertexShaderHandle != null) - { - _gl.DetachShader(_programHandle, vertexShaderHandle.Value); - _gl.DeleteShader(vertexShaderHandle.Value); - } - if (geometryShaderHandle != null) - { - _gl.DetachShader(_programHandle, geometryShaderHandle.Value); - _gl.DeleteShader(geometryShaderHandle.Value); - } - if (fragmentShaderHandle != null) - { - _gl.DetachShader(_programHandle, fragmentShaderHandle.Value); - _gl.DeleteShader(fragmentShaderHandle.Value); - } + vertexShaderHandle = LoadShader(ShaderType.VertexShader, vertexShaderPath); + _gl.AttachShader(_programHandle, vertexShaderHandle.Value); } - public void Use() + uint? geometryShaderHandle = null; + if (!string.IsNullOrEmpty(geometryShaderPath)) { - _gl.UseProgram(_programHandle); + geometryShaderHandle = LoadShader(ShaderType.GeometryShader, geometryShaderPath); + _gl.AttachShader(_programHandle, geometryShaderHandle.Value); } - public void SetUniform(string name, int value, bool skipExistCheck = false) + uint? fragmentShaderHandle = null; + if (!string.IsNullOrEmpty(fragmentShaderPath)) { - var location = _gl.GetUniformLocation(_programHandle, name); - if (!skipExistCheck && location == -1) - throw new DotNet6502Exception($"{name} uniform not found on shader."); - _gl.Uniform1(location, value); + fragmentShaderHandle = LoadShader(ShaderType.FragmentShader, fragmentShaderPath); + _gl.AttachShader(_programHandle, fragmentShaderHandle.Value); } - public void SetUniform(string name, uint value, bool skipExistCheck = false) - { - var location = _gl.GetUniformLocation(_programHandle, name); - if (!skipExistCheck && location == -1) - throw new DotNet6502Exception($"{name} uniform not found on shader."); - _gl.Uniform1(location, value); - } - public void SetUniform(string name, bool value, bool skipExistCheck = false) - { - var location = _gl.GetUniformLocation(_programHandle, name); - if (!skipExistCheck && location == -1) - throw new DotNet6502Exception($"{name} uniform not found on shader."); - _gl.Uniform1(location, value ? 1 : 0); - } + _gl.LinkProgram(_programHandle); + _gl.GetProgram(_programHandle, GLEnum.LinkStatus, out var status); + if (status == 0) + throw new DotNet6502Exception($"Program failed to link with error: {_gl.GetProgramInfoLog(_programHandle)}"); - public unsafe void SetUniform(string name, Matrix4x4 value, bool skipExistCheck = false) + if (vertexShaderHandle != null) { - //A new overload has been created for setting a uniform so we can use the transform in our shader. - var location = _gl.GetUniformLocation(_programHandle, name); - if (!skipExistCheck && location == -1) - throw new DotNet6502Exception($"{name} uniform not found on shader."); - _gl.UniformMatrix4(location, 1, false, (float*)&value); + _gl.DetachShader(_programHandle, vertexShaderHandle.Value); + _gl.DeleteShader(vertexShaderHandle.Value); } - - public unsafe void SetUniform(string name, Vector4 value, bool skipExistCheck = false) + if (geometryShaderHandle != null) { - //A new overload has been created for setting a uniform so we can use the transform in our shader. - var location = _gl.GetUniformLocation(_programHandle, name); - if (!skipExistCheck && location == -1) - throw new DotNet6502Exception($"{name} uniform not found on shader."); - _gl.Uniform4(location, value.X, value.Y, value.Z, value.W); + _gl.DetachShader(_programHandle, geometryShaderHandle.Value); + _gl.DeleteShader(geometryShaderHandle.Value); } - - public unsafe void SetUniform(string name, Vector3 value, bool skipExistCheck = false) + if (fragmentShaderHandle != null) { - //A new overload has been created for setting a uniform so we can use the transform in our shader. - var location = _gl.GetUniformLocation(_programHandle, name); - if (!skipExistCheck && location == -1) - throw new DotNet6502Exception($"{name} uniform not found on shader."); - _gl.Uniform3(location, value.X, value.Y, value.Z); + _gl.DetachShader(_programHandle, fragmentShaderHandle.Value); + _gl.DeleteShader(fragmentShaderHandle.Value); } + } - public unsafe void SetUniform(string name, Vector2 value, bool skipExistCheck = false) - { - //A new overload has been created for setting a uniform so we can use the transform in our shader. - var location = _gl.GetUniformLocation(_programHandle, name); - if (!skipExistCheck && location == -1) - throw new DotNet6502Exception($"{name} uniform not found on shader."); - _gl.Uniform2(location, value.X, value.Y); - } + public void Use() + { + _gl.UseProgram(_programHandle); + } - public void SetUniform(string name, float value) - { - var location = _gl.GetUniformLocation(_programHandle, name); - if (location == -1) - throw new DotNet6502Exception($"{name} uniform not found on shader."); - _gl.Uniform1(location, value); - } + public void SetUniform(string name, int value, bool skipExistCheck = false) + { + var location = _gl.GetUniformLocation(_programHandle, name); + if (!skipExistCheck && location == -1) + throw new DotNet6502Exception($"{name} uniform not found on shader."); + _gl.Uniform1(location, value); + } - public void BindUBO(string uniformBlockName, uint uboHandle, uint binding_point_index = 0) - { - var block_index = _gl.GetUniformBlockIndex(_programHandle, uniformBlockName); - _gl.BindBufferBase(BufferTargetARB.UniformBuffer, binding_point_index, uboHandle); - _gl.UniformBlockBinding(_programHandle, block_index, binding_point_index); - } + public void SetUniform(string name, uint value, bool skipExistCheck = false) + { + var location = _gl.GetUniformLocation(_programHandle, name); + if (!skipExistCheck && location == -1) + throw new DotNet6502Exception($"{name} uniform not found on shader."); + _gl.Uniform1(location, value); + } - public void BindUBO(string uniformBlockName, BufferInfo bufferInfo, uint binding_point_index = 0) - { - var block_index = _gl.GetUniformBlockIndex(_programHandle, uniformBlockName); - _gl.BindBufferBase(BufferTargetARB.UniformBuffer, binding_point_index, bufferInfo.Handle); - _gl.UniformBlockBinding(_programHandle, block_index, binding_point_index); - } + public void SetUniform(string name, bool value, bool skipExistCheck = false) + { + var location = _gl.GetUniformLocation(_programHandle, name); + if (!skipExistCheck && location == -1) + throw new DotNet6502Exception($"{name} uniform not found on shader."); + _gl.Uniform1(location, value ? 1 : 0); + } - public void BindUBO(string uniformBlockName, BufferObject bufferObject, uint binding_point_index = 0) where TData : unmanaged - { - BindUBO(uniformBlockName, bufferObject.BufferInfo, binding_point_index); - } + public unsafe void SetUniform(string name, Matrix4x4 value, bool skipExistCheck = false) + { + //A new overload has been created for setting a uniform so we can use the transform in our shader. + var location = _gl.GetUniformLocation(_programHandle, name); + if (!skipExistCheck && location == -1) + throw new DotNet6502Exception($"{name} uniform not found on shader."); + _gl.UniformMatrix4(location, 1, false, (float*)&value); + } - public int GetAttribLocation(string attribName) - { - return _gl.GetAttribLocation(_programHandle, attribName); - } + public unsafe void SetUniform(string name, Vector4 value, bool skipExistCheck = false) + { + //A new overload has been created for setting a uniform so we can use the transform in our shader. + var location = _gl.GetUniformLocation(_programHandle, name); + if (!skipExistCheck && location == -1) + throw new DotNet6502Exception($"{name} uniform not found on shader."); + _gl.Uniform4(location, value.X, value.Y, value.Z, value.W); + } - public void Dispose() - { - _gl.DeleteProgram(_programHandle); - } + public unsafe void SetUniform(string name, Vector3 value, bool skipExistCheck = false) + { + //A new overload has been created for setting a uniform so we can use the transform in our shader. + var location = _gl.GetUniformLocation(_programHandle, name); + if (!skipExistCheck && location == -1) + throw new DotNet6502Exception($"{name} uniform not found on shader."); + _gl.Uniform3(location, value.X, value.Y, value.Z); + } - private uint LoadShader(ShaderType type, string path) - { - var src = File.ReadAllText(path); - var handle = _gl.CreateShader(type); - _gl.ShaderSource(handle, src); - _gl.CompileShader(handle); - var infoLog = _gl.GetShaderInfoLog(handle); - if (!string.IsNullOrWhiteSpace(infoLog)) - throw new DotNet6502Exception($"Error compiling shader of type {type}, failed with error {infoLog}"); - - return handle; - } + public unsafe void SetUniform(string name, Vector2 value, bool skipExistCheck = false) + { + //A new overload has been created for setting a uniform so we can use the transform in our shader. + var location = _gl.GetUniformLocation(_programHandle, name); + if (!skipExistCheck && location == -1) + throw new DotNet6502Exception($"{name} uniform not found on shader."); + _gl.Uniform2(location, value.X, value.Y); + } + + public void SetUniform(string name, float value) + { + var location = _gl.GetUniformLocation(_programHandle, name); + if (location == -1) + throw new DotNet6502Exception($"{name} uniform not found on shader."); + _gl.Uniform1(location, value); + } + + public void BindUBO(string uniformBlockName, uint uboHandle, uint binding_point_index = 0) + { + var block_index = _gl.GetUniformBlockIndex(_programHandle, uniformBlockName); + _gl.BindBufferBase(BufferTargetARB.UniformBuffer, binding_point_index, uboHandle); + _gl.UniformBlockBinding(_programHandle, block_index, binding_point_index); + } + + public void BindUBO(string uniformBlockName, BufferInfo bufferInfo, uint binding_point_index = 0) + { + var block_index = _gl.GetUniformBlockIndex(_programHandle, uniformBlockName); + _gl.BindBufferBase(BufferTargetARB.UniformBuffer, binding_point_index, bufferInfo.Handle); + _gl.UniformBlockBinding(_programHandle, block_index, binding_point_index); + } + + public void BindUBO(string uniformBlockName, BufferObject bufferObject, uint binding_point_index = 0) where TData : unmanaged + { + BindUBO(uniformBlockName, bufferObject.BufferInfo, binding_point_index); + } + + public int GetAttribLocation(string attribName) + { + return _gl.GetAttribLocation(_programHandle, attribName); + } + + public void Dispose() + { + _gl.DeleteProgram(_programHandle); + } + + private uint LoadShader(ShaderType type, string path) + { + var src = File.ReadAllText(path); + var handle = _gl.CreateShader(type); + _gl.ShaderSource(handle, src); + _gl.CompileShader(handle); + var infoLog = _gl.GetShaderInfoLog(handle); + if (!string.IsNullOrWhiteSpace(infoLog)) + throw new DotNet6502Exception($"Error compiling shader of type {type}, failed with error {infoLog}"); + + return handle; } } diff --git a/src/libraries/Highbyte.DotNet6502.Impl.SilkNet/OpenGLHelpers/VertexArrayObject.cs b/src/libraries/Highbyte.DotNet6502.Impl.SilkNet/OpenGLHelpers/VertexArrayObject.cs index 7fd6c03b..96553900 100644 --- a/src/libraries/Highbyte.DotNet6502.Impl.SilkNet/OpenGLHelpers/VertexArrayObject.cs +++ b/src/libraries/Highbyte.DotNet6502.Impl.SilkNet/OpenGLHelpers/VertexArrayObject.cs @@ -1,62 +1,60 @@ using Silk.NET.OpenGL; -using System; -namespace Highbyte.DotNet6502.Impl.SilkNet.OpenGLHelpers +namespace Highbyte.DotNet6502.Impl.SilkNet.OpenGLHelpers; + +//The vertex array object abstraction. +public class VertexArrayObject : IDisposable + where TVertexType : unmanaged + where TIndexType : unmanaged { - //The vertex array object abstraction. - public class VertexArrayObject : IDisposable - where TVertexType : unmanaged - where TIndexType : unmanaged + //Our handle and the GL instance this class will use, these are private because they have no reason to be public. + //Most of the time you would want to abstract items to make things like this invisible. + private uint _handle; + private GL _gl; + + public VertexArrayObject(GL gl, BufferObject vbo, BufferObject ebo) + { + //Saving the GL instance. + _gl = gl; + + //Setting out handle and binding the VBO and EBO to this VAO. + _handle = _gl.GenVertexArray(); + Bind(); + vbo.Bind(); + ebo.Bind(); + } + + public VertexArrayObject(GL gl, BufferObject vbo) + { + //Saving the GL instance. + _gl = gl; + + //Setting out handle and binding the VBO and EBO to this VAO. + _handle = _gl.GenVertexArray(); + Bind(); + vbo.Bind(); + } + + public unsafe void VertexAttributePointer(uint index, int count, VertexAttribPointerType type, uint vertexSize, int offSet) + { + //Setting up a vertex attribute pointer + _gl.VertexAttribPointer(index, count, type, false, vertexSize * (uint)sizeof(TVertexType), (void*)(offSet * sizeof(TVertexType))); + _gl.EnableVertexAttribArray(index); + } + + public void Bind() + { + _gl.BindVertexArray(_handle); + } + + public void Unbind() + { + _gl.BindVertexArray(0); + } + + public void Dispose() { - //Our handle and the GL instance this class will use, these are private because they have no reason to be public. - //Most of the time you would want to abstract items to make things like this invisible. - private uint _handle; - private GL _gl; - - public VertexArrayObject(GL gl, BufferObject vbo, BufferObject ebo) - { - //Saving the GL instance. - _gl = gl; - - //Setting out handle and binding the VBO and EBO to this VAO. - _handle = _gl.GenVertexArray(); - Bind(); - vbo.Bind(); - ebo.Bind(); - } - - public VertexArrayObject(GL gl, BufferObject vbo) - { - //Saving the GL instance. - _gl = gl; - - //Setting out handle and binding the VBO and EBO to this VAO. - _handle = _gl.GenVertexArray(); - Bind(); - vbo.Bind(); - } - - public unsafe void VertexAttributePointer(uint index, int count, VertexAttribPointerType type, uint vertexSize, int offSet) - { - //Setting up a vertex attribute pointer - _gl.VertexAttribPointer(index, count, type, false, vertexSize * (uint)sizeof(TVertexType), (void*)(offSet * sizeof(TVertexType))); - _gl.EnableVertexAttribArray(index); - } - - public void Bind() - { - _gl.BindVertexArray(_handle); - } - - public void Unbind() - { - _gl.BindVertexArray(0); - } - - public void Dispose() - { - //We don't delete the VBO and EBO here, as you can have one VBO stored under multiple VAO's. - _gl.DeleteVertexArray(_handle); - } + //We don't delete the VBO and EBO here, as you can have one VBO stored under multiple VAO's. + _gl.DeleteVertexArray(_handle); } } diff --git a/src/libraries/Highbyte.DotNet6502.Systems/Commodore64/Audio/AudioGlobalCommand.cs b/src/libraries/Highbyte.DotNet6502.Systems/Commodore64/Audio/AudioGlobalCommand.cs index 2fcd5c9f..63983525 100644 --- a/src/libraries/Highbyte.DotNet6502.Systems/Commodore64/Audio/AudioGlobalCommand.cs +++ b/src/libraries/Highbyte.DotNet6502.Systems/Commodore64/Audio/AudioGlobalCommand.cs @@ -1,33 +1,32 @@ -namespace Highbyte.DotNet6502.Systems.Commodore64.Audio +namespace Highbyte.DotNet6502.Systems.Commodore64.Audio; + +public enum AudioGlobalCommand { - public enum AudioGlobalCommand - { - None, - ChangeVolume, // Change volume on current playing audio. - } + None, + ChangeVolume, // Change volume on current playing audio. +} - public static class AudioGlobalCommandBuilder +public static class AudioGlobalCommandBuilder +{ + /// + /// Get commands for global SID settings such as volume. + /// + /// + /// + /// + public static AudioGlobalCommand GetGlobalAudioCommand( + InternalSidState sidState) { - /// - /// Get commands for global SID settings such as volume. - /// - /// - /// - /// - public static AudioGlobalCommand GetGlobalAudioCommand( - InternalSidState sidState) - { - AudioGlobalCommand command = AudioGlobalCommand.None; - - var isVolumeChanged = sidState.IsVolumeChanged; + AudioGlobalCommand command = AudioGlobalCommand.None; - // Check if SID volume has changed - if (isVolumeChanged) - { - command = AudioGlobalCommand.ChangeVolume; - } + var isVolumeChanged = sidState.IsVolumeChanged; - return command; + // Check if SID volume has changed + if (isVolumeChanged) + { + command = AudioGlobalCommand.ChangeVolume; } + + return command; } } diff --git a/src/libraries/Highbyte.DotNet6502.Systems/Commodore64/Audio/AudioGlobalParameter.cs b/src/libraries/Highbyte.DotNet6502.Systems/Commodore64/Audio/AudioGlobalParameter.cs index cc9d43a8..4a2c5580 100644 --- a/src/libraries/Highbyte.DotNet6502.Systems/Commodore64/Audio/AudioGlobalParameter.cs +++ b/src/libraries/Highbyte.DotNet6502.Systems/Commodore64/Audio/AudioGlobalParameter.cs @@ -1,29 +1,28 @@ -namespace Highbyte.DotNet6502.Systems.Commodore64.Audio +namespace Highbyte.DotNet6502.Systems.Commodore64.Audio; + +public class AudioGlobalParameter { - public class AudioGlobalParameter - { - public AudioGlobalCommand AudioCommand { get; set; } + public AudioGlobalCommand AudioCommand { get; set; } - public float Gain { get; set; } + public float Gain { get; set; } - public static AudioGlobalParameter BuildAudioGlobalParameter( - InternalSidState sidState - ) + public static AudioGlobalParameter BuildAudioGlobalParameter( + InternalSidState sidState + ) + { + // ---------- + // Map SID register values to audio parameters usable by Web Audio, and what to do with the audio. + // ---------- + var audioVoiceParameter = new AudioGlobalParameter { - // ---------- - // Map SID register values to audio parameters usable by Web Audio, and what to do with the audio. - // ---------- - var audioVoiceParameter = new AudioGlobalParameter - { - // What to do with the audio (Start ADS cycle, start Release cycle, stop audio, change frequency, change volume) - AudioCommand = AudioGlobalCommandBuilder.GetGlobalAudioCommand( - sidState), + // What to do with the audio (Start ADS cycle, start Release cycle, stop audio, change frequency, change volume) + AudioCommand = AudioGlobalCommandBuilder.GetGlobalAudioCommand( + sidState), - // Translate SID volume 0-15 to Gain 0.0-1.0 - // SID volume in lower 4 bits of SIGVOL register. - Gain = Math.Clamp((float)(sidState.GetVolume() / 15.0f), 0.0f, 1.0f), - }; - return audioVoiceParameter; - } + // Translate SID volume 0-15 to Gain 0.0-1.0 + // SID volume in lower 4 bits of SIGVOL register. + Gain = Math.Clamp((float)(sidState.GetVolume() / 15.0f), 0.0f, 1.0f), + }; + return audioVoiceParameter; } } diff --git a/src/libraries/Highbyte.DotNet6502.Systems/Commodore64/Audio/AudioVoiceCommand.cs b/src/libraries/Highbyte.DotNet6502.Systems/Commodore64/Audio/AudioVoiceCommand.cs index 0e4d5aee..5541ad25 100644 --- a/src/libraries/Highbyte.DotNet6502.Systems/Commodore64/Audio/AudioVoiceCommand.cs +++ b/src/libraries/Highbyte.DotNet6502.Systems/Commodore64/Audio/AudioVoiceCommand.cs @@ -1,73 +1,72 @@ -namespace Highbyte.DotNet6502.Systems.Commodore64.Audio +namespace Highbyte.DotNet6502.Systems.Commodore64.Audio; + +public enum AudioVoiceCommand { - public enum AudioVoiceCommand - { - None, - StartADS, // Start attack/decay/sustain cycle. - StartRelease, // Start release cycle, which will fade volume down to 0 during the release period. - ChangeFrequency, // Change frequency on current playing audio. - ChangePulseWidth, // Change pulse width on current playing audio (only for pulse oscillator) - Stop // Stop current playing audio right away. - } + None, + StartADS, // Start attack/decay/sustain cycle. + StartRelease, // Start release cycle, which will fade volume down to 0 during the release period. + ChangeFrequency, // Change frequency on current playing audio. + ChangePulseWidth, // Change pulse width on current playing audio (only for pulse oscillator) + Stop // Stop current playing audio right away. +} - public static class AudioVoiceCommandBuilder +public static class AudioVoiceCommandBuilder +{ + public static AudioVoiceCommand GetAudioCommand( + byte voice, + AudioVoiceStatus currentVoiceAudioStatus, + InternalSidState sidState) { - public static AudioVoiceCommand GetAudioCommand( - byte voice, - AudioVoiceStatus currentVoiceAudioStatus, - InternalSidState sidState) - { - AudioVoiceCommand command = AudioVoiceCommand.None; - - var gateControl = sidState.GetGateControl(voice); - var isFrequencyChanged = sidState.IsFrequencyChanged(voice); - var isPulseWidthChanged = sidState.IsPulseWidthChanged(voice); - var isVolumeChanged = sidState.IsVolumeChanged; + AudioVoiceCommand command = AudioVoiceCommand.None; - // New audio (ADS cycle) is started when - // - Starting ADS is selected in the SID gate register - // - and no audio is playing (or when the release cycle has started) - if (gateControl == InternalSidState.GateControl.StartAttackDecaySustain - && (currentVoiceAudioStatus == AudioVoiceStatus.Stopped || - currentVoiceAudioStatus == AudioVoiceStatus.ReleaseCycleStarted)) - { - command = AudioVoiceCommand.StartADS; - } + var gateControl = sidState.GetGateControl(voice); + var isFrequencyChanged = sidState.IsFrequencyChanged(voice); + var isPulseWidthChanged = sidState.IsPulseWidthChanged(voice); + var isVolumeChanged = sidState.IsVolumeChanged; - // Release cycle can be started when - // - Starting Release is selected in the SID gate register - // - ADS cycle has already been started - // - or ADS cycle has already stopped (which in case nothing will really happen - else if (gateControl == InternalSidState.GateControl.StartRelease - && (currentVoiceAudioStatus == AudioVoiceStatus.ADSCycleStarted || currentVoiceAudioStatus == AudioVoiceStatus.Stopped)) - { - command = AudioVoiceCommand.StartRelease; - } + // New audio (ADS cycle) is started when + // - Starting ADS is selected in the SID gate register + // - and no audio is playing (or when the release cycle has started) + if (gateControl == InternalSidState.GateControl.StartAttackDecaySustain + && (currentVoiceAudioStatus == AudioVoiceStatus.Stopped || + currentVoiceAudioStatus == AudioVoiceStatus.ReleaseCycleStarted)) + { + command = AudioVoiceCommand.StartADS; + } - // Audio is stopped immediately when - // - Gate is off (in gate register when gate bit is 0 and no waveform selected) - else if (gateControl == InternalSidState.GateControl.StopAudio) - { - command = AudioVoiceCommand.Stop; - } + // Release cycle can be started when + // - Starting Release is selected in the SID gate register + // - ADS cycle has already been started + // - or ADS cycle has already stopped (which in case nothing will really happen + else if (gateControl == InternalSidState.GateControl.StartRelease + && (currentVoiceAudioStatus == AudioVoiceStatus.ADSCycleStarted || currentVoiceAudioStatus == AudioVoiceStatus.Stopped)) + { + command = AudioVoiceCommand.StartRelease; + } - // Check if frequency has changed, and if any audio is currently playing. - else if (isFrequencyChanged - && (currentVoiceAudioStatus == AudioVoiceStatus.ADSCycleStarted - || currentVoiceAudioStatus == AudioVoiceStatus.ReleaseCycleStarted)) - { - command = AudioVoiceCommand.ChangeFrequency; - } + // Audio is stopped immediately when + // - Gate is off (in gate register when gate bit is 0 and no waveform selected) + else if (gateControl == InternalSidState.GateControl.StopAudio) + { + command = AudioVoiceCommand.Stop; + } - // Check if pulsewidth has changed, and if any audio is currently playing. - else if (isPulseWidthChanged - && (currentVoiceAudioStatus == AudioVoiceStatus.ADSCycleStarted - || currentVoiceAudioStatus == AudioVoiceStatus.ReleaseCycleStarted)) - { - command = AudioVoiceCommand.ChangePulseWidth; - } + // Check if frequency has changed, and if any audio is currently playing. + else if (isFrequencyChanged + && (currentVoiceAudioStatus == AudioVoiceStatus.ADSCycleStarted + || currentVoiceAudioStatus == AudioVoiceStatus.ReleaseCycleStarted)) + { + command = AudioVoiceCommand.ChangeFrequency; + } - return command; + // Check if pulsewidth has changed, and if any audio is currently playing. + else if (isPulseWidthChanged + && (currentVoiceAudioStatus == AudioVoiceStatus.ADSCycleStarted + || currentVoiceAudioStatus == AudioVoiceStatus.ReleaseCycleStarted)) + { + command = AudioVoiceCommand.ChangePulseWidth; } + + return command; } } diff --git a/src/libraries/Highbyte.DotNet6502.Systems/Commodore64/Audio/AudioVoiceParameter.cs b/src/libraries/Highbyte.DotNet6502.Systems/Commodore64/Audio/AudioVoiceParameter.cs index c7536969..7ed2e27b 100644 --- a/src/libraries/Highbyte.DotNet6502.Systems/Commodore64/Audio/AudioVoiceParameter.cs +++ b/src/libraries/Highbyte.DotNet6502.Systems/Commodore64/Audio/AudioVoiceParameter.cs @@ -1,78 +1,77 @@ -namespace Highbyte.DotNet6502.Systems.Commodore64.Audio +namespace Highbyte.DotNet6502.Systems.Commodore64.Audio; + +public class AudioVoiceParameter { - public class AudioVoiceParameter - { - public AudioVoiceCommand AudioCommand { get; set; } - public SidVoiceWaveForm SIDOscillatorType { get; set; } + public AudioVoiceCommand AudioCommand { get; set; } + public SidVoiceWaveForm SIDOscillatorType { get; set; } - public float Frequency { get; set; } - public float PulseWidth { get; set; } - public double AttackDurationSeconds { get; set; } - public double DecayDurationSeconds { get; set; } - public float SustainGain { get; internal set; } - public double ReleaseDurationSeconds { get; set; } + public float Frequency { get; set; } + public float PulseWidth { get; set; } + public double AttackDurationSeconds { get; set; } + public double DecayDurationSeconds { get; set; } + public float SustainGain { get; internal set; } + public double ReleaseDurationSeconds { get; set; } - public static AudioVoiceParameter BuildAudioVoiceParameter( - byte voice, - AudioVoiceStatus currentVoiceAudioStatus, - InternalSidState sidState - ) - { - // TODO: Read clock speed from config, different for NTSC and PAL. - float clockSpeed = 1022730; + public static AudioVoiceParameter BuildAudioVoiceParameter( + byte voice, + AudioVoiceStatus currentVoiceAudioStatus, + InternalSidState sidState + ) + { + // TODO: Read clock speed from config, different for NTSC and PAL. + float clockSpeed = 1022730; - // ---------- - // Map SID register values to audio parameters usable by Web Audio, and what to do with the audio. - // ---------- - var audioVoiceParameter = new AudioVoiceParameter - { - // What to do with the audio (Start ADS cycle, start Release cycle, stop audio, change frequency, change volume) - AudioCommand = AudioVoiceCommandBuilder.GetAudioCommand( - voice, - currentVoiceAudioStatus, - sidState), + // ---------- + // Map SID register values to audio parameters usable by Web Audio, and what to do with the audio. + // ---------- + var audioVoiceParameter = new AudioVoiceParameter + { + // What to do with the audio (Start ADS cycle, start Release cycle, stop audio, change frequency, change volume) + AudioCommand = AudioVoiceCommandBuilder.GetAudioCommand( + voice, + currentVoiceAudioStatus, + sidState), - // Oscillator type mapped from C64 SID wave form selection - SIDOscillatorType = sidState.GetWaveForm(voice), + // Oscillator type mapped from C64 SID wave form selection + SIDOscillatorType = sidState.GetWaveForm(voice), - // PeriodicWave used for SID pulse and random noise wave forms (mapped to WebAudio OscillatorType.Custom) - //PeriodicWaveOptions = (oscillatorSpecialType.HasValue && oscillatorSpecialType.Value == OscillatorSpecialType.Noise) ? GetPeriodicWaveNoiseOptions(voiceContext, sidState) : null, + // PeriodicWave used for SID pulse and random noise wave forms (mapped to WebAudio OscillatorType.Custom) + //PeriodicWaveOptions = (oscillatorSpecialType.HasValue && oscillatorSpecialType.Value == OscillatorSpecialType.Noise) ? GetPeriodicWaveNoiseOptions(voiceContext, sidState) : null, - // Translate SID frequency (0 - 65536) to actual frequency number - // Frequency = (REGISTER VALUE * CLOCK / 16777216) Hz - // where CLOCK equals the system clock frequency, 1022730 for American (NTSC)systems, 985250 for European(PAL). - // Range 0 Hz to about 4000 Hz. - Frequency = sidState.GetFrequency(voice) * clockSpeed / 16777216.0f, + // Translate SID frequency (0 - 65536) to actual frequency number + // Frequency = (REGISTER VALUE * CLOCK / 16777216) Hz + // where CLOCK equals the system clock frequency, 1022730 for American (NTSC)systems, 985250 for European(PAL). + // Range 0 Hz to about 4000 Hz. + Frequency = sidState.GetFrequency(voice) * clockSpeed / 16777216.0f, - // Translate 12 bit Pulse width (0 - 4095) to percentage - // Pulse width % = (REGISTER VALUE / 40.95) % - // The percentage is then transformed into a value between -1 and +1 that the .NET PulseOscillator uses. - // Example: Register value of 0 => 0% => 0 value => (0 * 2) -1 => -1 - // Example: Register value of 2047 => approx 50% => 0.5 value => (0.5 * 2) -1 => 0 - // Example: Register value of 4095 => 100% => 1.0 value => (1.0 * 2) -1 => 1 - PulseWidth = (((sidState.GetPulseWidth(voice) / 40.95f) / 100.0f) * 2) - 1, + // Translate 12 bit Pulse width (0 - 4095) to percentage + // Pulse width % = (REGISTER VALUE / 40.95) % + // The percentage is then transformed into a value between -1 and +1 that the .NET PulseOscillator uses. + // Example: Register value of 0 => 0% => 0 value => (0 * 2) -1 => -1 + // Example: Register value of 2047 => approx 50% => 0.5 value => (0.5 * 2) -1 => 0 + // Example: Register value of 4095 => 100% => 1.0 value => (1.0 * 2) -1 => 1 + PulseWidth = (((sidState.GetPulseWidth(voice) / 40.95f) / 100.0f) * 2) - 1, - // Translate SID attack duration in ms to seconds - // Attack: 0-15, highest 4 bits in ATDCY - // The values 0-15 represents different amount of milliseconds, read from lookup table. - AttackDurationSeconds = sidState.GetAttackDuration(voice) / 1000.0, + // Translate SID attack duration in ms to seconds + // Attack: 0-15, highest 4 bits in ATDCY + // The values 0-15 represents different amount of milliseconds, read from lookup table. + AttackDurationSeconds = sidState.GetAttackDuration(voice) / 1000.0, - // Translate SID decay duration in ms to seconds - // Decay: 0-15, lowest 4 bits in ATDCY - // The values 0-15 represents different amount of milliseconds, read from lookup table. - DecayDurationSeconds = sidState.GetDecayDuration(voice) / 1000.0, + // Translate SID decay duration in ms to seconds + // Decay: 0-15, lowest 4 bits in ATDCY + // The values 0-15 represents different amount of milliseconds, read from lookup table. + DecayDurationSeconds = sidState.GetDecayDuration(voice) / 1000.0, - // Translate SID sustain volume 0-15 to Sustain Gain 0.0-1.0 - // Sustain level: 0-15, highest 4 bits in SUREL - // The values 0-15 represents volume - SustainGain = Math.Clamp((float)(sidState.GetSustainGain(voice) / 15.0f), 0.0f, 1.0f), + // Translate SID sustain volume 0-15 to Sustain Gain 0.0-1.0 + // Sustain level: 0-15, highest 4 bits in SUREL + // The values 0-15 represents volume + SustainGain = Math.Clamp((float)(sidState.GetSustainGain(voice) / 15.0f), 0.0f, 1.0f), - // Translate SID release duration in ms to seconds - // Release: 0-15, lowest 4 bits in SUREL - // The values 0-15 represents different amount of milliseconds, read from lookup table. - ReleaseDurationSeconds = sidState.GetReleaseDuration(voice) / 1000.0, - }; - return audioVoiceParameter; - } + // Translate SID release duration in ms to seconds + // Release: 0-15, lowest 4 bits in SUREL + // The values 0-15 represents different amount of milliseconds, read from lookup table. + ReleaseDurationSeconds = sidState.GetReleaseDuration(voice) / 1000.0, + }; + return audioVoiceParameter; } } diff --git a/src/libraries/Highbyte.DotNet6502.Systems/Commodore64/Audio/AudioVoiceStatus.cs b/src/libraries/Highbyte.DotNet6502.Systems/Commodore64/Audio/AudioVoiceStatus.cs index f566d9ea..b5150c56 100644 --- a/src/libraries/Highbyte.DotNet6502.Systems/Commodore64/Audio/AudioVoiceStatus.cs +++ b/src/libraries/Highbyte.DotNet6502.Systems/Commodore64/Audio/AudioVoiceStatus.cs @@ -1,25 +1,24 @@ -namespace Highbyte.DotNet6502.Systems.Commodore64.Audio +namespace Highbyte.DotNet6502.Systems.Commodore64.Audio; + +public enum AudioVoiceStatus { - public enum AudioVoiceStatus - { - /// - /// Attack/Decay/Sustain cycle has been started. - /// It's started by setting the Gate bit to 1 (and a waveform has been selected). - /// - ADSCycleStarted, - /// - /// Release cycle has been started. - /// It's started by setting the Gate bit to 0. - /// During release cycle, a new audio can be started by setting the Gate bit to 1 (this will stop current sound and start a new one) - /// - ReleaseCycleStarted, + /// + /// Attack/Decay/Sustain cycle has been started. + /// It's started by setting the Gate bit to 1 (and a waveform has been selected). + /// + ADSCycleStarted, + /// + /// Release cycle has been started. + /// It's started by setting the Gate bit to 0. + /// During release cycle, a new audio can be started by setting the Gate bit to 1 (this will stop current sound and start a new one) + /// + ReleaseCycleStarted, - /// - /// The audio has stopped playing. - /// Happens by - /// - release cycle has completed. - /// - or stopping the audio right away by clearing all waveform selection bits - /// - Stopped - } + /// + /// The audio has stopped playing. + /// Happens by + /// - release cycle has completed. + /// - or stopping the audio right away by clearing all waveform selection bits + /// + Stopped } diff --git a/src/libraries/Highbyte.DotNet6502.Systems/Commodore64/Audio/SidVoiceWaveForm.cs b/src/libraries/Highbyte.DotNet6502.Systems/Commodore64/Audio/SidVoiceWaveForm.cs index d1d9271c..145d1897 100644 --- a/src/libraries/Highbyte.DotNet6502.Systems/Commodore64/Audio/SidVoiceWaveForm.cs +++ b/src/libraries/Highbyte.DotNet6502.Systems/Commodore64/Audio/SidVoiceWaveForm.cs @@ -1,11 +1,10 @@ -namespace Highbyte.DotNet6502.Systems.Commodore64.Audio +namespace Highbyte.DotNet6502.Systems.Commodore64.Audio; + +public enum SidVoiceWaveForm { - public enum SidVoiceWaveForm - { - None, - Triangle, - Sawtooth, - Pulse, - RandomNoise - } + None, + Triangle, + Sawtooth, + Pulse, + RandomNoise } diff --git a/src/libraries/Highbyte.DotNet6502.Systems/Commodore64/Video/IVic2SpriteManager.cs b/src/libraries/Highbyte.DotNet6502.Systems/Commodore64/Video/IVic2SpriteManager.cs index 5d2e101e..7677fdc1 100644 --- a/src/libraries/Highbyte.DotNet6502.Systems/Commodore64/Video/IVic2SpriteManager.cs +++ b/src/libraries/Highbyte.DotNet6502.Systems/Commodore64/Video/IVic2SpriteManager.cs @@ -1,36 +1,35 @@ -namespace Highbyte.DotNet6502.Systems.Commodore64.Video +namespace Highbyte.DotNet6502.Systems.Commodore64.Video; + +public interface IVic2SpriteManager { - public interface IVic2SpriteManager - { - public int SpritePointerStartAddress { get; } - public int NumberOfSprites { get; } - public int ScreenOffsetX { get; } - public int ScreenOffsetY { get; } + public int SpritePointerStartAddress { get; } + public int NumberOfSprites { get; } + public int ScreenOffsetX { get; } + public int ScreenOffsetY { get; } - public Vic2Sprite[] Sprites { get; } + public Vic2Sprite[] Sprites { get; } - public byte SpriteToSpriteCollisionStore { get; set; } - public bool SpriteToSpriteCollisionIRQBlock { get; set; } + public byte SpriteToSpriteCollisionStore { get; set; } + public bool SpriteToSpriteCollisionIRQBlock { get; set; } - public byte SpriteToBackgroundCollisionStore { get; set; } - public bool SpriteToBackgroundCollisionIRQBlock { get; set; } + public byte SpriteToBackgroundCollisionStore { get; set; } + public bool SpriteToBackgroundCollisionIRQBlock { get; set; } - public Vic2 Vic2 { get; } - public void SetAllDirty(); + public Vic2 Vic2 { get; } + public void SetAllDirty(); - public void DetectChangesToSpriteData(ushort vic2Address, byte value); - public void SetCollitionDetectionStatesAndIRQ(); + public void DetectChangesToSpriteData(ushort vic2Address, byte value); + public void SetCollitionDetectionStatesAndIRQ(); - public byte GetSpriteToSpriteCollision(); - public byte GetSpriteToBackgroundCollision(); + public byte GetSpriteToSpriteCollision(); + public byte GetSpriteToBackgroundCollision(); - public bool CheckCollisionAgainstBackground(Vic2Sprite sprite, int scrollX, int scrollY); + public bool CheckCollisionAgainstBackground(Vic2Sprite sprite, int scrollX, int scrollY); - public bool CheckCollision(ReadOnlySpan pixelData1, ReadOnlySpan pixelData2); + public bool CheckCollision(ReadOnlySpan pixelData1, ReadOnlySpan pixelData2); - public void GetSpriteRowLineData(Vic2Sprite sprite, int spriteScreenLine, ref Span spriteLineData); + public void GetSpriteRowLineData(Vic2Sprite sprite, int spriteScreenLine, ref Span spriteLineData); - public void GetSpriteRowLineDataMatchingOtherSpritePosition(Vic2Sprite sprite0, Vic2Sprite sprite1, int sprite0ScreenLine, ref Span bytes); - public void GetCharacterRowLineDataMatchingSpritePosition(Vic2Sprite sprite, int spriteScreenLine, int spriteBytesWidth, int scrollX, int scrollY, ref Span bytes); - } + public void GetSpriteRowLineDataMatchingOtherSpritePosition(Vic2Sprite sprite0, Vic2Sprite sprite1, int sprite0ScreenLine, ref Span bytes); + public void GetCharacterRowLineDataMatchingSpritePosition(Vic2Sprite sprite, int spriteScreenLine, int spriteBytesWidth, int scrollX, int scrollY, ref Span bytes); } diff --git a/src/libraries/Highbyte.DotNet6502/Logging/InMem/DotNet6502InMemLogStore.cs b/src/libraries/Highbyte.DotNet6502/Logging/InMem/DotNet6502InMemLogStore.cs index 1300e01f..94882e92 100644 --- a/src/libraries/Highbyte.DotNet6502/Logging/InMem/DotNet6502InMemLogStore.cs +++ b/src/libraries/Highbyte.DotNet6502/Logging/InMem/DotNet6502InMemLogStore.cs @@ -1,50 +1,49 @@ using System.Diagnostics; -namespace Highbyte.DotNet6502.Logging +namespace Highbyte.DotNet6502.Logging; + +public class DotNet6502InMemLogStore { - public class DotNet6502InMemLogStore - { - private readonly List _logMessages = new(); + private readonly List _logMessages = new(); - private int _maxLogMessages = 100; - public int MaxLogMessages + private int _maxLogMessages = 100; + public int MaxLogMessages + { + get => _maxLogMessages; + set { - get => _maxLogMessages; - set - { - if (value < 1) - throw new ArgumentException("MaxLogMessages must be greater than 0."); - _maxLogMessages = value; - - if (_logMessages.Count > _maxLogMessages) - _logMessages.RemoveRange(_maxLogMessages, _logMessages.Count - _maxLogMessages); - } - } + if (value < 1) + throw new ArgumentException("MaxLogMessages must be greater than 0."); + _maxLogMessages = value; - private bool _writeDebugMessage = false; - public bool WriteDebugMessage - { - get => _writeDebugMessage; - set - { - _writeDebugMessage = value; - } + if (_logMessages.Count > _maxLogMessages) + _logMessages.RemoveRange(_maxLogMessages, _logMessages.Count - _maxLogMessages); } + } - public void WriteLog(string logMessage) + private bool _writeDebugMessage = false; + public bool WriteDebugMessage + { + get => _writeDebugMessage; + set { - _logMessages.Insert(0, logMessage); + _writeDebugMessage = value; + } + } - if (_logMessages.Count > MaxLogMessages) - _logMessages.RemoveAt(MaxLogMessages); + public void WriteLog(string logMessage) + { + _logMessages.Insert(0, logMessage); - // Check if log also should be written to Debug output - if (WriteDebugMessage) - Debug.WriteLine(logMessage); + if (_logMessages.Count > MaxLogMessages) + _logMessages.RemoveAt(MaxLogMessages); - } - public List GetLogMessages() => _logMessages; + // Check if log also should be written to Debug output + if (WriteDebugMessage) + Debug.WriteLine(logMessage); - public void Clear() => _logMessages.Clear(); } + public List GetLogMessages() => _logMessages; + + public void Clear() => _logMessages.Clear(); } diff --git a/tests/Highbyte.DotNet6502.Tests/Instrumentation/AveragedStatTest.cs b/tests/Highbyte.DotNet6502.Tests/Instrumentation/AveragedStatTest.cs index 3af053a7..7c0a50fa 100644 --- a/tests/Highbyte.DotNet6502.Tests/Instrumentation/AveragedStatTest.cs +++ b/tests/Highbyte.DotNet6502.Tests/Instrumentation/AveragedStatTest.cs @@ -1,34 +1,33 @@ -namespace Highbyte.DotNet6502.Tests.Instrumentation +namespace Highbyte.DotNet6502.Tests.Instrumentation; + +public class AveragedStatTest { - public class AveragedStatTest + [Fact] + public void Value_WhenCalled_ReturnsValue() { - [Fact] - public void Value_WhenCalled_ReturnsValue() - { - // Arrange - var stat = new TestAveragedStat(10); - stat.UpdateStat(1.0); + // Arrange + var stat = new TestAveragedStat(10); + stat.UpdateStat(1.0); - // Act - var value = stat.Value; + // Act + var value = stat.Value; - // Assert - Assert.Equal(1.0, value); - } + // Assert + Assert.Equal(1.0, value); + } - [Fact] - public void Value_WhenCalled_ReturnsValue_Average() - { - // Arrange - var stat = new TestAveragedStat(sampleCount: 2); - stat.UpdateStat(1.0); - stat.UpdateStat(2.0); + [Fact] + public void Value_WhenCalled_ReturnsValue_Average() + { + // Arrange + var stat = new TestAveragedStat(sampleCount: 2); + stat.UpdateStat(1.0); + stat.UpdateStat(2.0); - // Act - var value = stat.Value; + // Act + var value = stat.Value; - // Assert - Assert.Equal(1.5, value); - } + // Assert + Assert.Equal(1.5, value); } } diff --git a/tests/Highbyte.DotNet6502.Tests/Instrumentation/ElapsedMillisecondsTimedStatSystemTest.cs b/tests/Highbyte.DotNet6502.Tests/Instrumentation/ElapsedMillisecondsTimedStatSystemTest.cs index 6116dde8..10f52e77 100644 --- a/tests/Highbyte.DotNet6502.Tests/Instrumentation/ElapsedMillisecondsTimedStatSystemTest.cs +++ b/tests/Highbyte.DotNet6502.Tests/Instrumentation/ElapsedMillisecondsTimedStatSystemTest.cs @@ -1,48 +1,47 @@ using Highbyte.DotNet6502.Instrumentation.Stats; using Highbyte.DotNet6502.Tests.Systems; -namespace Highbyte.DotNet6502.Tests.Instrumentation +namespace Highbyte.DotNet6502.Tests.Instrumentation; + +public class ElapsedMillisecondsTimedStatSystemTest { - public class ElapsedMillisecondsTimedStatSystemTest + [Fact] + public void StartAndStopPerformsMeasurementIfSystemHasInstrumentationEnabled() { - [Fact] - public void StartAndStopPerformsMeasurementIfSystemHasInstrumentationEnabled() - { - // Arrange - var system = new TestSystem { InstrumentationEnabled = true }; + // Arrange + var system = new TestSystem { InstrumentationEnabled = true }; - var stat = new ElapsedMillisecondsTimedStatSystem(system); + var stat = new ElapsedMillisecondsTimedStatSystem(system); - int sleepMs = 2; - stat.Start(); - Thread.Sleep(sleepMs); - stat.Stop(); + int sleepMs = 2; + stat.Start(); + Thread.Sleep(sleepMs); + stat.Stop(); - // Act - var elapsedMs = stat.GetStatMilliseconds(); + // Act + var elapsedMs = stat.GetStatMilliseconds(); - // Assert - Assert.True(elapsedMs >= sleepMs); - } + // Assert + Assert.True(elapsedMs >= sleepMs); + } - [Fact] - public void StartAndStopDoesNotPerformMeasurementIfSystemHasInstrumentationDisabled() - { - // Arrange - var system = new TestSystem { InstrumentationEnabled = false }; + [Fact] + public void StartAndStopDoesNotPerformMeasurementIfSystemHasInstrumentationDisabled() + { + // Arrange + var system = new TestSystem { InstrumentationEnabled = false }; - var stat = new ElapsedMillisecondsTimedStatSystem(system); + var stat = new ElapsedMillisecondsTimedStatSystem(system); - int sleepMs = 2; - stat.Start(); - Thread.Sleep(sleepMs); - stat.Stop(); + int sleepMs = 2; + stat.Start(); + Thread.Sleep(sleepMs); + stat.Stop(); - // Act - var elapsedMs = stat.GetStatMilliseconds(); + // Act + var elapsedMs = stat.GetStatMilliseconds(); - // Assert - Assert.Null(elapsedMs); - } + // Assert + Assert.Null(elapsedMs); } } diff --git a/tests/Highbyte.DotNet6502.Tests/Instrumentation/ElapsedMillisecondsTimedStatTest.cs b/tests/Highbyte.DotNet6502.Tests/Instrumentation/ElapsedMillisecondsTimedStatTest.cs index 991d6dc8..984b2eed 100644 --- a/tests/Highbyte.DotNet6502.Tests/Instrumentation/ElapsedMillisecondsTimedStatTest.cs +++ b/tests/Highbyte.DotNet6502.Tests/Instrumentation/ElapsedMillisecondsTimedStatTest.cs @@ -1,99 +1,98 @@ using Highbyte.DotNet6502.Instrumentation.Stats; -namespace Highbyte.DotNet6502.Tests.Instrumentation +namespace Highbyte.DotNet6502.Tests.Instrumentation; + +public class ElapsedMillisecondsTimedStatTest { - public class ElapsedMillisecondsTimedStatTest + [Fact] + public void Returns_ExecutionTime_In_GetStatMilliseconds() { - [Fact] - public void Returns_ExecutionTime_In_GetStatMilliseconds() - { - // Arrange - var stat = new ElapsedMillisecondsTimedStat(samples: 1); - - int sleepMs = 2; - stat.Start(); - Thread.Sleep(sleepMs); - stat.Stop(); - - // Act - var elapsedMs = stat.GetStatMilliseconds(); - - // Assert - Assert.True(elapsedMs >= sleepMs); + // Arrange + var stat = new ElapsedMillisecondsTimedStat(samples: 1); + + int sleepMs = 2; + stat.Start(); + Thread.Sleep(sleepMs); + stat.Stop(); + + // Act + var elapsedMs = stat.GetStatMilliseconds(); + + // Assert + Assert.True(elapsedMs >= sleepMs); #if !DEBUG - // In debug mode, the elapsed time may not accurate enough to make this test pass (if breakpoint is hit during sleep) - Assert.True(elapsedMs < sleepMs + 10); + // In debug mode, the elapsed time may not accurate enough to make this test pass (if breakpoint is hit during sleep) + Assert.True(elapsedMs < sleepMs + 10); #endif - } + } - [Fact] - public void When_Used_With_Cont_Returns_ExecutionTime_In_GetStatMilliseconds() - { - // Arrange - var stat = new ElapsedMillisecondsTimedStat(samples: 1); + [Fact] + public void When_Used_With_Cont_Returns_ExecutionTime_In_GetStatMilliseconds() + { + // Arrange + var stat = new ElapsedMillisecondsTimedStat(samples: 1); - int sleepMs = 2; - stat.Start(cont: true); - Thread.Sleep(sleepMs); - stat.Stop(cont: true); + int sleepMs = 2; + stat.Start(cont: true); + Thread.Sleep(sleepMs); + stat.Stop(cont: true); - int sleepNextMs = 3; - stat.Start(cont: true); - Thread.Sleep(sleepNextMs); - stat.Stop(cont: true); - stat.Stop(); + int sleepNextMs = 3; + stat.Start(cont: true); + Thread.Sleep(sleepNextMs); + stat.Stop(cont: true); + stat.Stop(); - // Act - var elapsedMs = stat.GetStatMilliseconds(); + // Act + var elapsedMs = stat.GetStatMilliseconds(); - // Assert - Assert.True(elapsedMs >= (sleepMs + sleepNextMs)); + // Assert + Assert.True(elapsedMs >= (sleepMs + sleepNextMs)); #if !DEBUG - // In debug mode, the elapsed time may not accurate enough to make this test pass (if breakpoint is hit during sleep) - Assert.True(elapsedMs < (sleepMs + sleepNextMs) + 10); + // In debug mode, the elapsed time may not accurate enough to make this test pass (if breakpoint is hit during sleep) + Assert.True(elapsedMs < (sleepMs + sleepNextMs) + 10); #endif - } - - [Fact] - public void GetDescription_WhenUsed_Returns_Null_When_No_Data_Yet() - { - // Arrange - var stat = new ElapsedMillisecondsTimedStat(samples: 1); - - // Act - // Assert - Assert.Equal("null", stat.GetDescription()); - } - - [Fact] - public void GetDescription_WhenUsed_Returns_Special_String_When_Duration_Is_Less_Than_OneHundreds_Of_A_Millisecond() - { - // Arrange - var stat = new ElapsedMillisecondsTimedStat(samples: 1); - - // Act - stat.SetFakeMSValue(0.0099); - - // Assert - Assert.Equal("< 0.01ms", stat.GetDescription()); - } - - [Fact] - public void GetDescription_WhenUsed_Returns_String_With_Milliseconds() - { - // Arrange - var stat = new ElapsedMillisecondsTimedStat(samples: 1); - - // Act - int sleepMs = 2; - stat.Start(); - Thread.Sleep(sleepMs); - stat.Stop(); - // Assert - var ms = stat.GetStatMilliseconds(); - Assert.NotNull(ms); - Assert.Equal($"{Math.Round(ms.Value, 2).ToString("0.00")}ms", stat.GetDescription()); - } + } + + [Fact] + public void GetDescription_WhenUsed_Returns_Null_When_No_Data_Yet() + { + // Arrange + var stat = new ElapsedMillisecondsTimedStat(samples: 1); + + // Act + // Assert + Assert.Equal("null", stat.GetDescription()); + } + + [Fact] + public void GetDescription_WhenUsed_Returns_Special_String_When_Duration_Is_Less_Than_OneHundreds_Of_A_Millisecond() + { + // Arrange + var stat = new ElapsedMillisecondsTimedStat(samples: 1); + + // Act + stat.SetFakeMSValue(0.0099); + + // Assert + Assert.Equal("< 0.01ms", stat.GetDescription()); + } + + [Fact] + public void GetDescription_WhenUsed_Returns_String_With_Milliseconds() + { + // Arrange + var stat = new ElapsedMillisecondsTimedStat(samples: 1); + + // Act + int sleepMs = 2; + stat.Start(); + Thread.Sleep(sleepMs); + stat.Stop(); + // Assert + var ms = stat.GetStatMilliseconds(); + Assert.NotNull(ms); + Assert.Equal($"{Math.Round(ms.Value, 2).ToString("0.00")}ms", stat.GetDescription()); } } diff --git a/tests/Highbyte.DotNet6502.Tests/Instrumentation/InstrumentationBagTest.cs b/tests/Highbyte.DotNet6502.Tests/Instrumentation/InstrumentationBagTest.cs index 7252e3e7..125eb88f 100644 --- a/tests/Highbyte.DotNet6502.Tests/Instrumentation/InstrumentationBagTest.cs +++ b/tests/Highbyte.DotNet6502.Tests/Instrumentation/InstrumentationBagTest.cs @@ -1,74 +1,73 @@ using Highbyte.DotNet6502.Instrumentation; -namespace Highbyte.DotNet6502.Tests.Instrumentation +namespace Highbyte.DotNet6502.Tests.Instrumentation; + +/// +/// InstrumentationBag is a static class that uses Instrumentations class under the hood. +/// +public class InstrumentationBagTest { - /// - /// InstrumentationBag is a static class that uses Instrumentations class under the hood. - /// - public class InstrumentationBagTest - { - [Fact] - public void Add_WhenCalled_AddsStatToStatsList() - { - // Arrange - InstrumentationBag.Clear(); + [Fact] + public void Add_WhenCalled_AddsStatToStatsList() + { + // Arrange + InstrumentationBag.Clear(); - // Act - var stat = new TestStat(); - var addedStat = InstrumentationBag.Add("stat1", stat); + // Act + var stat = new TestStat(); + var addedStat = InstrumentationBag.Add("stat1", stat); - // Assert - Assert.Single(InstrumentationBag.Stats); - Assert.Equal(addedStat, stat); - Assert.Equal(addedStat, InstrumentationBag.Stats.First().Stat); - Assert.Equal("stat1", InstrumentationBag.Stats.First().Name); - } + // Assert + Assert.Single(InstrumentationBag.Stats); + Assert.Equal(addedStat, stat); + Assert.Equal(addedStat, InstrumentationBag.Stats.First().Stat); + Assert.Equal("stat1", InstrumentationBag.Stats.First().Name); + } - [Fact] - public void Add_WhenCalled_AddsStatToStatsList2() - { - // Arrange - InstrumentationBag.Clear(); + [Fact] + public void Add_WhenCalled_AddsStatToStatsList2() + { + // Arrange + InstrumentationBag.Clear(); - // Act - var addedStat = InstrumentationBag.Add("stat1"); + // Act + var addedStat = InstrumentationBag.Add("stat1"); - // Assert - Assert.Single(InstrumentationBag.Stats); - Assert.Equal(addedStat, InstrumentationBag.Stats.First().Stat); - Assert.Equal("stat1", InstrumentationBag.Stats.First().Name); - } + // Assert + Assert.Single(InstrumentationBag.Stats); + Assert.Equal(addedStat, InstrumentationBag.Stats.First().Stat); + Assert.Equal("stat1", InstrumentationBag.Stats.First().Name); + } - [Fact] - public void Remove_WhenCalled_RemovesStatFromStatsList() - { - // Arrange - InstrumentationBag.Clear(); - InstrumentationBag.Add("stat1", new TestStat()); - InstrumentationBag.Add("stat2", new TestStat()); + [Fact] + public void Remove_WhenCalled_RemovesStatFromStatsList() + { + // Arrange + InstrumentationBag.Clear(); + InstrumentationBag.Add("stat1", new TestStat()); + InstrumentationBag.Add("stat2", new TestStat()); - // Act - InstrumentationBag.Remove("stat1"); + // Act + InstrumentationBag.Remove("stat1"); - // Assert - Assert.Single(InstrumentationBag.Stats); - } + // Assert + Assert.Single(InstrumentationBag.Stats); + } - [Fact] - public void Clear_WhenCalled_ClearsStatsList() - { - // Arrange - InstrumentationBag.Clear(); + [Fact] + public void Clear_WhenCalled_ClearsStatsList() + { + // Arrange + InstrumentationBag.Clear(); - InstrumentationBag.Add("stat1", new TestStat()); - InstrumentationBag.Add("stat2", new TestStat()); + InstrumentationBag.Add("stat1", new TestStat()); + InstrumentationBag.Add("stat2", new TestStat()); - // Act - InstrumentationBag.Clear(); + // Act + InstrumentationBag.Clear(); - // Assert - Assert.Empty(InstrumentationBag.Stats); - } + // Assert + Assert.Empty(InstrumentationBag.Stats); } } diff --git a/tests/Highbyte.DotNet6502.Tests/Instrumentation/InstrumentationsTest.cs b/tests/Highbyte.DotNet6502.Tests/Instrumentation/InstrumentationsTest.cs index 7fd84cc0..b9c81516 100644 --- a/tests/Highbyte.DotNet6502.Tests/Instrumentation/InstrumentationsTest.cs +++ b/tests/Highbyte.DotNet6502.Tests/Instrumentation/InstrumentationsTest.cs @@ -1,70 +1,69 @@ using Highbyte.DotNet6502.Instrumentation; -namespace Highbyte.DotNet6502.Tests.Instrumentation +namespace Highbyte.DotNet6502.Tests.Instrumentation; + +public class InstrumentationsTest { - public class InstrumentationsTest - { - [Fact] - public void Add_WhenCalled_AddsStatToStatsList() - { - // Arrange - var instrumentations = new Instrumentations(); + [Fact] + public void Add_WhenCalled_AddsStatToStatsList() + { + // Arrange + var instrumentations = new Instrumentations(); - // Act - var stat = new TestStat(); - var addedStat = instrumentations.Add("stat1", stat); + // Act + var stat = new TestStat(); + var addedStat = instrumentations.Add("stat1", stat); - // Assert - Assert.Single(instrumentations.Stats); - Assert.Equal(addedStat, stat); - Assert.Equal(addedStat, instrumentations.Stats.First().Stat); - Assert.Equal("stat1", instrumentations.Stats.First().Name); - } + // Assert + Assert.Single(instrumentations.Stats); + Assert.Equal(addedStat, stat); + Assert.Equal(addedStat, instrumentations.Stats.First().Stat); + Assert.Equal("stat1", instrumentations.Stats.First().Name); + } - [Fact] - public void Add_WhenCalled_AddsStatToStatsList2() - { - // Arrange - var instrumentations = new Instrumentations(); + [Fact] + public void Add_WhenCalled_AddsStatToStatsList2() + { + // Arrange + var instrumentations = new Instrumentations(); - // Act - var addedStat = instrumentations.Add("stat1"); + // Act + var addedStat = instrumentations.Add("stat1"); - // Assert - Assert.Single(instrumentations.Stats); - Assert.Equal(addedStat, instrumentations.Stats.First().Stat); - Assert.Equal("stat1", instrumentations.Stats.First().Name); - } + // Assert + Assert.Single(instrumentations.Stats); + Assert.Equal(addedStat, instrumentations.Stats.First().Stat); + Assert.Equal("stat1", instrumentations.Stats.First().Name); + } - [Fact] - public void Remove_WhenCalled_RemovesStatFromStatsList() - { - // Arrange - var instrumentations = new Instrumentations(); - instrumentations.Add("stat1", new TestStat()); - instrumentations.Add("stat2", new TestStat()); + [Fact] + public void Remove_WhenCalled_RemovesStatFromStatsList() + { + // Arrange + var instrumentations = new Instrumentations(); + instrumentations.Add("stat1", new TestStat()); + instrumentations.Add("stat2", new TestStat()); - // Act - instrumentations.Remove("stat1"); + // Act + instrumentations.Remove("stat1"); - // Assert - Assert.Single(instrumentations.Stats); - } + // Assert + Assert.Single(instrumentations.Stats); + } - [Fact] - public void Clear_WhenCalled_ClearsStatsList() - { - // Arrange - var instrumentations = new Instrumentations(); - instrumentations.Add("stat1", new TestStat()); - instrumentations.Add("stat2", new TestStat()); + [Fact] + public void Clear_WhenCalled_ClearsStatsList() + { + // Arrange + var instrumentations = new Instrumentations(); + instrumentations.Add("stat1", new TestStat()); + instrumentations.Add("stat2", new TestStat()); - // Act - instrumentations.Clear(); + // Act + instrumentations.Clear(); - // Assert - Assert.Empty(instrumentations.Stats); - } + // Assert + Assert.Empty(instrumentations.Stats); } } diff --git a/tests/Highbyte.DotNet6502.Tests/Instrumentation/PerSecondTimedStatTest.cs b/tests/Highbyte.DotNet6502.Tests/Instrumentation/PerSecondTimedStatTest.cs index d11f0bbc..ffc45283 100644 --- a/tests/Highbyte.DotNet6502.Tests/Instrumentation/PerSecondTimedStatTest.cs +++ b/tests/Highbyte.DotNet6502.Tests/Instrumentation/PerSecondTimedStatTest.cs @@ -1,63 +1,61 @@ using Highbyte.DotNet6502.Instrumentation.Stats; -using Newtonsoft.Json.Linq; -namespace Highbyte.DotNet6502.Tests.Instrumentation +namespace Highbyte.DotNet6502.Tests.Instrumentation; + +public class PerSecondTimedStatTest { - public class PerSecondTimedStatTest + // Test PerSecondTimedStatTest that it calculates per second correctly. + // This test is not very accurate, but it should be good enough to catch any major errors. + [Fact] + public void Update_WhenUsed_Returns_Correct_PerSecond_Value() { - // Test PerSecondTimedStatTest that it calculates per second correctly. - // This test is not very accurate, but it should be good enough to catch any major errors. - [Fact] - public void Update_WhenUsed_Returns_Correct_PerSecond_Value() - { - // Arrange - var stat = new PerSecondTimedStat(); - - // Act - stat.SetFakeFPSValue(60); - - // Assert - Assert.Equal(60, stat.Value); - } - - [Fact] - public void GetDescription_WhenUsed_Returns_Null_When_No_Data_Yet() - { - // Arrange - var stat = new PerSecondTimedStat(); - - // Act - // Assert - Assert.Equal("null", stat.GetDescription()); - } - - [Fact] - public void GetDescription_WhenUsed_Returns_Special_String_When_FPS_Is_Less_Than_OneHundreds_Of_A_Second() - { - // Arrange - var stat = new PerSecondTimedStat(); - - // Act - stat.SetFakeFPSValue(0.009); - - // Assert - Assert.Equal("< 0.01", stat.GetDescription()); - } - - [Fact] - public void GetDescription_WhenUsed_Returns_String_With_FPS() - { - // Arrange - var stat = new PerSecondTimedStat(); - - // Act - stat.Update(); - Thread.Sleep(16); - stat.Update(); - - // Assert - var fps = stat.Value; - Assert.Equal(Math.Round(fps ?? 0, 2).ToString(), stat.GetDescription()); - } + // Arrange + var stat = new PerSecondTimedStat(); + + // Act + stat.SetFakeFPSValue(60); + + // Assert + Assert.Equal(60, stat.Value); + } + + [Fact] + public void GetDescription_WhenUsed_Returns_Null_When_No_Data_Yet() + { + // Arrange + var stat = new PerSecondTimedStat(); + + // Act + // Assert + Assert.Equal("null", stat.GetDescription()); + } + + [Fact] + public void GetDescription_WhenUsed_Returns_Special_String_When_FPS_Is_Less_Than_OneHundreds_Of_A_Second() + { + // Arrange + var stat = new PerSecondTimedStat(); + + // Act + stat.SetFakeFPSValue(0.009); + + // Assert + Assert.Equal("< 0.01", stat.GetDescription()); + } + + [Fact] + public void GetDescription_WhenUsed_Returns_String_With_FPS() + { + // Arrange + var stat = new PerSecondTimedStat(); + + // Act + stat.Update(); + Thread.Sleep(16); + stat.Update(); + + // Assert + var fps = stat.Value; + Assert.Equal(Math.Round(fps ?? 0, 2).ToString(), stat.GetDescription()); } } From 107d6be842b112c32daf804b1b2da95ac23bcf35 Mon Sep 17 00:00:00 2001 From: Highbyte Date: Sun, 21 Jul 2024 23:55:43 +0200 Subject: [PATCH 07/40] Add file_scoped namespaces to .editorconfig --- .editorconfig | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.editorconfig b/.editorconfig index d04a22f3..ab213be1 100644 --- a/.editorconfig +++ b/.editorconfig @@ -214,6 +214,9 @@ csharp_preserve_single_line_blocks = true # Code block csharp_prefer_braces = when_multiline:warning +# File Scoped Namespaces +csharp_style_namespace_declarations = file_scoped + # ----------------------------------------------- # Misc Code Styles that should affect code analysis @@ -233,6 +236,9 @@ dotnet_style_prefer_compound_assignment = false:suggestion # IDE0055: Fix formatting. All formatting options have rule ID IDE0055 and title Fix formatting dotnet_diagnostic.IDE0055.severity = warning +# IDE0161: Use file-scoped namespace +dotnet_diagnostic.IDE0161.severity = warning + # ----------------------------------------------- # Misc Code Rules that should be disabled in code analysis From 133b9d1ddd36ad02d97e75e98738b76238884bd4 Mon Sep 17 00:00:00 2001 From: Highbyte Date: Mon, 22 Jul 2024 13:48:38 +0200 Subject: [PATCH 08/40] Remove unnecessary interfaces. Remove unused code. --- .../ISilkNetHostUIViewModel.cs | 30 --------- .../SilkNetHostApp.cs | 65 ++++++++++--------- .../SilkNetImGuiMenu.cs | 4 +- .../Emulator/IWASMHostUIViewModel.cs | 14 ---- .../Emulator/Skia/SkiaWASMHostApp.cs | 6 +- .../Emulator/WasmMonitor.cs | 4 +- .../Pages/Index.razor.cs | 2 +- .../Highbyte.DotNet6502/Systems/HostApp.cs | 14 ---- 8 files changed, 43 insertions(+), 96 deletions(-) delete mode 100644 src/apps/Highbyte.DotNet6502.App.SilkNetNative/ISilkNetHostUIViewModel.cs delete mode 100644 src/apps/Highbyte.DotNet6502.App.WASM/Emulator/IWASMHostUIViewModel.cs diff --git a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/ISilkNetHostUIViewModel.cs b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/ISilkNetHostUIViewModel.cs deleted file mode 100644 index ff2f68e8..00000000 --- a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/ISilkNetHostUIViewModel.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Highbyte.DotNet6502.Systems; - -namespace Highbyte.DotNet6502.App.SilkNetNative; - -public interface ISilkNetHostUIViewModel -{ - public EmulatorState EmulatorState { get; } - public Task Start(); - public void Pause(); - public void Stop(); - public Task Reset(); - - public void SetVolumePercent(float volumePercent); - public float Scale { get; set; } - - public void ToggleMonitor(); - public void ToggleStatsPanel(); - public void ToggleLogsPanel(); - - public HashSet AvailableSystemNames { get; } - public string SelectedSystemName { get; } - public void SelectSystem(string systemName); - - public Task IsSystemConfigValid(); - public Task GetSystemConfig(); - public IHostSystemConfig GetHostSystemConfig(); - public void UpdateSystemConfig(ISystemConfig newConfig); - - public ISystem? CurrentRunningSystem { get; } -} diff --git a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetHostApp.cs b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetHostApp.cs index c052b110..86113906 100644 --- a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetHostApp.cs +++ b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetHostApp.cs @@ -10,7 +10,7 @@ namespace Highbyte.DotNet6502.App.SilkNetNative; -public class SilkNetHostApp : HostApp, ISilkNetHostUIViewModel +public class SilkNetHostApp : HostApp { // -------------------- // Injected variables @@ -122,11 +122,16 @@ protected void OnLoad() { SetUninitializedWindow(); - InitRenderContext(); - InitInputContext(); - InitAudioContext(); + _inputContext = CreateSilkNetInput(); - base.SetAndInitContexts(() => _renderContextContainer, () => _inputHandlerContext, () => _audioHandlerContext); + _renderContextContainer = CreateRenderContext(); + _inputHandlerContext = CreateInputHandlerContext(); + _audioHandlerContext = CreateAudioHandlerContext(); + + base.SetContexts(() => _renderContextContainer, () => _inputHandlerContext, () => _audioHandlerContext); + base.InitRenderContext(); + base.InitInputHandlerContext(); + base.InitAudioHandlerContext(); InitImGui(); @@ -162,7 +167,10 @@ public override bool OnBeforeStart(ISystem systemAboutToBeStarted) var screen = systemAboutToBeStarted.Screen; _window.Size = new Vector2D((int)(screen.VisibleWidth * CanvasScale), (int)(screen.VisibleHeight * CanvasScale)); _window.UpdatesPerSecond = screen.RefreshFrequencyHz; - InitRenderContext(); + + _renderContextContainer?.Cleanup(); + _renderContextContainer = CreateRenderContext(); + base.InitRenderContext(); } return true; } @@ -185,7 +193,6 @@ public override void OnAfterClose() _renderContextContainer?.Cleanup(); _inputHandlerContext?.Cleanup(); _audioHandlerContext?.Cleanup(); - } /// @@ -298,12 +305,9 @@ private void OnResize(Vector2D vec2) } - private void InitRenderContext() + private SilkNetRenderContextContainer CreateRenderContext() { - _renderContextContainer?.Cleanup(); - - // Init SkipSharp resources (must be done in OnLoad, otherwise no OpenGL context will exist create by SilkNet.) - //_skiaRenderContext = new SkiaRenderContext(s_window.Size.X, s_window.Size.Y, _canvasScale); + // Init SkipSharp resources (must be done in OnLoad, otherwise no OpenGL context will exist created by SilkNet.) GRGlGetProcedureAddressDelegate getProcAddress = (name) => { var addrFound = _window.GLContext!.TryGetProcAddress(name, out var addr); @@ -318,24 +322,17 @@ private void InitRenderContext() var silkNetOpenGlRenderContext = new SilkNetOpenGlRenderContext(_window, _emulatorConfig.CurrentDrawScale); - _renderContextContainer = new SilkNetRenderContextContainer(skiaRenderContext, silkNetOpenGlRenderContext); + var renderContextContainer = new SilkNetRenderContextContainer(skiaRenderContext, silkNetOpenGlRenderContext); + return renderContextContainer; } - private void InitInputContext() + private SilkNetInputHandlerContext CreateInputHandlerContext() { - _inputHandlerContext = new SilkNetInputHandlerContext(_window, _loggerFactory); - - _inputContext = _window.CreateInput(); - // Listen to key to enable monitor - if (_inputContext.Keyboards == null || _inputContext.Keyboards.Count == 0) - throw new DotNet6502Exception("Keyboard not found"); - var primaryKeyboard = _inputContext.Keyboards[0]; - - // Listen to special key that will show/hide overlays for monitor/stats - primaryKeyboard.KeyDown += OnKeyDown; + var inputHandlerContext = new SilkNetInputHandlerContext(_window, _loggerFactory); + return inputHandlerContext; } - private void InitAudioContext() + private NAudioAudioHandlerContext CreateAudioHandlerContext() { // Output to NAudio built-in output (Windows only) //var wavePlayer = new WaveOutEvent @@ -351,11 +348,24 @@ private void InitAudioContext() DesiredLatency = 40 }; - _audioHandlerContext = new NAudioAudioHandlerContext( + return new NAudioAudioHandlerContext( wavePlayer, initialVolumePercent: 20); } + private IInputContext CreateSilkNetInput() + { + var inputContext = _window.CreateInput(); + // Listen to key to enable monitor + if (inputContext.Keyboards == null || inputContext.Keyboards.Count == 0) + throw new DotNet6502Exception("Keyboard not found"); + var primaryKeyboard = inputContext.Keyboards[0]; + + // Listen to special key that will show/hide overlays for monitor/stats + primaryKeyboard.KeyDown += OnKeyDown; + return inputContext; + } + public void SetVolumePercent(float volumePercent) { _defaultAudioVolumePercent = volumePercent; @@ -433,7 +443,6 @@ private void ToggleMainMenu() _menu.Enable(); } - #region ISilkNetHostViewModel (members that are not part of base class) public float Scale { @@ -506,6 +515,4 @@ public void ToggleLogsPanel() } } - #endregion - } diff --git a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetImGuiMenu.cs b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetImGuiMenu.cs index 09b296ad..9e4076b5 100644 --- a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetImGuiMenu.cs +++ b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetImGuiMenu.cs @@ -13,7 +13,7 @@ namespace Highbyte.DotNet6502.App.SilkNetNative; public class SilkNetImGuiMenu : ISilkNetImGuiWindow { - private readonly ISilkNetHostUIViewModel _hostViewModel; + private readonly SilkNetHostApp _hostViewModel; private EmulatorState EmulatorState => _hostViewModel.EmulatorState; public bool Visible { get; private set; } = true; @@ -49,7 +49,7 @@ public class SilkNetImGuiMenu : ISilkNetImGuiWindow private string _lastFileError = ""; - public SilkNetImGuiMenu(ISilkNetHostUIViewModel hostViewModel, string defaultSystemName, bool defaultAudioEnabled, float defaultAudioVolumePercent, IMapper mapper, ILoggerFactory loggerFactory) + public SilkNetImGuiMenu(SilkNetHostApp hostViewModel, string defaultSystemName, bool defaultAudioEnabled, float defaultAudioVolumePercent, IMapper mapper, ILoggerFactory loggerFactory) { _hostViewModel = hostViewModel; _screenScaleString = _hostViewModel.Scale.ToString(); diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/IWASMHostUIViewModel.cs b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/IWASMHostUIViewModel.cs deleted file mode 100644 index 63a04292..00000000 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/IWASMHostUIViewModel.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Highbyte.DotNet6502.App.WASM.Emulator; - -public interface IWASMHostUIViewModel -{ - Task SetDebugState(bool visible); - Task ToggleDebugState(); - void UpdateDebug(string debug); - - void UpdateStats(string stats); - Task SetStatsState(bool visible); - Task ToggleStatsState(); - - Task SetMonitorState(bool visible); -} diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/Skia/SkiaWASMHostApp.cs b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/Skia/SkiaWASMHostApp.cs index 7d77dac6..cd0a691c 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/Skia/SkiaWASMHostApp.cs +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/Skia/SkiaWASMHostApp.cs @@ -34,14 +34,12 @@ public class SkiaWASMHostApp : HostApp _monitor; - // Operations to update UI - private readonly IWASMHostUIViewModel _wasmHostUIViewModel; - private const int STATS_EVERY_X_FRAME = 60 * 1; private int _statsFrameCount = 0; @@ -58,7 +56,7 @@ public SkiaWASMHostApp( Func getAudioContext, GamepadList gamepadList, IJSRuntime jsRuntime, - IWASMHostUIViewModel wasmHostUIViewModel + Highbyte.DotNet6502.App.WASM.Pages.Index wasmHostUIViewModel ) : base("SilkNet", systemList, emulatorConfig.HostSystemConfigs, loggerFactory) { _loggerFactory = loggerFactory; diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/WasmMonitor.cs b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/WasmMonitor.cs index 1f44b177..26eab233 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/WasmMonitor.cs +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/WasmMonitor.cs @@ -15,7 +15,7 @@ public class WasmMonitor : MonitorBase private bool _hasBeenInitializedOnce = false; private readonly IJSRuntime _jsRuntime; - private readonly IWASMHostUIViewModel _wasmHostUIViewModel; + private readonly Highbyte.DotNet6502.App.WASM.Pages.Index _wasmHostUIViewModel; private ushort? _lastTriggeredLoadBinaryForceLoadAddress = null; private Action? _lastTriggeredAfterLoadCallback = null; @@ -24,7 +24,7 @@ public WasmMonitor( IJSRuntime jsRuntime, SystemRunner systemRunner, EmulatorConfig emulatorConfig, - IWASMHostUIViewModel wasmHostUIViewModel + Highbyte.DotNet6502.App.WASM.Pages.Index wasmHostUIViewModel ) : base(systemRunner, emulatorConfig.Monitor) { 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 f465a77c..7762cc32 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Index.razor.cs +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Index.razor.cs @@ -19,7 +19,7 @@ namespace Highbyte.DotNet6502.App.WASM.Pages; -public partial class Index : IWASMHostUIViewModel +public partial class Index { //private string Version => typeof(Program).Assembly.GetName().Version!.ToString(); private string Version => Assembly.GetEntryAssembly()!.GetCustomAttribute()!.InformationalVersion; diff --git a/src/libraries/Highbyte.DotNet6502/Systems/HostApp.cs b/src/libraries/Highbyte.DotNet6502/Systems/HostApp.cs index 39069ea3..cea5cf5e 100644 --- a/src/libraries/Highbyte.DotNet6502/Systems/HostApp.cs +++ b/src/libraries/Highbyte.DotNet6502/Systems/HostApp.cs @@ -21,7 +21,6 @@ public class HostApp where TAudioHandlerContext : IAudioHandlerContext { // Injected via constructor - private readonly ILoggerFactory _loggerFactory; private readonly ILogger _logger; private readonly SystemList _systemList; private readonly Dictionary _hostSystemConfigs; @@ -62,7 +61,6 @@ ILoggerFactory loggerFactory _updateFps = InstrumentationBag.Add($"{_hostName}-OnUpdateFPS"); _renderFps = InstrumentationBag.Add($"{_hostName}-OnRenderFPS"); - _loggerFactory = loggerFactory; _logger = loggerFactory.CreateLogger("HostApp"); if (systemList.Systems.Count == 0) @@ -73,18 +71,6 @@ ILoggerFactory loggerFactory _hostSystemConfigs = hostSystemConfigs; } - public void SetAndInitContexts( - Func? getRenderContext = null, - Func? getInputHandlerContext = null, - Func? getAudioHandlerContext = null - ) - { - _systemList.SetContext(getRenderContext, getInputHandlerContext, getAudioHandlerContext); - _systemList.InitRenderContext(); - _systemList.InitInputHandlerContext(); - _systemList.InitAudioHandlerContext(); - } - public void SetContexts( Func? getRenderContext = null, Func? getInputHandlerContext = null, From 35a80bf1c356e97899f2472794df040bf75b34c2 Mon Sep 17 00:00:00 2001 From: Highbyte Date: Mon, 22 Jul 2024 14:15:09 +0200 Subject: [PATCH 09/40] Minor fixes --- .../SilkNetImGuiMenu.cs | 2 +- .../Pages/Index.razor.cs | 33 ++++++++++--------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetImGuiMenu.cs b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetImGuiMenu.cs index 9e4076b5..1d95adb7 100644 --- a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetImGuiMenu.cs +++ b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetImGuiMenu.cs @@ -196,7 +196,7 @@ public void PostOnRender() ImGui.PushStyleColor(ImGuiCol.Text, s_informationColor); //ImGui.SetKeyboardFocusHere(0); ImGui.PushItemWidth(40); - if (systemConfig.AudioSupported) + if (systemConfig.AudioSupported && systemConfig.AudioEnabled) { if (ImGui.SliderFloat("Volume", ref _audioVolumePercent, 0f, 100f, "")) { 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 7762cc32..bd21684b 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Index.razor.cs +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Index.razor.cs @@ -30,8 +30,6 @@ public partial class Index /// public bool Initialized { get; private set; } = false; - private BrowserContext _browserContext = default!; - private AudioContextSync _audioContext = default!; private SKCanvas _canvas = default!; private GRContext _grContext = default!; @@ -137,7 +135,16 @@ protected override async Task OnInitializedAsync() _logger = LoggerFactory.CreateLogger(); _logger.LogDebug("OnInitializedAsync() was called"); - _browserContext = new() + // TODO: Make Automapper configuration more generic, incorporate in classes that need it? + var mapperConfiguration = new MapperConfiguration( + cfg => + { + cfg.CreateMap(); + } + ); + _mapper = mapperConfiguration.CreateMapper(); + + var browserContext = new BrowserContext() { Uri = NavManager!.ToAbsoluteUri(NavManager.Uri), HttpClient = HttpClient, @@ -151,11 +158,11 @@ protected override async Task OnInitializedAsync() { Renderer = C64HostRenderer.SkiaSharp, }; - var c64Setup = new C64Setup(_browserContext, LoggerFactory, c64HostConfig); + var c64Setup = new C64Setup(browserContext, LoggerFactory, c64HostConfig); systemList.AddSystem(c64Setup); var genericComputerHostConfig = new GenericComputerHostConfig(); - var genericComputerSetup = new GenericComputerSetup(_browserContext, LoggerFactory, genericComputerHostConfig); + var genericComputerSetup = new GenericComputerSetup(browserContext, LoggerFactory, genericComputerHostConfig); systemList.AddSystem(genericComputerSetup); // Add emulator config + system-specific host configs @@ -197,16 +204,7 @@ protected override async Task OnInitializedAsync() await SelectSystem(_emulatorConfig.DefaultEmulator); // Set parameters from query string - await SetDefaultsFromQueryParams(_browserContext.Uri); - - // TODO: Make Automapper configuration more generic, incorporate in classes that need it? - var mapperConfiguration = new MapperConfiguration( - cfg => - { - cfg.CreateMap(); - } - ); - _mapper = mapperConfiguration.CreateMapper(); + await SetDefaultsFromQueryParams(browserContext.Uri); Initialized = true; } @@ -235,6 +233,8 @@ protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender && !_wasmHost.IsAudioHandlerContextInitialized) { + _logger.LogDebug("OnAfterRenderAsync() was called with firstRender = true"); + _audioContext = await AudioContextSync.CreateAsync(Js!); _wasmHost.InitAudioHandlerContext(); } @@ -293,6 +293,8 @@ protected void OnPaintSurface(SKPaintGLSurfaceEventArgs e) if (_canvas != e.Surface.Canvas || _grContext != grContext) { + _logger.LogDebug("OnPaintSurface() was called with new canvas or context"); + if (_grContext != grContext) { _grContext?.Dispose(); @@ -303,6 +305,7 @@ protected void OnPaintSurface(SKPaintGLSurfaceEventArgs e) _canvas?.Dispose(); _canvas = e.Surface.Canvas; } + _wasmHost.InitRenderContext(); } From 222f5a25a3982ef1a7392b7c74541eb895f88d21 Mon Sep 17 00:00:00 2001 From: Highbyte Date: Wed, 24 Jul 2024 00:48:18 +0200 Subject: [PATCH 10/40] Refactor SadConsole host app to use new HostApp base class. Add basic UI to SadConsole host app for selecting system and starting/stopping. --- .../EmulatorConfig.cs | 52 +++ .../Highbyte.DotNet6502.App.SadConsole.csproj | 8 +- .../MenuConsole.cs | 140 ++++++++ .../Program.cs | 153 ++++----- .../SadConsoleHostApp.cs | 303 ++++++++++++++++++ .../SadConsoleUISettings.cs | 26 ++ .../SystemSetup/C64HostConfig.cs | 18 ++ .../SystemSetup/C64Setup.cs | 115 +++++++ .../SystemSetup/GenericComputerHostConfig.cs | 16 + .../SystemSetup/GenericComputerSetup.cs | 101 ++++++ .../SadConsoleHostSystemConfigBase.cs | 30 ++ .../appsettings.json | 130 ++++++++ .../appsettings_c64.json | 17 - .../appsettings_hello.json | 36 --- .../appsettings_scroll.json | 41 --- .../appsettings_snake.json | 47 --- .../SilkNetHostApp.cs | 4 + .../Emulator/Skia/SkiaWASMHostApp.cs | 4 + .../Video/C64SadConsoleRenderer.cs | 7 +- .../EmulatorConsole.cs | 49 +++ .../EmulatorHost.cs | 94 ------ .../Video/GenericSadConsoleRenderer.cs | 7 +- .../SadConsoleConfig.cs | 36 --- .../SadConsoleMain.cs | 91 ------ .../SadConsoleRenderContext.cs | 8 +- .../SadConsoleScreenObject.cs | 55 ---- .../Highbyte.DotNet6502/Systems/HostApp.cs | 4 - 27 files changed, 1060 insertions(+), 532 deletions(-) create mode 100644 src/apps/Highbyte.DotNet6502.App.SadConsole/EmulatorConfig.cs create mode 100644 src/apps/Highbyte.DotNet6502.App.SadConsole/MenuConsole.cs create mode 100644 src/apps/Highbyte.DotNet6502.App.SadConsole/SadConsoleHostApp.cs create mode 100644 src/apps/Highbyte.DotNet6502.App.SadConsole/SadConsoleUISettings.cs create mode 100644 src/apps/Highbyte.DotNet6502.App.SadConsole/SystemSetup/C64HostConfig.cs create mode 100644 src/apps/Highbyte.DotNet6502.App.SadConsole/SystemSetup/C64Setup.cs create mode 100644 src/apps/Highbyte.DotNet6502.App.SadConsole/SystemSetup/GenericComputerHostConfig.cs create mode 100644 src/apps/Highbyte.DotNet6502.App.SadConsole/SystemSetup/GenericComputerSetup.cs create mode 100644 src/apps/Highbyte.DotNet6502.App.SadConsole/SystemSetup/SadConsoleHostSystemConfigBase.cs create mode 100644 src/apps/Highbyte.DotNet6502.App.SadConsole/appsettings.json delete mode 100644 src/apps/Highbyte.DotNet6502.App.SadConsole/appsettings_c64.json delete mode 100644 src/apps/Highbyte.DotNet6502.App.SadConsole/appsettings_hello.json delete mode 100644 src/apps/Highbyte.DotNet6502.App.SadConsole/appsettings_scroll.json delete mode 100644 src/apps/Highbyte.DotNet6502.App.SadConsole/appsettings_snake.json create mode 100644 src/libraries/Highbyte.DotNet6502.Impl.SadConsole/EmulatorConsole.cs delete mode 100644 src/libraries/Highbyte.DotNet6502.Impl.SadConsole/EmulatorHost.cs delete mode 100644 src/libraries/Highbyte.DotNet6502.Impl.SadConsole/SadConsoleConfig.cs delete mode 100644 src/libraries/Highbyte.DotNet6502.Impl.SadConsole/SadConsoleMain.cs delete mode 100644 src/libraries/Highbyte.DotNet6502.Impl.SadConsole/SadConsoleScreenObject.cs diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/EmulatorConfig.cs b/src/apps/Highbyte.DotNet6502.App.SadConsole/EmulatorConfig.cs new file mode 100644 index 00000000..201b625d --- /dev/null +++ b/src/apps/Highbyte.DotNet6502.App.SadConsole/EmulatorConfig.cs @@ -0,0 +1,52 @@ +using Highbyte.DotNet6502.App.SadConsole.SystemSetup; +using Highbyte.DotNet6502.Impl.SadConsole; +using Highbyte.DotNet6502.Systems; + +namespace Highbyte.DotNet6502.App.SadConsole; + +public class EmulatorConfig +{ + public const string ConfigSectionName = "Highbyte.DotNet6502.SadConsoleConfig"; + + public string WindowTitle { get; set; } + + /// + /// Optional. If not specified, default SadConsole font is used. + /// To use a specific SadConsole Font, include it in your program output directory. + /// + public string? UIFont { get; set; } + + /// + /// The name of the emulator to start. + /// Ex: GenericComputer, C64 + /// + /// + public string DefaultEmulator { get; set; } + + + /// + /// SadConsole-specific configuration for specific system. + /// + public C64HostConfig C64HostConfig { get; set; } + /// + /// SadConsole-specific configuration for specific system. + /// + public GenericComputerHostConfig GenericComputerHostConfig { get; set; } + + public EmulatorConfig() + { + WindowTitle = "SadConsole + Highbyte.DotNet6502 emulator."; + UIFont = null; + DefaultEmulator = "C64"; + + C64HostConfig = new(); + GenericComputerHostConfig = new(); + } + + 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)}"); + //Monitor.Validate(); + } +} diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/Highbyte.DotNet6502.App.SadConsole.csproj b/src/apps/Highbyte.DotNet6502.App.SadConsole/Highbyte.DotNet6502.App.SadConsole.csproj index 82e2f5b2..11df772d 100644 --- a/src/apps/Highbyte.DotNet6502.App.SadConsole/Highbyte.DotNet6502.App.SadConsole.csproj +++ b/src/apps/Highbyte.DotNet6502.App.SadConsole/Highbyte.DotNet6502.App.SadConsole.csproj @@ -20,18 +20,12 @@ - + PreserveNewest PreserveNewest - - PreserveNewest - - - PreserveNewest - diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/MenuConsole.cs b/src/apps/Highbyte.DotNet6502.App.SadConsole/MenuConsole.cs new file mode 100644 index 00000000..af7b9654 --- /dev/null +++ b/src/apps/Highbyte.DotNet6502.App.SadConsole/MenuConsole.cs @@ -0,0 +1,140 @@ +using SadConsole.UI; +using SadConsole.UI.Controls; +using SadRogue.Primitives; + +namespace Highbyte.DotNet6502.App.SadConsole; +public class MenuConsole : ControlsConsole +{ + public const int CONSOLE_WIDTH = USABLE_WIDTH + (SadConsoleUISettings.UI_USE_CONSOLE_BORDER ? 2 : 0); + public const int CONSOLE_HEIGHT = USABLE_HEIGHT + (SadConsoleUISettings.UI_USE_CONSOLE_BORDER ? 2 : 0); + private const int USABLE_WIDTH = 21; + private const int USABLE_HEIGHT = 15; + + private readonly SadConsoleHostApp _sadConsoleHostApp; + + private MenuConsole(SadConsoleHostApp sadConsoleHostApp) : base(CONSOLE_WIDTH, CONSOLE_HEIGHT) + { + _sadConsoleHostApp = sadConsoleHostApp; + } + + public static MenuConsole Create(SadConsoleHostApp sadConsoleHostApp) + { + var console = new MenuConsole(sadConsoleHostApp); + + console.Surface.DefaultForeground = SadConsoleUISettings.UIConsoleForegroundColor; + console.Surface.DefaultBackground = SadConsoleUISettings.UIConsoleBackgroundColor; + console.Clear(); + + //FontSize = console.Font.GetFontSize(IFont.Sizes.One); + //console.Surface.UsePrintProcessor = true; + + console.UseMouse = true; + console.MouseMove += (s, e) => + { + }; + console.UseKeyboard = true; + + console.DrawUIItems(); + + if (SadConsoleUISettings.UI_USE_CONSOLE_BORDER) + console.Surface.DrawBox(new Rectangle(0, 0, console.Width, console.Height), SadConsoleUISettings.ConsoleDrawBoxBorderParameters); + + return console; + } + + private void DrawUIItems() + { + var systemLabel = CreateLabel("System:", 1, 1); + ComboBox selectSystemComboBox = new ComboBox(10, 15, 4, _sadConsoleHostApp.AvailableSystemNames.ToArray()) + { + Position = (systemLabel.Bounds.MaxExtentX + 2, systemLabel.Position.Y), + Name = "selectSystemComboBox", + SelectedItem = _sadConsoleHostApp.SelectedSystemName, + }; + selectSystemComboBox.SelectedItemChanged += (s, e) => { _sadConsoleHostApp.SelectSystem(selectSystemComboBox.SelectedItem.ToString()); IsDirty = true; }; + Controls.Add(selectSystemComboBox); + + var statusLabel = CreateLabel("Status:", 1, systemLabel.Bounds.MaxExtentY + 1); + CreateLabelValue(_sadConsoleHostApp.EmulatorState.ToString(), statusLabel.Bounds.MaxExtentX + 2, statusLabel.Position.Y, "statusValueLabel"); + + var startButton = new Button("Start") + { + Name = "startButton", + Position = (1, statusLabel.Bounds.MaxExtentY + 1), + }; + startButton.Click += async (s, e) => { await _sadConsoleHostApp.Start(); IsDirty = true; }; + Controls.Add(startButton); + + var pauseButton = new Button("Pause") + { + Name = "pauseButton", + Position = (11, startButton.Position.Y), + }; + pauseButton.Click += (s, e) => { _sadConsoleHostApp.Pause(); IsDirty = true; }; + Controls.Add(pauseButton); + + var stopButton = new Button("Stop") + { + Name = "stopButton", + Position = (1, startButton.Bounds.MaxExtentY + 1), + }; + stopButton.Click += (s, e) => { _sadConsoleHostApp.Stop(); IsDirty = true; }; + Controls.Add(stopButton); + + var resetButton = new Button("Reset") + { + Name = "resetButton", + Position = (11, stopButton.Position.Y), + }; + resetButton.Click += async (s, e) => { await _sadConsoleHostApp.Reset(); IsDirty = true; }; + Controls.Add(resetButton); + + // Helper function to create a label and add it to the console + Label CreateLabel(string text, int col, int row, string? name = null) + { + var labelTemp = new Label(text) { Position = new Point(col, row), Name = name }; + Controls.Add(labelTemp); + return labelTemp; + } + Label CreateLabelValue(string text, int col, int row, string? name = null) + { + var labelTemp = new Label(text) { Position = new Point(col, row), TextColor = Controls.GetThemeColors().Title, Name = name }; + Controls.Add(labelTemp); + return labelTemp; + } + + + // Force OnIsDirtyChanged event which will set control states (see SetControlStates) + OnIsDirtyChanged(); + } + + protected override void OnIsDirtyChanged() + { + if (IsDirty) + { + SetControlStates(); + } + } + + private void SetControlStates() + { + var systemComboBox = Controls["selectSystemComboBox"]; + systemComboBox.IsEnabled = _sadConsoleHostApp.EmulatorState == Systems.EmulatorState.Uninitialized; + + var statusLabel = Controls["statusValueLabel"] as Label; + statusLabel!.DisplayText = _sadConsoleHostApp.EmulatorState.ToString(); + + var startButton = Controls["startButton"]; + startButton.IsEnabled = _sadConsoleHostApp.EmulatorState != Systems.EmulatorState.Running; + + var pauseButton = Controls["pauseButton"]; + pauseButton.IsEnabled = _sadConsoleHostApp.EmulatorState == Systems.EmulatorState.Running; + + var stopButton = Controls["stopButton"]; ; + stopButton.IsEnabled = _sadConsoleHostApp.EmulatorState != Systems.EmulatorState.Uninitialized; + + var resetButton = Controls["resetButton"]; + resetButton.IsEnabled = _sadConsoleHostApp.EmulatorState != Systems.EmulatorState.Uninitialized; + } + +} diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/Program.cs b/src/apps/Highbyte.DotNet6502.App.SadConsole/Program.cs index e6f5333f..54e3b374 100644 --- a/src/apps/Highbyte.DotNet6502.App.SadConsole/Program.cs +++ b/src/apps/Highbyte.DotNet6502.App.SadConsole/Program.cs @@ -1,22 +1,19 @@ -// Host app for running Highbyte.DotNet6502 emulator in a SadConsole Window -// -// Generic 6502 example programs -// - Source (.asm)in: Examples/Assembler/Generic -// - Compiled with ACME cross-assembler: to Examples/Assembler/Generic/Build -// -// C64 example programs -// - Source (.asm)in: Examples/Assembler/C64 -// - Compiled with ACME cross-assembler: to Examples/Assembler/C64/Build - +using Highbyte.DotNet6502.App.SadConsole.SystemSetup; +using Highbyte.DotNet6502.App.SadConsole; using Highbyte.DotNet6502.Impl.SadConsole; -using Highbyte.DotNet6502.Systems.Generic.Config; -using Highbyte.DotNet6502.Systems.Commodore64.Config; -using Microsoft.Extensions.Configuration; using Highbyte.DotNet6502.Logging; -using Microsoft.Extensions.Logging; using Highbyte.DotNet6502.Logging.InMem; +using Highbyte.DotNet6502.Systems; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Highbyte.DotNet6502.Systems.Commodore64; +using Highbyte.DotNet6502.Systems.Generic; -IConfiguration Configuration; +// Get config file +var builder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json"); +IConfiguration Configuration = builder.Build(); // Create logging DotNet6502InMemLogStore logStore = new() { WriteDebugMessage = true }; @@ -28,88 +25,48 @@ builder.SetMinimumLevel(LogLevel.Trace); }); -// Get config options -var builder = new ConfigurationBuilder() - .SetBasePath(Directory.GetCurrentDirectory()) - // appsettings_scroll.json - // appsettings_hello.json - // appsettings_snake.json - // appsettings_c64.json - .AddJsonFile("appsettings_c64.json"); - -Configuration = builder.Build(); - -var sadConsoleConfig = new SadConsoleConfig(); -Configuration.GetSection(SadConsoleConfig.ConfigSectionName).Bind(sadConsoleConfig); - -var genericComputerConfig = new GenericComputerConfig(); -Configuration.GetSection(GenericComputerConfig.ConfigSectionName).Bind(genericComputerConfig); +// ---------- +// Get emulator host config +// ---------- +var emulatorConfig = new EmulatorConfig(); +Configuration.GetSection(EmulatorConfig.ConfigSectionName).Bind(emulatorConfig); -var c64Config = new C64Config(); -Configuration.GetSection(C64Config.ConfigSectionName).Bind(c64Config); - -// Alternative way, build config via code instead of reading from appsettings.json -//var sadConsoleConfig = ConfigViaCode(); - -// Init EmulatorHost and run! -var emulatorHost = new EmulatorHost( - sadConsoleConfig, - genericComputerConfig, - c64Config, - loggerFactory - ); -emulatorHost.Start(); - -//SadConsoleConfig ConfigViaCode() +//var emulatorConfig = new EmulatorConfig //{ -// // Define how emulator memory should be layed out recarding screen output and keyboard input -// var emulatorMemoryConfig = new EmulatorMemoryConfig -// { -// Screen = new EmulatorScreenConfig -// { -// // 6502 code running in emulator should have the same #rows & #cols as we setup in SadConsole -// Cols = 80, -// Rows = 25, - -// // If borders should be used. Currently only updateable with a color setting (see ScreenBackgroundColorAddress below) -// BorderCols = 4, -// BorderRows = 2, - -// // 6502 code must use these addresses as screen memory -// ScreenStartAddress = 0x0400, //80*25 = 2000(0x07d0) -> range 0x0400 - 0x0bcf -// ScreenColorStartAddress = 0xd800, //80*25 = 2000(0x07d0) -> range 0xd800 - 0xdfcf -// ScreenRefreshStatusAddress = 0xd000, -// ScreenBorderColorAddress = 0xd020, -// ScreenBackgroundColorAddress = 0xd021, -// DefaultBgColor = 0x00, // 0x00 = Black -// DefaultFgColor = 0x0f, // 0x0f = Light grey -// DefaultBorderColor = 0x0b, // 0x0b = Dark grey -// }, - -// Input = new EmulatorInputConfig -// { -// KeyPressedAddress = 0xe000 -// } -// }; - -// var emulatorConfig = new EmulatorConfig -// { -// ProgramBinaryFile = "../../../../../../samples/Assembler/Generic/Build/hostinteraction_scroll_text_and_cycle_colors.prg", -// Memory = emulatorMemoryConfig -// }; - -// // Configure overall SadConsole settings -// var sadConsoleConfig = new SadConsoleConfig -// { -// WindowTitle = "SadConsole screen updated from program running in Highbyte.DotNet6502 emulator", -// FontScale = 2 -// }; - -// var emulatorHostOptions = new Options -// { -// SadConsoleConfig = sadConsoleConfig, -// EmulatorConfig = emulatorConfig -// }; - -// return emulatorHostOptions; -//} +// DefaultEmulator = c64Setup.SystemName, +// UIFont = null, +// FontScale = 1, +// Font = "Fonts/C64.font", +// WindowTitle = "SadConsole with Highbyte.DotNet6502 emulator!", +// //Monitor = new MonitorConfig +// //{ +// // MaxLineLength = 100, +// //}, +//}; + +var hostSystemConfigs = new Dictionary +{ + { C64.SystemName, emulatorConfig.C64HostConfig }, + { GenericComputer.SystemName, emulatorConfig.GenericComputerHostConfig} +}; + +// ---------- +// Get systems +// ---------- +var systemList = new SystemList(); + +var c64HostConfig = new C64HostConfig { }; +var c64Setup = new C64Setup(loggerFactory, Configuration, c64HostConfig); +systemList.AddSystem(c64Setup); + +var genericComputerHostConfig = new GenericComputerHostConfig { }; +var genericComputerSetup = new GenericComputerSetup(loggerFactory, Configuration, genericComputerHostConfig); +systemList.AddSystem(genericComputerSetup); + +// ---------- +// Start emulator host app +// ---------- +emulatorConfig.Validate(systemList); + +var silkNetHostApp = new SadConsoleHostApp(systemList, loggerFactory, emulatorConfig, hostSystemConfigs, logStore, logConfig); +silkNetHostApp.Run(); diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/SadConsoleHostApp.cs b/src/apps/Highbyte.DotNet6502.App.SadConsole/SadConsoleHostApp.cs new file mode 100644 index 00000000..3dbeb9e1 --- /dev/null +++ b/src/apps/Highbyte.DotNet6502.App.SadConsole/SadConsoleHostApp.cs @@ -0,0 +1,303 @@ +using Highbyte.DotNet6502.App.SadConsole.SystemSetup; +using Highbyte.DotNet6502.Impl.SadConsole; +using Highbyte.DotNet6502.Logging; +using Highbyte.DotNet6502.Systems; +using Microsoft.Extensions.Logging; +using SadConsole.Configuration; +using SadRogue.Primitives; + + +namespace Highbyte.DotNet6502.App.SadConsole; + +/// +/// Host app for running Highbyte.DotNet6502 emulator in a SadConsole Window +/// +public class SadConsoleHostApp : HostApp +{ + // -------------------- + // Injected variables + // -------------------- + private readonly ILogger _logger; + private readonly EmulatorConfig _emulatorConfig; + public EmulatorConfig EmulatorConfig => _emulatorConfig; + + private readonly DotNet6502InMemLogStore _logStore; + private readonly DotNet6502InMemLoggerConfiguration _logConfig; + private readonly ILoggerFactory _loggerFactory; + //private readonly IMapper _mapper; + + // -------------------- + // Other variables / constants + // -------------------- + private ScreenObject? _sadConsoleScreen; + private MenuConsole? _menuConsole; + private EmulatorConsole? _sadConsoleEmulatorConsole; + + private SadConsoleRenderContext _renderContext = default!; + private SadConsoleInputHandlerContext _inputHandlerContext = default!; + private NullAudioHandlerContext _audioHandlerContext = default!; + + private const int MENU_POSITION_X = 0; + private const int MENU_POSITION_Y = 0; + + private int StartupScreenWidth => MenuConsole.CONSOLE_WIDTH + 40; + private int StartupScreenHeight => MenuConsole.CONSOLE_HEIGHT; + + /// + /// Constructor + /// + /// + /// + /// + /// + /// + public SadConsoleHostApp( + SystemList systemList, + ILoggerFactory loggerFactory, + + EmulatorConfig emulatorConfig, + Dictionary hostSystemConfigs, + //IWindow window, + DotNet6502InMemLogStore logStore, + DotNet6502InMemLoggerConfiguration logConfig + //IMapper mapper + + ) : base("SadConsole", systemList, hostSystemConfigs, loggerFactory) + { + _emulatorConfig = emulatorConfig; + //_window = window; + _logStore = logStore; + _logConfig = logConfig; + + _loggerFactory = loggerFactory; + //_mapper = mapper; + _logger = loggerFactory.CreateLogger(typeof(SadConsoleHostApp).Name); + } + + + public void Run() + { + //SetUninitializedWindow(); + + //_inputContext = CreateSilkNetInput(); + + _renderContext = CreateRenderContext(); + _inputHandlerContext = CreateInputHandlerContext(); + _audioHandlerContext = CreateAudioHandlerContext(); + + SetContexts(() => _renderContext, () => _inputHandlerContext, () => _audioHandlerContext); + InitRenderContext(); + InitInputHandlerContext(); + InitAudioHandlerContext(); + + // Set the default system + SelectSystem(_emulatorConfig.DefaultEmulator); + + // ---------- + // Main SadConsole screen + // ---------- + + var uiFont = _emulatorConfig.UIFont ?? string.Empty; + string[]? emulatorFonts = string.IsNullOrEmpty(CommonHostSystemConfig.Font) ? null : [CommonHostSystemConfig.Font]; + + var builder = new Builder() + .SetScreenSize(StartupScreenWidth, StartupScreenHeight) + .ConfigureFonts(customDefaultFont: uiFont, extraFonts: emulatorFonts) + .SetStartingScreen(CreateMainSadConsoleScreen) + .IsStartingScreenFocused(false) // Let the object focused in the create method remain. + .AddFrameUpdateEvent(UpdateSadConsole) + .AddFrameRenderEvent(RenderSadConsole) + ; + + Settings.WindowTitle = _emulatorConfig.WindowTitle; + Settings.ResizeMode = Settings.WindowResizeOptions.None; + + // Start SadConsole window + Game.Create(builder); + Game.Instance.Run(); + // Continues here after SadConsole window is closed + Game.Instance.Dispose(); + } + + + private IScreenObject CreateMainSadConsoleScreen(Game gameInstance) + { + //ScreenSurface screen = new(gameInstance.ScreenCellsX, gameInstance.ScreenCellsY); + //return screen; + _sadConsoleScreen = new ScreenObject(); + + _menuConsole = MenuConsole.Create(this); + _menuConsole.Position = (MENU_POSITION_X, MENU_POSITION_Y); + _sadConsoleScreen.Children.Add(_menuConsole); + + //_sadConsoleScreen.IsFocused = true; + _menuConsole.IsFocused = true; + + return _sadConsoleScreen; + } + + public override void OnAfterSelectSystem() + { + } + + public override bool OnBeforeStart(ISystem systemAboutToBeStarted) + { + // Create emulator console + if (_sadConsoleEmulatorConsole != null) + { + if (_sadConsoleScreen.Children.Contains(_sadConsoleEmulatorConsole)) + _sadConsoleScreen.Children.Remove(_sadConsoleEmulatorConsole); + } + + IFont font; + if (!string.IsNullOrEmpty(CommonHostSystemConfig.Font)) + { + var fontFileNameWithoutExtension = Path.GetFileNameWithoutExtension(CommonHostSystemConfig.Font); + font = Game.Instance.Fonts[fontFileNameWithoutExtension]; + } + else + { + font = Game.Instance.DefaultFont; + } + var fontSize = CommonHostSystemConfig.FontScale switch + { + 1 => IFont.Sizes.One, + 2 => IFont.Sizes.Two, + 3 => IFont.Sizes.Three, + _ => IFont.Sizes.One, + }; + _sadConsoleEmulatorConsole = EmulatorConsole.Create(systemAboutToBeStarted, font, fontSize, SadConsoleUISettings.ConsoleDrawBoxBorderParameters); + _sadConsoleEmulatorConsole.UsePixelPositioning = true; + _sadConsoleEmulatorConsole.Position = new Point((_menuConsole.Position.X * _menuConsole.Font.GlyphWidth) + (_menuConsole.Width * _menuConsole.Font.GlyphWidth), 0); + + _sadConsoleEmulatorConsole.IsFocused = true; + _sadConsoleScreen.Children.Add(_sadConsoleEmulatorConsole); + + // Resize main window to fit menu, emulator, and other consoles + Game.Instance.ResizeWindow(CalculateWindowWidthPixels(), CalculateWindowHeightPixels()); + + return true; + } + + public override void OnAfterStart(EmulatorState emulatorStateBeforeStart) + { + //// Init monitor for current system started if this system was not started before + //if (emulatorStateBeforeStart == EmulatorState.Uninitialized) + // _monitor.Init(CurrentSystemRunner!); + } + + public override void OnAfterStop() + { + if (_sadConsoleEmulatorConsole != null) + { + if (_sadConsoleScreen.Children.Contains(_sadConsoleEmulatorConsole)) + _sadConsoleScreen.Children.Remove(_sadConsoleEmulatorConsole); + _sadConsoleEmulatorConsole.Dispose(); + } + } + public override void OnAfterClose() + { + // Dispose Monitor/Instrumentations panel + //_monitor.Cleanup(); + + // Cleanup contexts + _renderContext?.Cleanup(); + _inputHandlerContext?.Cleanup(); + _audioHandlerContext?.Cleanup(); + } + + /// + /// Runs on every Update Frame event. + /// Runs the emulator logic for one frame. + /// + /// + private void UpdateSadConsole(object? sender, GameHost gameHost) + { + // RunEmulatorOneFrame() will first handle input, then emulator in run for one frame. + RunEmulatorOneFrame(); + } + + public override void OnBeforeRunEmulatorOneFrame(out bool shouldRun, out bool shouldReceiveInput) + { + // TODO: Check if montior is active? If no set shouldRun to false. + shouldRun = true; + + // TODO: Check if emulator console has focus? If not, set shouldReceiveInput to false. + shouldReceiveInput = true; + } + + public override void OnAfterRunEmulatorOneFrame(ExecEvaluatorTriggerResult execEvaluatorTriggerResult) + { + //// Show monitor if we encounter breakpoint or other break + //if (execEvaluatorTriggerResult.Triggered) + // _monitor.Enable(execEvaluatorTriggerResult); + } + + + /// + /// Runs on every Render Frame event. Draws one emulator frame on screen. + /// + /// + private void RenderSadConsole(object? sender, GameHost gameHost) + { + // Draw emulator on screen + DrawFrame(); + } + + public override void OnBeforeDrawFrame(bool emulatorWillBeRendered) + { + // If any ImGui window is visible, make sure to clear Gl buffer before rendering emulator + if (emulatorWillBeRendered) + { + } + } + public override void OnAfterDrawFrame(bool emulatorRendered) + { + } + + + private SadConsoleRenderContext CreateRenderContext() + { + return new SadConsoleRenderContext(() => _sadConsoleEmulatorConsole); + } + + private SadConsoleInputHandlerContext CreateInputHandlerContext() + { + return new SadConsoleInputHandlerContext(_loggerFactory); + } + + private NullAudioHandlerContext CreateAudioHandlerContext() + { + return new NullAudioHandlerContext(); + } + + private SadConsoleHostSystemConfigBase CommonHostSystemConfig => (SadConsoleHostSystemConfigBase)GetHostSystemConfig(); + + private int CalculateWindowWidthPixels() + { + var width = _menuConsole.WidthPixels + (_sadConsoleEmulatorConsole != null ? _sadConsoleEmulatorConsole.WidthPixels + ((CommonHostSystemConfig.FontScale - 1) * 16) : 0); + return width; + } + + private int CalculateWindowHeightPixels() + { + var height = Math.Max(_menuConsole.HeightPixels, _sadConsoleEmulatorConsole != null ? _sadConsoleEmulatorConsole.HeightPixels : 0); + return height; + } + + //private void OnKeyDown(IKeyboard keyboard, Key key, int x) + //{ + // if (key == Key.F6) + // ToggleMainMenu(); + // if (key == Key.F10) + // ToggleLogsPanel(); + + // if (EmulatorState == EmulatorState.Running || EmulatorState == EmulatorState.Paused) + // { + // if (key == Key.F11) + // ToggleStatsPanel(); + // if (key == Key.F12) + // ToggleMonitor(); + // } + //} +} diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/SadConsoleUISettings.cs b/src/apps/Highbyte.DotNet6502.App.SadConsole/SadConsoleUISettings.cs new file mode 100644 index 00000000..d713218f --- /dev/null +++ b/src/apps/Highbyte.DotNet6502.App.SadConsole/SadConsoleUISettings.cs @@ -0,0 +1,26 @@ +using SadRogue.Primitives; + +namespace Highbyte.DotNet6502.App.SadConsole; +internal class SadConsoleUISettings +{ + public static Color UIConsoleBackgroundColor = new Color(5, 15, 45); + public static Color UIConsoleForegroundColor = Color.White; + + public const bool UI_USE_CONSOLE_BORDER = true; + public readonly static ColoredGlyph ConsoleBorderGlyph = new(new Color(90, 90, 90), UIConsoleBackgroundColor); + public readonly static ShapeParameters ConsoleDrawBoxBorderParameters = new ShapeParameters( + hasBorder: true, + borderGlyph: ConsoleBorderGlyph, + ignoreBorderForeground: false, + ignoreBorderBackground: false, + ignoreBorderGlyph: false, + ignoreBorderMirror: false, + hasFill: false, + fillGlyph: null, + ignoreFillForeground: false, + ignoreFillBackground: false, + ignoreFillGlyph: false, + ignoreFillMirror: false, + boxBorderStyle: ICellSurface.ConnectedLineThin, + boxBorderStyleGlyphs: null); +} diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/SystemSetup/C64HostConfig.cs b/src/apps/Highbyte.DotNet6502.App.SadConsole/SystemSetup/C64HostConfig.cs new file mode 100644 index 00000000..92cd55f3 --- /dev/null +++ b/src/apps/Highbyte.DotNet6502.App.SadConsole/SystemSetup/C64HostConfig.cs @@ -0,0 +1,18 @@ +using Highbyte.DotNet6502.Systems; + +namespace Highbyte.DotNet6502.App.SadConsole.SystemSetup; + +public class C64HostConfig : SadConsoleHostSystemConfigBase +{ + public C64HostConfig() + { + Font = "Fonts/C64.font"; + FontScale = 1; + } + + public new object Clone() + { + var clone = (C64HostConfig)MemberwiseClone(); + return clone; + } +} diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/SystemSetup/C64Setup.cs b/src/apps/Highbyte.DotNet6502.App.SadConsole/SystemSetup/C64Setup.cs new file mode 100644 index 00000000..c6d58497 --- /dev/null +++ b/src/apps/Highbyte.DotNet6502.App.SadConsole/SystemSetup/C64Setup.cs @@ -0,0 +1,115 @@ +using Highbyte.DotNet6502.Impl.SadConsole; +using Highbyte.DotNet6502.Impl.SadConsole.Commodore64.Input; +using Highbyte.DotNet6502.Impl.SadConsole.Commodore64.Video; +using Highbyte.DotNet6502.Systems; +using Highbyte.DotNet6502.Systems.Commodore64; +using Highbyte.DotNet6502.Systems.Commodore64.Config; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace Highbyte.DotNet6502.App.SadConsole.SystemSetup; + +public class C64Setup : SystemConfigurer +{ + public string SystemName => C64.SystemName; + + private readonly ILoggerFactory _loggerFactory; + private readonly IConfiguration _configuration; + private readonly C64HostConfig _c64HostConfig; + + public C64Setup(ILoggerFactory loggerFactory, IConfiguration configuration, C64HostConfig c64HostConfig) + { + _loggerFactory = loggerFactory; + _configuration = configuration; + _c64HostConfig = c64HostConfig; + } + + public Task GetNewConfig(string configurationVariant) + { + var c64Config = new C64Config() { ROMs = new() }; + _configuration.GetSection(C64Config.ConfigSectionName).Bind(c64Config); + return Task.FromResult(c64Config); + + //var c64Config = new C64Config + //{ + // C64Model = "C64NTSC", // C64NTSC, C64PAL + // Vic2Model = "NTSC", // NTSC, NTSC_old, PAL + // //C64Model = "C64PAL", // C64NTSC, C64PAL + // //Vic2Model = "PAL", // NTSC, NTSC_old, PAL + + // //ROMDirectory = "%USERPROFILE%/Documents/C64/VICE/C64", + // ROMDirectory = "%HOME%/Downloads/C64", + // ROMs = new List + // { + // new ROM + // { + // Name = C64Config.BASIC_ROM_NAME, + // File = "basic.901226-01.bin", + // Data = null, + // Checksum = "79015323128650c742a3694c9429aa91f355905e", + // }, + // new ROM + // { + // Name = C64Config.CHARGEN_ROM_NAME, + // File = "characters.901225-01.bin", + // Data = null, + // Checksum = "adc7c31e18c7c7413d54802ef2f4193da14711aa", + // }, + // new ROM + // { + // Name = C64Config.KERNAL_ROM_NAME, + // File = "kernal.901227-03.bin", + // Data = null, + // Checksum = "1d503e56df85a62fee696e7618dc5b4e781df1bb", + // } + // }, + + // AudioSupported = false, + // AudioEnabled = false, + + // InstrumentationEnabled = false, // Start with instrumentation off by default + //}; + + ////c64Config.Validate(); + //return Task.FromResult(c64Config); + } + + public Task PersistConfig(ISystemConfig systemConfig) + { + var c64Config = (C64Config)systemConfig; + // TODO: Persist settings to file + + return Task.CompletedTask; + } + + public ISystem BuildSystem(ISystemConfig systemConfig) + { + var c64Config = (C64Config)systemConfig; + var c64 = C64.BuildC64(c64Config, _loggerFactory); + return c64; + } + + public Task GetHostSystemConfig() + { + return Task.FromResult((IHostSystemConfig)_c64HostConfig); + } + + public SystemRunner BuildSystemRunner( + ISystem system, + ISystemConfig systemConfig, + IHostSystemConfig hostSystemConfig, + SadConsoleRenderContext renderContext, + SadConsoleInputHandlerContext inputHandlerContext, + NullAudioHandlerContext audioHandlerContext + ) + { + var c64HostConfig = (C64HostConfig)hostSystemConfig; + var c64 = (C64)system; + + var renderer = new C64SadConsoleRenderer(c64, renderContext); + var inputHandler = new C64SadConsoleInputHandler(c64, inputHandlerContext, _loggerFactory); + var audioHandler = new NullAudioHandler(c64); + + return new SystemRunner(c64, renderer, inputHandler, audioHandler); + } +} diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/SystemSetup/GenericComputerHostConfig.cs b/src/apps/Highbyte.DotNet6502.App.SadConsole/SystemSetup/GenericComputerHostConfig.cs new file mode 100644 index 00000000..c37723d5 --- /dev/null +++ b/src/apps/Highbyte.DotNet6502.App.SadConsole/SystemSetup/GenericComputerHostConfig.cs @@ -0,0 +1,16 @@ +namespace Highbyte.DotNet6502.App.SadConsole.SystemSetup; + +public class GenericComputerHostConfig : SadConsoleHostSystemConfigBase +{ + public GenericComputerHostConfig() + { + Font = null; + FontScale = 1; + } + + public new object Clone() + { + var clone = (GenericComputerHostConfig)MemberwiseClone(); + return clone; + } +} diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/SystemSetup/GenericComputerSetup.cs b/src/apps/Highbyte.DotNet6502.App.SadConsole/SystemSetup/GenericComputerSetup.cs new file mode 100644 index 00000000..57095adb --- /dev/null +++ b/src/apps/Highbyte.DotNet6502.App.SadConsole/SystemSetup/GenericComputerSetup.cs @@ -0,0 +1,101 @@ +using Highbyte.DotNet6502.Impl.SadConsole; +using Highbyte.DotNet6502.Impl.SadConsole.Generic.Input; +using Highbyte.DotNet6502.Impl.SadConsole.Generic.Video; +using Highbyte.DotNet6502.Systems; +using Highbyte.DotNet6502.Systems.Commodore64.Config; +using Highbyte.DotNet6502.Systems.Generic; +using Highbyte.DotNet6502.Systems.Generic.Config; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace Highbyte.DotNet6502.App.SadConsole.SystemSetup; + +public class GenericComputerSetup : SystemConfigurer +{ + public string SystemName => GenericComputer.SystemName; + + private readonly ILoggerFactory _loggerFactory; + private readonly IConfiguration _configuration; + private readonly GenericComputerHostConfig _genericComputerHostConfig; + + public GenericComputerSetup(ILoggerFactory loggerFactory, IConfiguration configuration, GenericComputerHostConfig genericComputerHostConfig) + { + _loggerFactory = loggerFactory; + _configuration = configuration; + _genericComputerHostConfig = genericComputerHostConfig; + } + + public Task GetNewConfig(string configurationVariant) + { + var genericComputerConfig = new GenericComputerConfig() { }; + _configuration.GetSection(GenericComputerConfig.ConfigSectionName).Bind(genericComputerConfig); + return Task.FromResult(genericComputerConfig); + + //var genericComputerConfig = new GenericComputerConfig + //{ + // ProgramBinaryFile = "../../../../../../samples/Assembler/Generic/Build/hostinteraction_scroll_text_and_cycle_colors.prg", + // //ProgramBinaryFile = "%HOME%/source/repos/dotnet-6502/samples/Assembler/Generic/Build/hostinteraction_scroll_text_and_cycle_colors.prg", + // CPUCyclesPerFrame = 8000, + // Memory = new EmulatorMemoryConfig + // { + // Screen = new EmulatorScreenConfig + // { + // Cols = 40, + // Rows = 25, + // BorderCols = 3, + // BorderRows = 3, + // UseAscIICharacters = true, + // DefaultBgColor = 0x00, // 0x00 = Black (C64 scheme) + // DefaultFgColor = 0x01, // 0x0f = Light grey, 0x0e = Light Blue, 0x01 = White (C64 scheme) + // DefaultBorderColor = 0x0b, // 0x0b = Dark grey (C64 scheme) + // }, + // Input = new EmulatorInputConfig + // { + // KeyPressedAddress = 0xd030, + // KeyDownAddress = 0xd031, + // KeyReleasedAddress = 0xd031, + // } + // } + //}; + + //genericComputerConfig.Validate(); + + //return Task.FromResult(genericComputerConfig); + } + + public Task PersistConfig(ISystemConfig systemConfig) + { + var genericComputerConfig = (GenericComputerConfig)systemConfig; + // TODO: Save config settings to file + return Task.CompletedTask; + } + + public ISystem BuildSystem(ISystemConfig systemConfig) + { + var genericComputerConfig = (GenericComputerConfig)systemConfig; + return GenericComputerBuilder.SetupGenericComputerFromConfig(genericComputerConfig, _loggerFactory); + } + + public Task GetHostSystemConfig() + { + return Task.FromResult((IHostSystemConfig)_genericComputerHostConfig); + } + + public SystemRunner BuildSystemRunner( + ISystem system, + ISystemConfig systemConfig, + IHostSystemConfig hostSystemConfig, + SadConsoleRenderContext renderContext, + SadConsoleInputHandlerContext inputHandlerContext, + NullAudioHandlerContext audioHandlerContext) + { + var genericComputer = (GenericComputer)system; + var genericComputerConfig = (GenericComputerConfig)systemConfig; + + var renderer = new GenericSadConsoleRenderer(genericComputer, renderContext, genericComputerConfig.Memory.Screen); + var inputHandler = new GenericSadConsoleInputHandler(genericComputer, inputHandlerContext, genericComputerConfig.Memory.Input, _loggerFactory); + var audioHandler = new NullAudioHandler(genericComputer); + + return new SystemRunner(genericComputer, renderer, inputHandler, audioHandler); + } +} diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/SystemSetup/SadConsoleHostSystemConfigBase.cs b/src/apps/Highbyte.DotNet6502.App.SadConsole/SystemSetup/SadConsoleHostSystemConfigBase.cs new file mode 100644 index 00000000..51110a90 --- /dev/null +++ b/src/apps/Highbyte.DotNet6502.App.SadConsole/SystemSetup/SadConsoleHostSystemConfigBase.cs @@ -0,0 +1,30 @@ +using Highbyte.DotNet6502.Systems; + +namespace Highbyte.DotNet6502.App.SadConsole.SystemSetup; +public abstract class SadConsoleHostSystemConfigBase : IHostSystemConfig, ICloneable +{ + /// + /// Optional. If not specified, default SadConsole font is used. + /// To use a specific SadConsole Font, include it in your program output directory. + /// Example: Fonts/C64.font + /// + /// + public string? Font { get; set; } + /// + /// Font scale. 1 is default. + /// + /// + public int FontScale { get; set; } + + public SadConsoleHostSystemConfigBase() + { + Font = null; + FontScale = 1; + } + + public object Clone() + { + var clone = (SadConsoleHostSystemConfigBase)MemberwiseClone(); + return clone; + } +} diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/appsettings.json b/src/apps/Highbyte.DotNet6502.App.SadConsole/appsettings.json new file mode 100644 index 00000000..e19836e1 --- /dev/null +++ b/src/apps/Highbyte.DotNet6502.App.SadConsole/appsettings.json @@ -0,0 +1,130 @@ +{ + "Highbyte.DotNet6502.SadConsoleConfig": { + + "WindowTitle": "SadConsole displaying screen generated by machine code running in Highbyte.DotNet6502 emulator.", + + "DefaultEmulator": "C64", + + "C64HostConfig": { + "Font": "Fonts/C64.font", // C64 font copied from https://github.com/Thraka/SadConsole. Leave blank for default SadConsole font. + "FontScale": 1 + }, + + "GenericComputerHostConfig": { + "Font": "", // Leave blank for default SadConsole font. + "FontScale": 1 + } + }, + + "Highbyte.DotNet6502.C64": { + "C64Model": "C64NTSC", + "Vic2Model": "NTSC", + //"ROMDirectory": "%USERPROFILE%/Documents/C64/VICE/C64", + "ROMDirectory": "%HOME%/Downloads/C64", + "ROMs": [ + { + "Name": "basic", + "File": "basic.901226-01.bin" + }, + { + "Name": "kernal", + "File": "kernal.901227-03.bin" + }, + { + "Name": "chargen", + "File": "characters.901225-01.bin" + } + ], + "ColorMapName": "Default" + }, + + + "Highbyte.DotNet6502.GenericComputer": { + + "ProgramBinaryFile": "../../../../../../samples/Assembler/Generic/Build/hostinteraction_scroll_text_and_cycle_colors.prg", + "Memory": { + "Screen": { + "Cols": 40, + "Rows": 25, + "BorderCols": 3, + "BorderRows": 3, + "ScreenStartAddress": "0x0400", // 40*25 = 1000 (0x03e8) -> range 0x0400 - 0x07e7 + "ScreenColorStartAddress": "0xd800", // 40*25 = 1000 (0x03e8) -> range 0xd800 - 0xdbe7 + + "ScreenRefreshStatusAddress": "0xd000", // To sync 6502 code with host frame: The 6502 code should wait for bit 0 to become set, and then wait for it to become cleared. + + "ScreenBorderColorAddress": "0xd020", + "ScreenBackgroundColorAddress": "0xd021", + "DefaultBgColor": "0x00", // 0x00 = Black (C64 scheme) + "DefaultFgColor": "0x01", // 0x0f = Light grey, 0x0e = Light Blue, 0x01 = White (C64 scheme) + "DefaultBorderColor": "0x0b", // 0x0b = Dark grey (C64 scheme) + + "UseAscIICharacters": true + + }, + "Input": { + "KeyPressedAddress": "0xd030", + "KeyDownAddress": "0xd031", + "KeyReleasedAddress": "0xd032" + } + }, + + //"ProgramBinaryFile": "../../../../../../samples/Assembler/Generic/Build/snake6502.prg", + //"StopAtBRK": false, + //"Memory": { + // "Screen": { + // "Cols": 32, + // "Rows": 32, + // "BorderCols": 3, + // "BorderRows": 3, + // "ScreenStartAddress": "0x0200", + // "ScreenColorStartAddress": "0xd800", // Not used with this program + + // "ScreenRefreshStatusAddress": "0xd000", // The 6502 code should set bit 1 here when it's done for current frame + // "ScreenBorderColorAddress": "0xd020", + // "ScreenBackgroundColorAddress": "0xd021", + // "DefaultBgColor": "0x00", // 0x00 = Black (C64 scheme) + // "DefaultFgColor": "0x01", // 0x0f = Light grey, 0x0e = Light Blue, 0x01 = White (C64 scheme) + // "DefaultBorderColor": "0x0b", // 0x0b = Dark grey (C64 scheme) + + // "UseAscIICharacters": false, + // "CharacterMap": { + // "10": 32, + // "13": 32, + // "160": 219, + // "224": 219 + // } + // }, + // "Input": { + // "KeyPressedAddress": "0xd030", + // "KeyDownAddress": "0xd031", + // "KeyReleasedAddress": "0xd032" + // } + //} + + //"ProgramBinaryFile": "../../../../../../samples/Assembler/Generic/Build/hello_world.prg", + //"Memory": { + // "Screen": { + // "Cols": 80, + // "Rows": 25, + // "BorderCols": 6, + // "BorderRows": 3, + // "ScreenStartAddress": "0x0400", // 80*25 = 2000 (0x07d0) -> range 0x0400 - 0x0bcf + // "ScreenColorStartAddress": "0xd800", // 80*25 = 2000 (0x07d0) -> range 0xd800 - 0xdfcf + + // "ScreenRefreshStatusAddress": "0xd000", // The 6502 code should set bit 1 here when it's done for current frame + // "ScreenBorderColorAddress": "0xd020", + // "ScreenBackgroundColorAddress": "0xd021", + // "DefaultBgColor": "0x00", // 0x00 = Black (C64 scheme) + // "DefaultFgColor": "0x01", // 0x0f = Light grey, 0x0e = Light Blue, 0x01 = White (C64 scheme) + // "DefaultBorderColor": "0x0b" // 0x0b = Dark grey (C64 scheme) + // }, + // "Input": { + // "KeyPressedAddress": "0xd030", + // "KeyDownAddress": "0xd031", + // "KeyReleasedAddress": "0xd032" + // } + //} + + } +} \ No newline at end of file diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/appsettings_c64.json b/src/apps/Highbyte.DotNet6502.App.SadConsole/appsettings_c64.json deleted file mode 100644 index 566b26ee..00000000 --- a/src/apps/Highbyte.DotNet6502.App.SadConsole/appsettings_c64.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "Highbyte.DotNet6502.SadConsoleConfig": { - - "WindowTitle": "SadConsole displaying screen generated by machine code running in Highbyte.DotNet6502 emulator.", - "Font": "Fonts/C64.font", // Leave blank for default font. C64 font copied from https://github.com/Thraka/SadConsole - "FontScale": 2, - - "Emulator": "C64" - }, - - "Highbyte.DotNet6502.C64": { - "C64Model": "C64NTSC", - "Vic2Model": "NTSC", - "ROMDirectory": "%USERPROFILE%/Documents/C64/VICE/C64", - "ColorMapName": "Default" - } -} \ No newline at end of file diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/appsettings_hello.json b/src/apps/Highbyte.DotNet6502.App.SadConsole/appsettings_hello.json deleted file mode 100644 index d05f8328..00000000 --- a/src/apps/Highbyte.DotNet6502.App.SadConsole/appsettings_hello.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "Highbyte.DotNet6502.SadConsoleConfig": { - - "WindowTitle": "SadConsole with Highbyte.DotNet6502 emulator!", - "FontScale": 2, - - "Emulator": "GenericComputer" - }, - - "Highbyte.DotNet6502.GenericComputer": { - "ProgramBinaryFile": "../../../../../../samples/Assembler/Generic/Build/hello_world.prg", - - "Memory": { - "Screen": { - "Cols": 80, - "Rows": 25, - "BorderCols": 6, - "BorderRows": 3, - "ScreenStartAddress": "0x0400", // 80*25 = 2000 (0x07d0) -> range 0x0400 - 0x0bcf - "ScreenColorStartAddress": "0xd800", // 80*25 = 2000 (0x07d0) -> range 0xd800 - 0xdfcf - - "ScreenRefreshStatusAddress": "0xd000", // The 6502 code should set bit 1 here when it's done for current frame - "ScreenBorderColorAddress": "0xd020", - "ScreenBackgroundColorAddress": "0xd021", - "DefaultBgColor": "0x00", // 0x00 = Black (C64 scheme) - "DefaultFgColor": "0x01", // 0x0f = Light grey, 0x0e = Light Blue, 0x01 = White (C64 scheme) - "DefaultBorderColor": "0x0b" // 0x0b = Dark grey (C64 scheme) - }, - "Input": { - "KeyPressedAddress": "0xd030", - "KeyDownAddress": "0xd031", - "KeyReleasedAddress": "0xd032" - } - } - } -} \ No newline at end of file diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/appsettings_scroll.json b/src/apps/Highbyte.DotNet6502.App.SadConsole/appsettings_scroll.json deleted file mode 100644 index de8f986c..00000000 --- a/src/apps/Highbyte.DotNet6502.App.SadConsole/appsettings_scroll.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "Highbyte.DotNet6502.SadConsoleConfig": { - - "WindowTitle": "SadConsole displaying screen generated by machine code running in Highbyte.DotNet6502 emulator.", - "Font": "Fonts/C64.font", // Leave blank for default font. C64 font copied from https://github.com/Thraka/SadConsole - "FontScale": 2, - - "Emulator": "GenericComputer" - }, - - "Highbyte.DotNet6502.GenericComputer": { - "ProgramBinaryFile": "../../../../../../samples/Assembler/Generic/Build/hostinteraction_scroll_text_and_cycle_colors.prg", - - "Memory": { - "Screen": { - "Cols": 40, - "Rows": 25, - "BorderCols": 3, - "BorderRows": 3, - "ScreenStartAddress": "0x0400", // 40*25 = 1000 (0x03e8) -> range 0x0400 - 0x07e7 - "ScreenColorStartAddress": "0xd800", // 40*25 = 1000 (0x03e8) -> range 0xd800 - 0xdbe7 - - "ScreenRefreshStatusAddress": "0xd000", // To sync 6502 code with host frame: The 6502 code should wait for bit 0 to become set, and then wait for it to become cleared. - - "ScreenBorderColorAddress": "0xd020", - "ScreenBackgroundColorAddress": "0xd021", - "DefaultBgColor": "0x00", // 0x00 = Black (C64 scheme) - "DefaultFgColor": "0x01", // 0x0f = Light grey, 0x0e = Light Blue, 0x01 = White (C64 scheme) - "DefaultBorderColor": "0x0b", // 0x0b = Dark grey (C64 scheme) - - "UseAscIICharacters": true - - }, - "Input": { - "KeyPressedAddress": "0xd030", - "KeyDownAddress": "0xd031", - "KeyReleasedAddress": "0xd032" - } - } - } -} \ No newline at end of file diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/appsettings_snake.json b/src/apps/Highbyte.DotNet6502.App.SadConsole/appsettings_snake.json deleted file mode 100644 index 19769a2c..00000000 --- a/src/apps/Highbyte.DotNet6502.App.SadConsole/appsettings_snake.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "Highbyte.DotNet6502.SadConsoleConfig": { - - "WindowTitle": "DotNet6502 emualtor running Snake based on http://skilldrick.github.io/easy6502/#snake in SadConsole", - "Font": "Fonts/C64.font", // Leave blank for default font. C64 font copied from https://github.com/Thraka/SadConsole - "FontScale": 2, - - "Emulator": "GenericComputer" - }, - - "Highbyte.DotNet6502.GenericComputer": { - "ProgramBinaryFile": "../../../../../../samples/Assembler/Generic/Build/snake6502.prg", - - "StopAtBRK" : false, - - "Memory": { - "Screen": { - "Cols": 32, - "Rows": 32, - "BorderCols": 3, - "BorderRows": 3, - "ScreenStartAddress": "0x0200", - "ScreenColorStartAddress": "0xd800", // Not used with this program - - "ScreenRefreshStatusAddress": "0xd000", // The 6502 code should set bit 1 here when it's done for current frame - "ScreenBorderColorAddress": "0xd020", - "ScreenBackgroundColorAddress": "0xd021", - "DefaultBgColor": "0x00", // 0x00 = Black (C64 scheme) - "DefaultFgColor": "0x01", // 0x0f = Light grey, 0x0e = Light Blue, 0x01 = White (C64 scheme) - "DefaultBorderColor": "0x0b", // 0x0b = Dark grey (C64 scheme) - - "UseAscIICharacters": false, - "CharacterMap": { - "10": 32, - "13": 32, - "160": 219, - "224": 219 - } - }, - "Input": { - "KeyPressedAddress": "0xd030", - "KeyDownAddress": "0xd031", - "KeyReleasedAddress": "0xd032" - } - } - } -} \ No newline at end of file diff --git a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetHostApp.cs b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetHostApp.cs index 86113906..3b45a0c2 100644 --- a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetHostApp.cs +++ b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetHostApp.cs @@ -161,6 +161,10 @@ public override void OnAfterSelectSystem() public override bool OnBeforeStart(ISystem systemAboutToBeStarted) { + // Force a full GC to free up memory, so it won't risk accumulate memory usage if GC has not run for a while. + var m0 = GC.GetTotalMemory(forceFullCollection: true); + _logger.LogInformation("Allocated memory before starting emulator: " + m0); + // Make sure to adjust window size and render frequency to match the system that is about to be started if (EmulatorState == EmulatorState.Uninitialized) { diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/Skia/SkiaWASMHostApp.cs b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/Skia/SkiaWASMHostApp.cs index cd0a691c..3e38d493 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/Skia/SkiaWASMHostApp.cs +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/Skia/SkiaWASMHostApp.cs @@ -88,6 +88,10 @@ public override void OnAfterSelectSystem() public override bool OnBeforeStart(ISystem systemAboutToBeStarted) { + // Force a full GC to free up memory, so it won't risk accumulate memory usage if GC has not run for a while. + var m0 = GC.GetTotalMemory(forceFullCollection: true); + _logger.LogInformation("Allocated memory before starting emulator: " + m0); + return true; } diff --git a/src/libraries/Highbyte.DotNet6502.Impl.SadConsole/Commodore64/Video/C64SadConsoleRenderer.cs b/src/libraries/Highbyte.DotNet6502.Impl.SadConsole/Commodore64/Video/C64SadConsoleRenderer.cs index 2b6935de..51eb81a1 100644 --- a/src/libraries/Highbyte.DotNet6502.Impl.SadConsole/Commodore64/Video/C64SadConsoleRenderer.cs +++ b/src/libraries/Highbyte.DotNet6502.Impl.SadConsole/Commodore64/Video/C64SadConsoleRenderer.cs @@ -127,7 +127,7 @@ public void DrawEmulatorCharacterOnScreen(int x, int y, byte emulatorCharacter, // Default to C64 screen codes as source sadConsoleCharacter = TranslateC64ScreenCodeToSadConsoleC64Font(emulatorCharacter); - _sadConsoleRenderContext.Screen.DrawCharacter( + DrawCharacter( x, y, sadConsoleCharacter, @@ -163,4 +163,9 @@ private int GetBorderRows(C64 c64) { return c64.Vic2.Vic2Screen.VisibleTopBottomBorderHeight / c64.Vic2.Vic2Screen.CharacterHeight; } + + private void DrawCharacter(int x, int y, int sadConsoleCharCode, Color fgColor, Color bgColor) + { + _sadConsoleRenderContext.Console.SetGlyph(x, y, sadConsoleCharCode, fgColor, bgColor); + } } diff --git a/src/libraries/Highbyte.DotNet6502.Impl.SadConsole/EmulatorConsole.cs b/src/libraries/Highbyte.DotNet6502.Impl.SadConsole/EmulatorConsole.cs new file mode 100644 index 00000000..5551fcc7 --- /dev/null +++ b/src/libraries/Highbyte.DotNet6502.Impl.SadConsole/EmulatorConsole.cs @@ -0,0 +1,49 @@ +using Highbyte.DotNet6502.Systems; +using SadConsole.Host; +using static SadConsole.IFont; +using Console = SadConsole.Console; + +namespace Highbyte.DotNet6502.Impl.SadConsole; +public class EmulatorConsole : Console +{ + private const bool USE_CONSOLE_BORDER = true; + + private EmulatorConsole(int width, int height) : base(width, height) + { + } + + public static EmulatorConsole Create(ISystem system, IFont font, Sizes fontSize, ShapeParameters consoleDrawBoxBorderParameters) + { + var screen = system.Screen; + var textMode = (ITextMode)system.Screen; + + int totalCols = textMode.TextCols + (USE_CONSOLE_BORDER ? 2 : 0); + int totalRows = textMode.TextRows + (USE_CONSOLE_BORDER ? 2 : 0); + if (screen.HasBorder) + { + totalCols += (screen.VisibleLeftRightBorderWidth / textMode.CharacterWidth) * 2; + totalRows += (screen.VisibleTopBottomBorderHeight / textMode.CharacterHeight) * 2; + } + + var console = new EmulatorConsole(totalCols, totalRows); + + console.Font = font; + console.FontSize = console.Font.GetFontSize(fontSize); + + console.Surface.DefaultForeground = Color.White; + console.Surface.DefaultBackground = Color.Black; + console.Clear(); + + console.UseMouse = false; + console.UseKeyboard = true; + + if (USE_CONSOLE_BORDER) + console.Surface.DrawBox(new Rectangle(0, 0, console.Width, console.Height), consoleDrawBoxBorderParameters); + return console; + } + + public void SetGlyph(int x, int y, int sadConsoleCharCode, Color fgColor, Color bgColor) + { + CellSurfaceEditor.SetGlyph(this, x + (USE_CONSOLE_BORDER ? 1 : 0), y + (USE_CONSOLE_BORDER ? 1 : 0), sadConsoleCharCode, fgColor, bgColor); + } +} diff --git a/src/libraries/Highbyte.DotNet6502.Impl.SadConsole/EmulatorHost.cs b/src/libraries/Highbyte.DotNet6502.Impl.SadConsole/EmulatorHost.cs deleted file mode 100644 index 83cd6e75..00000000 --- a/src/libraries/Highbyte.DotNet6502.Impl.SadConsole/EmulatorHost.cs +++ /dev/null @@ -1,94 +0,0 @@ -using Highbyte.DotNet6502.Systems; -using Highbyte.DotNet6502.Systems.Generic; -using Highbyte.DotNet6502.Systems.Generic.Config; -using Highbyte.DotNet6502.Systems.Commodore64; -using Highbyte.DotNet6502.Systems.Commodore64.Config; -using Highbyte.DotNet6502.Impl.SadConsole.Generic.Video; -using Highbyte.DotNet6502.Impl.SadConsole.Generic.Input; -using Highbyte.DotNet6502.Impl.SadConsole.Commodore64.Video; -using Highbyte.DotNet6502.Impl.SadConsole.Commodore64.Input; -using Microsoft.Extensions.Logging; - -namespace Highbyte.DotNet6502.Impl.SadConsole; - -public class EmulatorHost -{ - private readonly SadConsoleConfig _sadConsoleConfig; - private readonly GenericComputerConfig _genericComputerConfig; - private readonly C64Config _c64Config; - private static SadConsoleMain s_sadConsoleMain = default!; - private readonly ILoggerFactory _loggerFactory; - - public EmulatorHost( - SadConsoleConfig sadConsoleConfig, - GenericComputerConfig genericComputerConfig, - C64Config c64Config, - ILoggerFactory loggerFactory - ) - { - _sadConsoleConfig = sadConsoleConfig; - _genericComputerConfig = genericComputerConfig; - _c64Config = c64Config; - _loggerFactory = loggerFactory; - } - - public void Start() - { - - SystemRunner systemRunner; - - var sadConsoleRenderContext = new SadConsoleRenderContext(GetSadConsoleScreen); - sadConsoleRenderContext.Init(); - - var sadConsoleInputHandlerContext = new SadConsoleInputHandlerContext(_loggerFactory); - sadConsoleInputHandlerContext.Init(); - - switch (_sadConsoleConfig.Emulator) - { - case "GenericComputer": - systemRunner = GetGenericSystemRunner(sadConsoleRenderContext, sadConsoleInputHandlerContext); - break; - - case "C64": - systemRunner = GetC64SystemRunner(sadConsoleRenderContext, sadConsoleInputHandlerContext); - break; - - default: - throw new DotNet6502Exception($"Unknown emulator name: {_sadConsoleConfig.Emulator}"); - } - - systemRunner.Init(); - - if (systemRunner.System.Screen is not ITextMode) - throw new DotNet6502Exception("SadConsole host only supports running emulator systems that supports text mode."); - - // Create the main SadConsole class that is responsible for configuring and starting up SadConsole and running the emulator code every frame with our preferred configuration. - s_sadConsoleMain = new SadConsoleMain( - _sadConsoleConfig, - systemRunner); - - // Start SadConsole. Will exit from this method after SadConsole window is closed. - s_sadConsoleMain.Run(); - } - - private SadConsoleScreenObject GetSadConsoleScreen() - { - return s_sadConsoleMain.SadConsoleScreen; - } - - private SystemRunner GetC64SystemRunner(SadConsoleRenderContext sadConsoleRenderContext, SadConsoleInputHandlerContext sadConsoleInputHandlerContext) - { - var c64 = C64.BuildC64(_c64Config, _loggerFactory); - var renderer = new C64SadConsoleRenderer(c64, sadConsoleRenderContext); - var inputHandler = new C64SadConsoleInputHandler(c64, sadConsoleInputHandlerContext, _loggerFactory); - return new SystemRunner(c64, renderer, inputHandler); - } - - private SystemRunner GetGenericSystemRunner(SadConsoleRenderContext renderContext, SadConsoleInputHandlerContext inputHandlerContext) - { - var genericComputer = GenericComputerBuilder.SetupGenericComputerFromConfig(_genericComputerConfig, _loggerFactory); - var renderer = new GenericSadConsoleRenderer(genericComputer, renderContext, _genericComputerConfig.Memory.Screen); - var inputHandler = new GenericSadConsoleInputHandler(genericComputer, inputHandlerContext, _genericComputerConfig.Memory.Input, _loggerFactory); - return new SystemRunner(genericComputer, renderer, inputHandler); - } -} diff --git a/src/libraries/Highbyte.DotNet6502.Impl.SadConsole/Generic/Video/GenericSadConsoleRenderer.cs b/src/libraries/Highbyte.DotNet6502.Impl.SadConsole/Generic/Video/GenericSadConsoleRenderer.cs index b06e1a99..b374ad6a 100644 --- a/src/libraries/Highbyte.DotNet6502.Impl.SadConsole/Generic/Video/GenericSadConsoleRenderer.cs +++ b/src/libraries/Highbyte.DotNet6502.Impl.SadConsole/Generic/Video/GenericSadConsoleRenderer.cs @@ -157,7 +157,7 @@ public void DrawEmulatorCharacterOnScreen(int x, int y, byte emulatorCharacter, sadConsoleCharacter = emulatorCharacter; } - _sadConsoleRenderContext.Screen.DrawCharacter( + DrawCharacter( x, y, sadConsoleCharacter, @@ -165,4 +165,9 @@ public void DrawEmulatorCharacterOnScreen(int x, int y, byte emulatorCharacter, GenericSadConsoleColors.SystemToSadConsoleColorMap[_emulatorScreenConfig.ColorMap[emulatorBgColor]] ); } + + private void DrawCharacter(int x, int y, int sadConsoleCharCode, Color fgColor, Color bgColor) + { + _sadConsoleRenderContext.Console.SetGlyph(x, y, sadConsoleCharCode, fgColor, bgColor); + } } diff --git a/src/libraries/Highbyte.DotNet6502.Impl.SadConsole/SadConsoleConfig.cs b/src/libraries/Highbyte.DotNet6502.Impl.SadConsole/SadConsoleConfig.cs deleted file mode 100644 index 0909830d..00000000 --- a/src/libraries/Highbyte.DotNet6502.Impl.SadConsole/SadConsoleConfig.cs +++ /dev/null @@ -1,36 +0,0 @@ -namespace Highbyte.DotNet6502.Impl.SadConsole; - -public class SadConsoleConfig -{ - public const string ConfigSectionName = "Highbyte.DotNet6502.SadConsoleConfig"; - - public string WindowTitle { get; set; } - - /// - /// Optional. If not specified, default SadConsole font is used. - /// To use a specific SadConsole Font, include it in your program output directory. - /// Example: Fonts/C64.font - /// - /// - public string? Font { get; set; } - /// - /// Font scale. 1 is default. - /// - /// - public int FontScale { get; set; } - - /// - /// The name of the emulator to start. - /// Ex: GenericComputer, C64 - /// - /// - public string Emulator { get; set; } - - public SadConsoleConfig() - { - WindowTitle = "SadConsole displaying screen generated by machine code running in Highbyte.DotNet6502 emulator."; - Font = null; - FontScale = 1; - Emulator = "GenericComputer"; - } -} diff --git a/src/libraries/Highbyte.DotNet6502.Impl.SadConsole/SadConsoleMain.cs b/src/libraries/Highbyte.DotNet6502.Impl.SadConsole/SadConsoleMain.cs deleted file mode 100644 index 27c2863e..00000000 --- a/src/libraries/Highbyte.DotNet6502.Impl.SadConsole/SadConsoleMain.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System.Diagnostics; -using Highbyte.DotNet6502.Systems; -using SadConsole.Configuration; -namespace Highbyte.DotNet6502.Impl.SadConsole; - -public class SadConsoleMain -{ - private readonly SadConsoleConfig _sadConsoleConfig; - private SadConsoleScreenObject _sadConsoleScreen = default!; - public SadConsoleScreenObject SadConsoleScreen => _sadConsoleScreen; - - private readonly SystemRunner _systemRunner; - - public SadConsoleMain( - SadConsoleConfig sadConsoleConfig, - SystemRunner systemRunner) - { - _sadConsoleConfig = sadConsoleConfig; - _systemRunner = systemRunner; - } - - public void Run() - { - Settings.WindowTitle = _sadConsoleConfig.WindowTitle; - - // Setup the SadConsole engine and create the main window. - // If font is null or empty, the default SadConsole font will be used. - var screen = _systemRunner.System.Screen; - if (screen is not ITextMode textMode) - throw new DotNet6502Exception("SadConsoleMain only supports system that implements ITextMode of Screen."); - - // int totalCols = (textMode.TextCols + (textMode.BorderCols * 2)); - // int totalRows = (textMode.TextRows + (textMode.BorderRows * 2)); - int totalCols = textMode.TextCols; - int totalRows = textMode.TextRows; - if (screen.HasBorder) - { - totalCols += (screen.VisibleLeftRightBorderWidth / textMode.CharacterWidth) * 2; - totalRows += (screen.VisibleTopBottomBorderHeight / textMode.CharacterHeight) * 2; - } - - global::SadConsole.Configuration.Builder builder - = new global::SadConsole.Configuration.Builder() - .SetScreenSize(totalCols * _sadConsoleConfig.FontScale, totalRows * _sadConsoleConfig.FontScale) - .ConfigureFonts(_sadConsoleConfig.Font ?? string.Empty) - .SetStartingScreen(CreateSadConsoleScreen) - .IsStartingScreenFocused(false) // Let the object focused in the create method remain. - .AddFrameUpdateEvent(UpdateSadConsole) - ; - - // Start the game. - global::SadConsole.Game.Create(builder); - global::SadConsole.Game.Instance.Run(); - global::SadConsole.Game.Instance.Dispose(); - } - - /// - /// Runs when SadConsole engine starts up - /// - private IScreenObject CreateSadConsoleScreen(Game game) - { - _sadConsoleScreen = new SadConsoleScreenObject((ITextMode)_systemRunner.System.Screen, _systemRunner.System.Screen, _sadConsoleConfig); - _sadConsoleScreen.ScreenConsole.IsFocused = true; - - return _sadConsoleScreen; - } - - /// - /// Runs every frame. - /// Responsible for letting the SadConsole engine interact with the emulator - /// - /// - private void UpdateSadConsole(object? sender, GameHost e) - { - // Capture SadConsole input - _systemRunner.ProcessInputBeforeFrame(); - - // Run CPU for one frame - var execEvaluatorTriggerResult = _systemRunner.RunEmulatorOneFrame(); - - // Update SadConsole screen - _systemRunner.Draw(); - - if (execEvaluatorTriggerResult.Triggered) - { - // TODO: Show monitor? - Debugger.Break(); - //Environment.Exit(0); - } - } -} diff --git a/src/libraries/Highbyte.DotNet6502.Impl.SadConsole/SadConsoleRenderContext.cs b/src/libraries/Highbyte.DotNet6502.Impl.SadConsole/SadConsoleRenderContext.cs index c28cff8c..b11e540f 100644 --- a/src/libraries/Highbyte.DotNet6502.Impl.SadConsole/SadConsoleRenderContext.cs +++ b/src/libraries/Highbyte.DotNet6502.Impl.SadConsole/SadConsoleRenderContext.cs @@ -3,14 +3,14 @@ namespace Highbyte.DotNet6502.Impl.SadConsole; public class SadConsoleRenderContext : IRenderContext { - private readonly Func _getSadConsoleScreen; - public SadConsoleScreenObject Screen => _getSadConsoleScreen(); + private readonly Func _getSadConsoleEmulatorConsole; + public EmulatorConsole Console => _getSadConsoleEmulatorConsole(); public bool IsInitialized { get; private set; } = false; - public SadConsoleRenderContext(Func getSadConsoleScreen) + public SadConsoleRenderContext(Func getSadConsoleEmulatorConsole) { - _getSadConsoleScreen = getSadConsoleScreen; + _getSadConsoleEmulatorConsole = getSadConsoleEmulatorConsole; } public void Init() diff --git a/src/libraries/Highbyte.DotNet6502.Impl.SadConsole/SadConsoleScreenObject.cs b/src/libraries/Highbyte.DotNet6502.Impl.SadConsole/SadConsoleScreenObject.cs deleted file mode 100644 index 6346a1d1..00000000 --- a/src/libraries/Highbyte.DotNet6502.Impl.SadConsole/SadConsoleScreenObject.cs +++ /dev/null @@ -1,55 +0,0 @@ -using Highbyte.DotNet6502.Systems; -using Console = SadConsole.Console; - -namespace Highbyte.DotNet6502.Impl.SadConsole; - -public class SadConsoleScreenObject : ScreenObject -{ - public Console ScreenConsole { get; } - - public SadConsoleScreenObject(ITextMode textMode, IScreen screen, SadConsoleConfig sadConsoleConfig) - { - ScreenConsole = CreateScreenConsole(textMode, screen, sadConsoleConfig); - } - - private Console CreateScreenConsole(ITextMode textMode, IScreen screen, SadConsoleConfig sadConsoleConfig) - { - // Setup console screen - // int totalCols = (textMode.TextCols + (textMode.BorderCols * 2)); - // int totalRows = (textMode.TextRows + (textMode.BorderRows * 2)); - int totalCols = textMode.TextCols; - int totalRows = textMode.TextRows; - if(screen.HasBorder) - { - totalCols += (screen.VisibleLeftRightBorderWidth / textMode.CharacterWidth) * 2; - totalRows += (screen.VisibleTopBottomBorderHeight / textMode.CharacterHeight) * 2; - } - - var console = new Console(totalCols, totalRows); - console.Surface.DefaultForeground = Color.White; - console.Surface.DefaultBackground = Color.Black; - - //screen.Position = new Point(VisibleLeftRightBorderWidth, VisibleTopBottomBorderHeight); - - // TODO: Better way to map numeric scale value to SadConsole.Font.FontSizes enum? - var fontSize = sadConsoleConfig.FontScale switch - { - 1 => IFont.Sizes.One, - 2 => IFont.Sizes.Two, - 3 => IFont.Sizes.Three, - _ => IFont.Sizes.One, - }; - console.FontSize = console.Font.GetFontSize(fontSize); - - console.Clear(); - console.Cursor.IsEnabled = false; - console.Cursor.IsVisible = false; - - console.Parent = this; - return console; - } - public void DrawCharacter(int x, int y, int sadConsoleCharCode, Color fgColor, Color bgColor) - { - ScreenConsole.SetGlyph(x, y, sadConsoleCharCode, fgColor, bgColor); - } -} diff --git a/src/libraries/Highbyte.DotNet6502/Systems/HostApp.cs b/src/libraries/Highbyte.DotNet6502/Systems/HostApp.cs index cea5cf5e..d013453c 100644 --- a/src/libraries/Highbyte.DotNet6502/Systems/HostApp.cs +++ b/src/libraries/Highbyte.DotNet6502/Systems/HostApp.cs @@ -112,10 +112,6 @@ public async Task Start() if (!_systemList.IsValidConfig(_selectedSystemName).Result) throw new DotNet6502Exception("Internal error. Cannot start emulator if current system config is invalid."); - // Force a full GC to free up memory, so it won't risk accumulate memory usage if GC has not run for a while. - var m0 = GC.GetTotalMemory(forceFullCollection: true); - _logger.LogInformation("Allocated memory before starting emulator: " + m0); - var systemAboutToBeStarted = await _systemList.GetSystem(_selectedSystemName); bool shouldStart = OnBeforeStart(systemAboutToBeStarted); if (!shouldStart) From a6851960994745ab5b98e4280af1912e4cb9fb0a Mon Sep 17 00:00:00 2001 From: Highbyte Date: Wed, 24 Jul 2024 15:01:25 +0200 Subject: [PATCH 11/40] Add SadConsole font size UI config --- .../EmulatorConfig.cs | 22 +++++++++++++++++++ .../MenuConsole.cs | 18 ++++++++++++++- .../Program.cs | 2 +- .../SadConsoleHostApp.cs | 21 ++++++++++-------- .../SystemSetup/C64HostConfig.cs | 1 - .../SystemSetup/GenericComputerHostConfig.cs | 1 - .../SadConsoleHostSystemConfigBase.cs | 6 ----- .../appsettings.json | 10 ++++----- 8 files changed, 57 insertions(+), 24 deletions(-) diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/EmulatorConfig.cs b/src/apps/Highbyte.DotNet6502.App.SadConsole/EmulatorConfig.cs index 201b625d..56bf65f0 100644 --- a/src/apps/Highbyte.DotNet6502.App.SadConsole/EmulatorConfig.cs +++ b/src/apps/Highbyte.DotNet6502.App.SadConsole/EmulatorConfig.cs @@ -1,6 +1,7 @@ using Highbyte.DotNet6502.App.SadConsole.SystemSetup; using Highbyte.DotNet6502.Impl.SadConsole; using Highbyte.DotNet6502.Systems; +using static SadConsole.IFont; namespace Highbyte.DotNet6502.App.SadConsole; @@ -16,6 +17,14 @@ public class EmulatorConfig ///
public string? UIFont { get; set; } + /// + /// Font size for emulator console only. UI is not affected. + /// Sizes.One is default. + /// + /// + public Sizes FontSize { get; set; } + + /// /// The name of the emulator to start. /// Ex: GenericComputer, C64 @@ -37,6 +46,7 @@ public EmulatorConfig() { WindowTitle = "SadConsole + Highbyte.DotNet6502 emulator."; UIFont = null; + FontSize = Sizes.One; DefaultEmulator = "C64"; C64HostConfig = new(); @@ -49,4 +59,16 @@ public void Validate(SystemList + FontSize switch + { + Sizes.Quarter => 0.25f, + Sizes.Half => 0.5f, + Sizes.One => 1, + Sizes.Two => 2, + Sizes.Three => 3, + Sizes.Four => 4, + _ => 1 + }; } diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/MenuConsole.cs b/src/apps/Highbyte.DotNet6502.App.SadConsole/MenuConsole.cs index af7b9654..b4e573ff 100644 --- a/src/apps/Highbyte.DotNet6502.App.SadConsole/MenuConsole.cs +++ b/src/apps/Highbyte.DotNet6502.App.SadConsole/MenuConsole.cs @@ -1,3 +1,4 @@ +using System; using SadConsole.UI; using SadConsole.UI.Controls; using SadRogue.Primitives; @@ -8,7 +9,7 @@ public class MenuConsole : ControlsConsole public const int CONSOLE_WIDTH = USABLE_WIDTH + (SadConsoleUISettings.UI_USE_CONSOLE_BORDER ? 2 : 0); public const int CONSOLE_HEIGHT = USABLE_HEIGHT + (SadConsoleUISettings.UI_USE_CONSOLE_BORDER ? 2 : 0); private const int USABLE_WIDTH = 21; - private const int USABLE_HEIGHT = 15; + private const int USABLE_HEIGHT = 12; private readonly SadConsoleHostApp _sadConsoleHostApp; @@ -89,6 +90,17 @@ private void DrawUIItems() resetButton.Click += async (s, e) => { await _sadConsoleHostApp.Reset(); IsDirty = true; }; Controls.Add(resetButton); + var fontSizeLabel = CreateLabel("Font size:", 1, stopButton.Bounds.MaxExtentY + 2); + ComboBox selectFontSizeBox = new ComboBox(9, 9, 5, Enum.GetValues().Select(x => (object)x).ToArray()) + { + Position = (fontSizeLabel.Bounds.MaxExtentX + 2, fontSizeLabel.Position.Y), + Name = "selectFontSizeComboBox", + SelectedItem = _sadConsoleHostApp.EmulatorConfig.FontSize, + }; + selectFontSizeBox.SelectedItemChanged += (s, e) => { _sadConsoleHostApp.EmulatorConfig.FontSize = (IFont.Sizes)e.Item; IsDirty = true; }; + Controls.Add(selectFontSizeBox); + + // Helper function to create a label and add it to the console Label CreateLabel(string text, int col, int row, string? name = null) { @@ -135,6 +147,10 @@ private void SetControlStates() var resetButton = Controls["resetButton"]; resetButton.IsEnabled = _sadConsoleHostApp.EmulatorState != Systems.EmulatorState.Uninitialized; + + var selectFontSizeComboBox = Controls["selectFontSizeComboBox"]; + selectFontSizeComboBox.IsEnabled = _sadConsoleHostApp.EmulatorState == Systems.EmulatorState.Uninitialized; + } } diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/Program.cs b/src/apps/Highbyte.DotNet6502.App.SadConsole/Program.cs index 54e3b374..2dbea314 100644 --- a/src/apps/Highbyte.DotNet6502.App.SadConsole/Program.cs +++ b/src/apps/Highbyte.DotNet6502.App.SadConsole/Program.cs @@ -35,7 +35,7 @@ //{ // DefaultEmulator = c64Setup.SystemName, // UIFont = null, -// FontScale = 1, +// FontSize = Sizes.One, // Font = "Fonts/C64.font", // WindowTitle = "SadConsole with Highbyte.DotNet6502 emulator!", // //Monitor = new MonitorConfig diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/SadConsoleHostApp.cs b/src/apps/Highbyte.DotNet6502.App.SadConsole/SadConsoleHostApp.cs index 3dbeb9e1..ad8128fa 100644 --- a/src/apps/Highbyte.DotNet6502.App.SadConsole/SadConsoleHostApp.cs +++ b/src/apps/Highbyte.DotNet6502.App.SadConsole/SadConsoleHostApp.cs @@ -159,14 +159,7 @@ public override bool OnBeforeStart(ISystem systemAboutToBeStarted) { font = Game.Instance.DefaultFont; } - var fontSize = CommonHostSystemConfig.FontScale switch - { - 1 => IFont.Sizes.One, - 2 => IFont.Sizes.Two, - 3 => IFont.Sizes.Three, - _ => IFont.Sizes.One, - }; - _sadConsoleEmulatorConsole = EmulatorConsole.Create(systemAboutToBeStarted, font, fontSize, SadConsoleUISettings.ConsoleDrawBoxBorderParameters); + _sadConsoleEmulatorConsole = EmulatorConsole.Create(systemAboutToBeStarted, font, _emulatorConfig.FontSize, SadConsoleUISettings.ConsoleDrawBoxBorderParameters); _sadConsoleEmulatorConsole.UsePixelPositioning = true; _sadConsoleEmulatorConsole.Position = new Point((_menuConsole.Position.X * _menuConsole.Font.GlyphWidth) + (_menuConsole.Width * _menuConsole.Font.GlyphWidth), 0); @@ -275,7 +268,17 @@ private NullAudioHandlerContext CreateAudioHandlerContext() private int CalculateWindowWidthPixels() { - var width = _menuConsole.WidthPixels + (_sadConsoleEmulatorConsole != null ? _sadConsoleEmulatorConsole.WidthPixels + ((CommonHostSystemConfig.FontScale - 1) * 16) : 0); + int fontSizeAdjustment; + // TODO: This is a bit of a hack for handling consoles with different font sizes, and positioning on main screen. Better way? + if (_emulatorConfig.FontSizeScaleFactor > 1) + { + fontSizeAdjustment = (((int)_emulatorConfig.FontSizeScaleFactor - 1) * 16); + } + else + { + fontSizeAdjustment = 0; + } + var width = _menuConsole.WidthPixels + (_sadConsoleEmulatorConsole != null ? _sadConsoleEmulatorConsole.WidthPixels + fontSizeAdjustment : 0); return width; } diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/SystemSetup/C64HostConfig.cs b/src/apps/Highbyte.DotNet6502.App.SadConsole/SystemSetup/C64HostConfig.cs index 92cd55f3..bce2c53c 100644 --- a/src/apps/Highbyte.DotNet6502.App.SadConsole/SystemSetup/C64HostConfig.cs +++ b/src/apps/Highbyte.DotNet6502.App.SadConsole/SystemSetup/C64HostConfig.cs @@ -7,7 +7,6 @@ public class C64HostConfig : SadConsoleHostSystemConfigBase public C64HostConfig() { Font = "Fonts/C64.font"; - FontScale = 1; } public new object Clone() diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/SystemSetup/GenericComputerHostConfig.cs b/src/apps/Highbyte.DotNet6502.App.SadConsole/SystemSetup/GenericComputerHostConfig.cs index c37723d5..32343339 100644 --- a/src/apps/Highbyte.DotNet6502.App.SadConsole/SystemSetup/GenericComputerHostConfig.cs +++ b/src/apps/Highbyte.DotNet6502.App.SadConsole/SystemSetup/GenericComputerHostConfig.cs @@ -5,7 +5,6 @@ public class GenericComputerHostConfig : SadConsoleHostSystemConfigBase public GenericComputerHostConfig() { Font = null; - FontScale = 1; } public new object Clone() diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/SystemSetup/SadConsoleHostSystemConfigBase.cs b/src/apps/Highbyte.DotNet6502.App.SadConsole/SystemSetup/SadConsoleHostSystemConfigBase.cs index 51110a90..f58f47d0 100644 --- a/src/apps/Highbyte.DotNet6502.App.SadConsole/SystemSetup/SadConsoleHostSystemConfigBase.cs +++ b/src/apps/Highbyte.DotNet6502.App.SadConsole/SystemSetup/SadConsoleHostSystemConfigBase.cs @@ -10,16 +10,10 @@ public abstract class SadConsoleHostSystemConfigBase : IHostSystemConfig, IClone /// /// public string? Font { get; set; } - /// - /// Font scale. 1 is default. - /// - /// - public int FontScale { get; set; } public SadConsoleHostSystemConfigBase() { Font = null; - FontScale = 1; } public object Clone() diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/appsettings.json b/src/apps/Highbyte.DotNet6502.App.SadConsole/appsettings.json index e19836e1..4a7c0d26 100644 --- a/src/apps/Highbyte.DotNet6502.App.SadConsole/appsettings.json +++ b/src/apps/Highbyte.DotNet6502.App.SadConsole/appsettings.json @@ -2,17 +2,17 @@ "Highbyte.DotNet6502.SadConsoleConfig": { "WindowTitle": "SadConsole displaying screen generated by machine code running in Highbyte.DotNet6502 emulator.", - "DefaultEmulator": "C64", + "UIFont": "", // Leave blank for default SadConsole font. + + "FontSize": "One", // FontSize affects emulator console only, not UI console. Possible values: "Quarter", "Half, "One", "Two", "Three", "Four", "Five" "C64HostConfig": { - "Font": "Fonts/C64.font", // C64 font copied from https://github.com/Thraka/SadConsole. Leave blank for default SadConsole font. - "FontScale": 1 + "Font": "Fonts/C64.font" // C64 font copied from https://github.com/Thraka/SadConsole. Leave blank for default SadConsole font. }, "GenericComputerHostConfig": { - "Font": "", // Leave blank for default SadConsole font. - "FontScale": 1 + "Font": "" // Leave blank for default SadConsole font. } }, From 8913fde3b179b99d7f9513fc811a29f124ad056d Mon Sep 17 00:00:00 2001 From: Highbyte Date: Wed, 24 Jul 2024 20:58:25 +0200 Subject: [PATCH 12/40] Add minimal SadConsole C64 config UI. Bump SadConsole to 10.4.1 --- .../C64ConfigUIConsole.cs | 273 ++++++++++++++++++ .../C64MenuConsole.cs | 113 ++++++++ .../FilePickerConsole.cs | 141 +++++++++ .../Highbyte.DotNet6502.App.SadConsole.csproj | 1 + .../MenuConsole.cs | 11 +- .../Program.cs | 4 +- .../SadConsoleHostApp.cs | 35 ++- .../appsettings.json | 2 +- ...Highbyte.DotNet6502.Impl.SadConsole.csproj | 2 +- .../Highbyte.DotNet6502/Systems/HostApp.cs | 2 + 10 files changed, 567 insertions(+), 17 deletions(-) create mode 100644 src/apps/Highbyte.DotNet6502.App.SadConsole/C64ConfigUIConsole.cs create mode 100644 src/apps/Highbyte.DotNet6502.App.SadConsole/C64MenuConsole.cs create mode 100644 src/apps/Highbyte.DotNet6502.App.SadConsole/FilePickerConsole.cs diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/C64ConfigUIConsole.cs b/src/apps/Highbyte.DotNet6502.App.SadConsole/C64ConfigUIConsole.cs new file mode 100644 index 00000000..323f2c86 --- /dev/null +++ b/src/apps/Highbyte.DotNet6502.App.SadConsole/C64ConfigUIConsole.cs @@ -0,0 +1,273 @@ +using Highbyte.DotNet6502.Systems; +using Highbyte.DotNet6502.Systems.Commodore64.Config; +using SadConsole.UI; +using SadConsole.UI.Controls; +using SadRogue.Primitives; + +namespace Highbyte.DotNet6502.App.SadConsole; +public class C64ConfigUIConsole : Window +{ + public const int CONSOLE_WIDTH = USABLE_WIDTH; + public const int CONSOLE_HEIGHT = USABLE_HEIGHT; + private const int USABLE_WIDTH = 60; + private const int USABLE_HEIGHT = 25; + + private readonly SadConsoleHostApp _sadConsoleHostApp; + + private C64Config _c64Config => (C64Config)_sadConsoleHostApp.GetSystemConfig().Result; + + private C64ConfigUIConsole(SadConsoleHostApp sadConsoleHostApp) : base(CONSOLE_WIDTH, CONSOLE_HEIGHT) + { + _sadConsoleHostApp = sadConsoleHostApp; + } + + public static C64ConfigUIConsole Create(SadConsoleHostApp sadConsoleHostApp) + { + var console = new C64ConfigUIConsole(sadConsoleHostApp); + + //console.Surface.DefaultForeground = SadConsoleUISettings.UIConsoleForegroundColor; + //console.Surface.DefaultBackground = SadConsoleUISettings.UIConsoleBackgroundColor; + //console.Clear(); + + console.Title = "C64 Config"; + Colors colors = console.Controls.GetThemeColors(); + + console.Cursor.PrintAppearanceMatchesHost = false; + console.Cursor.DisableWordBreak = true; + console.Cursor.SetPrintAppearance(colors.Title, console.Surface.DefaultBackground); + + + console.UseMouse = true; + console.MouseMove += (s, e) => + { + }; + console.UseKeyboard = true; + + console.DrawUIItems(); + + return console; + } + + private void DrawUIItems() + { + // ROM directory + var romDirectoryLabel = CreateLabel("ROM directory", 1, 1); + var romDirectoryTextBox = new TextBox(Width - 10) + { + Name = "romDirectoryTextBox", + Position = (1, romDirectoryLabel.Position.Y + 1), + }; + romDirectoryTextBox.TextChanged += (s, e) => { _c64Config.ROMDirectory = romDirectoryTextBox.Text; IsDirty = true; }; + Controls.Add(romDirectoryTextBox); + + var selectROMDirectoryButton = new Button("...") + { + Name = "selectROMDirectoryButton", + Position = (romDirectoryTextBox.Bounds.MaxExtentX + 2, romDirectoryTextBox.Position.Y), + }; + selectROMDirectoryButton.Click += (s, e) => ShowROMFolderPickerDialog(); + Controls.Add(selectROMDirectoryButton); + + // Kernal ROM file + var kernalROMLabel = CreateLabel("Kernal ROM", 1, romDirectoryTextBox.Position.Y + 2); + var kernalROMTextBox = new TextBox(Width - 10) + { + Name = "kernalROMTextBox", + Position = (1, kernalROMLabel.Position.Y + 1) + }; + kernalROMTextBox.TextChanged += (s, e) => { _c64Config.SetROM(C64Config.KERNAL_ROM_NAME, kernalROMTextBox!.Text); IsDirty = true; }; + Controls.Add(kernalROMTextBox); + + var selectKernalROMButton = new Button("...") + { + Name = "selectKernalROMButton", + Position = (kernalROMTextBox.Bounds.MaxExtentX + 2, kernalROMTextBox.Position.Y), + }; + selectKernalROMButton.Click += (s, e) => ShowROMFilePickerDialog(C64Config.KERNAL_ROM_NAME); + Controls.Add(selectKernalROMButton); + + // Basic ROM file + var basicROMLabel = CreateLabel("Basic ROM", 1, kernalROMTextBox.Bounds.MaxExtentY + 2); + var basicROMTextBox = new TextBox(Width - 10) + { + Name = "basicROMTextBox", + Position = (1, basicROMLabel.Position.Y + 1) + }; + basicROMTextBox.TextChanged += (s, e) => { _c64Config.SetROM(C64Config.BASIC_ROM_NAME, basicROMTextBox!.Text); IsDirty = true; }; + Controls.Add(basicROMTextBox); + + var selectBasicROMButton = new Button("...") + { + Name = "selectBasicROMButton", + Position = (basicROMTextBox.Bounds.MaxExtentX + 2, basicROMTextBox.Position.Y), + }; + selectBasicROMButton.Click += (s, e) => ShowROMFilePickerDialog(C64Config.BASIC_ROM_NAME); + Controls.Add(selectBasicROMButton); + + // Chargen ROM file + var chargenROMLabel = CreateLabel("Chargen ROM", 1, basicROMTextBox.Bounds.MaxExtentY + 2); + var chargenROMTextBox = new TextBox(Width - 10) + { + Name = "chargenROMTextBox", + Position = (1, chargenROMLabel.Position.Y + 1), + }; + chargenROMTextBox.TextChanged += (s, e) => { _c64Config.SetROM(C64Config.CHARGEN_ROM_NAME, chargenROMTextBox!.Text); IsDirty = true; }; + Controls.Add(chargenROMTextBox); + + var selectChargenROMButton = new Button("...") + { + Name = "selectChargenROMButton", + Position = (chargenROMTextBox.Bounds.MaxExtentX + 2, chargenROMTextBox.Position.Y), + }; + selectChargenROMButton.Click += (s, e) => ShowROMFilePickerDialog(C64Config.CHARGEN_ROM_NAME); + Controls.Add(selectChargenROMButton); + + + // URL for downloading C64 ROMs + var romDownloadsLabel = CreateLabel("ROM download link", 1, chargenROMTextBox.Bounds.MaxExtentY + 2); + var romDownloadLinkTextBox = new TextBox(Width - 10) + { + Name = "romDownloadLinkTextBox", + Position = (1, romDownloadsLabel.Bounds.MaxExtentY + 1), + IsEnabled = false, + Text = "https://www.commodore.ca/manuals/funet/cbm/firmware/computers/c64/index-t.html", + }; + Controls.Add(romDownloadLinkTextBox); + + var openROMDownloadURLButton = new Button("...") + { + Name = "openROMDownloadURLButton", + Position = (romDownloadLinkTextBox.Bounds.MaxExtentX + 2, romDownloadLinkTextBox.Position.Y), + }; + openROMDownloadURLButton.Click += (s, e) => OpenURL(romDownloadLinkTextBox.Text); + Controls.Add(openROMDownloadURLButton); + + + // Validaton errors + var validationErrorsLabel = CreateLabel("Validation errors", 1, romDownloadLinkTextBox.Bounds.MaxExtentY + 2, "validationErrorsLabel"); + var validationErrorsListBox = new ListBox(Width - 3, 3) + { + Name = "validationErrorsListBox", + Position = (1, validationErrorsLabel.Bounds.MaxExtentY + 1), + IsScrollBarVisible = true, + IsEnabled = true, + }; + Controls.Add(validationErrorsListBox); + + // Note: Currently Cancel button doesn't do anything, because the changes in this window are saved directly to the config object used by the emulator. + //Button cancelButton = new Button(10, 1) + //{ + // Text = "Cancel", + // Position = (1, Height - 2) + //}; + //cancelButton.Click += (s, e) => { DialogResult = false; Hide(); }; + //Controls.Add(cancelButton); + + Button okButton = new Button(6, 1) + { + Text = "OK", + Position = (Width - 1 - 7, Height - 2) + }; + okButton.Click += (s, e) => { DialogResult = true; Hide(); }; + Controls.Add(okButton); + + + // Helper function to create a label and add it to the console + Label CreateLabel(string text, int col, int row, string? name = null) + { + var labelTemp = new Label(text) { Position = new Point(col, row), Name = name }; + Controls.Add(labelTemp); + return labelTemp; + } + Label CreateLabelValue(string text, int col, int row, string? name = null) + { + var labelTemp = new Label(text) { Position = new Point(col, row), TextColor = Controls.GetThemeColors().Title, Name = name }; + Controls.Add(labelTemp); + return labelTemp; + } + + // Force OnIsDirtyChanged event which will set control states (see SetControlStates) + OnIsDirtyChanged(); + } + + private void OpenURL(string url) + { + if (!Uri.TryCreate(url, UriKind.Absolute, out Uri? uri)) + { + throw new Exception($"Invalid URL: {url}"); + } + // Launch the URL in the default browser + System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(uri.AbsoluteUri) { UseShellExecute = true }); + } + + private void ShowROMFilePickerDialog(string romName) + { + var currentFolder = PathHelper.ExpandOSEnvironmentVariables(_c64Config.ROMDirectory); + FilePickerConsole window = FilePickerConsole.Create(currentFolder, _c64Config.GetROM(romName).GetROMFilePath(currentFolder)); + window.Center(); + window.Closed += (s2, e2) => + { + if (window.DialogResult) + { + _c64Config.SetROM(romName, Path.GetFileName(window.SelectedFile!)); + IsDirty = true; + } + }; + window.Show(true); + } + + private void ShowROMFolderPickerDialog() + { + var currentFolder = PathHelper.ExpandOSEnvironmentVariables(_c64Config.ROMDirectory); + FilePickerConsole window = FilePickerConsole.Create(currentFolder, selectFolder: true); + window.Center(); + window.Closed += (s2, e2) => + { + if (window.DialogResult) + { + _c64Config.ROMDirectory = window.SelectedFile; + IsDirty = true; + } + }; + window.Show(true); + } + + protected override void OnIsDirtyChanged() + { + if (IsDirty) + { + SetControlStates(); + } + } + + private void SetControlStates() + { + var romDirectoryTextBox = Controls["romDirectoryTextBox"] as TextBox; + romDirectoryTextBox!.Text = _c64Config.ROMDirectory; + romDirectoryTextBox!.IsDirty = true; + + var kernalROMTextBox = Controls["kernalROMTextBox"] as TextBox; + kernalROMTextBox!.Text = _c64Config.ROMs.SingleOrDefault(x => x.Name == C64Config.KERNAL_ROM_NAME).File; + kernalROMTextBox!.IsDirty = true; + + var basicROMTextBox = Controls["basicROMTextBox"] as TextBox; + basicROMTextBox!.Text = _c64Config.ROMs.SingleOrDefault(x => x.Name == C64Config.BASIC_ROM_NAME).File; + basicROMTextBox!.IsDirty = true; + + var chargenROMTextBox = Controls["chargenROMTextBox"] as TextBox; + chargenROMTextBox!.Text = _c64Config.ROMs.SingleOrDefault(x => x.Name == C64Config.CHARGEN_ROM_NAME).File; + chargenROMTextBox!.IsDirty = true; + + (bool isOk, List validationErrors) = _sadConsoleHostApp.IsValidConfigWithDetails().Result; + var validationErrorsLabel = Controls["validationErrorsLabel"] as Label; + validationErrorsLabel!.IsVisible = !isOk; + var validationErrorsListBox = Controls["validationErrorsListBox"] as ListBox; + validationErrorsListBox.IsVisible = !isOk; + validationErrorsListBox!.Items.Clear(); + foreach (var error in validationErrors) + { + var coloredString = new ColoredString(error, foreground: Color.Red, background: Color.Black); + validationErrorsListBox!.Items.Add(coloredString); + } + } +} diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/C64MenuConsole.cs b/src/apps/Highbyte.DotNet6502.App.SadConsole/C64MenuConsole.cs new file mode 100644 index 00000000..a878f479 --- /dev/null +++ b/src/apps/Highbyte.DotNet6502.App.SadConsole/C64MenuConsole.cs @@ -0,0 +1,113 @@ +using Highbyte.DotNet6502.Systems.Commodore64.Config; +using SadConsole.UI; +using SadConsole.UI.Controls; +using SadRogue.Primitives; + +namespace Highbyte.DotNet6502.App.SadConsole; +public class C64MenuConsole : ControlsConsole +{ + public const int CONSOLE_WIDTH = USABLE_WIDTH + (SadConsoleUISettings.UI_USE_CONSOLE_BORDER ? 2 : 0); + public const int CONSOLE_HEIGHT = USABLE_HEIGHT + (SadConsoleUISettings.UI_USE_CONSOLE_BORDER ? 2 : 0); + private const int USABLE_WIDTH = 21; + private const int USABLE_HEIGHT = 12; + + private readonly SadConsoleHostApp _sadConsoleHostApp; + private C64Config _c64Config => (C64Config)_sadConsoleHostApp.GetSystemConfig().Result; + + private C64MenuConsole(SadConsoleHostApp sadConsoleHostApp) : base(CONSOLE_WIDTH, CONSOLE_HEIGHT) + { + _sadConsoleHostApp = sadConsoleHostApp; + } + + public static C64MenuConsole Create(SadConsoleHostApp sadConsoleHostApp) + { + var console = new C64MenuConsole(sadConsoleHostApp); + + console.Surface.DefaultForeground = SadConsoleUISettings.UIConsoleForegroundColor; + console.Surface.DefaultBackground = SadConsoleUISettings.UIConsoleBackgroundColor; + console.Clear(); + + //console.Surface.UsePrintProcessor = true; + + console.UseMouse = true; + console.MouseMove += (s, e) => + { + }; + console.UseKeyboard = true; + + console.DrawUIItems(); + + if (SadConsoleUISettings.UI_USE_CONSOLE_BORDER) + console.Surface.DrawBox(new Rectangle(0, 0, console.Width, console.Height), SadConsoleUISettings.ConsoleDrawBoxBorderParameters); + + return console; + } + + private void DrawUIItems() + { + + var c64ConfigButton = new Button("C64 Config") + { + Name = "c64ConfigButton", + Position = (1, 1), + }; + c64ConfigButton.Click += C64ConfigButton_Click; + Controls.Add(c64ConfigButton); + + var validationMessageValueLabel = CreateLabelValue(new string(' ', 20), 1, c64ConfigButton.Bounds.MaxExtentY + 2, "validationMessageValueLabel"); + validationMessageValueLabel.TextColor = Controls.GetThemeColors().Red; + + // Helper function to create a label and add it to the console + Label CreateLabel(string text, int col, int row, string? name = null) + { + var labelTemp = new Label(text) { Position = new Point(col, row), Name = name }; + Controls.Add(labelTemp); + return labelTemp; + } + Label CreateLabelValue(string text, int col, int row, string? name = null) + { + var labelTemp = new Label(text) { Position = new Point(col, row), TextColor = Controls.GetThemeColors().Title, Name = name }; + Controls.Add(labelTemp); + return labelTemp; + } + + // Force OnIsDirtyChanged event which will set control states (see SetControlStates) + OnIsDirtyChanged(); + } + + protected override void OnIsDirtyChanged() + { + if (IsDirty) + { + SetControlStates(); + } + } + + private void SetControlStates() + { + var systemComboBox = Controls["c64ConfigButton"]; + systemComboBox.IsEnabled = _sadConsoleHostApp.EmulatorState == Systems.EmulatorState.Uninitialized; + + var validationMessageValueLabel = Controls["validationMessageValueLabel"] as Label; + (bool isOk, List validationErrors) = _sadConsoleHostApp.IsValidConfigWithDetails().Result; + //validationMessageValueLabel!.DisplayText = isOk ? "" : string.Join(",", validationErrors!); + validationMessageValueLabel!.DisplayText = isOk ? "" : "Config errors."; + validationMessageValueLabel!.IsVisible = !isOk; + } + + private void C64ConfigButton_Click(object sender, EventArgs e) + { + C64ConfigUIConsole window = C64ConfigUIConsole.Create(_sadConsoleHostApp); + + window.Center(); + window.Closed += (s2, e2) => + { + if (window.DialogResult) + { + IsDirty = true; + _sadConsoleHostApp.MenuConsole.IsDirty = true; + } + }; + window.Show(true); + } +} diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/FilePickerConsole.cs b/src/apps/Highbyte.DotNet6502.App.SadConsole/FilePickerConsole.cs new file mode 100644 index 00000000..fc669b10 --- /dev/null +++ b/src/apps/Highbyte.DotNet6502.App.SadConsole/FilePickerConsole.cs @@ -0,0 +1,141 @@ +using SadConsole.UI; +using SadConsole.UI.Controls; + +namespace Highbyte.DotNet6502.App.SadConsole; +public class FilePickerConsole : Window +{ + public const int CONSOLE_WIDTH = 40; + public const int CONSOLE_HEIGHT = 20; + + private readonly string _defaultFolder; + private readonly string _defaultFile; + private readonly bool _selectFolder; + private string? _selectedFile; + public string? SelectedFile => _selectedFile; + + private FilePickerConsole(string defaultFolder, string defaultFile = "", bool selectFolder = false) : base(CONSOLE_WIDTH, CONSOLE_HEIGHT) + { + _defaultFolder = defaultFolder; + _defaultFile = defaultFile; + _selectFolder = selectFolder; + } + + public static FilePickerConsole Create(string defaultFolder, string defaultFile = "", bool selectFolder = false) + { + var console = new FilePickerConsole(defaultFolder, defaultFile, selectFolder); + + console.Title = selectFolder ? "Select folder" : "Select file"; + Colors colors = console.Controls.GetThemeColors(); + + console.Cursor.PrintAppearanceMatchesHost = false; + console.Cursor.DisableWordBreak = true; + console.Cursor.SetPrintAppearance(colors.Title, console.Surface.DefaultBackground); + + + console.UseMouse = true; + console.MouseMove += (s, e) => + { + }; + console.UseKeyboard = true; + + console.DrawUIItems(); + + return console; + } + + private void DrawUIItems() + { + string currentFolder; + if (!Directory.Exists(_defaultFolder)) + currentFolder = Environment.CurrentDirectory; + else + currentFolder = _defaultFolder; + + FileDirectoryListbox fileListBox = new FileDirectoryListbox(30, Height - 5); + fileListBox.FileFilter = "*.*"; + fileListBox.OnlyRootAndSubDirs = false; + fileListBox.HideNonFilterFiles = false; + fileListBox.Position = (1, 2); + fileListBox.SelectedItemChanged += FileListBox_SelectedItemChanged; + + if (_selectFolder) + { + var parentFolder = Directory.GetParent(currentFolder); + if (parentFolder != null) + { + fileListBox.CurrentFolder = parentFolder.FullName; + //fileListBox.SelectedItem = Path.GetFileName(currentFolder); + //fileListBox.ScrollToSelectedItem(); + } + else + { + fileListBox.CurrentFolder = currentFolder; + } + } + else + { + fileListBox.CurrentFolder = currentFolder; + if (File.Exists(_defaultFile)) + { + //fileListBox.SelectedItem = _defaultFile; + //fileListBox.ScrollToSelectedItem(); + } + } + + //((SadConsole.UI.Themes.ListBoxTheme)fileListBox.Theme).DrawBorder = true; + Controls.Add(fileListBox); + + Button cancelButton = new Button(10, 1) + { + Name = "cancelButton", + Text = "Cancel", + Position = (1, Height - 2) + }; + cancelButton.Click += (s, e) => { DialogResult = false; Hide(); }; + Controls.Add(cancelButton); + + Button okButton = new Button(6, 1) + { + Name = "okButton", + Text = "OK", + Position = (Width - 1 - 7, Height - 2), + IsEnabled = false + }; + okButton.Click += (s, e) => { DialogResult = true; Hide(); }; + Controls.Add(okButton); + } + + private void FileListBox_SelectedItemChanged(object? sender, ListBox.SelectedItemEventArgs? e) + { + if (e.Item != null) + { + var okButton = Controls["okButton"] as Button; + string selectedItem = e.Item.ToString(); + if (_selectFolder) + { + if (Directory.Exists(selectedItem)) + { + okButton.IsEnabled = true; + _selectedFile = selectedItem; + } + else + { + okButton.IsEnabled = false; + } + } + else + { + if (File.Exists(selectedItem)) + { + okButton.IsEnabled = true; + _selectedFile = selectedItem; + } + else + { + okButton.IsEnabled = false; + } + } + } + + } +} diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/Highbyte.DotNet6502.App.SadConsole.csproj b/src/apps/Highbyte.DotNet6502.App.SadConsole/Highbyte.DotNet6502.App.SadConsole.csproj index 11df772d..79f9d1d5 100644 --- a/src/apps/Highbyte.DotNet6502.App.SadConsole/Highbyte.DotNet6502.App.SadConsole.csproj +++ b/src/apps/Highbyte.DotNet6502.App.SadConsole/Highbyte.DotNet6502.App.SadConsole.csproj @@ -17,6 +17,7 @@ + diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/MenuConsole.cs b/src/apps/Highbyte.DotNet6502.App.SadConsole/MenuConsole.cs index b4e573ff..9644c71b 100644 --- a/src/apps/Highbyte.DotNet6502.App.SadConsole/MenuConsole.cs +++ b/src/apps/Highbyte.DotNet6502.App.SadConsole/MenuConsole.cs @@ -1,4 +1,3 @@ -using System; using SadConsole.UI; using SadConsole.UI.Controls; using SadRogue.Primitives; @@ -61,7 +60,7 @@ private void DrawUIItems() var startButton = new Button("Start") { Name = "startButton", - Position = (1, statusLabel.Bounds.MaxExtentY + 1), + Position = (1, statusLabel.Bounds.MaxExtentY + 2), }; startButton.Click += async (s, e) => { await _sadConsoleHostApp.Start(); IsDirty = true; }; Controls.Add(startButton); @@ -110,12 +109,11 @@ Label CreateLabel(string text, int col, int row, string? name = null) } Label CreateLabelValue(string text, int col, int row, string? name = null) { - var labelTemp = new Label(text) { Position = new Point(col, row), TextColor = Controls.GetThemeColors().Title, Name = name }; + var labelTemp = new Label(text) { Position = new Point(col, row), TextColor = Controls.GetThemeColors().White, Name = name }; Controls.Add(labelTemp); return labelTemp; } - // Force OnIsDirtyChanged event which will set control states (see SetControlStates) OnIsDirtyChanged(); } @@ -137,7 +135,7 @@ private void SetControlStates() statusLabel!.DisplayText = _sadConsoleHostApp.EmulatorState.ToString(); var startButton = Controls["startButton"]; - startButton.IsEnabled = _sadConsoleHostApp.EmulatorState != Systems.EmulatorState.Running; + startButton.IsEnabled = _sadConsoleHostApp.GetSystemConfig().Result.IsValid(out _) && _sadConsoleHostApp.EmulatorState != Systems.EmulatorState.Running; var pauseButton = Controls["pauseButton"]; pauseButton.IsEnabled = _sadConsoleHostApp.EmulatorState == Systems.EmulatorState.Running; @@ -151,6 +149,7 @@ private void SetControlStates() var selectFontSizeComboBox = Controls["selectFontSizeComboBox"]; selectFontSizeComboBox.IsEnabled = _sadConsoleHostApp.EmulatorState == Systems.EmulatorState.Uninitialized; + if (_sadConsoleHostApp.SystemMenuConsole != null) + _sadConsoleHostApp.SystemMenuConsole.IsDirty = true; } - } diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/Program.cs b/src/apps/Highbyte.DotNet6502.App.SadConsole/Program.cs index 2dbea314..2bd765e3 100644 --- a/src/apps/Highbyte.DotNet6502.App.SadConsole/Program.cs +++ b/src/apps/Highbyte.DotNet6502.App.SadConsole/Program.cs @@ -4,10 +4,10 @@ using Highbyte.DotNet6502.Logging; using Highbyte.DotNet6502.Logging.InMem; using Highbyte.DotNet6502.Systems; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; using Highbyte.DotNet6502.Systems.Commodore64; using Highbyte.DotNet6502.Systems.Generic; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; // Get config file var builder = new ConfigurationBuilder() diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/SadConsoleHostApp.cs b/src/apps/Highbyte.DotNet6502.App.SadConsole/SadConsoleHostApp.cs index ad8128fa..5bf70b33 100644 --- a/src/apps/Highbyte.DotNet6502.App.SadConsole/SadConsoleHostApp.cs +++ b/src/apps/Highbyte.DotNet6502.App.SadConsole/SadConsoleHostApp.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Logging; using SadConsole.Configuration; using SadRogue.Primitives; +using Console = SadConsole.Console; namespace Highbyte.DotNet6502.App.SadConsole; @@ -31,17 +32,21 @@ public class SadConsoleHostApp : HostApp _menuConsole!; + private Console? _systemMenuConsole; + public Console? SystemMenuConsole => _systemMenuConsole; + private EmulatorConsole? _sadConsoleEmulatorConsole; + private SadConsoleRenderContext _renderContext = default!; private SadConsoleInputHandlerContext _inputHandlerContext = default!; private NullAudioHandlerContext _audioHandlerContext = default!; - private const int MENU_POSITION_X = 0; private const int MENU_POSITION_Y = 0; private int StartupScreenWidth => MenuConsole.CONSOLE_WIDTH + 40; - private int StartupScreenHeight => MenuConsole.CONSOLE_HEIGHT; + private int StartupScreenHeight => MenuConsole.CONSOLE_HEIGHT + 14; /// /// Constructor @@ -90,9 +95,6 @@ public void Run() InitInputHandlerContext(); InitAudioHandlerContext(); - // Set the default system - SelectSystem(_emulatorConfig.DefaultEmulator); - // ---------- // Main SadConsole screen // ---------- @@ -120,7 +122,7 @@ public void Run() } - private IScreenObject CreateMainSadConsoleScreen(Game gameInstance) + private IScreenObject CreateMainSadConsoleScreen(GameHost gameHost) { //ScreenSurface screen = new(gameInstance.ScreenCellsX, gameInstance.ScreenCellsY); //return screen; @@ -133,11 +135,30 @@ private IScreenObject CreateMainSadConsoleScreen(Game gameInstance) //_sadConsoleScreen.IsFocused = true; _menuConsole.IsFocused = true; + // Trigger sadConsoleHostApp.SelectSystem call which in turn may trigger other system-specific UI stuff. + SelectSystem(SelectedSystemName); + return _sadConsoleScreen; } public override void OnAfterSelectSystem() { + // Clear any old system specific menu console + if (_systemMenuConsole != null) + { + if (_sadConsoleScreen.Children.Contains(_systemMenuConsole)) + _sadConsoleScreen.Children.Remove(_systemMenuConsole); + _systemMenuConsole.Dispose(); + _systemMenuConsole = null; + } + + // Create system specific menu console + if (SelectedSystemName == "C64") + { + _systemMenuConsole = C64MenuConsole.Create(this); + _systemMenuConsole.Position = (MENU_POSITION_X, _menuConsole.Height); + _sadConsoleScreen.Children.Add(_systemMenuConsole); + } } public override bool OnBeforeStart(ISystem systemAboutToBeStarted) @@ -284,7 +305,7 @@ private int CalculateWindowWidthPixels() private int CalculateWindowHeightPixels() { - var height = Math.Max(_menuConsole.HeightPixels, _sadConsoleEmulatorConsole != null ? _sadConsoleEmulatorConsole.HeightPixels : 0); + var height = Math.Max(_menuConsole.HeightPixels + (_systemMenuConsole != null ? _systemMenuConsole.HeightPixels : 0), _sadConsoleEmulatorConsole != null ? _sadConsoleEmulatorConsole.HeightPixels : 0); return height; } diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/appsettings.json b/src/apps/Highbyte.DotNet6502.App.SadConsole/appsettings.json index 4a7c0d26..fad6c7ac 100644 --- a/src/apps/Highbyte.DotNet6502.App.SadConsole/appsettings.json +++ b/src/apps/Highbyte.DotNet6502.App.SadConsole/appsettings.json @@ -1,7 +1,7 @@ { "Highbyte.DotNet6502.SadConsoleConfig": { - "WindowTitle": "SadConsole displaying screen generated by machine code running in Highbyte.DotNet6502 emulator.", + "WindowTitle": "Highbyte.DotNet6502 emulator + SadConsole", "DefaultEmulator": "C64", "UIFont": "", // Leave blank for default SadConsole font. diff --git a/src/libraries/Highbyte.DotNet6502.Impl.SadConsole/Highbyte.DotNet6502.Impl.SadConsole.csproj b/src/libraries/Highbyte.DotNet6502.Impl.SadConsole/Highbyte.DotNet6502.Impl.SadConsole.csproj index 314d1977..f5e32f60 100644 --- a/src/libraries/Highbyte.DotNet6502.Impl.SadConsole/Highbyte.DotNet6502.Impl.SadConsole.csproj +++ b/src/libraries/Highbyte.DotNet6502.Impl.SadConsole/Highbyte.DotNet6502.Impl.SadConsole.csproj @@ -30,6 +30,6 @@ - + diff --git a/src/libraries/Highbyte.DotNet6502/Systems/HostApp.cs b/src/libraries/Highbyte.DotNet6502/Systems/HostApp.cs index d013453c..5e8389f0 100644 --- a/src/libraries/Highbyte.DotNet6502/Systems/HostApp.cs +++ b/src/libraries/Highbyte.DotNet6502/Systems/HostApp.cs @@ -96,6 +96,8 @@ public void SelectSystem(string systemName) if (!_systemList.Systems.Contains(systemName)) throw new DotNet6502Exception($"System not found: {systemName}"); _selectedSystemName = systemName; + + OnAfterSelectSystem(); } public virtual void OnAfterSelectSystem() { } From 31bbf827258e381e33410f3c5669575c47937f1b Mon Sep 17 00:00:00 2001 From: Highbyte Date: Thu, 25 Jul 2024 20:19:03 +0200 Subject: [PATCH 13/40] Add SadConsole monitor --- .../{ => ConfigUI}/C64ConfigUIConsole.cs | 18 +-- .../{ => ConfigUI}/C64MenuConsole.cs | 10 +- .../EmulatorConfig.cs | 6 + .../MenuConsole.cs | 15 +- .../MonitorConsole.cs | 146 +++++++++++++++++ .../SadConsoleHostApp.cs | 152 ++++++++++++++---- .../SadConsoleMonitor.cs | 116 +++++++++++++ 7 files changed, 411 insertions(+), 52 deletions(-) rename src/apps/Highbyte.DotNet6502.App.SadConsole/{ => ConfigUI}/C64ConfigUIConsole.cs (94%) rename src/apps/Highbyte.DotNet6502.App.SadConsole/{ => ConfigUI}/C64MenuConsole.cs (90%) create mode 100644 src/apps/Highbyte.DotNet6502.App.SadConsole/MonitorConsole.cs create mode 100644 src/apps/Highbyte.DotNet6502.App.SadConsole/SadConsoleMonitor.cs diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/C64ConfigUIConsole.cs b/src/apps/Highbyte.DotNet6502.App.SadConsole/ConfigUI/C64ConfigUIConsole.cs similarity index 94% rename from src/apps/Highbyte.DotNet6502.App.SadConsole/C64ConfigUIConsole.cs rename to src/apps/Highbyte.DotNet6502.App.SadConsole/ConfigUI/C64ConfigUIConsole.cs index 323f2c86..85c29ce6 100644 --- a/src/apps/Highbyte.DotNet6502.App.SadConsole/C64ConfigUIConsole.cs +++ b/src/apps/Highbyte.DotNet6502.App.SadConsole/ConfigUI/C64ConfigUIConsole.cs @@ -4,7 +4,7 @@ using SadConsole.UI.Controls; using SadRogue.Primitives; -namespace Highbyte.DotNet6502.App.SadConsole; +namespace Highbyte.DotNet6502.App.SadConsole.ConfigUI; public class C64ConfigUIConsole : Window { public const int CONSOLE_WIDTH = USABLE_WIDTH; @@ -30,7 +30,7 @@ public static C64ConfigUIConsole Create(SadConsoleHostApp sadConsoleHostApp) //console.Clear(); console.Title = "C64 Config"; - Colors colors = console.Controls.GetThemeColors(); + var colors = console.Controls.GetThemeColors(); console.Cursor.PrintAppearanceMatchesHost = false; console.Cursor.DisableWordBreak = true; @@ -163,7 +163,7 @@ private void DrawUIItems() //cancelButton.Click += (s, e) => { DialogResult = false; Hide(); }; //Controls.Add(cancelButton); - Button okButton = new Button(6, 1) + var okButton = new Button(6, 1) { Text = "OK", Position = (Width - 1 - 7, Height - 2) @@ -192,10 +192,8 @@ Label CreateLabelValue(string text, int col, int row, string? name = null) private void OpenURL(string url) { - if (!Uri.TryCreate(url, UriKind.Absolute, out Uri? uri)) - { + if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) throw new Exception($"Invalid URL: {url}"); - } // Launch the URL in the default browser System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(uri.AbsoluteUri) { UseShellExecute = true }); } @@ -203,7 +201,7 @@ private void OpenURL(string url) private void ShowROMFilePickerDialog(string romName) { var currentFolder = PathHelper.ExpandOSEnvironmentVariables(_c64Config.ROMDirectory); - FilePickerConsole window = FilePickerConsole.Create(currentFolder, _c64Config.GetROM(romName).GetROMFilePath(currentFolder)); + var window = FilePickerConsole.Create(currentFolder, _c64Config.GetROM(romName).GetROMFilePath(currentFolder)); window.Center(); window.Closed += (s2, e2) => { @@ -219,7 +217,7 @@ private void ShowROMFilePickerDialog(string romName) private void ShowROMFolderPickerDialog() { var currentFolder = PathHelper.ExpandOSEnvironmentVariables(_c64Config.ROMDirectory); - FilePickerConsole window = FilePickerConsole.Create(currentFolder, selectFolder: true); + var window = FilePickerConsole.Create(currentFolder, selectFolder: true); window.Center(); window.Closed += (s2, e2) => { @@ -235,9 +233,7 @@ private void ShowROMFolderPickerDialog() protected override void OnIsDirtyChanged() { if (IsDirty) - { SetControlStates(); - } } private void SetControlStates() @@ -258,7 +254,7 @@ private void SetControlStates() chargenROMTextBox!.Text = _c64Config.ROMs.SingleOrDefault(x => x.Name == C64Config.CHARGEN_ROM_NAME).File; chargenROMTextBox!.IsDirty = true; - (bool isOk, List validationErrors) = _sadConsoleHostApp.IsValidConfigWithDetails().Result; + (var isOk, var validationErrors) = _sadConsoleHostApp.IsValidConfigWithDetails().Result; var validationErrorsLabel = Controls["validationErrorsLabel"] as Label; validationErrorsLabel!.IsVisible = !isOk; var validationErrorsListBox = Controls["validationErrorsListBox"] as ListBox; diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/C64MenuConsole.cs b/src/apps/Highbyte.DotNet6502.App.SadConsole/ConfigUI/C64MenuConsole.cs similarity index 90% rename from src/apps/Highbyte.DotNet6502.App.SadConsole/C64MenuConsole.cs rename to src/apps/Highbyte.DotNet6502.App.SadConsole/ConfigUI/C64MenuConsole.cs index a878f479..052af2d4 100644 --- a/src/apps/Highbyte.DotNet6502.App.SadConsole/C64MenuConsole.cs +++ b/src/apps/Highbyte.DotNet6502.App.SadConsole/ConfigUI/C64MenuConsole.cs @@ -1,9 +1,8 @@ -using Highbyte.DotNet6502.Systems.Commodore64.Config; using SadConsole.UI; using SadConsole.UI.Controls; using SadRogue.Primitives; -namespace Highbyte.DotNet6502.App.SadConsole; +namespace Highbyte.DotNet6502.App.SadConsole.ConfigUI; public class C64MenuConsole : ControlsConsole { public const int CONSOLE_WIDTH = USABLE_WIDTH + (SadConsoleUISettings.UI_USE_CONSOLE_BORDER ? 2 : 0); @@ -12,7 +11,6 @@ public class C64MenuConsole : ControlsConsole private const int USABLE_HEIGHT = 12; private readonly SadConsoleHostApp _sadConsoleHostApp; - private C64Config _c64Config => (C64Config)_sadConsoleHostApp.GetSystemConfig().Result; private C64MenuConsole(SadConsoleHostApp sadConsoleHostApp) : base(CONSOLE_WIDTH, CONSOLE_HEIGHT) { @@ -78,9 +76,7 @@ Label CreateLabelValue(string text, int col, int row, string? name = null) protected override void OnIsDirtyChanged() { if (IsDirty) - { SetControlStates(); - } } private void SetControlStates() @@ -89,7 +85,7 @@ private void SetControlStates() systemComboBox.IsEnabled = _sadConsoleHostApp.EmulatorState == Systems.EmulatorState.Uninitialized; var validationMessageValueLabel = Controls["validationMessageValueLabel"] as Label; - (bool isOk, List validationErrors) = _sadConsoleHostApp.IsValidConfigWithDetails().Result; + (var isOk, var validationErrors) = _sadConsoleHostApp.IsValidConfigWithDetails().Result; //validationMessageValueLabel!.DisplayText = isOk ? "" : string.Join(",", validationErrors!); validationMessageValueLabel!.DisplayText = isOk ? "" : "Config errors."; validationMessageValueLabel!.IsVisible = !isOk; @@ -97,7 +93,7 @@ private void SetControlStates() private void C64ConfigButton_Click(object sender, EventArgs e) { - C64ConfigUIConsole window = C64ConfigUIConsole.Create(_sadConsoleHostApp); + var window = C64ConfigUIConsole.Create(_sadConsoleHostApp); window.Center(); window.Closed += (s2, e2) => diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/EmulatorConfig.cs b/src/apps/Highbyte.DotNet6502.App.SadConsole/EmulatorConfig.cs index 56bf65f0..1ac0d820 100644 --- a/src/apps/Highbyte.DotNet6502.App.SadConsole/EmulatorConfig.cs +++ b/src/apps/Highbyte.DotNet6502.App.SadConsole/EmulatorConfig.cs @@ -1,5 +1,6 @@ using Highbyte.DotNet6502.App.SadConsole.SystemSetup; using Highbyte.DotNet6502.Impl.SadConsole; +using Highbyte.DotNet6502.Monitor; using Highbyte.DotNet6502.Systems; using static SadConsole.IFont; @@ -33,6 +34,9 @@ public class EmulatorConfig public string DefaultEmulator { get; set; } + public MonitorConfig Monitor { get; set; } + + /// /// SadConsole-specific configuration for specific system. /// @@ -49,6 +53,8 @@ public EmulatorConfig() FontSize = Sizes.One; DefaultEmulator = "C64"; + Monitor = new(); + C64HostConfig = new(); GenericComputerHostConfig = new(); } diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/MenuConsole.cs b/src/apps/Highbyte.DotNet6502.App.SadConsole/MenuConsole.cs index 9644c71b..a1c2c64b 100644 --- a/src/apps/Highbyte.DotNet6502.App.SadConsole/MenuConsole.cs +++ b/src/apps/Highbyte.DotNet6502.App.SadConsole/MenuConsole.cs @@ -89,7 +89,17 @@ private void DrawUIItems() resetButton.Click += async (s, e) => { await _sadConsoleHostApp.Reset(); IsDirty = true; }; Controls.Add(resetButton); - var fontSizeLabel = CreateLabel("Font size:", 1, stopButton.Bounds.MaxExtentY + 2); + + var monitorButton = new Button("Monitor (F12)") + { + Name = "monitorButton", + Position = (1, resetButton.Position.Y + 2), + }; + monitorButton.Click += (s, e) => { _sadConsoleHostApp.ToggleMonitor(); IsDirty = true; }; + Controls.Add(monitorButton); + + + var fontSizeLabel = CreateLabel("Font size:", 1, monitorButton.Bounds.MaxExtentY + 2); ComboBox selectFontSizeBox = new ComboBox(9, 9, 5, Enum.GetValues().Select(x => (object)x).ToArray()) { Position = (fontSizeLabel.Bounds.MaxExtentX + 2, fontSizeLabel.Position.Y), @@ -146,6 +156,9 @@ private void SetControlStates() var resetButton = Controls["resetButton"]; resetButton.IsEnabled = _sadConsoleHostApp.EmulatorState != Systems.EmulatorState.Uninitialized; + var monitorButton = Controls["monitorButton"]; + monitorButton.IsEnabled = _sadConsoleHostApp.EmulatorState != Systems.EmulatorState.Uninitialized; + var selectFontSizeComboBox = Controls["selectFontSizeComboBox"]; selectFontSizeComboBox.IsEnabled = _sadConsoleHostApp.EmulatorState == Systems.EmulatorState.Uninitialized; diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/MonitorConsole.cs b/src/apps/Highbyte.DotNet6502.App.SadConsole/MonitorConsole.cs new file mode 100644 index 00000000..1d2d050d --- /dev/null +++ b/src/apps/Highbyte.DotNet6502.App.SadConsole/MonitorConsole.cs @@ -0,0 +1,146 @@ +using Highbyte.DotNet6502.Monitor; +using SadConsole.Components; +using SadRogue.Primitives; +using Console = SadConsole.Console; + +namespace Highbyte.DotNet6502.App.SadConsole; +internal class MonitorConsole : Console +{ + public const int CONSOLE_WIDTH = USABLE_WIDTH + (SadConsoleUISettings.UI_USE_CONSOLE_BORDER ? 2 : 0); + public const int CONSOLE_HEIGHT = USABLE_HEIGHT + (SadConsoleUISettings.UI_USE_CONSOLE_BORDER ? 2 : 0); + private const int USABLE_WIDTH = 58; + private const int USABLE_HEIGHT = 25; + + private const int CURSOR_START_Y = CONSOLE_HEIGHT - 6; + + private readonly SadConsoleHostApp _sadConsoleHostApp; + private readonly MonitorConfig _monitorConfig; + private SadConsoleMonitor _monitor; + + private readonly ClassicConsoleKeyboardHandler _keyboardHandlerObject; + + public event EventHandler? MonitorStateChange; + protected virtual void OnMonitorStateChange(bool monitorEnabled) + { + var handler = MonitorStateChange; + handler?.Invoke(this, monitorEnabled); + } + + + /// + /// + /// + /// + /// + public MonitorConsole(SadConsoleHostApp sadConsoleHostApp, MonitorConfig monitorConfig) + : base(CONSOLE_WIDTH, CONSOLE_HEIGHT) + { + _sadConsoleHostApp = sadConsoleHostApp; + _monitorConfig = monitorConfig; + + // Initially not visible. Call Init() to initialize with the current system, then Enable() to show it. + IsVisible = false; + + // Custom keyboard handler that handles the cursor in the console, + // and has call back for processing commands after pressing enter. + _keyboardHandlerObject = new ClassicConsoleKeyboardHandler("> "); + _keyboardHandlerObject.EnterPressedAction = EnterPressedActionHandler; + SadComponents.Add(_keyboardHandlerObject); + + // Enable the keyboard + UseKeyboard = true; + + // Disable the cursor because custom keyboard handler will process cursor + Cursor.IsEnabled = false; + } + + public void InitScreen() + { + // Note: Don't know why it's necessary to remove (and later add) the keyboard handler object for the Prompt to be displayed correctly. + if (SadComponents.Contains(_keyboardHandlerObject)) + SadComponents.Remove(_keyboardHandlerObject); + + this.Clear(); + Cursor.Position = new Point(0, CURSOR_START_Y); + _keyboardHandlerObject.CursorLastY = CURSOR_START_Y; + + DisplayInitialText(); + + Surface.TimesShiftedUp = 0; + + SadComponents.Add(_keyboardHandlerObject); + } + + private void EnterPressedActionHandler(ClassicConsoleKeyboardHandler keyboardComponent, Cursor cursor, string value) + { + if (string.IsNullOrEmpty(value)) + return; + + //_monitor.WriteOutput(value, MessageSeverity.Information); // The entered command has already been printed to console here + var commandResult = _monitor.SendCommand(value); + if (commandResult == CommandResult.Quit) + { + //Quit = true; + Disable(); + } + else if (commandResult == CommandResult.Continue) + { + Disable(); + } + } + + private void MonitorOutputPrint(string message, MessageSeverity severity) + { + Cursor.Print($" {message}").NewLine(); + } + + /// + /// Initializes monitor for the current system. + /// Should be called once after a system is started. + /// + public void Init() + { + _monitor = new SadConsoleMonitor(_sadConsoleHostApp.CurrentSystemRunner!, _monitorConfig, MonitorOutputPrint); + + InitScreen(); + } + + private void DisplayInitialText() + { + Cursor.DisableWordBreak = false; + _monitor.ShowDescription(); + _monitor.WriteOutput("", MessageSeverity.Information); + _monitor.WriteOutput("Type '?' for help.", MessageSeverity.Information); + _monitor.WriteOutput("Type '[command] -?' for help on command.", MessageSeverity.Information); + _monitor.WriteOutput("Examples:", MessageSeverity.Information); + _monitor.WriteOutput(" d", MessageSeverity.Information); + _monitor.WriteOutput(" d c000", MessageSeverity.Information); + _monitor.WriteOutput(" m c000", MessageSeverity.Information); + _monitor.WriteOutput(" z", MessageSeverity.Information); + _monitor.WriteOutput(" g", MessageSeverity.Information); + _monitor.WriteOutput("", MessageSeverity.Information); + //_monitor.ShowHelp(); + Cursor.DisableWordBreak = true; + } + + public void Enable(ExecEvaluatorTriggerResult? execEvaluatorTriggerResult = null) + { + _monitor.Reset(); // Reset monitor working variables (like last disassembly location) + + if (execEvaluatorTriggerResult != null) + _monitor.ShowInfoAfterBreakTriggerEnabled(execEvaluatorTriggerResult); + + IsVisible = true; + IsFocused = true; + + OnMonitorStateChange(monitorEnabled: true); + } + + public void Disable() + { + IsVisible = false; + IsFocused = false; + + OnMonitorStateChange(monitorEnabled: false); + } +} diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/SadConsoleHostApp.cs b/src/apps/Highbyte.DotNet6502.App.SadConsole/SadConsoleHostApp.cs index 5bf70b33..212a37b3 100644 --- a/src/apps/Highbyte.DotNet6502.App.SadConsole/SadConsoleHostApp.cs +++ b/src/apps/Highbyte.DotNet6502.App.SadConsole/SadConsoleHostApp.cs @@ -1,9 +1,12 @@ +using Highbyte.DotNet6502.App.SadConsole.ConfigUI; using Highbyte.DotNet6502.App.SadConsole.SystemSetup; using Highbyte.DotNet6502.Impl.SadConsole; using Highbyte.DotNet6502.Logging; +using Highbyte.DotNet6502.Monitor; using Highbyte.DotNet6502.Systems; using Microsoft.Extensions.Logging; using SadConsole.Configuration; +using SadConsole.Input; using SadRogue.Primitives; using Console = SadConsole.Console; @@ -31,13 +34,18 @@ public class SadConsoleHostApp : HostApp _menuConsole!; + private Console? _systemMenuConsole; public Console? SystemMenuConsole => _systemMenuConsole; private EmulatorConsole? _sadConsoleEmulatorConsole; + private MonitorConsole? _monitorConsole; + //private MonitorConsole? _monitorConsole; + private SadConsoleRenderContext _renderContext = default!; private SadConsoleInputHandlerContext _inputHandlerContext = default!; @@ -132,6 +140,45 @@ private IScreenObject CreateMainSadConsoleScreen(GameHost gameHost) _menuConsole.Position = (MENU_POSITION_X, MENU_POSITION_Y); _sadConsoleScreen.Children.Add(_menuConsole); + //_monitorConsole = MonitorConsole.Create(this, EmulatorConfig.Monitor); + _monitorConsole = new MonitorConsole(this, EmulatorConfig.Monitor); + _monitorConsole.IsVisible = false; + _monitorConsole.Position = (_menuConsole.Position.X + _menuConsole.Width, _menuConsole.Position.Y); // Temporary position while invisible. Will be moved after a system is started. + _monitorConsole.MonitorStateChange += (s, monitorEnabled) => + { + //_inputHandlerContext.ListenForKeyboardInput(enabled: !monitorEnabled); + //if (monitorEnabled) + // statsPanel.Disable(); + + if (monitorEnabled) + { + // Position monitor to the right of the emulator console + _monitorConsole.UsePixelPositioning = true; + //_monitorConsole.Position = new Point((_sadConsoleEmulatorConsole.Position.X * _sadConsoleEmulatorConsole.Font.GlyphWidth) + (_sadConsoleEmulatorConsole.Width * _sadConsoleEmulatorConsole.Font.GlyphWidth), 0); + // Note: _sadConsoleEmulatorConsole has already changed to UsePixelPositioning = true, so its Position.X is in pixels (not Width though). + _monitorConsole.Position = new Point(_sadConsoleEmulatorConsole.Position.X + (_sadConsoleEmulatorConsole.Width * _sadConsoleEmulatorConsole.Font.GlyphWidth), 0); + + _sadConsoleEmulatorConsole.IsFocused = false; + } + else + { + if (_sadConsoleEmulatorConsole != null) + _sadConsoleEmulatorConsole.IsFocused = true; + else + _menuConsole.IsFocused = true; + + //if (_statsWasEnabled) + //{ + // CurrentRunningSystem!.InstrumentationEnabled = true; + // _statsPanel.Enable(); + //} + } + + // Resize main window to fit menu, emulator, monitor and other visible consoles + Game.Instance.ResizeWindow(CalculateWindowWidthPixels(), CalculateWindowHeightPixels()); + }; + _sadConsoleScreen.Children.Add(_monitorConsole); + //_sadConsoleScreen.IsFocused = true; _menuConsole.IsFocused = true; @@ -183,7 +230,6 @@ public override bool OnBeforeStart(ISystem systemAboutToBeStarted) _sadConsoleEmulatorConsole = EmulatorConsole.Create(systemAboutToBeStarted, font, _emulatorConfig.FontSize, SadConsoleUISettings.ConsoleDrawBoxBorderParameters); _sadConsoleEmulatorConsole.UsePixelPositioning = true; _sadConsoleEmulatorConsole.Position = new Point((_menuConsole.Position.X * _menuConsole.Font.GlyphWidth) + (_menuConsole.Width * _menuConsole.Font.GlyphWidth), 0); - _sadConsoleEmulatorConsole.IsFocused = true; _sadConsoleScreen.Children.Add(_sadConsoleEmulatorConsole); @@ -195,20 +241,32 @@ public override bool OnBeforeStart(ISystem systemAboutToBeStarted) public override void OnAfterStart(EmulatorState emulatorStateBeforeStart) { - //// Init monitor for current system started if this system was not started before - //if (emulatorStateBeforeStart == EmulatorState.Uninitialized) - // _monitor.Init(CurrentSystemRunner!); + // Init monitor for current system started if this system was not started before + if (emulatorStateBeforeStart == EmulatorState.Uninitialized) + _monitorConsole.Init(); } public override void OnAfterStop() { + // Disable monitor if it is visible + if (_monitorConsole.IsVisible) + { + _monitorConsole.Disable(); + // Resize window to that monitor console is not shown. + Game.Instance.ResizeWindow(CalculateWindowWidthPixels(), CalculateWindowHeightPixels()); + } + + // Remove the console containing the running system if (_sadConsoleEmulatorConsole != null) { if (_sadConsoleScreen.Children.Contains(_sadConsoleEmulatorConsole)) _sadConsoleScreen.Children.Remove(_sadConsoleEmulatorConsole); _sadConsoleEmulatorConsole.Dispose(); + _sadConsoleEmulatorConsole = null; } + } + public override void OnAfterClose() { // Dispose Monitor/Instrumentations panel @@ -227,27 +285,36 @@ public override void OnAfterClose() /// private void UpdateSadConsole(object? sender, GameHost gameHost) { + // Handle UI-specific keyboard inputs such as toggle monitor, stats panel etc. + HandleUIKeyboardInput(); + // RunEmulatorOneFrame() will first handle input, then emulator in run for one frame. RunEmulatorOneFrame(); } public override void OnBeforeRunEmulatorOneFrame(out bool shouldRun, out bool shouldReceiveInput) { - // TODO: Check if montior is active? If no set shouldRun to false. + shouldRun = false; + shouldReceiveInput = false; + + // Don't update emulator state when monitor is visible + if (_monitorConsole.IsVisible) + return; + shouldRun = true; - // TODO: Check if emulator console has focus? If not, set shouldReceiveInput to false. - shouldReceiveInput = true; + // Only receive input to emulator if it has focus + if (_sadConsoleEmulatorConsole.IsFocused) + shouldReceiveInput = true; } public override void OnAfterRunEmulatorOneFrame(ExecEvaluatorTriggerResult execEvaluatorTriggerResult) { - //// Show monitor if we encounter breakpoint or other break - //if (execEvaluatorTriggerResult.Triggered) - // _monitor.Enable(execEvaluatorTriggerResult); + // Show monitor if we encounter breakpoint or other break + if (execEvaluatorTriggerResult.Triggered) + _monitorConsole.Enable(execEvaluatorTriggerResult); } - /// /// Runs on every Render Frame event. Draws one emulator frame on screen. /// @@ -289,39 +356,58 @@ private NullAudioHandlerContext CreateAudioHandlerContext() private int CalculateWindowWidthPixels() { - int fontSizeAdjustment; + int emulatorConsoleFontSizeAdjustment; // TODO: This is a bit of a hack for handling consoles with different font sizes, and positioning on main screen. Better way? if (_emulatorConfig.FontSizeScaleFactor > 1) { - fontSizeAdjustment = (((int)_emulatorConfig.FontSizeScaleFactor - 1) * 16); + emulatorConsoleFontSizeAdjustment = (((int)_emulatorConfig.FontSizeScaleFactor - 1) * 16); } else { - fontSizeAdjustment = 0; + emulatorConsoleFontSizeAdjustment = 0; } - var width = _menuConsole.WidthPixels + (_sadConsoleEmulatorConsole != null ? _sadConsoleEmulatorConsole.WidthPixels + fontSizeAdjustment : 0); - return width; + + var emulatorConsoleWidthPixels = (_sadConsoleEmulatorConsole != null ? _sadConsoleEmulatorConsole.WidthPixels + emulatorConsoleFontSizeAdjustment : 0); + var monitorConsoleWidthPixels = (_monitorConsole != null && _monitorConsole.IsVisible ? _monitorConsole.WidthPixels : 0); + var widthPixels = _menuConsole.WidthPixels + emulatorConsoleWidthPixels + monitorConsoleWidthPixels; + return widthPixels; } private int CalculateWindowHeightPixels() { - var height = Math.Max(_menuConsole.HeightPixels + (_systemMenuConsole != null ? _systemMenuConsole.HeightPixels : 0), _sadConsoleEmulatorConsole != null ? _sadConsoleEmulatorConsole.HeightPixels : 0); - return height; + var heightPixels = Math.Max(_menuConsole.HeightPixels + (_systemMenuConsole != null ? _systemMenuConsole.HeightPixels : 0), _sadConsoleEmulatorConsole != null ? _sadConsoleEmulatorConsole.HeightPixels : 0); + return heightPixels; } - //private void OnKeyDown(IKeyboard keyboard, Key key, int x) - //{ - // if (key == Key.F6) - // ToggleMainMenu(); - // if (key == Key.F10) - // ToggleLogsPanel(); - - // if (EmulatorState == EmulatorState.Running || EmulatorState == EmulatorState.Paused) - // { - // if (key == Key.F11) - // ToggleStatsPanel(); - // if (key == Key.F12) - // ToggleMonitor(); - // } - //} + + public void ToggleMonitor() + { + // Only be able to toggle monitor if emulator is running or paused + if (EmulatorState == EmulatorState.Uninitialized) + return; + + if (_monitorConsole!.IsVisible) + { + _monitorConsole.Disable(); + } + else + { + _monitorConsole.Enable(); + } + } + + private void HandleUIKeyboardInput() + { + var keyboard = GameHost.Instance.Keyboard; + //if (keyboard.IsKeyPressed(Keys.F10)) + // ToggleLogsPanel(); + + if (EmulatorState == EmulatorState.Running || EmulatorState == EmulatorState.Paused) + { + //if (keyboard.IsKeyPressed(Keys.F11)) + // ToggleStatsPanel(); + if (keyboard.IsKeyPressed(Keys.F12)) + ToggleMonitor(); + } + } } diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/SadConsoleMonitor.cs b/src/apps/Highbyte.DotNet6502.App.SadConsole/SadConsoleMonitor.cs new file mode 100644 index 00000000..43afa0f8 --- /dev/null +++ b/src/apps/Highbyte.DotNet6502.App.SadConsole/SadConsoleMonitor.cs @@ -0,0 +1,116 @@ +using Highbyte.DotNet6502.Monitor; +using Highbyte.DotNet6502.Systems; + +namespace Highbyte.DotNet6502.App.SadConsole; + +public class SadConsoleMonitor : MonitorBase +{ + private readonly MonitorConfig _monitorConfig; + private readonly Action _monitorOutputPrint; + public const int MONITOR_CMD_HISTORY_VIEW_ROWS = 200; + public List<(string Message, MessageSeverity Severity)> MonitorCmdHistory { get; private set; } = new(); + + public SadConsoleMonitor( + SystemRunner systemRunner, + MonitorConfig monitorConfig, + Action monitorOutputPrint + ) : base(systemRunner, monitorConfig) + { + _monitorConfig = monitorConfig; + _monitorOutputPrint = monitorOutputPrint; + } + + public override bool LoadBinary(string fileName, out ushort loadedAtAddress, out ushort fileLength, ushort? forceLoadAddress = null, Action? afterLoadCallback = null) + { + if (!Path.IsPathFullyQualified(fileName)) + fileName = $"{_monitorConfig.DefaultDirectory}/{fileName}"; + + if (!File.Exists(fileName)) + { + WriteOutput($"File not found: {fileName}", MessageSeverity.Error); + loadedAtAddress = 0; + fileLength = 0; + return false; + } + + try + { + BinaryLoader.Load( + Mem, + fileName, + out loadedAtAddress, + out fileLength, + forceLoadAddress); + + return true; + } + catch (Exception ex) + { + WriteOutput($"Load error: {ex.Message}", MessageSeverity.Error); + loadedAtAddress = 0; + fileLength = 0; + return false; + } + } + + public override bool LoadBinary(out ushort loadedAtAddress, out ushort fileLength, ushort? forceLoadAddress = null, Action? afterLoadCallback = null) + { + WriteOutput($"Loading file via file picker dialog not implemented.", MessageSeverity.Warning); + loadedAtAddress = 0; + fileLength = 0; + return false; + + // TODO: Opening native Dialog here leads to endless Enter keypress events being sent to inputtext field. + //var dialogResult = Dialog.FileOpen(@"prg;*"); + //if (dialogResult.IsOk) + //{ + // var fileName = dialogResult.Path; + // BinaryLoader.Load( + // SystemRunner.System.Mem, + // fileName, + // out loadedAtAddress, + // out fileLength); + // return true; + //} + + //loadedAtAddress = 0; + //fileLength = 0; + //return false; + } + + public override void SaveBinary(string fileName, ushort startAddress, ushort endAddress, bool addFileHeaderWithLoadAddress) + { + if (!Path.IsPathFullyQualified(fileName)) + fileName = $"{_monitorConfig.DefaultDirectory}/{fileName}"; + + try + { + BinarySaver.Save( + Mem, + fileName, + startAddress, + endAddress, + addFileHeaderWithLoadAddress: addFileHeaderWithLoadAddress); + + WriteOutput($"Program saved to {fileName}"); + } + catch (Exception ex) + { + WriteOutput($"Save error: {ex.Message}", MessageSeverity.Error); + } + } + + public override void WriteOutput(string message) + { + WriteOutput(message, MessageSeverity.Information); + } + + public override void WriteOutput(string message, MessageSeverity severity) + { + MonitorCmdHistory.Add((message, severity)); + if (MonitorCmdHistory.Count > MONITOR_CMD_HISTORY_VIEW_ROWS) + MonitorCmdHistory.RemoveAt(0); + + _monitorOutputPrint(message, severity); + } +} From 505e05606fb587544fd2cf5fb8c7cdc886913206 Mon Sep 17 00:00:00 2001 From: Highbyte Date: Fri, 26 Jul 2024 16:06:05 +0200 Subject: [PATCH 14/40] Add SadConsole monitor CPU status display. --- .../ConfigUI/C64MenuConsole.cs | 2 +- .../MenuConsole.cs | 2 + .../MonitorConsole.cs | 99 ++++++++------- .../MonitorStatusConsole.cs | 113 ++++++++++++++++++ .../SadConsoleHostApp.cs | 48 ++++++-- .../CommandLineApp.cs | 9 +- 6 files changed, 211 insertions(+), 62 deletions(-) create mode 100644 src/apps/Highbyte.DotNet6502.App.SadConsole/MonitorStatusConsole.cs diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/ConfigUI/C64MenuConsole.cs b/src/apps/Highbyte.DotNet6502.App.SadConsole/ConfigUI/C64MenuConsole.cs index 052af2d4..27ecb26b 100644 --- a/src/apps/Highbyte.DotNet6502.App.SadConsole/ConfigUI/C64MenuConsole.cs +++ b/src/apps/Highbyte.DotNet6502.App.SadConsole/ConfigUI/C64MenuConsole.cs @@ -25,7 +25,7 @@ public static C64MenuConsole Create(SadConsoleHostApp sadConsoleHostApp) console.Surface.DefaultBackground = SadConsoleUISettings.UIConsoleBackgroundColor; console.Clear(); - //console.Surface.UsePrintProcessor = true; + console.FocusedMode = FocusBehavior.None; console.UseMouse = true; console.MouseMove += (s, e) => diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/MenuConsole.cs b/src/apps/Highbyte.DotNet6502.App.SadConsole/MenuConsole.cs index a1c2c64b..cd952a4c 100644 --- a/src/apps/Highbyte.DotNet6502.App.SadConsole/MenuConsole.cs +++ b/src/apps/Highbyte.DotNet6502.App.SadConsole/MenuConsole.cs @@ -28,6 +28,8 @@ public static MenuConsole Create(SadConsoleHostApp sadConsoleHostApp) //FontSize = console.Font.GetFontSize(IFont.Sizes.One); //console.Surface.UsePrintProcessor = true; + console.FocusedMode = FocusBehavior.None; + console.UseMouse = true; console.MouseMove += (s, e) => { diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/MonitorConsole.cs b/src/apps/Highbyte.DotNet6502.App.SadConsole/MonitorConsole.cs index 1d2d050d..11051d2b 100644 --- a/src/apps/Highbyte.DotNet6502.App.SadConsole/MonitorConsole.cs +++ b/src/apps/Highbyte.DotNet6502.App.SadConsole/MonitorConsole.cs @@ -1,5 +1,6 @@ using Highbyte.DotNet6502.Monitor; using SadConsole.Components; +using SadConsole.UI.Controls; using SadRogue.Primitives; using Console = SadConsole.Console; @@ -8,35 +9,30 @@ internal class MonitorConsole : Console { public const int CONSOLE_WIDTH = USABLE_WIDTH + (SadConsoleUISettings.UI_USE_CONSOLE_BORDER ? 2 : 0); public const int CONSOLE_HEIGHT = USABLE_HEIGHT + (SadConsoleUISettings.UI_USE_CONSOLE_BORDER ? 2 : 0); - private const int USABLE_WIDTH = 58; - private const int USABLE_HEIGHT = 25; - - private const int CURSOR_START_Y = CONSOLE_HEIGHT - 6; + private const int USABLE_WIDTH = 60; + private const int USABLE_HEIGHT = 24; private readonly SadConsoleHostApp _sadConsoleHostApp; private readonly MonitorConfig _monitorConfig; + private readonly Action _displayCPUStatus; private SadConsoleMonitor _monitor; - + public SadConsoleMonitor Monitor => _monitor; private readonly ClassicConsoleKeyboardHandler _keyboardHandlerObject; - public event EventHandler? MonitorStateChange; - protected virtual void OnMonitorStateChange(bool monitorEnabled) - { - var handler = MonitorStateChange; - handler?.Invoke(this, monitorEnabled); - } - + private Label _processorStatusLabel; + private List