tsii and tpif; more vt stuffs

This commit is contained in:
minjaesong
2026-06-15 22:09:03 +09:00
parent e8112e78c8
commit 930e867b3e
6 changed files with 532 additions and 118 deletions

View File

@@ -17,7 +17,10 @@
// +1 switch_request u8 (0 = none, 1..6 = target; set by chvt, cleared by dispatcher)
// +2 debounce_held u8
// +3 vt_spawned_bits u8 (bit n-1 set if VT n is alive)
// +4..63 reserved
// +4 raw_grab_vt u8 (0 = none; else VT n holding a raw-keyboard grab,
// i.e. a fullscreen app reading -41..-48 directly, e.g. DOOM. While set
// and == active_vt, the dispatcher stops feeding that pane's ring.)
// +5..63 reserved
// VT block (× MAX_VT) starting at base + 64, each VT_BLOCK_SIZE bytes
// +0..7 reserved (cursor & color state lives inside text plane itself)
// +8 queue_head u8 (next-read index)
@@ -38,6 +41,7 @@ const CTRL_ACTIVE_VT = 0
const CTRL_SWITCH_REQUEST = 1
const CTRL_DEBOUNCE_HELD = 2
const CTRL_SPAWNED_BITS = 3
const CTRL_RAW_GRAB_VT = 4
const GPU_TEXTAREA_OFFSET = 253950
const TEXT_COLS = 80
@@ -381,6 +385,8 @@ con.prnch = function(c) {
// keyboard MMIO — that's the dispatcher's exclusive territory. Cooperative
// gate on active_vt keeps background panes parked when they call getch.
const RAW_GRAB_ADDR = CTRL + ${CTRL_RAW_GRAB_VT}
function queuePop() {
let head = sys.peek(QUEUE_HEAD_ADDR)
let tail = sys.peek(QUEUE_TAIL_ADDR)
@@ -390,6 +396,10 @@ function queuePop() {
return b
}
con.getch = function() {
// Reading cooked input means we are NOT a raw-keyboard grabber; drop any
// stale grab this VT left set (e.g. a fullscreen app that crashed without
// releasing) so the dispatcher resumes feeding our ring.
if (sys.peek(RAW_GRAB_ADDR) === VT_NUM) sys.poke(RAW_GRAB_ADDR, 0)
while (true) {
if (sys.peek(ACTIVE_VT_ADDR) === VT_NUM) {
let k = queuePop()
@@ -398,6 +408,18 @@ con.getch = function() {
sys.sleep(20)
}
}
// A fullscreen app that reads the raw keyboard snapshot (-41..-48) directly,
// bypassing this ring, must grab so the dispatcher stops piling cooked chars
// into a ring it never drains. Flush any prior type-ahead on grab; the
// dispatcher keeps the ring empty while held. Release on exit (or the next
// con.getch self-heals a grab leaked by a crashed app).
con.grabRawKeyboard = function() {
sys.poke(RAW_GRAB_ADDR, VT_NUM)
sys.poke(QUEUE_HEAD_ADDR, sys.peek(QUEUE_TAIL_ADDR))
}
con.releaseRawKeyboard = function() {
if (sys.peek(RAW_GRAB_ADDR) === VT_NUM) sys.poke(RAW_GRAB_ADDR, 0)
}
con.hitterminate = function() { return false }
con.hiteof = function() { return false }
con.resetkeybuf = function() { sys.poke(QUEUE_HEAD_ADDR, sys.peek(QUEUE_TAIL_ADDR)) }
@@ -555,11 +577,23 @@ while (running) {
// keyboardBuffer, so doing it every frame would drop chars typed last frame.
if (sys.peek(-39) === 0) sys.poke(-39, 1)
// drain typed chars into the active pane's queue
// drain typed chars into the active pane's queue — UNLESS that pane holds
// a raw-keyboard grab (a fullscreen app reading -41..-48 directly, e.g.
// DOOM). Such an app never reads its ring, so cooked chars would pile up
// there and flood its shell the instant the app exits. We still drain the
// cooked buffer (must read -38 to clear the -50 count) but discard the
// chars, and keep the grabbing pane's ring flushed so nothing surfaces
// later. Alt-N is unaffected — it reads the raw snapshot above.
const rawGrab = sys.peek(CTRL + CTRL_RAW_GRAB_VT)
const feedRing = rawGrab !== active
while (sys.peek(-50) !== 0) {
let k = sys.peek(-38)
if (k < 0) k += 256
queuePush(active, k)
if (feedRing) queuePush(active, k)
}
if (!feedRing) {
const qb = vtBlockAddr(active)
sys.poke(qb + 8, sys.peek(qb + 9)) // head = tail: flush stale type-ahead
}
sys.sleep(33)

View File

@@ -9,6 +9,15 @@
const TAUD_MAGIC = [0x1F,0x54,0x53,0x56,0x4D,0x61,0x75,0x64] // \x1F TSVMaud
const TAUD_VERSION = 1
const TAUD_HEADER_SIZE = 32 // magic(8) + version(1) + numSongs(1) + compSize(4) + projOff(4) + sig(14)
// Container kind = top two bits of the version byte (terranmon.txt:3342-3401).
// 00 → full .taud (sample+inst image + song table + patterns)
// 10 → .tsii (sample+inst image only; numSongs = 0, no song table)
// 11 → .tpif (patterns only; sample+inst compSize = 0 — section absent —
// instruments come from a previously-loaded .tsii)
const TAUD_KIND_MASK = 0xC0
const TAUD_KIND_FULL = 0x00
const TAUD_KIND_SAMPLEINST = 0x80
const TAUD_KIND_PATTERN = 0xC0
const TAUD_SONG_ENTRY = 32 // see encodeSongEntry / decodeSongEntry below
// Sample+instrument image: 8 MB sample pool (banked, 16 × 512 K) + 64 K instrument bin = 8256 kB total.
// (terranmon.txt:1985-1997, 2533-2564 — bank-switched via MMIO 46.)
@@ -46,12 +55,18 @@ function _pokeU32LE(ptr, off, v) {
// ── uploadTaudFile ──────────────────────────────────────────────────────────
/**
* Load one song from a Taud file into the tracker hardware and configure the
* given playhead ready to play.
* Load a Taud container into the tracker hardware. Handles all three kinds
* (terranmon.txt:3342-3401), distinguished by the top two bits of the version
* byte:
* - full .taud (00): uploads the sample+instrument image AND loads one song.
* - .tsii (10): uploads the sample+instrument image ONLY (the shared bank for a
* collection of .tpif files). songIndex / playhead are ignored.
* - .tpif (11): loads one song's patterns ONLY, leaving the resident
* sample+instrument bank untouched — load the companion .tsii FIRST.
*
* @param inFile Full path with drive letter, e.g. "A:/music/song.taud"
* @param songIndex 0-based index of the song in the SONG TABLE
* @param playhead Playhead number (0-3) to configure
* @param songIndex 0-based index of the song in the SONG TABLE (ignored for .tsii)
* @param playhead Playhead number (0-3) to configure (ignored for .tsii)
*/
function uploadTaudFile(inFile, songIndex, playhead) {
const drive = inFile[0].toUpperCase()
@@ -92,21 +107,32 @@ function uploadTaudFile(inFile, songIndex, playhead) {
pos += 14 // signature
// pos == 32 == TAUD_HEADER_SIZE
if (songIndex < 0 || songIndex >= numSongs) {
sys.free(filePtr)
throw Error("taud: songIndex " + songIndex + " out of range (numSongs=" + numSongs + ")")
}
const kind = version & TAUD_KIND_MASK
const isSampleInst = (kind === TAUD_KIND_SAMPLEINST) // .tsii: instruments only
const isPattern = (kind === TAUD_KIND_PATTERN) // .tpif: patterns only
// -- 4. Decompress and upload sample+instrument bin -----------------------
// The decompressed image is 8256 kB (8 MB samples bank-major + 64 K instruments)
// which exceeds the 8 MB user-space cap, so we route through a hardware helper
// that decompresses straight into the adapter's native sample/instrument
// storage instead of staging a buffer in user memory.
audio.uploadSampleInstBlob(filePtr + pos, compressedSize)
audio.setSampleBank(0)
pos += compressedSize
// Skipped for .tpif — its sample+inst section is absent (compSize = 0) and the
// resident bank (from a previously-loaded .tsii) must be left intact.
if (!isPattern) {
audio.uploadSampleInstBlob(filePtr + pos, compressedSize)
audio.setSampleBank(0)
pos += compressedSize
}
// -- 5. Parse song-table entry for the requested song --------------------
// -- 5. Song table → patterns → cues → playhead (full .taud / .tpif only) --
// A .tsii carries no song table (numSongs = 0); it stops after the bank + Ixmp.
if (!isSampleInst) {
if (songIndex < 0 || songIndex >= numSongs) {
sys.free(filePtr)
throw Error("taud: songIndex " + songIndex + " out of range (numSongs=" + numSongs + ")")
}
// -- 5a. Parse song-table entry for the requested song -------------------
let entryOff = pos + songIndex * TAUD_SONG_ENTRY
let songOffset = _peekU32LE(filePtr, entryOff)
let numVoices = sys.peek(filePtr + entryOff + 4) & 0xFF
@@ -156,8 +182,11 @@ function uploadTaudFile(inFile, songIndex, playhead) {
audio.setTrackerMixerFlags(playhead, mixerflags)
audio.setSongGlobalVolume(playhead, songGlobalVolume)
audio.setSongMixingVolume(playhead, songMixingVolume)
} // end !isSampleInst (song table / patterns / cues / playhead)
// -- 9. Project Data — walk Ixmp blocks for multi-sample instruments -----
// Runs for every kind: a .tsii carries its instruments' Ixmp patches here; a
// .tpif carries only p/s blocks (sMet) and contributes no patches.
// Terranmon spec: Project Data starts at `projOff` (zero = absent), magic is
// \x1ETaudPrJ + 8 reserved bytes, then a stream of FourCC + Uint32-length
// sections. We only consume "Ixmp" here; other sections (PNam, INam, sMet,