Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 26 additions & 4 deletions docs/machines/tnt/tnt.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,9 @@ DMA architecture:
which carries an interim instant-consume sink until the Phase D AWACS
datapath: Open Firmware plays its boot beep through channel 8 and
polls for completion, so a stalling channel would hang the boot in
firmware.
firmware. A channel's interrupt command asserts a LEVEL on Grand
Central source n that the host's Clear write acknowledges (see "The
interrupt fabric").
- **Cuda / VIA1** — the third instantiation of the shared behavioral Cuda
(machines/av/cuda.c, firmware 2.37) on one real 6522 behind the island
decode. TNT-driven additions to the shared model: the polled no-TIP
Expand Down Expand Up @@ -199,6 +201,25 @@ The mode-1 latch semantics are emulator-derived (pinned by the boot
reaching a correct 60.15 Hz tick rate, and by the T11 desktop requiring
the deassert-change law); revisit if a later rung contradicts them.

**DBDMA channel completions are LEVELS, not pulses.** A channel that
executes an interrupt command holds its request asserted — visible in
Levels — until the host acknowledges that bit through the Clear
register (`tnt_dbdma_irq` → `tnt_gc_set_source`; the Clear write
deasserts sources 0–10). This is the only way Mac OS can see one:
`ExtIntHandlerTNT` classifies from Levels & Mask and never reads
Events, so a completion raised as a one-shot event (the model until
the sound fix) reached the 68k side never. The shipping sound driver
states the contract from its end — it acknowledges channel 8
(`Clear = $100`) before starting a program, and its completion handler
acknowledges again. With the pulse model the Sound Manager's first
two-buffer program parked on its STOP and its handler never ran: the
firmware beep played (Open Firmware polls), the 68k startup chime and
every later sound — alert beeps, the Sound control panel, Quake — were
silence. With levels, all of them play (`tests/unit/suites/tnt_gc`
`test_mode1_dbdma_level_ack`; `tnt-hd-boot` asserts the chime). For
mode 0 (MkLinux/Linux) nothing changes: their handlers acknowledge by
writing the event bits, which deasserts the level too.

### PCI slot topology

Each model declares a `pci_slot_decl_t` table (`pm7500.c` and friends)
Expand Down Expand Up @@ -313,9 +334,10 @@ community's "BoxID bits 11-12" reading:

Known open items at this phase:

- The 68k startup chime STILL does not play by the T11 desktop (the OF
beep is rung T10's instrument); the boot reaches its media hunt
chime-less — an open question for Phase E.
- ~~The 68k startup chime STILL does not play by the T11 desktop~~ —
solved: DBDMA completions were modelled as pulses the NanoKernel's
Levels-classifying handler could not see ("The interrupt fabric"
above). The chime, alert beeps and Quake's audio all play now.
- The 7500's 601 RTC tick source keeps the PDM 7,833,600 Hz assumption
until a ladder rung measures it (proposal §4.4).
- The `interruptableDeviceTable` / per-channel SCC interrupt split: the
Expand Down
12 changes: 11 additions & 1 deletion src/machines/tnt/dbdma.c
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,10 @@ static void partial_writeback(tnt_dbdma_t *d, int n) {
uint32_t tnt_dbdma_reg_read(tnt_dbdma_t *d, int chan, uint32_t offset) {
assert(chan >= 0 && chan < TNT_DBDMA_CHANNELS);
dbdma_chan_t *c = &d->chan[chan];
// The register-level trace (level 4): every read, with the status it
// answers from — the instrument for a driver that polls something the
// model never changes.
LOG(4, "ch%d rd +$%02X (status $%04X cmdptr $%08X)", chan, offset & 0xFCu, status16(d, chan), c->cmdptr);
switch (offset & 0xFCu) {
case TNT_DBDMA_REG_CONTROL:
return 0; // write-only in effect
Expand All @@ -418,6 +422,7 @@ uint32_t tnt_dbdma_reg_read(tnt_dbdma_t *d, int chan, uint32_t offset) {
void tnt_dbdma_reg_write(tnt_dbdma_t *d, int chan, uint32_t offset, uint32_t value) {
assert(chan >= 0 && chan < TNT_DBDMA_CHANNELS);
dbdma_chan_t *c = &d->chan[chan];
LOG(4, "ch%d wr +$%02X = $%08X (status $%04X)", chan, offset & 0xFCu, value, status16(d, chan));
switch (offset & 0xFCu) {
case TNT_DBDMA_REG_CONTROL: {
// Mask/value convention: only bits set in the upper half change,
Expand Down Expand Up @@ -462,7 +467,12 @@ void tnt_dbdma_reg_write(tnt_dbdma_t *d, int chan, uint32_t offset, uint32_t val
}
case TNT_DBDMA_REG_CMDPTRLO:
// Loadable only while the channel is disarmed (drivers write it
// before setting RUN; hardware ignores it mid-program).
// before setting RUN; hardware ignores it mid-program). The
// shipping ROM's native sound driver does write it once on a
// channel still parked on Open Firmware's beep STOP (RUN=1,
// ACTIVE=0) at its init; nothing ever starts that program — the
// Sound Manager's ring is loaded later with RUN cleared first —
// so the parked case stays ignored, as the T9 ladder rung pins.
if (c->status & (TNT_DBDMA_RUN | TNT_DBDMA_ACTIVE)) {
LOG(1, "ch%d cmdptr write $%08X ignored while running", chan, value);
break;
Expand Down
9 changes: 9 additions & 0 deletions src/machines/tnt/grand_central.c
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,15 @@ static void int_write(config_t *cfg, uint32_t offset, uint32_t value) {
gc->int_latch = 0; // the acknowledge drops the line
}
LOG(3, "clear $%08X -> events $%08X (mode %d)", value, gc->int_events, gc->int_mode1);
// The DBDMA channels (sources 0-10) hold their completion request
// as a level until acknowledged here (tnt_dbdma_irq): the clear
// deasserts it, which in mode 1 is itself the change the
// NanoKernel needs to lower the posted IPL again.
for (int n = 0; n < TNT_DBDMA_CHANNELS; n++) {
uint32_t bit = 1u << n;
if ((value & bit) && (gc->int_levels & bit))
tnt_gc_set_source(cfg, n, false);
}
break;
case INT_MASK: {
// Enabling a source whose event or level is already pending
Expand Down
13 changes: 12 additions & 1 deletion src/machines/tnt/tnt.c
Original file line number Diff line number Diff line change
Expand Up @@ -285,8 +285,19 @@ static void tnt_dbdma_mem_write(void *ctx, uint32_t phys, const uint8_t *buf, ui

// Channel completion -> Grand Central interrupt n (== channel n), an
// edge event into the fabric (interrupt-map §2.1).
// A DBDMA channel interrupt is a LEVEL, not a pulse: the channel holds
// its request asserted — visible in Grand Central's Levels register —
// until the host acknowledges it through the interrupt-clear register.
// That is the only way Mac OS can see one at all: the NanoKernel's
// ExtIntHandlerTNT classifies from Levels & Mask and never reads Events,
// so a completion raised as a one-shot event reached the 68k side never.
// The shipping sound driver shows the contract from the other end: it
// acknowledges channel 8 (clear $100) before it starts a program, and
// its completion handler acknowledges again — with a pulse model those
// acknowledges cleared nothing, the handler never ran after the first
// program parked, and every sound after the firmware beep was silence.
static void tnt_dbdma_irq(void *ctx, int chan) {
tnt_gc_pulse_event((config_t *)ctx, chan);
tnt_gc_set_source((config_t *)ctx, chan, true);
}

// ============================================================
Expand Down
10 changes: 10 additions & 0 deletions tests/integration/tnt-hd-boot/test.script
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@ assert debug.mac.globals.read("BootDrive") == 0x8023 "BootDrive not set (driver
# old data-phase wall bombed here with $66 dsOldSystem).
assert debug.mac.globals.read("DSErrCode") == 0 "system error during startup"

# The 68k startup chime played: the Sound Manager's DBDMA ring on
# channel 8 ran past the firmware beep's 51 928 frames and louder than
# its peak (2 921). It needs the channel's completion interrupt to
# reach the 68k side — a LEVEL in Grand Central's Levels register, the
# only thing the NanoKernel's handler classifies from; modelled as a
# pulse, the ring parked after its first two buffers and the machine
# was silent after the beep (the chime, alert beeps, every game).
assert machine.sound.frames > 55000 "the startup chime never played (frames ${machine.sound.frames})"
assert machine.sound.peak > 4000 "the startup chime was silent (peak ${machine.sound.peak})"

# The Finder desktop: menu bar, mounted "Macintosh HD" volume, Trash,
# and the Energy Saver first-boot dialog. The menu-bar clock area is
# excluded — it tracks the pinned RTC plus guest uptime, so it shifts
Expand Down
Binary file modified tests/integration/tnt-voodoo2-glide/quake-ingame.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
38 changes: 38 additions & 0 deletions tests/unit/suites/tnt_gc/test.c
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,43 @@ TEST(test_mode0_pulse_w1c) {
ASSERT_EQ_INT(s_line, 0);
}

// A DBDMA channel completion is a LEVEL held until acknowledged through
// the clear register (the tnt.c wiring: tnt_dbdma_irq -> set_source).
// In mode 1 that is the only way the NanoKernel's ExtIntHandlerTNT —
// which classifies from Levels & Mask and never reads Events — can see a
// completion at all; the acknowledge deasserts it, and that deassert is
// the change that lets the kernel lower the posted IPL again.
TEST(test_mode1_dbdma_level_ack) {
fixture();
reg_write(R_MASK, 1u << 8); // DBDMA audio out
reg_write(R_CLEAR, 0x80000000u); // mode 1
ASSERT_EQ_INT(s_line, 0);
tnt_gc_set_source(&s_cfg, 8, true); // channel 8 completes an interrupt command
ASSERT_EQ_INT(s_line, 1);
reg_write(R_CLEAR, 0x80000000u); // the kernel's acknowledge drops the latch...
ASSERT_EQ_INT(s_line, 0);
ASSERT_EQ_INT((int)(reg_read(R_LEVELS) >> 8) & 1, 1); // ...but the level stands: Levels & Mask classifies it
ASSERT_EQ_INT((int)(reg_read(R_EVENTS) >> 8) & 1, 1);
// The driver's acknowledge of the channel (a plain W1C, bit 31
// clear, so it also selects mode 0) deasserts the level: Levels and
// Events both drop, and the combinational line reads the quiet
// controller. (On the live machine other sources stand in Events,
// so this same write re-asserts the line at once and the kernel's
// next acknowledge re-reads quiet Levels — how the posted IPL falls.)
reg_write(R_CLEAR, 1u << 8);
ASSERT_EQ_INT((int)(reg_read(R_LEVELS) >> 8) & 1, 0);
ASSERT_EQ_INT((int)(reg_read(R_EVENTS) >> 8) & 1, 0);
ASSERT_EQ_INT(s_line, 0);
// Back in mode 1: an acknowledge of a channel that is not asserted
// changes nothing, and a fresh completion asserts again.
reg_write(R_CLEAR, 0x80000000u);
reg_write(R_CLEAR, 1u << 8);
ASSERT_EQ_INT(s_line, 0);
reg_write(R_CLEAR, 0x80000000u);
tnt_gc_set_source(&s_cfg, 8, true);
ASSERT_EQ_INT(s_line, 1);
}

// The NanoKernel acknowledge: $80000000 selects mode 1, clears no device
// bits, and drops the latch; a standing level does NOT re-fire until its
// next CHANGE. Mode 1 is an interrupt-on-change scheme (the AMIC
Expand Down Expand Up @@ -460,6 +497,7 @@ int main(void) {
RUN(test_mklinux_init_and_ack);
RUN(test_mode0_pulse_w1c);
RUN(test_mode1_latch);
RUN(test_mode1_dbdma_level_ack);
RUN(test_mode1_unmask_pending);
RUN(test_mode1_mask_quiets_a_latched_source);
RUN(test_nvram_banking);
Expand Down
Loading