From 245a16c5b35396ef52115d38d7bc32a7f8adfc4f Mon Sep 17 00:00:00 2001 From: Highbyte Date: Thu, 17 Sep 2026 14:37:40 +0200 Subject: [PATCH] Let the VIC-II see CPU writes to its bank at their own cycle, fix the bank ranges' last byte, add Chars Sucks to Download & Run --- docs/systems/c64/compatible-programs.md | 1 + docs/systems/c64/libraries.md | 2 +- .../ViewModels/C64MenuViewModel.cs | 1 + .../Pages/Commodore64/C64Menu.razor | 1 + .../C64.cs | 8 +++ .../Render/Rasterizer/IVic2CycleRenderer.cs | 14 ++++ .../IVic2RasterizerPixelGenerator.cs | 4 +- .../Render/Rasterizer/Vic2Rasterizer.cs | 12 ++-- .../Vic2RasterizerPixelGenerator.cs | 8 +-- .../Vic2RasterizerSequencerPixelGenerator.cs | 8 +-- .../Video/Vic2.cs | 29 +++++--- .../Video/Vic2BusStalls.cs | 2 +- .../C64VideoMemoryWriteTimingTests.cs | 67 +++++++++++++++++++ .../Vic2RasterizerPixelGeneratorTests.cs | 14 ++-- ...2RasterizerSequencerPixelGeneratorTests.cs | 30 ++++----- 15 files changed, 153 insertions(+), 48 deletions(-) create mode 100644 src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Render/Rasterizer/IVic2CycleRenderer.cs create mode 100644 tests/Highbyte.DotNet6502.Systems.Tests/Commodore64/C64VideoMemoryWriteTimingTests.cs diff --git a/docs/systems/c64/compatible-programs.md b/docs/systems/c64/compatible-programs.md index 5fed4ea5b..5844fe45b 100644 --- a/docs/systems/c64/compatible-programs.md +++ b/docs/systems/c64/compatible-programs.md @@ -37,6 +37,7 @@ For advanced use, see [Useful tools](useful-tools.md) for how to extract PRG fil | Unfortunate Coincidence | | `unf-coincidence.prg` | C64 PAL variant, per-line sprites switched on. Sprites only: sprite multiplexing with per-line changes of the scroll/mode and memory setup registers. | | Smooth and Wonders | | `Smooth_And_Wonders.prg` | C64 PAL variant, per-line sprites switched on. Sprites only: full-frame pictures and text on a 384x273 hyperscreen with the display off and the borders opened, drawn with sprites stretched by rewriting the Y-expand register every line and shown as two alternating frames. | | Krestage 3 | | `KRESTAGE3.D64` → first file | C64 PAL variant, per-line sprites switched on. Two pictures under a scroller, drawn with X-expanded sprites whose expand, multicolour and priority bits are switched while they shift (the sprite split effect), nine sprites on a line and 50-pixel-wide sprites among them; the demo checks the chip for these before it starts. | +| Chars Sucks | | `TRIAD_Charssucks.d64` → first file | C64 PAL variant, per-line sprites switched on. No characters at all: the display is off for the whole frame and the vertical border left open, the logo and the scroller are sprites behind the idle graphics, and the blocks' shading on the two X-expanded sprites is the idle byte of the VIC-II's bank, rewritten twice per line for a few cycles at a time. | ## Online / modem-style software diff --git a/docs/systems/c64/libraries.md b/docs/systems/c64/libraries.md index ddc6924aa..b48de79da 100644 --- a/docs/systems/c64/libraries.md +++ b/docs/systems/c64/libraries.md @@ -55,7 +55,7 @@ rasterizer does hold a character row's 40 screen codes and colour nibbles the wa the VIC-II does: fetched on the row's first line and shown for its remaining seven, so a screen write made after that fetch appears from the next row on. When a CPU read is stalled, the VIC-II and the renderer are brought through the stalled cycles before the CPU continues, so what the -VIC-II fetched during the stall reflects memory before the stalled instruction's write. The VIC-II's bank follows the levels on CIA 2 port A's two bank pins, not the bytes written: a bit the direction register makes an input floats up through its pull-up, so a program selects a bank with `$DD02` as well as with `$DD00`. +VIC-II fetched during the stall reflects memory before the stalled instruction's write. A CPU write into the VIC-II's bank is likewise seen by the chip's fetches from the cycle after the write on, not from the instruction's end: the chip reads in a cycle's first clock phase and the CPU writes in its second, so a byte rewritten in the middle of a line reaches the screen from the column after the write's cycle (an idle byte changed mid-line, graphics rewritten as they are fetched). The VIC-II's bank follows the levels on CIA 2 port A's two bank pins, not the bytes written: a bit the direction register makes an input floats up through its pull-up, so a program selects a bank with `$DD02` as well as with `$DD00`. The VIC-II also takes the bus from the CPU as on hardware: 40 cycles on every bad line (BA low from cycle 12, video matrix fetches in cycles 15-54) and two cycles per sprite with DMA on, BA diff --git a/src/apps/Avalonia/Highbyte.DotNet6502.App.Avalonia.Shell.Commodore64/ViewModels/C64MenuViewModel.cs b/src/apps/Avalonia/Highbyte.DotNet6502.App.Avalonia.Shell.Commodore64/ViewModels/C64MenuViewModel.cs index 882aac274..1b413af9f 100644 --- a/src/apps/Avalonia/Highbyte.DotNet6502.App.Avalonia.Shell.Commodore64/ViewModels/C64MenuViewModel.cs +++ b/src/apps/Avalonia/Highbyte.DotNet6502.App.Avalonia.Shell.Commodore64/ViewModels/C64MenuViewModel.cs @@ -53,6 +53,7 @@ public class C64MenuViewModel : ViewModelBase, ISystemMenuContributor {"unfortunatecoincidence", new C64DownloadProgramInfo("Unfortunate Coincidence", "https://csdb.dk/release/download.php?id=245796", downloadType: C64DownloadProgramType.Prg, c64Variant: "C64PAL", audioEnabled: true, requiresPerLineSprites: true)}, {"smoothandwonders", new C64DownloadProgramInfo("Smooth and Wonders", "https://csdb.dk/release/download.php?id=245318", downloadType: C64DownloadProgramType.Prg, c64Variant: "C64PAL", audioEnabled: true, requiresPerLineSprites: true)}, {"krestage3", new C64DownloadProgramInfo("Krestage 3", "https://csdb.dk/release/download.php?id=58941", downloadType: C64DownloadProgramType.D64Zip, c64Variant: "C64PAL", audioEnabled: true, requiresPerLineSprites: true, directLoadPRGName: "*")}, + {"charssucks", new C64DownloadProgramInfo("Chars Sucks", "https://csdb.dk/release/download.php?id=244748", downloadType: C64DownloadProgramType.D64, c64Variant: "C64PAL", audioEnabled: true, requiresPerLineSprites: true, directLoadPRGName: "*")}, {"ghostngoblins", new C64DownloadProgramInfo("Ghosts 'n Goblins", "https://csdb.dk/release/download.php?id=120333", downloadType: C64DownloadProgramType.D64Zip, c64Variant: "C64PAL", keyboardJoystickEnabled: true, keyboardJoystickNumber: 2, requiresBitmap: true, audioEnabled: true, directLoadPRGName: "*")}, {"gianasisters", new C64DownloadProgramInfo("Giana Sisters", "https://csdb.dk/release/download.php?id=161456", downloadType: C64DownloadProgramType.D64Zip, c64Variant: "C64PAL", keyboardJoystickEnabled: true, keyboardJoystickNumber: 2, requiresBitmap: true, audioEnabled: true, directLoadPRGName: "*")}, {"greenberet", new C64DownloadProgramInfo("Green Beret", "https://csdb.dk/release/download.php?id=157678", downloadType: C64DownloadProgramType.D64Zip, c64Variant: "C64PAL", keyboardJoystickEnabled: true, keyboardJoystickNumber: 2, requiresBitmap: true, audioEnabled: true)}, diff --git a/src/apps/BlazorWASM/Highbyte.DotNet6502.App.WASM.Shell.Commodore64/Pages/Commodore64/C64Menu.razor b/src/apps/BlazorWASM/Highbyte.DotNet6502.App.WASM.Shell.Commodore64/Pages/Commodore64/C64Menu.razor index cab5167f7..43eaf1765 100644 --- a/src/apps/BlazorWASM/Highbyte.DotNet6502.App.WASM.Shell.Commodore64/Pages/Commodore64/C64Menu.razor +++ b/src/apps/BlazorWASM/Highbyte.DotNet6502.App.WASM.Shell.Commodore64/Pages/Commodore64/C64Menu.razor @@ -188,6 +188,7 @@ {"unfortunatecoincidence", new C64DownloadProgramInfo("Unfortunate Coincidence", "https://csdb.dk/release/download.php?id=245796", downloadType: C64DownloadProgramType.Prg, c64Variant: "C64PAL", audioEnabled: true, requiresPerLineSprites: true)}, {"smoothandwonders", new C64DownloadProgramInfo("Smooth and Wonders", "https://csdb.dk/release/download.php?id=245318", downloadType: C64DownloadProgramType.Prg, c64Variant: "C64PAL", audioEnabled: true, requiresPerLineSprites: true)}, {"krestage3", new C64DownloadProgramInfo("Krestage 3", "https://csdb.dk/release/download.php?id=58941", downloadType: C64DownloadProgramType.D64Zip, c64Variant: "C64PAL", audioEnabled: true, requiresPerLineSprites: true, directLoadPRGName: "*")}, + {"charssucks", new C64DownloadProgramInfo("Chars Sucks", "https://csdb.dk/release/download.php?id=244748", downloadType: C64DownloadProgramType.D64, c64Variant: "C64PAL", audioEnabled: true, requiresPerLineSprites: true, directLoadPRGName: "*")}, {"lastninja", new C64DownloadProgramInfo("Last Ninja", "https://csdb.dk/release/download.php?id=101848", downloadType: C64DownloadProgramType.D64Zip, keyboardJoystickEnabled: true, keyboardJoystickNumber: 2, requiresBitmap: true, audioEnabled: false, directLoadPRGName: "*")}, {"minizork", new C64DownloadProgramInfo("Mini Zork", "https://csdb.dk/release/download.php?id=42919", audioEnabled: false, directLoadPRGName: "*")}, {"montezuma", new C64DownloadProgramInfo("Montezuma's Revenge", "https://csdb.dk/release/download.php?id=128101", downloadType: C64DownloadProgramType.D64Zip, keyboardJoystickEnabled: true, keyboardJoystickNumber: 2, audioEnabled: true, directLoadPRGName: "*")}, diff --git a/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/C64.cs b/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/C64.cs index 2029e0016..2a35a3ec9 100644 --- a/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/C64.cs +++ b/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/C64.cs @@ -71,6 +71,12 @@ public class C64 : ISystem, ISystemMonitor, ISystemState, ISystemCleanup, ISyste private IRenderProvider? _renderProvider; public IRenderProvider? RenderProvider => _renderProvider; + + /// + /// The current render provider if it draws cycle by cycle and can be caught up to the VIC-II in + /// the middle of an instruction (see ), otherwise null. + /// + internal IVic2CycleRenderer? Vic2CycleRenderer { get; private set; } public List RenderProviders { get; } = new(); private IAudioProvider? _audioProvider; @@ -429,11 +435,13 @@ private void SetCurrentRenderProvider(Type? renderProviderType) if (renderProviderType == null) { _renderProvider = null; + Vic2CycleRenderer = null; return; } var renderProvider = RenderProviders.SingleOrDefault(rp => rp.GetType() == renderProviderType) ?? throw new ArgumentException("The specified render provider type is not available."); _renderProvider = renderProvider; + Vic2CycleRenderer = renderProvider as IVic2CycleRenderer; } private static void ConfigureRenderer(C64 c64, C64Config config) diff --git a/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Render/Rasterizer/IVic2CycleRenderer.cs b/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Render/Rasterizer/IVic2CycleRenderer.cs new file mode 100644 index 000000000..ecaf49703 --- /dev/null +++ b/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Render/Rasterizer/IVic2CycleRenderer.cs @@ -0,0 +1,14 @@ +namespace Highbyte.DotNet6502.Systems.Commodore64.Render.Rasterizer; + +/// +/// A render provider that draws the VIC-II's output cycle by cycle and can therefore be brought +/// up to the chip's position in the middle of a CPU instruction, not only when the instruction +/// has ended (). The C64 asks for that where the +/// order of the chip's fetches and the CPU's accesses within an instruction decides the picture: +/// while a read is held by the chip's bus request, and before a write into the chip's bank lands. +/// +public interface IVic2CycleRenderer +{ + /// Draw every cycle between where the renderer last stopped and where the VIC-II is now. + void CatchUpToVic2(); +} diff --git a/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Render/Rasterizer/IVic2RasterizerPixelGenerator.cs b/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Render/Rasterizer/IVic2RasterizerPixelGenerator.cs index bba6ae593..17d0bdcd5 100644 --- a/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Render/Rasterizer/IVic2RasterizerPixelGenerator.cs +++ b/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Render/Rasterizer/IVic2RasterizerPixelGenerator.cs @@ -9,8 +9,8 @@ namespace Highbyte.DotNet6502.Systems.Commodore64.Render.Rasterizer; /// public interface IVic2RasterizerPixelGenerator { - /// Draw the pixels of the cycles the CPU has executed since the last call. - void OnAfterInstruction(); + /// Draw the pixels of every cycle between where the generator last stopped and where the VIC-II is now. + void CatchUpToVic2(); /// Finish the frame: the last line, the end-of-frame sprite pass, register resync. void OnEndFrame(); diff --git a/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Render/Rasterizer/Vic2Rasterizer.cs b/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Render/Rasterizer/Vic2Rasterizer.cs index 924ccb317..9bcde9354 100644 --- a/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Render/Rasterizer/Vic2Rasterizer.cs +++ b/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Render/Rasterizer/Vic2Rasterizer.cs @@ -28,7 +28,7 @@ namespace Highbyte.DotNet6502.Systems.Commodore64.Render.Rasterizer; /// - Fine scroll per raster line /// - Sprites (Standard, MultiColor). Multiplexing supported when perLineSprites is enabled. -public sealed class Vic2Rasterizer : IRenderProvider, IVideoFrameLayerProvider +public sealed class Vic2Rasterizer : IRenderProvider, IVideoFrameLayerProvider, IVic2CycleRenderer { public string Name => "Vic2Rasterizer"; @@ -114,10 +114,14 @@ public Vic2Rasterizer(C64 c64, bool useDoubleBuffering = true, bool perLineSprit //} // Called after each instruction - public void OnAfterInstruction() + public void OnAfterInstruction() => CatchUpToVic2(); + + // Called after each instruction, and by the C64 in the middle of one where the order of the + // chip's fetches and the CPU's accesses decides the picture (see IVic2CycleRenderer). + public void CatchUpToVic2() { // Write pixels of current x,y into _back at [y*StrideBytes + x*4 ..] - _pixelGenerator.OnAfterInstruction(); + _pixelGenerator.CatchUpToVic2(); } //public void OnEndScanline(int y) @@ -125,7 +129,7 @@ public void OnAfterInstruction() // ScanlineCompleted?.Invoke(this, y); //} - // Called once per frame after all OnAfterInstruction calls are executed + // Called once per frame after all OnAfterInstruction and CatchUpToVic2 calls are executed public void OnEndFrame() { _pixelGenerator.OnEndFrame(); diff --git a/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Render/Rasterizer/Vic2RasterizerPixelGenerator.cs b/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Render/Rasterizer/Vic2RasterizerPixelGenerator.cs index f0791e9fd..690b45425 100644 --- a/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Render/Rasterizer/Vic2RasterizerPixelGenerator.cs +++ b/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Render/Rasterizer/Vic2RasterizerPixelGenerator.cs @@ -118,7 +118,7 @@ public sealed class Vic2RasterizerUintPixelGenerator : IVic2RasterizerPixelGener private byte _backgroundColor3; // Journal of VIC-II register writes, filled by the VIC-II as the CPU writes (see - // Vic2.RegisterWriteObserver) and consumed cycle by cycle in OnAfterInstruction, which then + // Vic2.RegisterWriteObserver) and consumed cycle by cycle in CatchUpToVic2, which then // keeps only the entries it could not apply yet (a write on the very cycle it stopped at takes // effect on the next one). One instruction makes at most a few writes, so the capacity is only // reached when this generator is not the render provider being driven; then the journal is @@ -175,7 +175,7 @@ private struct RegisterWrite private readonly Action, int, int, int> _setForegroundPixels; // source, sourceIndex, destIndex, width private readonly Action _clearForegroundPixels; // destIndex, width - // When true, sprites are rendered per raster line during OnAfterInstruction (enables + // When true, sprites are rendered per raster line during CatchUpToVic2 (enables // sprite multiplexing) instead of once at end-of-frame. See DrawSpritesForLine. private readonly bool _perLineSprites; @@ -521,7 +521,7 @@ private void FinishLineRuns() /// Write screen data for all clock cycles since last time this method was called. /// Instructions can take different amount of cycles to execute, so this method is called after each instruction to update the screen data and will catch up on what's to do since last time it was called. /// - public void OnAfterInstruction() + public void CatchUpToVic2() { if (_registerWritesOverflowed) ResyncColorRegisters(); @@ -685,7 +685,7 @@ public void OnEndFrame() FinishLineRuns(); ResyncColorRegisters(); - // Per-line mode draws sprites during OnAfterInstruction; skip the end-of-frame pass. + // Per-line mode draws sprites during CatchUpToVic2; skip the end-of-frame pass. if (!_perLineSprites) { DrawSpritesToBitmapBackedByPixelArray(); diff --git a/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Render/Rasterizer/Vic2RasterizerSequencerPixelGenerator.cs b/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Render/Rasterizer/Vic2RasterizerSequencerPixelGenerator.cs index e2a6f230b..59c43be2f 100644 --- a/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Render/Rasterizer/Vic2RasterizerSequencerPixelGenerator.cs +++ b/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Render/Rasterizer/Vic2RasterizerSequencerPixelGenerator.cs @@ -179,7 +179,7 @@ public sealed class Vic2RasterizerSequencerPixelGenerator : IVic2RasterizerPixel private byte _backgroundColor3; // Journal of VIC-II register writes, filled by the VIC-II as the CPU writes (see - // Vic2.RegisterWriteObserver) and consumed cycle by cycle in OnAfterInstruction, which then + // Vic2.RegisterWriteObserver) and consumed cycle by cycle in CatchUpToVic2, which then // keeps only the entries it could not apply yet (a write on the very cycle it stopped at takes // effect on the next one). One instruction makes at most a few writes, so the capacity is only // reached when this generator is not the render provider being driven; then the journal is @@ -235,7 +235,7 @@ private struct RegisterWrite private readonly Action, int, int, int> _setForegroundPixels; // source, sourceIndex, destIndex, width private readonly Action _clearForegroundPixels; // destIndex, width - // When true, sprites are rendered per raster line during OnAfterInstruction (enables + // When true, sprites are rendered per raster line during CatchUpToVic2 (enables // sprite multiplexing) instead of once at end-of-frame. See DrawSpritesForLine. private readonly bool _perLineSprites; @@ -668,7 +668,7 @@ private void FinishLineRuns() /// Write screen data for all clock cycles since last time this method was called. /// Instructions can take different amount of cycles to execute, so this method is called after each instruction to update the screen data and will catch up on what's to do since last time it was called. /// - public void OnAfterInstruction() + public void CatchUpToVic2() { if (_registerWritesOverflowed) ResyncColorRegisters(); @@ -829,7 +829,7 @@ public void OnEndFrame() FinishLineRuns(); ResyncColorRegisters(); - // Per-line mode draws sprites during OnAfterInstruction; skip the end-of-frame pass. A + // Per-line mode draws sprites during CatchUpToVic2; skip the end-of-frame pass. A // sprite the VIC-II never output this frame (its X never met the beam, or met it only // inside its own fetch) is not shown, as on the chip. if (!_perLineSprites) diff --git a/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Video/Vic2.cs b/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Video/Vic2.cs index 0b59fdf82..d71ac14f7 100644 --- a/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Video/Vic2.cs +++ b/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Video/Vic2.cs @@ -626,7 +626,13 @@ private void MapRegisterMirrors( /// /// Method to be called before each write to memory by the CPU. - /// It's used for optimization to detect changes in VIC2 video memory. + /// For a write into the VIC-II's bank it first brings the VIC-II and the renderer through the + /// write's own cycle, so the fetches of the cycles before the write, and the fetch of the + /// write's cycle itself (the chip reads in the first clock phase, the CPU writes in the + /// second), see memory as it was: a byte the CPU rewrites in the middle of a line reaches the + /// screen from the column after the write on, as on hardware (an idle byte changed mid-line, + /// a sprite's data rewritten as it is fetched). It's also used for optimization to detect + /// changes in VIC2 video memory. /// /// /// @@ -635,6 +641,9 @@ public void InspectVic2MemoryValueUpdateFromCPU(ushort c64Address, byte value) var vic2Address = GetVic2FromC64Address(c64Address); if (vic2Address.HasValue) { + CatchUpTo(C64.CPU.BusCycles); + C64.Vic2CycleRenderer?.CatchUpToVic2(); + SpriteManager.DetectChangesToSpriteData(vic2Address.Value, value); if (DisplayMode == DispMode.Text) @@ -660,9 +669,9 @@ public void InspectVic2MemoryValueUpdateFromCPU(ushort c64Address, byte value) case 0: vic2Address = c64Address switch { - >= 0x0000 and < 0x0fff => c64Address, // video ram - >= 0x1000 and < 0x1fff => c64Address, // chargen ROM - >= 0x2000 and < 0x3fff => c64Address, // video ram + >= 0x0000 and <= 0x0fff => c64Address, // video ram + >= 0x1000 and <= 0x1fff => c64Address, // chargen ROM + >= 0x2000 and <= 0x3fff => c64Address, // video ram _ => null, // not a address mapped by VIC2 }; break; @@ -670,7 +679,7 @@ public void InspectVic2MemoryValueUpdateFromCPU(ushort c64Address, byte value) case 1: vic2Address = c64Address switch { - >= 0x4000 and < 0x7fff => (ushort)(c64Address - 0x4000), // video ram + >= 0x4000 and <= 0x7fff => (ushort)(c64Address - 0x4000), // video ram _ => null, // not a address mapped by VIC2 }; break; @@ -678,9 +687,9 @@ public void InspectVic2MemoryValueUpdateFromCPU(ushort c64Address, byte value) case 2: vic2Address = c64Address switch { - >= 0x8000 and < 0x8fff => (ushort)(c64Address - 0x8000), // video ram - >= 0x9000 and < 0x9fff => (ushort)(c64Address - 0x8000), // chargen rom - >= 0xa000 and < 0xbfff => (ushort)(c64Address - 0x8000), // video ram + >= 0x8000 and <= 0x8fff => (ushort)(c64Address - 0x8000), // video ram + >= 0x9000 and <= 0x9fff => (ushort)(c64Address - 0x8000), // chargen rom + >= 0xa000 and <= 0xbfff => (ushort)(c64Address - 0x8000), // video ram _ => null, // not a address mapped by VIC2 }; break; @@ -688,7 +697,7 @@ public void InspectVic2MemoryValueUpdateFromCPU(ushort c64Address, byte value) case 3: vic2Address = c64Address switch { - >= 0xc000 and < 0xffff => (ushort)(c64Address - 0xc000), // video ram + >= 0xc000 and <= 0xffff => (ushort)(c64Address - 0xc000), // video ram _ => null, // not a address mapped by VIC2 }; break; @@ -1610,7 +1619,7 @@ private void AdvanceRaster(ulong cyclesConsumed, ulong endBusCycle) // Per-line sprite processing (rendering + collision are gated by the same config flag). // Capture the shared start-of-line sprite snapshot once here; both the per-line collision // (below) and the rasterizer's per-line sprite pass (later this instruction, in its - // OnAfterInstruction) read it - so the registers are sampled once per line, not twice. + // CatchUpToVic2) read it - so the registers are sampled once per line, not twice. if (SpriteManager.PerLineCollisionEnabled) { SpriteManager.CaptureLineSpriteSnapshot(line); diff --git a/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Video/Vic2BusStalls.cs b/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Video/Vic2BusStalls.cs index 42c4f9a31..f455ee6a1 100644 --- a/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Video/Vic2BusStalls.cs +++ b/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Video/Vic2BusStalls.cs @@ -74,7 +74,7 @@ public ulong StallCyclesForRead(ulong busCycle, out ulong nextCheckBusCycle) // fetched (a bad line's video matrix row) reflects memory as it was, not as the // stalled instruction is about to leave it. _vic2.CatchUpTo(busCycle - 1 + stall); - _c64.RenderProvider?.OnAfterInstruction(); + _c64.Vic2CycleRenderer?.CatchUpToVic2(); return stall; } } diff --git a/tests/Highbyte.DotNet6502.Systems.Tests/Commodore64/C64VideoMemoryWriteTimingTests.cs b/tests/Highbyte.DotNet6502.Systems.Tests/Commodore64/C64VideoMemoryWriteTimingTests.cs new file mode 100644 index 000000000..b97ea0eed --- /dev/null +++ b/tests/Highbyte.DotNet6502.Systems.Tests/Commodore64/C64VideoMemoryWriteTimingTests.cs @@ -0,0 +1,67 @@ +using Highbyte.DotNet6502.Systems.Commodore64; +using Highbyte.DotNet6502.Systems.Commodore64.Config; +using Highbyte.DotNet6502.Systems.Commodore64.Render.Rasterizer; +using Highbyte.DotNet6502.Systems.Commodore64.Video; +using Highbyte.DotNet6502.Utils; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Highbyte.DotNet6502.Systems.Tests.Commodore64; + +/// +/// A CPU write into the VIC-II's bank is seen by the chip's fetches from the cycle after the +/// write on: the chip reads in a cycle's first clock phase and the CPU writes in its second, so +/// the fetch of the write's own cycle still sees the old byte, and so do the fetches of the +/// instruction's earlier cycles. A byte rewritten in the middle of a line therefore reaches the +/// screen from the column after the write's cycle, as on hardware. +/// +public class C64VideoMemoryWriteTimingTests +{ + private const ushort Start = 0x1000; + + [Theory] + [InlineData(0, 0x3FFF)] + [InlineData(1, 0x7FFF)] // the last byte of each bank: the idle byte's address there + [InlineData(2, 0xBFFF)] + [InlineData(3, 0xFFFF)] + public void A_write_to_the_idle_byte_shows_from_the_column_after_the_writes_cycle(int bank, int idleByteAddress) + { + // YSCROLL 7: the first bad line is 55, so lines 51-54 are idle lines inside the display + // window and show the byte at $3FFF, black for its set bits, in every column. The program + // starts in cycle index 24 of line 52: LDA #$FF (24, 25), STA to it (26, 27, 28, the write + // in 29), then a loop for the rest of the frame. The g-access of index 29 (the chip's cycle + // 30, column 14) reads before the write, that of index 30 (column 15) after it. + var c64 = C64.BuildC64(new C64Config + { + LoadROMs = false, + C64Model = "C64PAL", + Vic2Model = "PAL", + RenderProviderType = typeof(Vic2Rasterizer), + Vic2RasterizerPerLineSprites = true, + }, NullLoggerFactory.Instance); + c64.Mem.StoreData(Start, [0xA9, 0xFF, 0x8D, (byte)(idleByteAddress & 0xFF), (byte)(idleByteAddress >> 8), 0x4C, 0x05, 0x10]); + c64.CPU.PC = Start; + c64.Mem.Write(0xDD02, 0x3F); // CIA 2 port A's bank bits as outputs + c64.Mem.Write(0xDD00, (byte)(3 - bank)); // the bank, inverted + c64.Mem.Write(0xD016, 0xC8); + c64.Mem.Write(0xD021, 6); + c64.Mem.Write(0xD011, 0x1F); + c64.Vic2.Vic2Mem[0x3FFF] = 0x00; + var cyclesPerLine = c64.Vic2.Vic2Model.CyclesPerLine; + c64.Vic2.AdvanceRaster(52 * cyclesPerLine + 24); + + c64.ExecuteOneFrame(); + + var rasterizer = (Vic2Rasterizer)c64.RenderProvider!; + var foreground = rasterizer.CurrentFrontLayerBuffers[1].ToArray(); + var layout = c64.Vic2.ScreenLayouts.GetLayout(Vic2ScreenLayouts.LayoutType.VisibleNormalized, for24RowMode: false, for38ColMode: false); + var width = c64.Screen.VisibleWidth; + var y = layout.Screen.Start.Y + 52 - 51; + uint Fg(int column, int pixel) => foreground[y * width + layout.Screen.Start.X + column * 8 + pixel]; + + Assert.Equal(0u, Fg(0, 0)); // the old byte, blank, before the write + Assert.Equal(0u, Fg(11, 0)); // the instruction's own earlier cycles fetch the old byte + Assert.Equal(0u, Fg(14, 0)); // the write's cycle: the fetch comes first + Assert.NotEqual(0u, Fg(15, 0)); // from the cycle after the write: $FF, black + Assert.NotEqual(0u, Fg(39, 7)); + } +} diff --git a/tests/Highbyte.DotNet6502.Systems.Tests/Commodore64/Render/Vic2RasterizerPixelGeneratorTests.cs b/tests/Highbyte.DotNet6502.Systems.Tests/Commodore64/Render/Vic2RasterizerPixelGeneratorTests.cs index 09ea50a4e..26ec57443 100644 --- a/tests/Highbyte.DotNet6502.Systems.Tests/Commodore64/Render/Vic2RasterizerPixelGeneratorTests.cs +++ b/tests/Highbyte.DotNet6502.Systems.Tests/Commodore64/Render/Vic2RasterizerPixelGeneratorTests.cs @@ -63,7 +63,7 @@ public void DrawSprites_clips_right_edge_to_38_column_border() for (var rasterLine = 0; rasterLine < c64.Vic2.Vic2Model.TotalHeight; rasterLine++) { c64.Vic2.AdvanceRaster(c64.Vic2.Vic2Model.CyclesPerLine); - generator.OnAfterInstruction(); + generator.CatchUpToVic2(); } generator.OnEndFrame(); @@ -240,7 +240,7 @@ uint[] Render(bool openBorder) { c64.Mem.Write(0xD011, openBorder && rasterLine is 250 or 251 ? (byte)0x13 : (byte)0x1B); vic2.AdvanceRaster(vic2.Vic2Model.CyclesPerLine); - generator.OnAfterInstruction(); + generator.CatchUpToVic2(); } generator.OnEndFrame(); return foreground; @@ -291,7 +291,7 @@ public void A_sprite_that_begins_above_the_visible_area_shows_its_visible_rows_i { c64.Mem.Write(0xD011, rasterLine is 250 or 251 ? (byte)0x13 : (byte)0x1B); vic2.AdvanceRaster(vic2.Vic2Model.CyclesPerLine); - generator.OnAfterInstruction(); + generator.CatchUpToVic2(); } generator.OnEndFrame(); } @@ -327,7 +327,7 @@ public void A_sprite_that_runs_past_the_ntsc_frame_end_continues_in_the_frames_l { c64.Mem.Write(0xD011, rasterLine is 250 or 251 ? (byte)0x13 : (byte)0x1B); vic2.AdvanceRaster(vic2.Vic2Model.CyclesPerLine); - generator.OnAfterInstruction(); + generator.CatchUpToVic2(); } generator.OnEndFrame(); } @@ -605,7 +605,7 @@ public void Border_colour_written_on_every_line_of_a_frame_keeps_landing_at_its_ vic2.AdvanceRaster((ulong)writeCycle); c64.Mem.Write(0xD020, (byte)(rasterLine % 16)); vic2.AdvanceRaster(cyclesPerLine - (ulong)writeCycle); - generator.OnAfterInstruction(); + generator.CatchUpToVic2(); } // Every line of the top border must show its own two colours, split at the write. @@ -670,7 +670,7 @@ private static void RenderFrameWithMidLineWrite(C64 c64, Vic2RasterizerUintPixel { vic2.AdvanceRaster(cyclesPerLine); } - generator.OnAfterInstruction(); + generator.CatchUpToVic2(); } } @@ -795,7 +795,7 @@ private static (uint[] Background, uint[] Foreground) RenderFrame(C64 c64, Func< c64.Mem.Write(0xD011, d011ForRasterLine(rasterLine)); beforeRasterLine?.Invoke(rasterLine); vic2.AdvanceRaster(cyclesPerLine); - generator.OnAfterInstruction(); + generator.CatchUpToVic2(); } return (background, foreground); } diff --git a/tests/Highbyte.DotNet6502.Systems.Tests/Commodore64/Render/Vic2RasterizerSequencerPixelGeneratorTests.cs b/tests/Highbyte.DotNet6502.Systems.Tests/Commodore64/Render/Vic2RasterizerSequencerPixelGeneratorTests.cs index 35b7a323e..673a73c57 100644 --- a/tests/Highbyte.DotNet6502.Systems.Tests/Commodore64/Render/Vic2RasterizerSequencerPixelGeneratorTests.cs +++ b/tests/Highbyte.DotNet6502.Systems.Tests/Commodore64/Render/Vic2RasterizerSequencerPixelGeneratorTests.cs @@ -63,7 +63,7 @@ public void DrawSprites_clips_right_edge_to_38_column_border() for (var rasterLine = 0; rasterLine < c64.Vic2.Vic2Model.TotalHeight; rasterLine++) { c64.Vic2.AdvanceRaster(c64.Vic2.Vic2Model.CyclesPerLine); - generator.OnAfterInstruction(); + generator.CatchUpToVic2(); } generator.OnEndFrame(); @@ -243,7 +243,7 @@ uint[] Render(bool openBorder) { c64.Mem.Write(0xD011, openBorder && rasterLine is 250 or 251 ? (byte)0x13 : (byte)0x1B); vic2.AdvanceRaster(vic2.Vic2Model.CyclesPerLine); - generator.OnAfterInstruction(); + generator.CatchUpToVic2(); } generator.OnEndFrame(); return foreground; @@ -294,7 +294,7 @@ public void A_sprite_that_begins_above_the_visible_area_shows_its_visible_rows_i { c64.Mem.Write(0xD011, rasterLine is 250 or 251 ? (byte)0x13 : (byte)0x1B); vic2.AdvanceRaster(vic2.Vic2Model.CyclesPerLine); - generator.OnAfterInstruction(); + generator.CatchUpToVic2(); } generator.OnEndFrame(); } @@ -337,11 +337,11 @@ public void Clearing_the_y_expand_bit_mid_sprite_shows_the_rows_the_data_counter for (var rasterLine = 0; rasterLine < vic2.Vic2Model.TotalHeight; rasterLine++) { vic2.AdvanceRaster((ulong)(writeCycle - 1)); - generator.OnAfterInstruction(); + generator.CatchUpToVic2(); if (rasterLine == spriteY + 3) c64.Mem.Write(Vic2Addr.SPRITE_Y_EXPAND, 0x00); vic2.AdvanceRaster((ulong)(cyclesPerLine - (writeCycle - 1))); - generator.OnAfterInstruction(); + generator.CatchUpToVic2(); } generator.OnEndFrame(); @@ -389,7 +389,7 @@ public void A_sprite_that_runs_past_the_ntsc_frame_end_continues_in_the_frames_l { c64.Mem.Write(0xD011, rasterLine is 250 or 251 ? (byte)0x13 : (byte)0x1B); vic2.AdvanceRaster(vic2.Vic2Model.CyclesPerLine); - generator.OnAfterInstruction(); + generator.CatchUpToVic2(); } generator.OnEndFrame(); } @@ -574,7 +574,7 @@ private static (uint[] Background, uint[] Foreground) RenderPerLineSpriteFrame(C for (var rasterLine = 0; rasterLine < vic2.Vic2Model.TotalHeight; rasterLine++) { vic2.AdvanceRaster(vic2.Vic2Model.CyclesPerLine); - generator.OnAfterInstruction(); + generator.CatchUpToVic2(); } generator.OnEndFrame(); return (background, foreground); @@ -781,7 +781,7 @@ public void Border_colour_written_on_every_line_of_a_frame_keeps_landing_at_its_ vic2.AdvanceRaster((ulong)writeCycle); c64.Mem.Write(0xD020, (byte)(rasterLine % 16)); vic2.AdvanceRaster(cyclesPerLine - (ulong)writeCycle); - generator.OnAfterInstruction(); + generator.CatchUpToVic2(); } // Every line of the top border must show its own two colours, split at the write. @@ -963,7 +963,7 @@ public void A_bad_line_condition_created_mid_line_starts_the_row_there_with_the_ { vic2.AdvanceRaster(cyclesPerLine); } - generator.OnAfterInstruction(); + generator.CatchUpToVic2(); } var y = normalizedLayout.Screen.Start.Y + 68 - 51; // the line the condition was created on @@ -1011,7 +1011,7 @@ public void Display_enabled_mid_line_48_starts_bad_lines_from_the_cycle_after_th c64.Mem.Write(0xD011, 0x1B); // YSCROLL 3: line 51 is the next bad line vic2.AdvanceRaster(cyclesPerLine); } - generator.OnAfterInstruction(); + generator.CatchUpToVic2(); } Assert.Equal(expectedFirstCode, FirstCellCodeOnLine(c64, foreground, 51)); @@ -1044,7 +1044,7 @@ public void Display_enabled_at_the_edge_of_line_48_counts_only_up_to_its_last_cy { vic2.AdvanceRaster(cyclesPerLine); } - generator.OnAfterInstruction(); + generator.CatchUpToVic2(); } Assert.Equal(expectDisplay ? 1 : 0, FirstCellCodeOnLine(c64, foreground, 51)); @@ -1058,7 +1058,7 @@ private static void RenderDisplayedFrame(C64 c64, Vic2RasterizerSequencerPixelGe for (var rasterLine = 0; rasterLine < vic2.Vic2Model.TotalHeight; rasterLine++) { vic2.AdvanceRaster(vic2.Vic2Model.CyclesPerLine); - generator.OnAfterInstruction(); + generator.CatchUpToVic2(); } } @@ -1094,7 +1094,7 @@ public void The_g_access_of_the_cycle_a_dma_delay_starts_in_reads_38FF() { vic2.AdvanceRaster(cyclesPerLine); } - generator.OnAfterInstruction(); + generator.CatchUpToVic2(); } var normalizedLayout = c64.Vic2.ScreenLayouts.GetLayout(Vic2ScreenLayouts.LayoutType.VisibleNormalized, for24RowMode: false, for38ColMode: false); @@ -1266,7 +1266,7 @@ private static void RenderFrameWithMidLineWrite(C64 c64, Vic2RasterizerSequencer { vic2.AdvanceRaster(cyclesPerLine); } - generator.OnAfterInstruction(); + generator.CatchUpToVic2(); } } @@ -1391,7 +1391,7 @@ private static (uint[] Background, uint[] Foreground) RenderFrame(C64 c64, Func< c64.Mem.Write(0xD011, d011ForRasterLine(rasterLine)); beforeRasterLine?.Invoke(rasterLine); vic2.AdvanceRaster(cyclesPerLine); - generator.OnAfterInstruction(); + generator.CatchUpToVic2(); } return (background, foreground); }