diff --git a/harness/mame/RENDER_REPORT.md b/harness/mame/RENDER_REPORT.md new file mode 100644 index 0000000..21698f2 --- /dev/null +++ b/harness/mame/RENDER_REPORT.md @@ -0,0 +1,159 @@ +# VT1200 MAME renderer — the graphical milestone + +**Status: DONE.** The driver now paints the terminal's screen. The level-1 +self-test **CP437 character table is visible and legible**, and the level-1 +error screen it writes later is legible too. + +--- + +## 1. The defect + +MAME's TMS34010 renderer does **not** read VRAM. `tms340x0_ind16` +(`tms34010.cpp`) calls the driver's scanline callback for the active region and +paints only the blanking regions itself: + +```cpp +if (params.enabled) + m_scanline_ind16_cb(screen, bitmap, cliprect.min_y, ¶ms); // active pixels -> driver +... +/* blank out the blank regions */ +``` + +Our callback (`vt1200_state::scanline`) drew **nothing** when `m_render` was +true — it only blanked when `m_render` was false. So the active area was never +painted by anyone, and the frame dump was whatever the bitmap started as (all +zero = black on the first frame). The stale comment in `screen_update` — +*"Let the TMS34010 display controller do the rendering"* — was the +misconception; the controller delegates. + +## 2. What I implemented + +`vt1200_state::render_active_line()` (called from `scanline()`), an +active-pixel scanline renderer: + +* Blanks the whole scanline first (the device only repaints the H-blanking + regions, so stale bitmap data would otherwise show). +* Maps screen line `y` to framebuffer row `y - params->veblnk` and screen + x `heblnk..hsblnk` to framebuffer x `0..1023`. +* Reads each pixel from the video-RAM share: bit address + `0x04000000 + row*1024 + x`, i.e. word index `(row*64) + (x>>4)` and bit + `x&15` of that 16-bit word — **LSB-first within the word**. +* `VT1200_NO_RENDER=1` restores the old blank-everything behaviour. + +A new opt-in diagnostic `VT1200_DPYLOG=` (plus `VT1200_DPYLOG_FRAMES`) +dumps the TMS34010 display params per scanline; it is how the geometry below +was pinned. + +## 3. Which geometry I trusted, and why + +The real-machine facts I was given were **1280×1024, 160 bytes/scanline, +163840-byte framebuffer**. I trusted the **video-RAM base and the bit/pixel +model** from those facts, but I did **not** use 160 bytes/scanline — I used the +geometry the firmware itself demonstrates, and I can reconcile the two: + +| quantity | value I rendered | source / evidence | +|---|---|---| +| VRAM base | bit `0x04000000` | the mapping that unblocked the level-1 VIDEO MEMORY test; the reset routine's `MOVI 0x4000000,B4` sets it as the blitter bitmap base | +| row pitch | **1024 bits = 128 bytes = 0x40 words** | the firmware's own character blitter: the `PIXBLT` at `pc=3FDF9A00` writes glyph rows at word offsets `0x118C, 0x11CC, 0x120C …`, i.e. **+0x40 words per row** | +| bit order | LSB-first in each 16-bit word | the TMS34010 pixel order; only this order makes the text readable | +| active width | 1024 px | MAME derives `heblnk..hsblnk = 192..1216`, which is **exactly 1024 px — the framebuffer width** | +| screen line → row | `y - veblnk` | 1:1; `fb_row = scanline - params->veblnk` reproduces the dumped VRAM exactly | + +**The decisive test** was rendering the dumped VRAM content both ways: + +* pitch **128 bytes, LSB-first** → perfect text: + `LEVEL1/2 ERROR -> HOST PORT - 15 001EFF01` +* pitch **160 bytes** → sheared, unreadable gibberish. + +So the *framebuffer* is 128 bytes/line (1024 px). The given "160 bytes/ +scanline" is consistent with the **total** raster width (`1280/8 = 160`), +while only the 1024 active pixels are stored — and MAME's derived active span +of 1024 px independently agrees. The requested 160-byte number is therefore +not contradicted; it is just the total-raster figure, not the memory pitch. +In short: I trusted the real-machine base and mono/1bpp model, and the +real-machine active width (via MAME's 1024-px blanking span), but the memory +pitch comes from the firmware's own blitter because rendering proves it. + +MAME's derived **80×901** geometry is a separate artefact (the timing registers +`HEBLNK/HSBLNK/VTOTAL` are not initialised to the real monitor's values); +the renderer does not use those numbers to index memory. + +## 4. Frame dumps (before / after) + +Regenerate with: + +```sh +M=/home/arti/code/k-space/mame0289/vt1200 +VT1200_VRAM=1 VT1200_BOARD_MODEL=handshake VT1200_BOARD_BUSY_TICKS=2 \ +VT1200_BOARD_SWITCH="0640000D:70:70" VT1200_DUART=1 VT1200_DUART_SEL=1 \ +VT1200_DUMP_DIR=harness/mame VT1200_DUMP_FRAME= \ + $M vt1200 -rompath harness/mame/roms -video none -sound none -nothrottle \ + -debug -debugger none -seconds_to_run -autoboot_delay 0 +``` + +**Before:** the three pre-existing frame dumps (`vt1200-frame-60.ppm` and +friends) were byte-identical all-black — the active pixels were never painted. +They have been regenerated (and the `.ppm` files are gitignored as generated +artefacts). + +**After** (committed as PNG evidence): + +| file | what it shows | +|---|---| +| `harness/mame/vt1200-frame-160.png` | **the full CP437 character table** | +| `harness/mame/vt1200-frame-200.png` | the table being wiped (white block) | +| `harness/mame/vt1200-frame-600.png` | the level-1 error screen | + +The matching `.ppm` files are written by `VT1200_DUMP_FRAME` in the same +directory. + +## 5. Is the character table visible and legible? — **Yes** + +`vt1200-frame-160.png` shows the complete 32×16 CP437 font table, readable +glyph by glyph: + +``` +`abcdefghijklmnopqrstuvwxyz{|}~⌂ +!"#$%&'()*+,-./0123456789:;<=>? +@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_ +ÇüéâäàåçêëèïîìÄÅÉæÆôöòûùÿÖÜ¢£¥₧ƒ +áíóúñѪº¿⌐¬½¼¡«»░▒▓│┤╡╢╖╕╣║╗╝╜╛┐ +... +:¤¤¤·ΔφÃΓ~¤θÑÂφ≡∃ΠΨΩ∴∆Θ... +¬¤Üø>δεφψηιοκλϑνόπρσςƒσξις+→↓ +``` + +This matches the real machine's boot order (character table first, then a +wipe): frame 160 is the table, frame 200 the wipe, frames 90–140 a full-screen +test pattern, and frame 600 the later level-1 error screen. The level-1 pie +graph is **not** reached because the firmware is stuck at the (capped, +handed-over) HOST PORT gate — as expected. + +## 6. Regression + +With **no** `VT1200_*` environment variables: + +``` +PC=3FDCE400 samples=199 +``` + +Holds — identical to before the change. The renderer only affects the bitmap; +it never touches CPU state, and with no `VT1200_VRAM` the video-RAM share is +absent, so the code just blanks the line (as it did before). + +## 7. Unresolved / caveats + +* **Vertical mapping is empirical.** I map framebuffer row `= scanline - + veblnk`; the device's reconstructed `rowaddr` advances only every second + scanline (DUDATE-derived), which does not describe the memory pitch, so I do + not use it. The visible 864 lines show framebuffer rows 0–863; if the real + VEBLNK/VSBLNK differ (they are known to be uninitialised) the last rows are + simply not dumped. +* **The screen is still MAME's derived 1280×901**, with the 1024×864 image + drawn into the active span (x 192..1215). The frame dump therefore has black + borders. Reconfiguring the raster to the real geometry is a separate change. +* **Main RAM is still 256 KiB** — deliberately not touched here (`BRINGUP_LOG` + flags enlarging it as a separate experiment). +* The video-RAM dump for a single frame is ~58 MB because the share covers + the whole `0x04000000..0x1fffffff` mapping; the framebuffer itself is the + first ~128 KiB. diff --git a/harness/mame/vt1200-frame-160.png b/harness/mame/vt1200-frame-160.png new file mode 100644 index 0000000..4ea52c2 Binary files /dev/null and b/harness/mame/vt1200-frame-160.png differ diff --git a/harness/mame/vt1200-frame-200.png b/harness/mame/vt1200-frame-200.png new file mode 100644 index 0000000..7ef9062 Binary files /dev/null and b/harness/mame/vt1200-frame-200.png differ diff --git a/harness/mame/vt1200-frame-600.png b/harness/mame/vt1200-frame-600.png new file mode 100644 index 0000000..fb05227 Binary files /dev/null and b/harness/mame/vt1200-frame-600.png differ diff --git a/harness/mame/vt1200.cpp b/harness/mame/vt1200.cpp index 523d591..ff7cb0b 100644 --- a/harness/mame/vt1200.cpp +++ b/harness/mame/vt1200.cpp @@ -54,6 +54,16 @@ VT1200_DUART_LB1/LB2=1 external TX->RX loopback on DUART1 / DUART2. VT1200_DUARTLOG= every DUART access (not de-duplicated). + Video: the driver paints the active scanlines itself, reading the + framebuffer out of video RAM (see render_active_line). VT1200_NO_RENDER=1 + blanks instead. Dumps: + + VT1200_DUMP_FRAME= write frame as vt1200-frame-.ppm + VT1200_DUMP_DIR= directory for the dumps (default ".") + VT1200_DPYLOG= dump the TMS34010 display params, one line + per scanline, for the first + VT1200_DPYLOG_FRAMES frames (default 1) + VT1200_DUART also wires each DUART's IRQ output to the TMS34010's external interrupt inputs (DUART1 -> INT1, DUART2 -> INT2). This is required: the level-1 host-port test's transmit is entirely ISR-driven (the ISR writes @@ -102,6 +112,20 @@ namespace { // that uses byte addressing). static const u32 BOARD_BASE[4] = { VT1200_WIN0, VT1200_WIN1, VT1200_WIN2, VT1200_WIN3 }; +// --- Framebuffer geometry (measured, not derived from MAME) ----------------- +// Video RAM is mapped at bit address 0x04000000 (the base that unblocks the +// level-1 VIDEO MEMORY test). The firmware's own blitter writes it as a +// linear 1-bit-per-pixel buffer with a 0x40-word (128-byte) row pitch; the +// TMS34010 paints the visible pixels LSB-first within each 16-bit word. +// Verified by rendering the dumped VRAM: at this geometry the level-1 error +// screen is legible ("LEVEL1/2 ERROR -> HOST PORT - 15"); at the derived +// 160-byte / 80-word pitch MAME computed from HEBLNK/HSBLNK/HTOTAL the text is +// sheared and unreadable. See RENDER_REPORT.md. +static const u32 VRAM_BASE_BITS = 0x04000000; // where the framebuffer lives +static const int VMEM_PITCH_BITS = 1024; // 0x40 words = 128 bytes +static const int VMEM_WIDTH = 1024; // visible pixels per row +static const int VMEM_HEIGHT = 1024; // rows in the framebuffer + class vt1200_state : public driver_device { public: @@ -125,6 +149,8 @@ public: // scanline callback: renders (or blanks) the line, and can dump a frame void scanline(screen_device &screen, bitmap_ind16 &bitmap, int scanline, const tms340x0_device::display_params *params); + void render_active_line(bitmap_ind16 &bitmap, int scanline, + const tms340x0_device::display_params *params); void dump_frame(screen_device &screen, bitmap_ind16 &bitmap); void dump_ram(); void dump_vram(); @@ -189,6 +215,13 @@ private: long m_dump_frame = -1; std::string m_dump_dir = "."; + // VT1200_DPYLOG=: dump the TMS34010 display params for the first + // VT1200_DPYLOG_FRAMES frames (default 1), one line per visible scanline. + // Used to pin the VRAM fetch address/geometry against the real machine's + // 1280x1024 (160 bytes/line) numbers. + FILE *m_dpylog = nullptr; + long m_dpylog_frames = 1; + // VT1200_DUMP_RAM= writes main RAM at that frame to // vt1200-ram-.bin so the framebuffer can be located offline. long m_dump_ram = -1; @@ -358,6 +391,14 @@ void vt1200_state::machine_start() m_dump_vram = strtol(f, nullptr, 0); if (const char *d = getenv("VT1200_DUMP_DIR")) m_dump_dir = d; + if (const char *f = getenv("VT1200_DPYLOG")) + { + m_dpylog = fopen(f, "w"); + if (m_dpylog) + fprintf(m_dpylog, "# line rowaddr coladdr yoff heblnk hsblnk veblnk vsblnk vcount\n"); + } + if (const char *f = getenv("VT1200_DPYLOG_FRAMES")) + m_dpylog_frames = strtol(f, nullptr, 0); if (const char *p = getenv("VT1200_LOG_UNMAPPED")) { @@ -640,7 +681,7 @@ void vt1200_state::program_map(address_map &map) // 0x40000C in the same >>4 units, i.e. bit 0x040000C0), which is why the // firmware reports error 22 and restarts before it ever draws anything. if (getenv("VT1200_VRAM")) - map(0x04000000, 0x1fffffff).ram().share("vram"); + map(VRAM_BASE_BITS, 0x1fffffff).ram().share("vram"); // Transparent logging proxy over the first 64 KiB of video memory, registered // last so it wins over the .ram() above. off = (bitaddr - base) >> 4, so this @@ -885,6 +926,54 @@ void vt1200_state::log_duart(int win, char kind, u32 reg, u16 value) fflush(m_duartlog); } +// Copy one active scanline out of the video RAM framebuffer. +// +// The TMS34010 screen-refresh address is reconstructed by the device into +// `params`, but the firmware's timing registers (HEBLNK/HSBLNK/VTOTAL) are not +// initialised to the real machine's values, so the geometry MAME derives from +// them (a 1280x901 screen, active x 192..1215) does not describe the memory +// layout. The measured memory layout does: +// +// * base bit 0x04000000 (the mapped video RAM) +// * pitch 1024 bits (0x40 words / 128 bytes) per row — taken from the +// firmware's own blitter, which advances its destination by +// 0x40 words per glyph row (pc=3FDF9A00 in VRAM write logs) +// * bit order LSB-first within each 16-bit word — the TMS34010 pixel order +// * active x the device's heblnk..hsblnk span is 1024 pixels, exactly the +// framebuffer width, so screen x-heblnk maps to framebuffer x +// +// The screen line maps 1:1 to a framebuffer row, offset by the vertical +// blanking start (params->veblnk). +void vt1200_state::render_active_line(bitmap_ind16 &bitmap, int scanline, + const tms340x0_device::display_params *params) +{ + // Blank first: the device only repaints the horizontal blanking regions, + // so anything we leave here would be stale bitmap contents. + std::fill_n(&bitmap.pix(scanline, 0), bitmap.width(), u16(0)); + + if (!m_vram) // no video RAM mapped -> nothing to draw + return; + + const int fby = scanline - (int)params->veblnk; + if (fby < 0 || fby >= VMEM_HEIGHT) + return; + + const u16 *const vram = m_vram.target(); + const int x0 = (int)params->heblnk; + const int x1 = (int)params->hsblnk; + const u32 row_bit = (u32)fby * VMEM_PITCH_BITS; + + for (int fx = 0; fx < VMEM_WIDTH; fx++) + { + const int x = x0 + fx; + if (x >= x1 || x >= bitmap.width()) + break; + const u32 bit = row_bit + (u32)fx; + // Address-map units are bits, so a 16-bit word is 16 units. + bitmap.pix(scanline, x) = (vram[bit >> 4] >> (bit & 15)) & 1; + } +} + void vt1200_state::scanline(screen_device &screen, bitmap_ind16 &bitmap, int scanline, const tms340x0_device::display_params *params) { @@ -894,7 +983,22 @@ void vt1200_state::scanline(screen_device &screen, bitmap_ind16 &bitmap, int sca osd_printf_info("vt1200: scanline#%d line=%d enabled=%d\n", calls, scanline, params ? (params->enabled ? 1 : 0) : -1); } - if (!m_render) + if (m_dpylog && params && (long)screen.frame_number() < m_dpylog_frames) + { + fprintf(m_dpylog, "%d %04X %04X %d %d %d %d %d %d\n", + scanline, params->rowaddr, params->coladdr, params->yoffset, + params->heblnk, params->hsblnk, params->veblnk, params->vsblnk, params->vcount); + fflush(m_dpylog); + } + + // Paint the active pixels ourselves. MAME's TMS34010 renderer does NOT + // read VRAM; tms340x0_ind16 calls this callback for the active region and + // only paints the blanking regions itself. If we leave the active pixels + // untouched the frame stays whatever the bitmap was (all black on the + // first frame). + if (m_render && params && params->enabled) + render_active_line(bitmap, scanline, params); + else std::fill_n(&bitmap.pix(scanline, 0), bitmap.width(), u16(0)); // Dump on the last visible line of the target frame: by then the whole @@ -962,7 +1066,9 @@ uint32_t vt1200_state::screen_update(screen_device &screen, bitmap_ind16 &bitmap screen.width(), screen.height()); } - // Let the TMS34010 display controller do the rendering. + // The TMS34010 display controller does not read VRAM: it calls our + // scanline callback for the active region (which paints it from the + // framebuffer) and blanks the blanking region itself. const uint32_t changed = m_maincpu->tms340x0_ind16(screen, bitmap, cliprect); // Dump once the whole frame has been rendered (the video manager calls us