From b5953a723bcc42b65211c832bdfb8852dc3b7350 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Tue, 4 Aug 2026 17:25:02 -0400 Subject: [PATCH] add ac_yield_load and ac_yield_tail for delay slot optimization opportunities. --- code/duffle/gen/duffle.macs.h | 18 ++- code/duffle/gen/duffle.offsets.h | 2 +- code/duffle/lottes_tape.h | 16 ++- code/hello_joypad/gen/hello_joypad.macs.h | 2 +- code/hello_joypad/gen/hello_joypad.offsets.h | 6 +- code/hello_joypad/hello_joypad.tape.c | 30 +++-- scripts/passes/static_analysis.lua | 130 ++++++++++++++++++- 7 files changed, 185 insertions(+), 19 deletions(-) diff --git a/code/duffle/gen/duffle.macs.h b/code/duffle/gen/duffle.macs.h index 218ed79..d0af41b 100644 --- a/code/duffle/gen/duffle.macs.h +++ b/code/duffle/gen/duffle.macs.h @@ -2,7 +2,7 @@ #pragma once #endif // Auto-generated by ps1_meta.lua — DO NOT EDIT -// Source: C:\projects\Pikuma\ps1\code\duffle\lottes_tape.h +// Source: C:\projects\pikuma\ps1\code\duffle\lottes_tape.h // Component atoms (MipsAtomComp_(ac_*)) -> macro variants (mac_*) #ifndef WORD_COUNT @@ -15,6 +15,10 @@ * These do NOT yield. They are expanded inline inside Tape Atoms. * ---------------------------------------------------------------------------*/ // The 'Yield' sequence for Tape Atoms (mac_yield). +// - mac_yield() is the safe default for atom-endings: 4 words, BD-slot of jr is mandatory nop. +// - mac_yield_load() + mac_yield_tail(): +// - unconditional branch: mac_yield_load fills the branch's BD-slot (replaces a nop); +// - mac_yield_tail runs at the branch target (does NOT re-load R_AtomJmp). #define mac_yield(...) \ load_word(R_AtomJmp, R_TapePtr, 0) \ , add_ui_self( R_TapePtr, S_(MipsCode)) \ @@ -22,6 +26,18 @@ , nop WORD_COUNT(mac_yield, 4) +/* atom_dbg_skip */ +#define mac_yield_load(...) \ + load_word(R_AtomJmp, R_TapePtr, 0) +WORD_COUNT(mac_yield_load, 1) + +/* atom_dbg_skip */ +#define mac_yield_tail(...) \ + add_ui_self(R_TapePtr, S_(MipsCode)) \ +, jump_reg( R_AtomJmp) \ +, nop +WORD_COUNT(mac_yield_tail, 3) + /* atom_dbg_skip */ /* Words: 3; Loads 3 S2 indices from the face array */ #define mac_load_tri_indices(...) \ diff --git a/code/duffle/gen/duffle.offsets.h b/code/duffle/gen/duffle.offsets.h index eabc7da..59d1a47 100644 --- a/code/duffle/gen/duffle.offsets.h +++ b/code/duffle/gen/duffle.offsets.h @@ -1,5 +1,5 @@ // Auto-generated by ps1_meta.lua (passes/offsets.lua) — DO NOT EDIT -// Source: C:\projects\Pikuma\ps1\code\duffle\lottes_tape.h +// Source: C:\projects\pikuma\ps1\code\duffle\lottes_tape.h #pragma once #pragma region lottes_tape diff --git a/code/duffle/lottes_tape.h b/code/duffle/lottes_tape.h index 8f70ae6..581d4da 100644 --- a/code/duffle/lottes_tape.h +++ b/code/duffle/lottes_tape.h @@ -122,13 +122,27 @@ FI_ void tb_scope_run_end(TapeBuilder* tb) { tb_emit(tb,tape_exit); tape_run(tb_ * These do NOT yield. They are expanded inline inside Tape Atoms. * ---------------------------------------------------------------------------*/ -// The 'Yield' sequence for Tape Atoms (mac_yield). +// The 'Yield' sequence for Tape Atoms (mac_yield). +// - mac_yield() is the safe default for atom-endings: 4 words, BD-slot of jr is mandatory nop. +// - mac_yield_load() + mac_yield_tail(): +// - unconditional branch: mac_yield_load fills the branch's BD-slot (replaces a nop); +// - mac_yield_tail runs at the branch target (does NOT re-load R_AtomJmp). + atom_dbg_skip MipsAtomComp_(ac_yield) { load_word(R_AtomJmp, R_TapePtr, 0), add_ui_self( R_TapePtr, S_(MipsCode)), jump_reg( R_AtomJmp), nop, }; +atom_dbg_skip MipsAtomComp_(ac_yield_load) { + load_word(R_AtomJmp, R_TapePtr, 0), +}; + +atom_dbg_skip MipsAtomComp_(ac_yield_tail) { + add_ui_self(R_TapePtr, S_(MipsCode)), + jump_reg( R_AtomJmp), nop, +}; + enum { R_PrimCursor = R_T7 atom_reg atom_type(U4*), /* VRAM output cursor (primitive buffer) */ R_FaceCursor = R_T4 atom_reg atom_type(V4_S2*), /* Cube face-index cursor (V4_S2*); floor context switches to V3_S2* via atom_phase */ diff --git a/code/hello_joypad/gen/hello_joypad.macs.h b/code/hello_joypad/gen/hello_joypad.macs.h index 137868b..4902919 100644 --- a/code/hello_joypad/gen/hello_joypad.macs.h +++ b/code/hello_joypad/gen/hello_joypad.macs.h @@ -2,7 +2,7 @@ #pragma once #endif // Auto-generated by ps1_meta.lua — DO NOT EDIT -// Source: C:\projects\Pikuma\ps1\code\hello_joypad\hello_joypad.tape.c +// Source: C:\projects\pikuma\ps1\code\hello_joypad\hello_joypad.tape.c // Component atoms (MipsAtomComp_(ac_*)) -> macro variants (mac_*) #ifndef WORD_COUNT diff --git a/code/hello_joypad/gen/hello_joypad.offsets.h b/code/hello_joypad/gen/hello_joypad.offsets.h index 44a9c6c..ba093f5 100644 --- a/code/hello_joypad/gen/hello_joypad.offsets.h +++ b/code/hello_joypad/gen/hello_joypad.offsets.h @@ -1,5 +1,5 @@ // Auto-generated by ps1_meta.lua (passes/offsets.lua) — DO NOT EDIT -// Source: C:\projects\Pikuma\ps1\code\hello_joypad\hello_joypad.tape.c +// Source: C:\projects\pikuma\ps1\code\hello_joypad\hello_joypad.tape.c #pragma once #pragma region hello_joypad.tape @@ -25,7 +25,7 @@ enum { atom_offset_bounds_chk_floor_f3_face_exit = _atom_offset_bounds_chk_floor_f3_face_exit, }; -// --- atom: pad_bios_snapshot (78 words) --- +// --- atom: pad_bios_snapshot (77 words) --- #define _atom_offset_snap_root_skip_disconnected 8 #define _atom_offset_disconnected_snap_end 60 @@ -51,7 +51,7 @@ enum { atom_offset_analog_pad_snap_end = _atom_offset_analog_pad_snap_end, }; -// --- atom: pad_apply_input (60 words) --- +// --- atom: pad_apply_input (59 words) --- #define _atom_offset_dpad_left_exit_dpad_left 6 #define _atom_offset_dpad_right_exit_dpad_right 6 diff --git a/code/hello_joypad/hello_joypad.tape.c b/code/hello_joypad/hello_joypad.tape.c index a86c4d3..624f3ec 100644 --- a/code/hello_joypad/hello_joypad.tape.c +++ b/code/hello_joypad/hello_joypad.tape.c @@ -400,7 +400,11 @@ atom_label(disconnected) /* === Disconnected body. */ load_upper_i(R_T4, 0x8080), or_i_self(R_T4, 0x8080), store_word( R_T4, R_PadState, O_(PadState,left_x)), store_byte( R_RawId, R_PadState, O_(PadState,id)), - jump_rel(atom_offset(disconnected, snap_end)), nop, + jump_rel(atom_offset(disconnected, snap_end)), + /* BD-slot: load next atom's entry point (replaces the nop). + * The unconditional branch always jumps to snap_end, where mac_yield_tail() + * transfers control to R_AtomJmp without re-loading it. */ + mac_yield_load(), atom_label(skip_disconnected) /* === Case 2: Pending (status == 0 && id == 0) @@ -418,7 +422,8 @@ atom_label(pending) /* === Pending body */ load_upper_i(R_T4, 0x8080), or_i_self(R_T4, 0x8080), store_word( R_T4, R_PadState, O_(PadState,left_x)), store_byte( R_RawId, R_PadState, O_(PadState,id)), - jump_rel(atom_offset(pending, snap_end)), nop, + jump_rel(atom_offset(pending, snap_end)), + mac_yield_load(), atom_label(id_dispatch) /* === Case 3-6: ID dispatch */ add_ui(R_T4, R_0, 0x41), branch_ne(R_RawId, R_T4, atom_offset(id_dispatch, try_analog_stick)), @@ -440,7 +445,8 @@ atom_label(id_dispatch) /* === Case 3-6: ID dispatch */ add_ui( R_T4, R_0, 0x41), store_byte( R_T4, R_PadState, O_(PadState,id)), - jump_rel(atom_offset(id_dispatch, snap_end)), nop, + jump_rel(atom_offset(id_dispatch, snap_end)), + mac_yield_load(), atom_label(try_analog_stick) /* === Case 4: AnalogStick (id == 0x53)*/ add_ui(R_T4, R_0, 0x53), branch_ne(R_RawId, R_T4, atom_offset(try_analog_stick, try_analog_pad)), @@ -461,7 +467,8 @@ atom_label(analog_stick) /* === AnalogStick body store_half( R_T4, R_PadState, O_(PadState,right_x)), add_ui( R_T5, R_0, 0x53), /* R_T5 = id value (clobbers left_xy, already stored) */ store_byte( R_T5, R_PadState, O_(PadState,id)), - jump_rel(atom_offset(analog_stick, snap_end)), nop, + jump_rel(atom_offset(analog_stick, snap_end)), + mac_yield_load(), atom_label(try_analog_pad) /* === Case 5-6: AnalogPad (id & 0xF0 == 0x70) */ and_i( R_T4, R_RawId, 0xF0), @@ -483,7 +490,8 @@ atom_label(analog_pad) /* === AnalogPad body store_half( R_T4, R_PadState, O_(PadState,right_x)), store_byte( R_RawId, R_PadState, O_(PadState,id)), - jump_rel(atom_offset(analog_pad, snap_end)), nop, + jump_rel(atom_offset(analog_pad, snap_end)), + mac_yield_load(), atom_label(try_unsupported) /* === Case 7: Unsupported — fall through from the AnalogPad range-check miss. */ add_ui( R_T4, R_0, PadStatus_Unsupported), @@ -497,7 +505,8 @@ atom_label(try_unsupported) /* === Case 7: Unsupported — fall through from the /* Fall through to snap_end. */ atom_label(snap_end) - mac_yield(), + /* NOT mac_yield() — R_AtomJmp was already loaded in the BD-slot of the case-exit branch. */ + mac_yield_tail(), }; /* ----- pad_apply_input ----- @@ -581,7 +590,8 @@ atom_label(dead_check_upper) /* R_T4 = (0x90 < left_x) ? 1 : 0 → (left_x > 0x90) ? 1 : 0 */ set_lt_u(R_T4, R_T4, R_T3), branch_ne(R_T4, R_0, atom_offset(dead_zone_high_check, dead_high_active)), add_ui( R_T4, R_0, 0x80), /* BD-slot: pre-load 0x80 for dead_high_active */ - jump_rel(atom_offset(dead_zone_skip, exit_stick)), nop, + jump_rel(atom_offset(dead_zone_skip, exit_stick)), + mac_yield_load(), atom_label(dead_low_active) /* R_T3 = left_x (from line 632 lbu; not clobbered between dead_zone_low_check branch + its BD-slot `add_ui R_T4, 0x80`). @@ -603,7 +613,8 @@ atom_label(dead_low_active) add_u( R_T0, R_T0, R_T4), store_half( R_T0, R_FloorRot, O_(V3_S2,y)), - jump_rel(atom_offset(end_low, exit_stick)), nop, + jump_rel(atom_offset(end_low, exit_stick)), + mac_yield_load(), atom_label(dead_high_active) /* R_T3 = left_x (from line 641 lbu in dead_check_upper; not clobbered between dead_zone_high_check branch + its BD-slot `add_ui R_T4, 0x80`). @@ -625,7 +636,8 @@ atom_label(dead_high_active) store_half( R_T0, R_FloorRot, O_(V3_S2,y)), atom_label(exit_stick) - mac_yield(), + /* NOT mac_yield() — R_AtomJmp was already loaded in the BD-slot of the dead-zone/exit branch. */ + mac_yield_tail(), }; #pragma endregion Baked Atoms diff --git a/scripts/passes/static_analysis.lua b/scripts/passes/static_analysis.lua index e91ed29..f25f4b2 100644 --- a/scripts/passes/static_analysis.lua +++ b/scripts/passes/static_analysis.lua @@ -124,7 +124,7 @@ local OUTPUT_EXTENSION = ".static_analysis.txt" --- @field info table[] -- finding-level info (kind == "info"); distinct from per-source scanned/cycles summary rows --- @alias AtomName string -- lower_snake_case atom nameMacroName string -- lower_snake_case macro identifier ---- @alias CheckName string -- "transfer_hazards" | "control_transfer_delay_slot_use" | "mac_yield_uniformity" | "abi_handoff" | "gpu_portstore_shape" | "per_atom_cycle_budget" | "enum_alias_membership" | "atom_type_consistency" | "binds_no_substruct_deref" +--- @alias CheckName string -- "transfer_hazards" | "control_transfer_delay_slot_use" | "mac_yield_uniformity" | "yield_load_tail_pairing" | "abi_handoff" | "gpu_portstore_shape" | "per_atom_cycle_budget" | "enum_alias_membership" | "atom_type_consistency" | "binds_no_substruct_deref" --- @class AtomBody --- @field line integer -- source line of the atom declaration @@ -277,7 +277,10 @@ local function classify_tokens(tokens) if ident == "nop" then nop_words = 1 elseif ident == "nop2" then nop_words = 2 end - local is_yield = ident == "mac_yield" + -- `mac_yield_tail` is the canonical end-of-atom terminator when paired with `mac_yield_load` + -- in a preceding branch's BD-slot. It runs `addiu_self R_TapePtr; jr R_AtomJmp; nop` — the + -- "tail" half of the lego split. Treat it as a yield for the `mac_yield_uniformity` check. + local is_yield = ident == "mac_yield" or ident == "mac_yield_tail" local is_atom_label = false local label_name = nil local is_branch = false @@ -1574,7 +1577,127 @@ local function check_mac_yield_uniformity(atom, pipe_ctx, findings) end -- ════════════════════════════════════════════════════════════════════════════ --- Check #3: Binding handoff discipline +-- Check #3: mac_yield_load / mac_yield_tail pairing (the lego split) +-- ════════════════════════════════════════════════════════════════════════════ + +--- The lego split — `mac_yield_load()` in a branch BD-slot + `mac_yield_tail()` at the branch's target +--- label — must be used as a pair. Otherwise `R_AtomJmp` is not loaded for the tail's `jr R_AtomJmp`, +--- and the tape runtime would jump to garbage. +--- +--- Rules: +--- 1. Every `mac_yield_load()` must be in a branch BD-slot (the immediately preceding token must be a branch). +--- 2. Every `mac_yield_tail()` must be the first instruction after an `atom_label()`, AND +--- at least one branch targeting that label must have `mac_yield_load()` in its BD-slot. +--- 3. `mac_yield_tail()` as the atom-end terminator (last token) is a WARNING, not an error +--- (the safe default for atom-endings is `mac_yield()` which re-loads `R_AtomJmp`). +--- +--- Per-atom. Runtime-helper atoms (`debug_skip`) are exempt. +--- Takes `(atom, pipe_ctx, findings)`; `pipe_ctx` is unused. +local function check_yield_load_tail_pairing(atom, _pipe_ctx, findings) + if atom.kind ~= "atom" then return end + if is_runtime_helper(atom) then return end + + local tokens = atom.paths.tokens + local line_in_body = atom.paths.line_in_body + local tc = atom.paths.tok_class + local n = #tokens + + local function line_for(idx) + return atom.line + line_in_body[tokens[idx].rel] + end + + -- ── Rule 1: every `mac_yield_load()` must be in a branch BD-slot. + for tok_idx = 1, n do + local c = tc[tok_idx] + if c.ident == "mac_yield_load" then + if tok_idx < 2 or not tc[tok_idx - 1].is_branch then + local prev_ident = (tok_idx >= 2) and (tc[tok_idx - 1].ident or "?") or "" + findings[#findings + 1] = { + atom = atom.name, + line = tok_idx >= 2 and line_for(tok_idx) or atom.line, + check = "yield_load_tail_pairing", + kind = "error", + msg = string.format( + "%s at line %d has `mac_yield_load()` at word %d but the previous token is `%s`, not a branch — `mac_yield_load()` must fill a branch BD-slot." + , atom.name, tok_idx >= 2 and line_for(tok_idx) or atom.line, tok_idx, prev_ident), + } + end + end + end + + -- ── Rule 2: every `mac_yield_tail()` must be at a labeled target whose branch BD-slot is `mac_yield_load()`. + for tok_idx = 1, n do + local c = tc[tok_idx] + if c.ident ~= "mac_yield_tail" then goto continue end + + -- The immediately preceding token must be an `atom_label()` (no instructions between them). + local prev_idx = tok_idx - 1 + if prev_idx < 1 or not tc[prev_idx].is_atom_label then + if tok_idx == n then + -- Atom-ending case: last token is `mac_yield_tail()` without a preceding label. WARNING. + findings[#findings + 1] = { + atom = atom.name, + line = line_for(tok_idx), + check = "yield_load_tail_pairing", + kind = "warning", + msg = string.format( + "%s at line %d has `mac_yield_tail()` as the atom-end terminator. The safe default for atom-endings is `mac_yield()` (which re-loads R_AtomJmp). The split is for BD-slot fill, not atom-endings." + , atom.name, line_for(tok_idx)), + } + else + findings[#findings + 1] = { + atom = atom.name, + line = line_for(tok_idx), + check = "yield_load_tail_pairing", + kind = "error", + msg = string.format( + "%s at line %d has `mac_yield_tail()` at word %d but it's not the first instruction after an `atom_label()` — `mac_yield_tail()` must be the first token of its target label's body." + , atom.name, line_for(tok_idx), tok_idx), + } + end + goto continue + end + + local label_name = tc[prev_idx].label_name + -- Find at least one branch targeting `label_name` whose BD-slot is `mac_yield_load()`. + local found_pairing = false + for branch_idx = 1, n do + local bt = tc[branch_idx] + if bt.is_branch and bt.branch_label == label_name then + local bd_idx = branch_idx + 1 + local bd_tc = bd_idx <= n and tc[bd_idx] or nil + if bd_tc and bd_tc.ident == "mac_yield_load" then + found_pairing = true + else + findings[#findings + 1] = { + atom = atom.name, + line = line_for(branch_idx), + check = "yield_load_tail_pairing", + kind = "error", + msg = string.format( + "%s at line %d has `mac_yield_tail()` at label `%s` (word %d) but the branch targeting it (at word %d) has BD-slot `%s` instead of `mac_yield_load()`." + , atom.name, line_for(branch_idx), label_name, tok_idx, branch_idx, bd_tc and bd_tc.ident or "?"), + } + end + end + end + if not found_pairing then + findings[#findings + 1] = { + atom = atom.name, + line = line_for(tok_idx), + check = "yield_load_tail_pairing", + kind = "error", + msg = string.format( + "%s at line %d has `mac_yield_tail()` at label `%s` but no branch in the body targets this label with `mac_yield_load()` in its BD-slot — R_AtomJmp would not be loaded." + , atom.name, line_for(tok_idx), label_name), + } + end + ::continue:: + end +end + +-- ════════════════════════════════════════════════════════════════════════════ +-- Check #4: Binding handoff discipline -- ════════════════════════════════════════════════════════════════════════════ --- For every atom with `atom_bind(Binds_X)`, verify the atom body reads every field of `Binds_X` from R_TapePtr (in any order) @@ -2178,6 +2301,7 @@ local CHECK_RULES = { { name = "control_transfer_delay_slot_use",per_atom = check_control_transfer_delay_slot_use}, { name = "load_delay_violation", per_atom = check_load_delay_slots }, { name = "mac_yield_uniformity", per_atom = check_mac_yield_uniformity }, + { name = "yield_load_tail_pairing", per_atom = check_yield_load_tail_pairing }, { name = "abi_handoff", per_atom = check_abi_handoff }, { name = "gpu_portstore_shape", per_atom = check_gpu_portstore_shape }, { name = "per_atom_cycle_budget", per_atom = check_per_atom_cycle_budget },