diff --git a/src/Spice86/Spice86DependencyInjection.cs b/src/Spice86/Spice86DependencyInjection.cs index 4a1deeebcf..91d0d93ca3 100644 --- a/src/Spice86/Spice86DependencyInjection.cs +++ b/src/Spice86/Spice86DependencyInjection.cs @@ -541,24 +541,24 @@ internal Spice86DependencyInjection(Configuration configuration, MainWindow? mai StructureViewModelFactory structureViewModelFactory = new(configuration, state, loggerService, pauseHandler); - messenger.Register(this, (_, m) => { - var vm = new MemoryBitmapViewModel(videoState, hostStorageProvider); - m.SetInstance(vm); - }); + MemoryBitmapViewModelFactory memoryBitmapViewModelFactory = new(videoState, hostStorageProvider); MemoryViewModel memoryViewModel = new(memory, memoryDataExporter, state, breakpointsViewModel, pauseHandler, messenger, uiDispatcher, textClipboard, hostStorageProvider, structureViewModelFactory, + memoryBitmapViewModelFactory, canCloseTab: false); StackMemoryViewModel stackMemoryViewModel = new(memory, memoryDataExporter, state, stack, breakpointsViewModel, pauseHandler, messenger, uiDispatcher, textClipboard, hostStorageProvider, structureViewModelFactory, + memoryBitmapViewModelFactory, canCloseTab: false); DataSegmentMemoryViewModel dataSegmentViewModel = new(memory, memoryDataExporter, state, breakpointsViewModel, pauseHandler, messenger, uiDispatcher, textClipboard, hostStorageProvider, structureViewModelFactory, + memoryBitmapViewModelFactory, canCloseTab: false); DebugWindowViewModel debugWindowViewModel = new( diff --git a/src/Spice86/ViewModels/DataSegmentMemoryViewModel.cs b/src/Spice86/ViewModels/DataSegmentMemoryViewModel.cs index bbb5f604ff..536f6e1b3d 100644 --- a/src/Spice86/ViewModels/DataSegmentMemoryViewModel.cs +++ b/src/Spice86/ViewModels/DataSegmentMemoryViewModel.cs @@ -16,11 +16,12 @@ public DataSegmentMemoryViewModel(IMemory memory, MemoryDataExporter memoryDataE BreakpointsViewModel breakpointsViewModel, IPauseHandler pauseHandler, IMessenger messenger, IUIDispatcher uiDispatcher, ITextClipboard textClipboard, IHostStorageProvider storageProvider, IStructureViewModelFactory structureViewModelFactory, + IMemoryBitmapViewModelFactory memoryBitmapViewModelFactory, bool canCloseTab = false, string? startAddress = null, string? endAddress = null) : base(memory, memoryDataExporter, state, breakpointsViewModel, pauseHandler, messenger, uiDispatcher, textClipboard, storageProvider, structureViewModelFactory, - canCloseTab, startAddress, endAddress) { + memoryBitmapViewModelFactory, canCloseTab, startAddress, endAddress) { Title = "Data Segment"; pauseHandler.Paused += () => uiDispatcher.Post(() => UpdateDataSegmentMemoryViewModel(this, state), DispatcherPriority.Background); diff --git a/src/Spice86/ViewModels/MemoryBitmapDisplayMode.cs b/src/Spice86/ViewModels/MemoryBitmapDisplayMode.cs new file mode 100644 index 0000000000..0981f4ba39 --- /dev/null +++ b/src/Spice86/ViewModels/MemoryBitmapDisplayMode.cs @@ -0,0 +1,31 @@ +namespace Spice86.ViewModels; + +/// +/// Defines the display modes for rendering memory as bitmaps. +/// +public enum MemoryBitmapDisplayMode { + /// + /// VGA 256-color indexed mode (8 bits per pixel). + /// + Vga8Bpp, + + /// + /// CGA 4-color graphics mode. + /// + Cga4Color, + + /// + /// EGA 16-color graphics mode. + /// + Ega16Color, + + /// + /// Text mode with IBM PC fonts. + /// + TextMode, + + /// + /// Hercules monochrome graphics mode. + /// + HerculesMonochrome +} diff --git a/src/Spice86/ViewModels/MemoryBitmapViewModel.cs b/src/Spice86/ViewModels/MemoryBitmapViewModel.cs index 6fa7dada7f..2cc2b70fb5 100644 --- a/src/Spice86/ViewModels/MemoryBitmapViewModel.cs +++ b/src/Spice86/ViewModels/MemoryBitmapViewModel.cs @@ -1,6 +1,7 @@ namespace Spice86.ViewModels; using Avalonia; +using Avalonia.Media; using Avalonia.Media.Imaging; using Avalonia.Platform; @@ -22,6 +23,15 @@ public partial class MemoryBitmapViewModel : ViewModelBase { [ObservableProperty] private byte[]? _data; + [ObservableProperty] + private MemoryBitmapDisplayMode _displayMode = MemoryBitmapDisplayMode.Vga8Bpp; + + [ObservableProperty] + private bool _showOverlay; + + [ObservableProperty] + private uint _startAddress; + public int WidthPixels { get; set; } public MemoryBitmapViewModel(IVideoState videoState, IHostStorageProvider storage) { @@ -29,7 +39,11 @@ public MemoryBitmapViewModel(IVideoState videoState, IHostStorageProvider storag _storage = storage; } - partial void OnDataChanged(byte[]? value) => BuildAsVgaEightBitsPerPixelImage(); + partial void OnDataChanged(byte[]? value) => RenderBitmap(); + + partial void OnDisplayModeChanged(MemoryBitmapDisplayMode value) => RenderBitmap(); + + partial void OnShowOverlayChanged(bool value) => RenderBitmap(); [RelayCommand] private async Task Save() { @@ -38,14 +52,34 @@ private async Task Save() { } } - private void BuildAsVgaEightBitsPerPixelImage() { + private void RenderBitmap() { if (Data is null || Data.Length == 0 || WidthPixels <= 0) { Bitmap = null; return; } + switch (DisplayMode) { + case MemoryBitmapDisplayMode.Vga8Bpp: + BuildAsVgaEightBitsPerPixelImage(); + break; + case MemoryBitmapDisplayMode.Cga4Color: + BuildAsCgaFourColorImage(); + break; + case MemoryBitmapDisplayMode.Ega16Color: + BuildAsEgaSixteenColorImage(); + break; + case MemoryBitmapDisplayMode.TextMode: + BuildAsTextModeImage(); + break; + case MemoryBitmapDisplayMode.HerculesMonochrome: + BuildAsHerculesMonochromeImage(); + break; + } + } + + private void BuildAsVgaEightBitsPerPixelImage() { int width = WidthPixels; - int height = Math.Max(1, (Data.Length + width - 1) / width); + int height = Math.Max(1, (Data!.Length + width - 1) / width); var writeableBitmap = new WriteableBitmap(new PixelSize(width, height), new Vector(96, 96), PixelFormat.Bgra8888, AlphaFormat.Opaque); @@ -78,5 +112,223 @@ private void BuildAsVgaEightBitsPerPixelImage() { } Bitmap = writeableBitmap; + + if (ShowOverlay) { + ApplyOverlay(); + } + } + + private void BuildAsCgaFourColorImage() { + // CGA 4-color mode: 2 bits per pixel, 4 colors from palette + int width = WidthPixels; + int pixelsPerByte = 4; // 2 bits per pixel + int height = Math.Max(1, (Data!.Length * pixelsPerByte + width - 1) / width); + + var writeableBitmap = new WriteableBitmap(new PixelSize(width, height), + new Vector(96, 96), PixelFormat.Bgra8888, AlphaFormat.Opaque); + + using ILockedFramebuffer uiFrameBuffer = writeableBitmap.Lock(); + ArgbPalette palette = _videoState.DacRegisters.ArgbPalette; + + unsafe { + byte* dstBase = (byte*)uiFrameBuffer.Address; + int dstStride = uiFrameBuffer.RowBytes; + uint* rowPtr = stackalloc uint[width]; + + int pixelIndex = 0; + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int byteIndex = pixelIndex / pixelsPerByte; + if (byteIndex < Data.Length) { + int bitPos = 6 - (pixelIndex % pixelsPerByte) * 2; + byte colorIndex = (byte)((Data[byteIndex] >> bitPos) & 0x03); + rowPtr[x] = palette[colorIndex]; + } else { + rowPtr[x] = 0xFF000000; + } + pixelIndex++; + } + + Span srcBytes = new Span(rowPtr, width * sizeof(uint)); + var dst = new Span(dstBase + y * dstStride, Math.Min(srcBytes.Length, dstStride)); + srcBytes[..dst.Length].CopyTo(dst); + } + } + + Bitmap = writeableBitmap; + + if (ShowOverlay) { + ApplyOverlay(); + } + } + + private void BuildAsEgaSixteenColorImage() { + // EGA 16-color mode: 4 bits per pixel + int width = WidthPixels; + int pixelsPerByte = 2; // 4 bits per pixel + int height = Math.Max(1, (Data!.Length * pixelsPerByte + width - 1) / width); + + var writeableBitmap = new WriteableBitmap(new PixelSize(width, height), + new Vector(96, 96), PixelFormat.Bgra8888, AlphaFormat.Opaque); + + using ILockedFramebuffer uiFrameBuffer = writeableBitmap.Lock(); + ArgbPalette palette = _videoState.DacRegisters.ArgbPalette; + + unsafe { + byte* dstBase = (byte*)uiFrameBuffer.Address; + int dstStride = uiFrameBuffer.RowBytes; + uint* rowPtr = stackalloc uint[width]; + + int pixelIndex = 0; + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int byteIndex = pixelIndex / pixelsPerByte; + if (byteIndex < Data.Length) { + int bitPos = (1 - (pixelIndex % pixelsPerByte)) * 4; + byte colorIndex = (byte)((Data[byteIndex] >> bitPos) & 0x0F); + rowPtr[x] = palette[colorIndex]; + } else { + rowPtr[x] = 0xFF000000; + } + pixelIndex++; + } + + Span srcBytes = new Span(rowPtr, width * sizeof(uint)); + var dst = new Span(dstBase + y * dstStride, Math.Min(srcBytes.Length, dstStride)); + srcBytes[..dst.Length].CopyTo(dst); + } + } + + Bitmap = writeableBitmap; + + if (ShowOverlay) { + ApplyOverlay(); + } + } + + private void BuildAsTextModeImage() { + // Text mode: Each character is 2 bytes (character code + attribute) + // Character dimensions are typically 8x16 or 8x14 + const int charWidth = 8; + const int charHeight = 16; + + int charsPerRow = WidthPixels / charWidth; + if (charsPerRow == 0) { + charsPerRow = 1; + } + + int bytesPerChar = 2; + int totalChars = Data!.Length / bytesPerChar; + int rows = Math.Max(1, (totalChars + charsPerRow - 1) / charsPerRow); + + int width = charsPerRow * charWidth; + int height = rows * charHeight; + + var writeableBitmap = new WriteableBitmap(new PixelSize(width, height), + new Vector(96, 96), PixelFormat.Bgra8888, AlphaFormat.Opaque); + + using ILockedFramebuffer uiFrameBuffer = writeableBitmap.Lock(); + ArgbPalette palette = _videoState.DacRegisters.ArgbPalette; + + unsafe { + byte* dstBase = (byte*)uiFrameBuffer.Address; + int dstStride = uiFrameBuffer.RowBytes; + + // For now, render as a placeholder pattern showing character codes + // TODO: Load and render actual IBM PC fonts from video memory plane 2 + for (int charRow = 0; charRow < rows; charRow++) { + for (int charCol = 0; charCol < charsPerRow; charCol++) { + int charIndex = charRow * charsPerRow + charCol; + int dataOffset = charIndex * bytesPerChar; + + if (dataOffset + 1 < Data.Length) { + byte charCode = Data[dataOffset]; + byte attribute = Data[dataOffset + 1]; + + uint fgColor = palette[attribute & 0x0F]; + uint bgColor = palette[(attribute >> 4) & 0x0F]; + + // Render a simple block pattern for now + for (int py = 0; py < charHeight; py++) { + int screenY = charRow * charHeight + py; + if (screenY >= height) { + break; + } + + uint* rowPtr = (uint*)(dstBase + screenY * dstStride) + charCol * charWidth; + + // Simple pattern: checkerboard based on char code + bool useChar = (charCode & (1 << (py % 8))) != 0; + + for (int px = 0; px < charWidth; px++) { + rowPtr[px] = useChar ? fgColor : bgColor; + } + } + } + } + } + } + + Bitmap = writeableBitmap; + + if (ShowOverlay) { + ApplyOverlay(); + } + } + + private void BuildAsHerculesMonochromeImage() { + // Hercules monochrome: 1 bit per pixel, typically 720x348 + int width = WidthPixels; + int pixelsPerByte = 8; + int height = Math.Max(1, (Data!.Length * pixelsPerByte + width - 1) / width); + + var writeableBitmap = new WriteableBitmap(new PixelSize(width, height), + new Vector(96, 96), PixelFormat.Bgra8888, AlphaFormat.Opaque); + + using ILockedFramebuffer uiFrameBuffer = writeableBitmap.Lock(); + + const uint white = 0xFFFFFFFF; + const uint black = 0xFF000000; + + unsafe { + byte* dstBase = (byte*)uiFrameBuffer.Address; + int dstStride = uiFrameBuffer.RowBytes; + uint* rowPtr = stackalloc uint[width]; + + int pixelIndex = 0; + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int byteIndex = pixelIndex / pixelsPerByte; + if (byteIndex < Data.Length) { + int bitPos = 7 - (pixelIndex % pixelsPerByte); + bool isSet = (Data[byteIndex] & (1 << bitPos)) != 0; + rowPtr[x] = isSet ? white : black; + } else { + rowPtr[x] = black; + } + pixelIndex++; + } + + Span srcBytes = new Span(rowPtr, width * sizeof(uint)); + var dst = new Span(dstBase + y * dstStride, Math.Min(srcBytes.Length, dstStride)); + srcBytes[..dst.Length].CopyTo(dst); + } + } + + Bitmap = writeableBitmap; + + if (ShowOverlay) { + ApplyOverlay(); + } + } + + private void ApplyOverlay() { + if (Bitmap is null || Data is null) { + return; + } + + // TODO: Implement overlay grid showing memory addresses and values + // This would require rendering text/lines on top of the bitmap + // For now, this is a placeholder for future implementation } } diff --git a/src/Spice86/ViewModels/MemoryViewModel.cs b/src/Spice86/ViewModels/MemoryViewModel.cs index 91ec28409e..5c10c920d3 100644 --- a/src/Spice86/ViewModels/MemoryViewModel.cs +++ b/src/Spice86/ViewModels/MemoryViewModel.cs @@ -24,6 +24,7 @@ public partial class MemoryViewModel : ViewModelWithErrorDialog { private readonly IMemory _memory; private readonly IStructureViewModelFactory _structureViewModelFactory; + private readonly IMemoryBitmapViewModelFactory _memoryBitmapViewModelFactory; private readonly IMessenger _messenger; private readonly IPauseHandler _pauseHandler; private readonly BreakpointsViewModel _breakpointsViewModel; @@ -35,6 +36,7 @@ public MemoryViewModel(IMemory memory, MemoryDataExporter memoryDataExporter, IPauseHandler pauseHandler, IMessenger messenger, IUIDispatcher uiDispatcher, ITextClipboard textClipboard, IHostStorageProvider storageProvider, IStructureViewModelFactory structureViewModelFactory, + IMemoryBitmapViewModelFactory memoryBitmapViewModelFactory, bool canCloseTab = false, string? startAddress = null, string? endAddress = null) : base(uiDispatcher, textClipboard) { _state = state; @@ -48,6 +50,7 @@ public MemoryViewModel(IMemory memory, MemoryDataExporter memoryDataExporter, _messenger = messenger; _storageProvider = storageProvider; _structureViewModelFactory = structureViewModelFactory; + _memoryBitmapViewModelFactory = memoryBitmapViewModelFactory; if (TryParseAddressString(startAddress, _state, out uint? startAddressValue)) { StartAddress = ConvertUtils.ToHex32(startAddressValue.Value); } else { @@ -477,7 +480,7 @@ private void CreateNewMemoryView(uint? startAddress = null) { MemoryViewModel memoryViewModel = new(_memory, _memoryDataExporter, _state, _breakpointsViewModel, _pauseHandler, _messenger, _uiDispatcher, _textClipboard, - _storageProvider, _structureViewModelFactory, canCloseTab: true); + _storageProvider, _structureViewModelFactory, _memoryBitmapViewModelFactory, canCloseTab: true); if (startAddress is not null) { memoryViewModel.StartAddress = ConvertUtils.ToHex32(startAddress.Value); } @@ -657,11 +660,11 @@ private void CancelViewAsBitmap() { [RelayCommand] private void ConfirmViewAsBitmap() { if (BitmapViewWidth <= 0 || BitmapViewHeight <= 0) { - ShowError(new ArgumentOutOfRangeException($"Invalid bitmap size: {BitmapViewWidth}x{BitmapViewHeight}")); + ShowError(new ArgumentOutOfRangeException(nameof(BitmapViewWidth), $"Invalid bitmap size: {BitmapViewWidth}x{BitmapViewHeight}")); return; } if (!TryParseAddressString(BitmapViewStartAddress, _state, out uint? startAddress)) { - ShowError(new ArgumentOutOfRangeException($"Invalid start address: {BitmapViewStartAddress}")); + ShowError(new ArgumentOutOfRangeException(nameof(BitmapViewStartAddress), $"Invalid start address: {BitmapViewStartAddress}")); return; } @@ -670,7 +673,7 @@ private void ConfirmViewAsBitmap() { if (TryParseAddressString(BitmapViewEndAddress, _state, out uint? endAddressParsed)) { // Respect the provided end address and clamp the read length accordingly if (!GetIsMemoryRangeValid(startAddress, endAddressParsed, 0)) { - ShowError(new ArgumentOutOfRangeException($"Invalid address range: {BitmapViewStartAddress} - {BitmapViewEndAddress}")); + ShowError(new ArgumentOutOfRangeException(nameof(BitmapViewStartAddress), $"Invalid address range: {BitmapViewStartAddress} - {BitmapViewEndAddress}")); return; } uint available = endAddressParsed.Value - startAddress.Value + 1; @@ -680,7 +683,7 @@ private void ConfirmViewAsBitmap() { // Else, compute end from length endAddress = startAddress.Value + bytesToRead - 1; if (!GetIsMemoryRangeValid(startAddress, endAddress, 0)) { - ShowError(new ArgumentOutOfRangeException($"Computed address range out of bounds:" + + ShowError(new ArgumentOutOfRangeException(nameof(BitmapViewStartAddress), $"Computed address range out of bounds:" + $" {ConvertUtils.ToHex32(startAddress.Value)} - {ConvertUtils.ToHex32(endAddress)}")); return; } @@ -688,14 +691,9 @@ private void ConfirmViewAsBitmap() { byte[] bytes = _memory.ReadRam(startAddress.Value, bytesToRead); - MemoryBitmapViewModel? vm = null; - _messenger.Send(new CreateMemoryBitmapViewModelMessage(m => vm = m)); - if (vm is null) { - ShowError(new InvalidOperationException("No MemoryBitmapViewModel provider is registered.")); - return; - } - + MemoryBitmapViewModel vm = _memoryBitmapViewModelFactory.CreateNew(); vm.WidthPixels = BitmapViewWidth; + vm.StartAddress = startAddress.Value; vm.Data = bytes; MemoryBitmap = vm; _memBitmapUpdateOnPause = () => { diff --git a/src/Spice86/ViewModels/Messages/CreateMemoryBitmapViewModelMessage.cs b/src/Spice86/ViewModels/Messages/CreateMemoryBitmapViewModelMessage.cs deleted file mode 100644 index ab12c859f6..0000000000 --- a/src/Spice86/ViewModels/Messages/CreateMemoryBitmapViewModelMessage.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Spice86.ViewModels.Messages; - -using System; - -public record CreateMemoryBitmapViewModelMessage(Action SetInstance); \ No newline at end of file diff --git a/src/Spice86/ViewModels/Services/IMemoryBitmapViewModelFactory.cs b/src/Spice86/ViewModels/Services/IMemoryBitmapViewModelFactory.cs new file mode 100644 index 0000000000..6dda327023 --- /dev/null +++ b/src/Spice86/ViewModels/Services/IMemoryBitmapViewModelFactory.cs @@ -0,0 +1,12 @@ +namespace Spice86.ViewModels.Services; + +/// +/// Factory for creating MemoryBitmapViewModel instances with proper dependency injection. +/// +public interface IMemoryBitmapViewModelFactory { + /// + /// Creates a new instance of MemoryBitmapViewModel. + /// + /// A new MemoryBitmapViewModel instance. + MemoryBitmapViewModel CreateNew(); +} diff --git a/src/Spice86/ViewModels/Services/MemoryBitmapViewModelFactory.cs b/src/Spice86/ViewModels/Services/MemoryBitmapViewModelFactory.cs new file mode 100644 index 0000000000..edf6978a46 --- /dev/null +++ b/src/Spice86/ViewModels/Services/MemoryBitmapViewModelFactory.cs @@ -0,0 +1,21 @@ +namespace Spice86.ViewModels.Services; + +using Spice86.Core.Emulator.Devices.Video; + +/// +/// Factory for creating MemoryBitmapViewModel instances with proper dependency injection. +/// +public class MemoryBitmapViewModelFactory : IMemoryBitmapViewModelFactory { + private readonly IVideoState _videoState; + private readonly IHostStorageProvider _storage; + + public MemoryBitmapViewModelFactory(IVideoState videoState, IHostStorageProvider storage) { + _videoState = videoState; + _storage = storage; + } + + /// + public MemoryBitmapViewModel CreateNew() { + return new MemoryBitmapViewModel(_videoState, _storage); + } +} diff --git a/src/Spice86/ViewModels/StackMemoryViewModel.cs b/src/Spice86/ViewModels/StackMemoryViewModel.cs index abbeb081b3..97a727f738 100644 --- a/src/Spice86/ViewModels/StackMemoryViewModel.cs +++ b/src/Spice86/ViewModels/StackMemoryViewModel.cs @@ -16,11 +16,12 @@ public StackMemoryViewModel(IMemory memory, MemoryDataExporter memoryDataExporte BreakpointsViewModel breakpointsViewModel, IPauseHandler pauseHandler, IMessenger messenger, IUIDispatcher uiDispatcher, ITextClipboard textClipboard, IHostStorageProvider storageProvider, IStructureViewModelFactory structureViewModelFactory, + IMemoryBitmapViewModelFactory memoryBitmapViewModelFactory, bool canCloseTab = false, string? startAddress = null, string? endAddress = null) : base(memory, memoryDataExporter, state, breakpointsViewModel, pauseHandler, messenger, uiDispatcher, textClipboard, storageProvider, structureViewModelFactory, - canCloseTab, startAddress, endAddress) { + memoryBitmapViewModelFactory, canCloseTab, startAddress, endAddress) { Title = "CPU Stack Memory"; pauseHandler.Paused += () => uiDispatcher.Post(() => UpdateStackMemoryViewModel(this, state), DispatcherPriority.Background); diff --git a/src/Spice86/Views/MemoryBitmapView.axaml b/src/Spice86/Views/MemoryBitmapView.axaml index 0061c26e4e..837ace7c2f 100644 --- a/src/Spice86/Views/MemoryBitmapView.axaml +++ b/src/Spice86/Views/MemoryBitmapView.axaml @@ -6,9 +6,40 @@ mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:DataType="viewModels:MemoryBitmapViewModel" x:Class="Spice86.Views.MemoryBitmapView"> - -