mirror of
https://git.k-space.ee/arti/vt1200.git
synced 2026-10-30 12:03:54 +02:00
harness: render the TMS34010 active scanlines from video RAM
MAME's TMS34010 renderer does not read VRAM: tms340x0_ind16 calls the driver's scanline callback for the active region and paints only the blanking regions. Our callback drew nothing, so the active area was never painted and every frame dump was all-black. Add render_active_line(), called from scanline(): it reads the framebuffer out of the video-RAM share at bit 0x04000000 with a measured 1024-bit (0x40-word, 128-byte) row pitch and LSB-first bit order within each 16-bit word. The pitch comes from the firmware's own character blitter (PIXBLT at pc=3FDF9A00 advances its destination by 0x40 words per glyph row); at the 160-byte pitch MAME derived from HEBLNK/HSBLNK/HTOTAL the text is sheared and unreadable. Validated: frame 160 shows the level-1 CP437 character table legibly, frame 600 the level-1 error screen. VT1200_DPYLOG added as an opt-in per-scanline display-params dump. Regression (no VT1200_* env): PC=3FDCE400 samples=199 -- holds. See harness/mame/RENDER_REPORT.md.
This commit is contained in:
1 parent
bcbd7924af
commit
571850e564
5 files changed
+268
-3
No files matched your search
@@ -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=<path>` (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=<n> \
|
||||
$M vt1200 -rompath harness/mame/roms -video none -sound none -nothrottle \
|
||||
-debug -debugger none -seconds_to_run <n/50+2> -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.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 5.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
+109
-3
@@ -54,6 +54,16 @@
|
||||
VT1200_DUART_LB1/LB2=1 external TX->RX loopback on DUART1 / DUART2.
|
||||
VT1200_DUARTLOG=<path> 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=<n> write frame <n> as vt1200-frame-<n>.ppm
|
||||
VT1200_DUMP_DIR=<dir> directory for the dumps (default ".")
|
||||
VT1200_DPYLOG=<path> 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=<path>: 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=<frame> writes main RAM at that frame to
|
||||
// vt1200-ram-<frame>.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
|
||||
|
||||
Reference in new issue
Block a user