velxio/test/test_custom_chips/sdk/examples/galaksija-ram-display.c

226 lines
11 KiB
C
Raw Normal View History

fix(chipbus): Galaksija boots + displays + types live in the browser The gallery example loaded but the Z80 never visibly ran: the screen stayed frozen on garbage. Two multi-chip async-load races, neither caught by the existing headless tests (which drive RESET manually and attach the display before boot): 1. RESET edge-vs-level race. The Z80 only left reset on the RISING edge of RESET (a pin watch). In the browser the 7 chips instantiate asynchronously, so the small power-on-reset chip releases RESET before the larger Z80 has registered its watch -> the edge is lost and the CPU stays in reset forever. Fix: on_clock samples the RESET level (hardware-accurate; RESET is level-sensitive) so a missed edge self-corrects. An undriven RESET reads low, so the CPU safely stays in reset until something drives it high. Repro/guard: chipbus-galaksija-reset-race (race ordering must still boot). 2. Display-snoop load-order race. galaksija-display was a passive write-snoop; the ROM paints the screen ONCE at boot then idles, so a display that comes up late misses every write and shows stale content forever. A snoop cannot recover writes it never saw. Fix: fold the screen into the RAM chip (galaksija-ram-display) and render from the ACTUAL video RAM (0x2800-0x2BFF, internal 0x0800 with A0-A12 wiring) on a ~30 fps timer - correct regardless of load order, exactly how the real machine scans video RAM. Repro/guard: chipbus-galaksija-display-snoop-race (late snoop shows nothing) + chipbus-galaksija-ram-display (renders even when first paint is post-boot). The example now has 6 chips (RAM+display merged, gdisp dropped), 76 wires. Verified live in the browser: boots to "@'READY", shows the ">" prompt, and pressing A echoes ">A_" through keyboard -> Z80 -> video RAM -> display. The full chipbus suite is 45/45. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 01:47:30 +07:00
/*
* galaksija-ram-display - 64 KB SRAM that ALSO renders the Galaksija text
* screen from its own video RAM (Phase 3, project/multichip-bus/).
*
* This is galaksija-ram (64 KB, yields 0x2000-0x203F reads to the keyboard)
* with the display folded in. Crucially it renders from the ACTUAL contents of
* its video RAM (0x2800-0x2BFF) on a ~30 fps timer, NOT from snooped bus writes.
*
* The earlier split (separate galaksija-ram + galaksija-display) made the
* display a passive write-snoop. That loses the picture whenever it misses a
* write: the Galaksija ROM paints the whole screen ONCE at boot then idles
* polling the keyboard, so a snoop that comes up late (the chips load
* asynchronously in the browser) shows stale/blank content forever. Reading the
* memory it already owns makes the display correct regardless of load order -
* exactly how the real machine generates its picture by scanning video RAM.
*
* Pin contract: idealised 64 KB byte-wide SRAM.
* A0..A15 input 16-bit address
* D0..D7 bidirectional 8-bit data (output on read, input on write)
* CE/OE/WE input active-low chip/output/write enables
* VCC, GND power
* Read 0x2000-0x203F is released (the memory-mapped keyboard drives it).
*
* Video RAM: the ROM stores ASCII codes at 0x2800-0x2BFF (32x16). Rendered with
* the public-domain IBM/VGA 8x8 font (font8x8 by Daniel Hepper / Marcel
* Sondaar), green-on-black phosphor; 1 byte/row, bit 0 (LSB) = leftmost pixel.
*/
#include "velxio-chip.h"
#include <stdint.h>
#include <stdbool.h>
#include <stdlib.h>
#define RAM_SIZE 0x10000 /* 64 KB */
/* Video RAM as THIS chip sees it. The board wires A0-A12 (A13 selects the chip
via CE), so the CPU's 0x2000-0x3FFF window maps to internal mem[0x0000-0x1FFF]
and the 0x2800 video RAM lands at internal 0x0800. The memory-mapped keyboard
(0x2000-0x203F real -> 0x00-0x3F here) is yielded in update_outputs(). */
#define VRAM_BASE 0x0800
#define COLS 32
#define ROWS 16
#define FB_W (COLS*8)
#define FB_H (ROWS*8)
static const uint8_t font8x8[1024] = {
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x18,0x3c,0x3c,0x18,0x18,0x00,0x18,0x00,
0x36,0x36,0x00,0x00,0x00,0x00,0x00,0x00,0x36,0x36,0x7f,0x36,0x7f,0x36,0x36,0x00,
0x0c,0x3e,0x03,0x1e,0x30,0x1f,0x0c,0x00,0x00,0x63,0x33,0x18,0x0c,0x66,0x63,0x00,
0x1c,0x36,0x1c,0x6e,0x3b,0x33,0x6e,0x00,0x06,0x06,0x03,0x00,0x00,0x00,0x00,0x00,
0x18,0x0c,0x06,0x06,0x06,0x0c,0x18,0x00,0x06,0x0c,0x18,0x18,0x18,0x0c,0x06,0x00,
0x00,0x66,0x3c,0xff,0x3c,0x66,0x00,0x00,0x00,0x0c,0x0c,0x3f,0x0c,0x0c,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x0c,0x0c,0x06,0x00,0x00,0x00,0x3f,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x0c,0x0c,0x00,0x60,0x30,0x18,0x0c,0x06,0x03,0x01,0x00,
0x3e,0x63,0x73,0x7b,0x6f,0x67,0x3e,0x00,0x0c,0x0e,0x0c,0x0c,0x0c,0x0c,0x3f,0x00,
0x1e,0x33,0x30,0x1c,0x06,0x33,0x3f,0x00,0x1e,0x33,0x30,0x1c,0x30,0x33,0x1e,0x00,
0x38,0x3c,0x36,0x33,0x7f,0x30,0x78,0x00,0x3f,0x03,0x1f,0x30,0x30,0x33,0x1e,0x00,
0x1c,0x06,0x03,0x1f,0x33,0x33,0x1e,0x00,0x3f,0x33,0x30,0x18,0x0c,0x0c,0x0c,0x00,
0x1e,0x33,0x33,0x1e,0x33,0x33,0x1e,0x00,0x1e,0x33,0x33,0x3e,0x30,0x18,0x0e,0x00,
0x00,0x0c,0x0c,0x00,0x00,0x0c,0x0c,0x00,0x00,0x0c,0x0c,0x00,0x00,0x0c,0x0c,0x06,
0x18,0x0c,0x06,0x03,0x06,0x0c,0x18,0x00,0x00,0x00,0x3f,0x00,0x00,0x3f,0x00,0x00,
0x06,0x0c,0x18,0x30,0x18,0x0c,0x06,0x00,0x1e,0x33,0x30,0x18,0x0c,0x00,0x0c,0x00,
0x3e,0x63,0x7b,0x7b,0x7b,0x03,0x1e,0x00,0x0c,0x1e,0x33,0x33,0x3f,0x33,0x33,0x00,
0x3f,0x66,0x66,0x3e,0x66,0x66,0x3f,0x00,0x3c,0x66,0x03,0x03,0x03,0x66,0x3c,0x00,
0x1f,0x36,0x66,0x66,0x66,0x36,0x1f,0x00,0x7f,0x46,0x16,0x1e,0x16,0x46,0x7f,0x00,
0x7f,0x46,0x16,0x1e,0x16,0x06,0x0f,0x00,0x3c,0x66,0x03,0x03,0x73,0x66,0x7c,0x00,
0x33,0x33,0x33,0x3f,0x33,0x33,0x33,0x00,0x1e,0x0c,0x0c,0x0c,0x0c,0x0c,0x1e,0x00,
0x78,0x30,0x30,0x30,0x33,0x33,0x1e,0x00,0x67,0x66,0x36,0x1e,0x36,0x66,0x67,0x00,
0x0f,0x06,0x06,0x06,0x46,0x66,0x7f,0x00,0x63,0x77,0x7f,0x7f,0x6b,0x63,0x63,0x00,
0x63,0x67,0x6f,0x7b,0x73,0x63,0x63,0x00,0x1c,0x36,0x63,0x63,0x63,0x36,0x1c,0x00,
0x3f,0x66,0x66,0x3e,0x06,0x06,0x0f,0x00,0x1e,0x33,0x33,0x33,0x3b,0x1e,0x38,0x00,
0x3f,0x66,0x66,0x3e,0x36,0x66,0x67,0x00,0x1e,0x33,0x07,0x0e,0x38,0x33,0x1e,0x00,
0x3f,0x2d,0x0c,0x0c,0x0c,0x0c,0x1e,0x00,0x33,0x33,0x33,0x33,0x33,0x33,0x3f,0x00,
0x33,0x33,0x33,0x33,0x33,0x1e,0x0c,0x00,0x63,0x63,0x63,0x6b,0x7f,0x77,0x63,0x00,
0x63,0x63,0x36,0x1c,0x1c,0x36,0x63,0x00,0x33,0x33,0x33,0x1e,0x0c,0x0c,0x1e,0x00,
0x7f,0x63,0x31,0x18,0x4c,0x66,0x7f,0x00,0x1e,0x06,0x06,0x06,0x06,0x06,0x1e,0x00,
0x03,0x06,0x0c,0x18,0x30,0x60,0x40,0x00,0x1e,0x18,0x18,0x18,0x18,0x18,0x1e,0x00,
0x08,0x1c,0x36,0x63,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,
0x0c,0x0c,0x18,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x1e,0x30,0x3e,0x33,0x6e,0x00,
0x07,0x06,0x06,0x3e,0x66,0x66,0x3b,0x00,0x00,0x00,0x1e,0x33,0x03,0x33,0x1e,0x00,
0x38,0x30,0x30,0x3e,0x33,0x33,0x6e,0x00,0x00,0x00,0x1e,0x33,0x3f,0x03,0x1e,0x00,
0x1c,0x36,0x06,0x0f,0x06,0x06,0x0f,0x00,0x00,0x00,0x6e,0x33,0x33,0x3e,0x30,0x1f,
0x07,0x06,0x36,0x6e,0x66,0x66,0x67,0x00,0x0c,0x00,0x0e,0x0c,0x0c,0x0c,0x1e,0x00,
0x30,0x00,0x30,0x30,0x30,0x33,0x33,0x1e,0x07,0x06,0x66,0x36,0x1e,0x36,0x67,0x00,
0x0e,0x0c,0x0c,0x0c,0x0c,0x0c,0x1e,0x00,0x00,0x00,0x33,0x7f,0x7f,0x6b,0x63,0x00,
0x00,0x00,0x1f,0x33,0x33,0x33,0x33,0x00,0x00,0x00,0x1e,0x33,0x33,0x33,0x1e,0x00,
0x00,0x00,0x3b,0x66,0x66,0x3e,0x06,0x0f,0x00,0x00,0x6e,0x33,0x33,0x3e,0x30,0x78,
0x00,0x00,0x3b,0x6e,0x66,0x06,0x0f,0x00,0x00,0x00,0x3e,0x03,0x1e,0x30,0x1f,0x00,
0x08,0x0c,0x3e,0x0c,0x0c,0x2c,0x18,0x00,0x00,0x00,0x33,0x33,0x33,0x33,0x6e,0x00,
0x00,0x00,0x33,0x33,0x33,0x1e,0x0c,0x00,0x00,0x00,0x63,0x6b,0x7f,0x7f,0x36,0x00,
0x00,0x00,0x63,0x36,0x1c,0x36,0x63,0x00,0x00,0x00,0x33,0x33,0x33,0x3e,0x30,0x1f,
0x00,0x00,0x3f,0x19,0x0c,0x26,0x3f,0x00,0x38,0x0c,0x0c,0x07,0x0c,0x0c,0x38,0x00,
0x18,0x18,0x18,0x00,0x18,0x18,0x18,0x00,0x07,0x0c,0x0c,0x38,0x0c,0x0c,0x07,0x00,
0x6e,0x3b,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
};
typedef struct {
vx_pin a[16];
vx_pin d[8];
vx_pin ce, oe, we, vcc, gnd;
uint8_t* mem;
bool driving;
int we_last;
vx_buffer fb;
vx_timer paint;
} chip_t;
static chip_t G;
static uint8_t fbpix[FB_W*FB_H*4];
static uint16_t read_addr(void) {
uint16_t v = 0;
for (int i = 0; i < 16; i++) if (vx_pin_read(G.a[i])) v |= (1u << i);
return v;
}
static uint8_t read_data_bus(void) {
uint8_t v = 0;
for (int i = 0; i < 8; i++) if (vx_pin_read(G.d[i])) v |= (1u << i);
return v;
}
static void drive_data(uint8_t v) {
for (int i = 0; i < 8; i++) { vx_pin_set_mode(G.d[i], VX_OUTPUT); vx_pin_write(G.d[i], (v >> i) & 1); }
G.driving = true;
}
static void release_data(void) {
if (!G.driving) return;
for (int i = 0; i < 8; i++) vx_pin_set_mode(G.d[i], VX_INPUT);
G.driving = false;
}
static void update_outputs(void) {
int ce_low = (vx_pin_read(G.ce) == 0);
int oe_low = (vx_pin_read(G.oe) == 0);
int we_low = (vx_pin_read(G.we) == 0);
if (ce_low && oe_low && !we_low) {
uint16_t addr = read_addr();
if (addr < 0x40) { release_data(); return; } /* keyboard owns 0x2000-0x203F */
drive_data(G.mem[addr]);
} else {
release_data();
}
}
static void on_addr_or_ctrl(void* u, vx_pin p, int v) { (void)u;(void)p;(void)v; update_outputs(); }
static void on_we(void* u, vx_pin p, int value) {
(void)u;(void)p;
int ce_low = (vx_pin_read(G.ce) == 0);
if (G.we_last == 0 && value == 1 && ce_low) {
uint16_t addr = read_addr();
G.mem[addr] = read_data_bus();
}
G.we_last = value;
update_outputs();
}
static void put_px(int x, int y, uint8_t lit) {
if ((unsigned)x < FB_W && (unsigned)y < FB_H) {
int o = (y*FB_W + x)*4;
fbpix[o]=lit?0x33:0x00; fbpix[o+1]=lit?0xE0:0x12; fbpix[o+2]=lit?0x33:0x00; fbpix[o+3]=0xFF;
}
}
/* Render the whole 32x16 screen from video RAM, then blit. Reading the memory
we own (rather than snooping writes) keeps the picture correct no matter when
this chip was instantiated relative to the CPU. */
static void on_paint(void* u) {
(void)u;
for (int row = 0; row < ROWS; row++) {
for (int col = 0; col < COLS; col++) {
uint8_t ch = G.mem[VRAM_BASE + row*COLS + col] & 0x7f;
for (int line = 0; line < 8; line++) {
uint8_t bits = font8x8[ch*8 + line];
for (int px = 0; px < 8; px++) put_px(col*8+px, row*8+line, (bits >> px) & 1);
}
}
}
vx_buffer_write(G.fb, 0, fbpix, sizeof(fbpix));
}
void chip_setup(void) {
char name[4];
for (int i = 0; i < 16; i++) {
name[0]='A';
if (i<10) { name[1]='0'+i; name[2]=0; } else { name[1]='1'; name[2]='0'+(i-10); name[3]=0; }
G.a[i] = vx_pin_register(name, VX_INPUT);
}
for (int i = 0; i < 8; i++) { name[0]='D'; name[1]='0'+i; name[2]=0; G.d[i] = vx_pin_register(name, VX_INPUT); }
G.ce = vx_pin_register("CE", VX_INPUT);
G.oe = vx_pin_register("OE", VX_INPUT);
G.we = vx_pin_register("WE", VX_INPUT);
G.vcc = vx_pin_register("VCC", VX_INPUT);
G.gnd = vx_pin_register("GND", VX_INPUT);
G.mem = (uint8_t*)calloc(RAM_SIZE, 1);
G.driving = false;
G.we_last = vx_pin_read(G.we);
for (int i = 0; i < 16; i++) vx_pin_watch(G.a[i], VX_EDGE_BOTH, on_addr_or_ctrl, 0);
vx_pin_watch(G.ce, VX_EDGE_BOTH, on_addr_or_ctrl, 0);
vx_pin_watch(G.oe, VX_EDGE_BOTH, on_addr_or_ctrl, 0);
vx_pin_watch(G.we, VX_EDGE_BOTH, on_we, 0);
update_outputs();
uint32_t w, h;
G.fb = vx_framebuffer_init(&w, &h);
for (int i = 0; i < FB_W*FB_H; i++) { fbpix[i*4]=0; fbpix[i*4+1]=0x12; fbpix[i*4+2]=0; fbpix[i*4+3]=0xFF; }
vx_buffer_write(G.fb, 0, fbpix, sizeof(fbpix));
G.paint = vx_timer_create(on_paint, 0);
vx_timer_start(G.paint, 33000000ULL, true); /* ~30 fps */
}