Audit of the color-monitor decode paths in CassoEmuCore/Video/. Two defects, plus one idealization I think we should keep and document rather than fix.
Scope: HGR and DHGR color paths only. Lo-res, text and 80-column were not audited.
Background
The Apple II has no color hardware. Video is a 1-bit dot stream clocked at 14.318 MHz — exactly 4x the NTSC colorburst at 3.579545 MHz. An NTSC decoder cannot separate luminance detail at that rate from chroma, so dot patterns near the subcarrier become color. Color is therefore a function of the recent dot history and the local subcarrier phase, continuously, with no boundaries anywhere in the signal. Both defects below come from modelling color as a property of a fixed grid cell instead.
Defect 1 — DHGR decodes fixed 4-dot cells instead of a sliding window
AppleDoubleHiResMode.cpp:236 groups dots into cells locked to absolute positions 0-3, 4-7, 8-11 ... and paints one color across each cell. The hardware has no such grid: the subcarrier does not reset at multiples of 4 dots, so a feature that does not begin on a multiple of 4 is decoded against the wrong window.
Concrete case — dots 2,3,4,5 lit, everything else dark:
|
dots 0-3 |
dots 4-7 |
| Casso today |
(d2 -> 8) | (d3 -> 1) = 9 -> Orange |
(d4 -> 2) | (d5 -> 4) = 6 -> Medium Blue |
| Hardware |
one solid 4-dot color band spanning dots 2-5 |
dark |
A 4-dot feature renders 8 dots wide, in two colors, neither of them the right one. The decode is correct only for cell-aligned content; for the three of four phases that are not aligned, both the color and the extent are wrong. DHGR art is routinely not cell-aligned, so this is visible on ordinary content rather than a corner case.
Fix: decode a sliding 4-dot window per output dot rather than one window per 4-dot cell — for each dot position, index the palette with the four dots ending at that position, keeping the existing rotation. Same table, same per-dot cost, no new state.
Not affected, and worth keeping: the palette rotation (d0->2)|(d1->4)|(d2->8)|(d3->1) at AppleDoubleHiResMode.cpp:238 is correct — it checks out against the Apple IIe Technical Note #3 magenta pattern cited in the comment above it (aux $08 lights only dot 3, giving index 1 = magenta). Reading main and aux from the banks directly rather than through the bus is also correct, for the reason the ReadDhrByte comment gives.
Defect 2 — the HGR color path ignores the half-dot shift its own monochrome path models
AppleHiResMode::Render runs two different physical models in the same function.
The monochrome pass at AppleHiResMode.cpp:178 places each pixel at slot = x * 2 + palBit in the 560-half-dot array and OR-s overlaps. That is right, and the comment above it explains why.
The color pass at AppleHiResMode.cpp:214 tests pixels[x - 1] / pixels[x + 1] on raw 280-pixel indices, where the half-dot shift does not exist, and calls any adjacent pair white (:219).
At a byte transition from bit7=1 to bit7=0, the shifted pixel occupies half-dots 13-14 and its unshifted neighbor occupies 14-15. They overlap, covering three half-dots, not four. Three quarters of a subcarrier period is a bright color, not white — but the color path paints white because both bits are set and adjacent. The same scanline decoded monochrome and decoded color disagree about whether those two dots even touch.
This is also why the color path cannot express the result today: it renders 280 pixels doubled to 560 at AppleHiResMode.cpp:238, so half-dot positions are not representable in color mode even in principle.
Fix: run the color decode over the 560-half-dot array the monochrome path already builds, instead of over pixels[280]. The half-dot array is already correct; it is just not used by the color pass.
Not a defect — no fringing at run ends
leftOn || rightOn -> white makes every run of two or more dots pure white end to end. Real HGR shows colored fringes at both ends of a white run, and more than six colors overall.
I would leave this as-is. It is a deliberate idealization, and it is the reason our 80-column text is legible: a faithful composite decode fringes text heavily, which is authentic — it is why //e owners who lived in 80 columns bought monochrome monitors — but it is the wrong default for a modern user. Reproducing it belongs behind a separate "composite" monitor option, not in the default color path.
Notes
Both fixes are local to the existing renderers. Neither requires the common-dot-stream refactor that a full composite decode would need.
Existing screenshot baselines will change for any DHGR content that is not cell-aligned, and for HGR content with palette-bit transitions inside a run. Those baselines are currently recording the bugs.
Audit of the color-monitor decode paths in
CassoEmuCore/Video/. Two defects, plus one idealization I think we should keep and document rather than fix.Scope: HGR and DHGR color paths only. Lo-res, text and 80-column were not audited.
Background
The Apple II has no color hardware. Video is a 1-bit dot stream clocked at 14.318 MHz — exactly 4x the NTSC colorburst at 3.579545 MHz. An NTSC decoder cannot separate luminance detail at that rate from chroma, so dot patterns near the subcarrier become color. Color is therefore a function of the recent dot history and the local subcarrier phase, continuously, with no boundaries anywhere in the signal. Both defects below come from modelling color as a property of a fixed grid cell instead.
Defect 1 — DHGR decodes fixed 4-dot cells instead of a sliding window
AppleDoubleHiResMode.cpp:236groups dots into cells locked to absolute positions 0-3, 4-7, 8-11 ... and paints one color across each cell. The hardware has no such grid: the subcarrier does not reset at multiples of 4 dots, so a feature that does not begin on a multiple of 4 is decoded against the wrong window.Concrete case — dots 2,3,4,5 lit, everything else dark:
(d2 -> 8) | (d3 -> 1)= 9 -> Orange(d4 -> 2) | (d5 -> 4)= 6 -> Medium BlueA 4-dot feature renders 8 dots wide, in two colors, neither of them the right one. The decode is correct only for cell-aligned content; for the three of four phases that are not aligned, both the color and the extent are wrong. DHGR art is routinely not cell-aligned, so this is visible on ordinary content rather than a corner case.
Fix: decode a sliding 4-dot window per output dot rather than one window per 4-dot cell — for each dot position, index the palette with the four dots ending at that position, keeping the existing rotation. Same table, same per-dot cost, no new state.
Not affected, and worth keeping: the palette rotation
(d0->2)|(d1->4)|(d2->8)|(d3->1)atAppleDoubleHiResMode.cpp:238is correct — it checks out against the Apple IIe Technical Note #3 magenta pattern cited in the comment above it (aux$08lights only dot 3, giving index 1 = magenta). Reading main and aux from the banks directly rather than through the bus is also correct, for the reason theReadDhrBytecomment gives.Defect 2 — the HGR color path ignores the half-dot shift its own monochrome path models
AppleHiResMode::Renderruns two different physical models in the same function.The monochrome pass at
AppleHiResMode.cpp:178places each pixel atslot = x * 2 + palBitin the 560-half-dot array and OR-s overlaps. That is right, and the comment above it explains why.The color pass at
AppleHiResMode.cpp:214testspixels[x - 1]/pixels[x + 1]on raw 280-pixel indices, where the half-dot shift does not exist, and calls any adjacent pair white (:219).At a byte transition from bit7=1 to bit7=0, the shifted pixel occupies half-dots 13-14 and its unshifted neighbor occupies 14-15. They overlap, covering three half-dots, not four. Three quarters of a subcarrier period is a bright color, not white — but the color path paints white because both bits are set and adjacent. The same scanline decoded monochrome and decoded color disagree about whether those two dots even touch.
This is also why the color path cannot express the result today: it renders 280 pixels doubled to 560 at
AppleHiResMode.cpp:238, so half-dot positions are not representable in color mode even in principle.Fix: run the color decode over the 560-half-dot array the monochrome path already builds, instead of over
pixels[280]. The half-dot array is already correct; it is just not used by the color pass.Not a defect — no fringing at run ends
leftOn || rightOn -> whitemakes every run of two or more dots pure white end to end. Real HGR shows colored fringes at both ends of a white run, and more than six colors overall.I would leave this as-is. It is a deliberate idealization, and it is the reason our 80-column text is legible: a faithful composite decode fringes text heavily, which is authentic — it is why //e owners who lived in 80 columns bought monochrome monitors — but it is the wrong default for a modern user. Reproducing it belongs behind a separate "composite" monitor option, not in the default color path.
Notes
Both fixes are local to the existing renderers. Neither requires the common-dot-stream refactor that a full composite decode would need.
Existing screenshot baselines will change for any DHGR content that is not cell-aligned, and for HGR content with palette-bit transitions inside a run. Those baselines are currently recording the bugs.