From e42c75a26a6234fc7ed4b8681454c97edc8bdb6e Mon Sep 17 00:00:00 2001 From: Ed_ Date: Sun, 9 Aug 2026 18:49:59 -0400 Subject: [PATCH] WIP: preparing for major changes to atoms to fullfill needs of resolve_look_at and atom ported normalize_v3s4. --- code/duffle/dsl.h | 3 + code/duffle/gen/macs.h | 80 ++++++++++ code/duffle/gen/offsets.h | 10 ++ code/duffle/gp.atom.c | 6 - code/duffle/gte.atom.c | 208 ++++++++++++++++++++++++++ code/duffle/gte.h | 73 ++++++--- code/duffle/lottes_tape.h | 67 +++++++-- code/duffle/mips.h | 23 +-- code/duffle/word_count.metadata.h | 9 ++ code/hello_camera/hello_camera.atom.c | 12 +- code/hello_camera/hello_camera.c | 38 ++++- scripts/duffle.lua | 74 +++++++-- scripts/passes/components.lua | 4 +- scripts/passes/static_analysis.lua | 115 ++++++++++---- 14 files changed, 625 insertions(+), 97 deletions(-) diff --git a/code/duffle/dsl.h b/code/duffle/dsl.h index a501f00..4359d46 100644 --- a/code/duffle/dsl.h +++ b/code/duffle/dsl.h @@ -169,6 +169,8 @@ def_signed_ops(le, <=) #undef def_signed_ops #undef def_signed_op +// Unused, we arent' doing any C-like asm since we have the asm dsl. We'll keep the non-generics if we somehow do. +#if 0 #define def_generic_sop(op, a, ...) _Generic((a), U1: op ## _s1, U2: op ## _s2, U4: op ## _s4) (a, __VA_ARGS__) #define add_s(a,b) def_generic_sop(add,a,b) #define sub_s(a,b) def_generic_sop(sub,a,b) @@ -178,6 +180,7 @@ def_signed_ops(le, <=) #define ge_s(a,b) def_generic_sop(ge, a,b) #define le_s(a,b) def_generic_sop(le, a,b) #undef def_generic_sop +#endif #define alignas _Alignas #define alignof _Alignof diff --git a/code/duffle/gen/macs.h b/code/duffle/gen/macs.h index b29be3c..72dfd8b 100644 --- a/code/duffle/gen/macs.h +++ b/code/duffle/gen/macs.h @@ -147,6 +147,86 @@ WORD_COUNT(mac_gte_store_g4_p012, 3) gte_sw(C2_SXY2, r_primitive_cursor, O_(Poly_G4,p3)) WORD_COUNT(mac_gte_store_g4_p3, 1) +/* atom_dbg_skip */ +#define mac_gte_sqr_v3(r_sx, r_sy, r_sz, r_sq_x, r_sq_y, r_sq_z) \ + gte_mv_to_data_r(r_sx, C2_IR1) \ +, gte_mv_to_data_r(r_sy, C2_IR2) \ +, gte_mv_to_data_r(r_sz, C2_IR3) \ +, nop \ +, gte_cmdw_sqr \ +, gte_mv_from_data_r(r_sq_x, C2_MAC1) \ +, gte_mv_from_data_r(r_sq_y, C2_MAC2) \ +, gte_mv_from_data_r(r_sq_z, C2_MAC3) +WORD_COUNT(mac_gte_sqr_v3, 8) + +/* atom_dbg_skip */ +#define mac_gte_gpf_scale(r_sx, r_sy, r_sz, r_recip_est, r_shift, r_dx, r_dy, r_dz) \ + gte_mv_to_data_r(r_recip_est, C2_IR0) \ +, gte_mv_to_data_r(r_sx, C2_IR1) \ +, gte_mv_to_data_r(r_sy, C2_IR2) \ +, gte_mv_to_data_r(r_sz, C2_IR3) \ +, nop2 /* retire IR0..IR3 → GPF input pre-fill (matches libgte 0x80016134..0x80016138) */ \ +, gte_cmdw_gpf \ +, gte_mv_from_data_r(r_dx, C2_MAC1) \ +, gte_mv_from_data_r(r_dy, C2_MAC2) \ +, gte_mv_from_data_r(r_dz, C2_MAC3) \ +, shift_aright_var(r_dx, r_dx, r_shift) \ +, shift_aright_var(r_dy, r_dy, r_shift) \ +, shift_aright_var(r_dz, r_dz, r_shift) +WORD_COUNT(mac_gte_gpf_scale, 13) + +/* atom_dbg_skip */ +#define mac_normalize_v3s4(r_sx, r_sy, r_sz, r_sq_y, r_sq_z, r_recip_est, r_lzcr, r_shift, r_tmp) \ + gte_mv_to_data_r(r_sx, C2_IR1) \ +, gte_mv_to_data_r(r_sy, C2_IR2) \ +, gte_mv_to_data_r(r_sz, C2_IR3) \ +, nop \ +, gte_cmdw_sqr /* ─── Stage 2: mfc2 MAC1/2/3, sum, mtc2 LZCS ─── // Note: r_recip_est first used as the sum accumulator (= |v|²), which is also what LZCS needs. */ \ +, gte_mv_from_data_r(r_sq_y, C2_MAC1) /* r_sq_y = MAC1 = sx² */ \ +, gte_mv_from_data_r(r_sq_z, C2_MAC2) /* r_sq_z = MAC2 = sy² */ \ +, gte_mv_from_data_r(r_recip_est, C2_MAC3) /* r_recip_est = MAC3 = sz² */ \ +, nop /* MFC2→GPR load delay (1 slot) */ \ +, add_u(r_recip_est, r_recip_est, r_sq_z) /* r_recip_est += sy² */ \ +, add_u(r_recip_est, r_recip_est, r_sq_y) /* r_recip_est += sx² (sum = |v|²) */ \ +, gte_mv_to_data_r( r_recip_est, C2_LZCS) /* LZCS = |v|² */ \ +, nop2 \ +, gte_mv_from_data_r(r_lzcr, C2_LZCR) /* r_lzcr = LZCR (count of leading bits) */ \ +, nop /* MFC2→GPR load delay (1 slot) */ /* ─── Stage 3: compute shift amount, align |v|² to bit 24, lookup 1/|v| ─── // Matches libgte `bltz +0x10 ; nop ; b +0x14 ; sllv t4,v0,t3` pattern: // - bltz TAKEN → nop (BD), jump to srav_path; sllv SKIPPED // - bltz !TAKEN → nop (BD), b +0x14 jumps to aligned_done; sllv (BD of b) executes */ \ +, and_i( r_lzcr, r_lzcr, -2) /* r_lzcr &= ~1 (force even for halving) */ \ +, li_s( r_shift, 31) /* r_shift = 31 */ \ +, sub_s( r_shift, r_shift, r_lzcr) /* r_shift = 31 - LZCR */ \ +, shift_aright( r_shift, r_shift, 1) /* r_shift = (31 - LZCR) / 2 */ \ +, add_si( r_tmp, r_lzcr, -24) /* r_tmp = LZCR - 24 (signed, for branch) */ \ +, branch_lt_zero(r_tmp, atom_offset(srav_path, aligned_done)) \ +, nop \ +, jump_rel( atom_offset(aligned_done, srav_path)) \ +, shift_lleft_var(r_recip_est, r_recip_est, r_tmp) /* BD-slot of branch_equal: r_recip_est = |v|² << (LZCR - 24) */ \ +, atom_label(srav_path) /* SRAV path: |v|² is small (top bit < bit 24) */ \ +, li_s( r_tmp, 24) \ +, sub_s( r_tmp, r_tmp, r_lzcr) /* r_tmp = 24 - LZCR */ \ +, shift_aright_var(r_recip_est, r_recip_est, r_tmp) /* r_recip_est = |v|² >> (24 - LZCR) */ \ +, atom_label(aligned_done) /* Both paths converge here with |v|² aligned to bit 24 */ /* r_recip_est now holds |v|² aligned to bit 24 — convert to byte offset, -64 to skip zero pad. */ \ +, add_si( r_recip_est, r_recip_est, -64) \ +, shift_lleft( r_recip_est, r_recip_est, 1) /* r_recip_est *= 2 (half-word index) */ /* Reference OUR local sqrtbl via &-address split. Compiler/linker resolves both halves. */ \ +, load_upper_i( r_tmp, u4_hi(& gte_normalize_sqr_tbl)) /* lui */ \ +, or_i_self( r_tmp, u4_lo(& gte_normalize_sqr_tbl)) /* ori */ \ +, add_u( r_tmp, r_tmp, r_recip_est) /* r_tmp = sqrtbl base + byte offset (matches libgte 0x80016118: addu t5,t5,t4) */ \ +, load_half( r_recip_est, r_tmp, 0) /* r_recip_est = sqrtbl[r_recip_est] = 1/|v| estimate */ \ +, nop /* retire load_half before MTC2 (matches libgte 0x80016120: nop) */ /* ─── Stage 4: mtc2 IR0..3, GPF (MAC = IR0*IR), mfc2 MAC, srav finalize ─── // Componentized equivalent: mac_gte_gpf_scale. */ \ +, gte_mv_to_data_r(r_recip_est, C2_IR0) /* IR0 = 1/|v| estimate */ \ +, gte_mv_to_data_r(r_sx, C2_IR1) /* IR1 = src.x */ \ +, gte_mv_to_data_r(r_sy, C2_IR2) /* IR2 = src.y */ \ +, gte_mv_to_data_r(r_sz, C2_IR3) /* IR3 = src.z */ \ +, nop2 /* COP2 transfer latency (2 slots) */ \ +, gte_cmdw_gpf \ +, gte_mv_from_data_r(r_sx, C2_MAC1) /* MAC1 → r_sx (overwrites src.x with raw reciprocal-scaled) */ \ +, gte_mv_from_data_r(r_sy, C2_MAC2) \ +, gte_mv_from_data_r(r_sz, C2_MAC3) \ +, shift_aright_var(r_sx, r_sx, r_shift) \ +, shift_aright_var(r_sy, r_sy, r_shift) \ +, shift_aright_var(r_sz, r_sz, r_shift) +WORD_COUNT(mac_normalize_v3s4, 48) + #define mac_gcmd_push(cmd, reg_transfer, reg_base, port) \ load_upper_i(reg_transfer, cmd >> 16) \ , or_i_self( reg_transfer, cmd & 0xFFFF) \ diff --git a/code/duffle/gen/offsets.h b/code/duffle/gen/offsets.h index 3d5988c..f15733d 100644 --- a/code/duffle/gen/offsets.h +++ b/code/duffle/gen/offsets.h @@ -25,6 +25,16 @@ #pragma region duffle +// --- atom: ac_normalize_v3s4 (48 words) --- + +#define _atom_offset_srav_path_aligned_done 6 +#define _atom_offset_aligned_done_srav_path 1 + +enum { + atom_offset_srav_path_aligned_done = _atom_offset_srav_path_aligned_done, + atom_offset_aligned_done_srav_path = _atom_offset_aligned_done_srav_path, +}; + // --- atom: pad_bios_snapshot (84 words) --- #define _atom_offset_snap_root_skip_disconnected 10 diff --git a/code/duffle/gp.atom.c b/code/duffle/gp.atom.c index ce1acdc..012c759 100644 --- a/code/duffle/gp.atom.c +++ b/code/duffle/gp.atom.c @@ -21,8 +21,6 @@ FI_ Slice_MipsCode ac_store_rgb8(U1 rr, U1 rg, U1 rb, U4 base, U4 offset) atom_d store_byte(rb, base, offset + O_(RGB8,b)), }) -/* Words: 3; Emits one (cmd|color) word to R_PrimCursor at the given - * byte offset. Internal helper used by the *_format_*_color macros. */ FI_ Slice_MipsCode ac_pack_color_word(U4 r_base, U4 off, U4 cmd, U1 r, U1 g, U1 b) atom_dbg_skip MipsAtomComp_Proc_(ac_pack_color_word, { load_upper_i(R_AT, (cmd) << 8 | (b)), @@ -30,13 +28,9 @@ atom_dbg_skip MipsAtomComp_Proc_(ac_pack_color_word, { store_word( R_AT, r_base, (off)), }) -/* Words: 3; Emits the F3 command+color word (cmd byte | BLUE | GREEN | RED) - * Args: _r, _g, _b are 8-bit RGB byte values (not raw 16-bit fields). */ FI_ Slice_MipsCode ac_format_f3_color(U4 r_base, U1 r, U1 g, U1 b) atom_dbg_skip MipsAtomComp_Proc_(ac_format_f3_color, { mac_pack_color_word(r_base, O_(Poly_F3,color), gp0_cmd_poly_f3, r, g, b) }) -/* Words: 12; Emits the four (code|color) words of a Poly_G4. - * Args: rN,gN,bN are 8-bit RGB byte values for each of the 4 vertices. */ FI_ Slice_MipsCode ac_format_g4_color(U4 r_prim_cursor, U1 r0, U1 g0, U1 b0, U1 r1, U1 g1, U1 b1, diff --git a/code/duffle/gte.atom.c b/code/duffle/gte.atom.c index 9bc8f8f..a65dc98 100644 --- a/code/duffle/gte.atom.c +++ b/code/duffle/gte.atom.c @@ -49,6 +49,214 @@ FI_ Slice_MipsCode ac_gte_store_g4_p012(U4 r_primitive_cursor) atom_dbg_skip Mip */ FI_ Slice_MipsCode ac_gte_store_g4_p3(U4 r_primitive_cursor) atom_dbg_skip MipsAtomComp_Proc_(ac_gte_store_g4_p3, { gte_sw(C2_SXY2, r_primitive_cursor, O_(Poly_G4,p3)) }) +/* ─── STAGE 1 of normalize: SQR + mfc2 MAC1/2/3 ─── + * Emits squared magnitude per component (in MAC1/2/3) into caller-provided scratch regs. + * Stage 2 of normalize consumes these directly. + * Words: 8. Clobbers: IR1/2/3, MAC1/2/3. Uses gte_cmdw_sqr (sf=0, lm=1). */ +FI_ Slice_MipsCode ac_gte_sqr_v3(U4 r_sx, U4 r_sy, U4 r_sz, U4 r_sq_x, U4 r_sq_y, U4 r_sq_z) atom_dbg_skip MipsAtomComp_Proc_(ac_gte_sqr_v3, { + gte_mv_to_data_r(r_sx, C2_IR1), + gte_mv_to_data_r(r_sy, C2_IR2), + gte_mv_to_data_r(r_sz, C2_IR3), + nop, gte_cmdw_sqr, + gte_mv_from_data_r(r_sq_x, C2_MAC1), + gte_mv_from_data_r(r_sq_y, C2_MAC2), + gte_mv_from_data_r(r_sq_z, C2_MAC3), +}) + +/* ─── STAGE 4 of normalize: mtc2 IR0..3 + GPF + mfc2 MAC + srav finalize ─── + * Reusable standalone — given an IR0 = 1/|v| estimate (typically from a sqrtbl lookup) and a shift count + * (typically (31 - LZCR)/2), multiplies IR0*IR[i] via GPF and shifts right to produce the normalized output. + * Used standalone for "scale vector by scalar". + * Words: 11. Clobbers: IR0..3, MAC1..3. Uses gte_cmdw_gpf (sf=0, lm=0). */ +FI_ Slice_MipsCode ac_gte_gpf_scale(U4 r_sx, U4 r_sy, U4 r_sz, U4 r_recip_est, U4 r_shift, U4 r_dx, U4 r_dy, U4 r_dz) atom_dbg_skip MipsAtomComp_Proc_(ac_gte_gpf_scale, { + gte_mv_to_data_r(r_recip_est, C2_IR0), + gte_mv_to_data_r(r_sx, C2_IR1), + gte_mv_to_data_r(r_sy, C2_IR2), + gte_mv_to_data_r(r_sz, C2_IR3), + nop2, /* retire IR0..IR3 → GPF input pre-fill (matches libgte 0x80016134..0x80016138) */ + gte_cmdw_gpf, + gte_mv_from_data_r(r_dx, C2_MAC1), + gte_mv_from_data_r(r_dy, C2_MAC2), + gte_mv_from_data_r(r_dz, C2_MAC3), + shift_aright_var(r_dx, r_dx, r_shift), + shift_aright_var(r_dy, r_dy, r_shift), + shift_aright_var(r_dz, r_dz, r_shift), +}) + +/* ─── Local copy of PSYQ's sqrtbl (1/sqrt lookup table for VectorNormal). ─── + * Source: PSYQ 4.7 libgte sqrtbl at 0x800185B4 in hello_camera.elf. + * objdump -s --start-address=0x800185B4 --stop-address=0x800185F4 hello_camera.elf + * → 192 entries × 16-bit signed, in 1.12 fixed-point (max value 0x1000 = 1.0). + * + * Data is identical to the libgte original (byte-for-byte verified). + * + * ─── Per-entry semantics (decoded from libgte msc02 VectorNormal) ─── + * Each entry is `1/sqrt(x)` in 1.12 fixed point (value / 4096). + * The 192 entries span 4 octaves of the input magnitude, with 48 entries per octave: + * Octave 0 (entries 0- 47): mantissa in [0x8000, 0x10000) output ~[1.000, 0.707] + * Octave 1 (entries 48- 95): mantissa in [0x10000, 0x20000) output ~[0.707, 0.500] + * Octave 2 (entries 96-143): mantissa in [0x20000, 0x40000) output ~[0.500, 0.354] + * Octave 3 (entries144-191): mantissa in [0x40000, 0x80000) output ~[0.354, 0.251] + * Within each octave, 8 sub-entries interpolate over the 8 fractional bits of the + * mantissa (the byte `(0x80 | (i mod 8))` for the lower-byte of the aligned value). + * Sampling the first value of each octave: + * [0] 0x1000 = 1.0000 ; 1 / sqrt(1.0000) + * [48] 0x0e4f = 0.8940 ; 1 / sqrt(1.2500) + * [96] 0x0d10 = 0.8164 ; 1 / sqrt(1.5000) + * [144] 0x0c0a = 0.7520 ; 1 / sqrt(1.7500) + * And representative sub-entries within octave 0 (mantissa in [0x8000, 0x8100)): + * [0] 0x1000 = 1.0000 ; 1 / sqrt(0x8000) + * [1] 0x0fe0 = 0.9922 ; 1 / sqrt(0x8100) + * [2] 0x0fc1 = 0.9846 ; 1 / sqrt(0x8200) + * [3] 0x0fa3 = 0.9773 ; 1 / sqrt(0x8300) + * [4] 0x0f85 = 0.9700 ; 1 / sqrt(0x8400) + * [5] 0x0f68 = 0.9629 ; 1 / sqrt(0x8500) + * [6] 0x0f4c = 0.9561 ; 1 / sqrt(0x8600) + * [7] 0x0f30 = 0.9492 ; 1 / sqrt(0x8700) + * + * The algorithm's `addi -64 / sll 1 / lh` selects the entry at `(aligned - 64) * 2` for the case where `aligned` has its top bit at bit 24. + * After the sllv/srav pair, `aligned` always lands in `[0x80, 0x100)` + * (with top bit at bit 24 → after `sub $aligned - 64`, the index sits in `[0x40, 0x80) * 2 = [0x80, 0x100)` bytes = entries [64, 128) within the sqrtbl). + * The earlier 64 entries (octave 0) are reached when the magnitude after shifting puts the top bit below bit 24 (the `sllv` branch), + * and the load upper_halves of the table bracket the input range. + * The later 64 entries (octaves 2-3) are the `srav` branch when the magnitude's top bit is well above bit 24. + * + * 192-entry table is reproduced verbatim from libgte (verified against libpsn00b/psxgte/vector.s:100-123 — 24 rows × 8 halfwords, last entry 0x0804). */ +internal S2 const gte_normalize_sqr_tbl[192] align_(2) = { + 0x1000, 0x0fe0, 0x0fc1, 0x0fa3, 0x0f85, 0x0f68, 0x0f4c, 0x0f30, + 0x0f15, 0x0efb, 0x0ee1, 0x0ec7, 0x0eae, 0x0e96, 0x0e7e, 0x0e66, + 0x0e4f, 0x0e38, 0x0e22, 0x0e0c, 0x0df7, 0x0de2, 0x0dcd, 0x0db9, + 0x0da5, 0x0d91, 0x0d7e, 0x0d6b, 0x0d58, 0x0d45, 0x0d33, 0x0d21, + 0x0d10, 0x0cff, 0x0cee, 0x0cdd, 0x0ccc, 0x0cbc, 0x0cac, 0x0c9c, + 0x0c8d, 0x0c7d, 0x0c6e, 0x0c5f, 0x0c51, 0x0c42, 0x0c34, 0x0c26, + 0x0c18, 0x0c0a, 0x0bfd, 0x0bef, 0x0be2, 0x0bd5, 0x0bc8, 0x0bbb, + 0x0baf, 0x0ba2, 0x0b96, 0x0b8a, 0x0b7e, 0x0b72, 0x0b67, 0x0b5b, + 0x0b50, 0x0b45, 0x0b39, 0x0b2e, 0x0b24, 0x0b19, 0x0b0e, 0x0b04, + 0x0af9, 0x0aef, 0x0ae5, 0x0adb, 0x0ad1, 0x0ac7, 0x0abd, 0x0ab4, + 0x0aaa, 0x0aa1, 0x0a97, 0x0a8e, 0x0a85, 0x0a7c, 0x0a73, 0x0a6a, + 0x0a61, 0x0a59, 0x0a50, 0x0a47, 0x0a3f, 0x0a37, 0x0a2e, 0x0a26, + 0x0a1e, 0x0a16, 0x0a0e, 0x0a06, 0x09fe, 0x09f6, 0x09ef, 0x09e7, + 0x09e0, 0x09d8, 0x09d1, 0x09c9, 0x09c2, 0x09bb, 0x09b4, 0x09ad, + 0x09a5, 0x099e, 0x0998, 0x0991, 0x098a, 0x0983, 0x097c, 0x0976, + 0x096f, 0x0969, 0x0962, 0x095c, 0x0955, 0x094f, 0x0949, 0x0943, + 0x093c, 0x0936, 0x0930, 0x092a, 0x0924, 0x091e, 0x0918, 0x0912, + 0x090d, 0x0907, 0x0901, 0x08fb, 0x08f6, 0x08f0, 0x08eb, 0x08e5, + 0x08e0, 0x08da, 0x08d5, 0x08cf, 0x08ca, 0x08c5, 0x08bf, 0x08ba, + 0x08b5, 0x08b0, 0x08ab, 0x08a6, 0x08a1, 0x089c, 0x0897, 0x0892, + 0x088d, 0x0888, 0x0883, 0x087e, 0x087a, 0x0875, 0x0870, 0x086b, + 0x0867, 0x0862, 0x085e, 0x0859, 0x0855, 0x0850, 0x084c, 0x0847, + 0x0843, 0x083e, 0x083a, 0x0836, 0x0831, 0x082d, 0x0829, 0x0824, + 0x0820, 0x081c, 0x0818, 0x0814, 0x0810, 0x080c, 0x0808, 0x0804, +}; + +/* ─── Full normalize (all 4 stages inline) ─── + * Direct port of PSYQ libgte msc02.rel.text VectorNormal disassembly (0x800160a0..0x8001615c). + * + * Component variants that could apply: + * - `ac_gte_sqr_v3` (line ~56) covers stage 1's `mtc2 IR1/2/3 + nop + gte_cmdw_sqr`. + * We do NOT call it because the inlined version of stage 1 is followed immediately by stage 2's `mfc2 MAC1/2/3` chain + * (the operands of `ac_gte_sqr_v3`'s r_sq_x/r_sq_y/r_sq_z would each require an explicit GPR to receive the MAC result, + * then a move to land in r_recip_est for the partial-sum chain). + * Inlining saves ~3 cycles of `or`-merge + register pressure + * (squared MAC3 lands DIRECTLY in r_recip_est which doubles as the partial-sum accumulator and the LZCS input — see r_recip_est row below). + * - `ac_gte_gpf_scale` (line ~71) covers stage 4's `mtc2 IR0..3 + nop2 + gte_cmdw_gpf + mfc2 MAC1/2/3 + sra`. + * We do NOT call it for the symmetric reason: the normalize in-place semantics overwrite the input regs (r_sx/r_sy/r_sz) with the normalized output, + * which `ac_gte_gpf_scale`'s r_dx/r_dy/r_dz output GPRs would not match. + * `gte_cmdw_sqr` and `gte_cmdw_gpf` primitive macros ARE used in the inlined body, so changes to those primitives + * (e.g., the libgte `fake_cmd` signature bits) propagate automatically. The components remain available for callers that want the explicit GPR-shape variants. + * + * Argument aliasing (9 unique physical regs needed, can drop to 8 with r_sq_y ≡ r_lzcr): + * r_sx, r_sy, r_sz : src components in regs (clobbered by mtc2 → IR1/2/3 in stage 1, then by mfc2 MAC1/2/3 in stage 4 — in-place semantics) + * r_sq_y, r_sq_z : MAC2, MAC3 → DIE after stage 2 accumulate (r_sq_y can alias r_lzcr after stage 2 to save one reg) + * r_recip_est : ≡ r_sqmag — multi-purpose (holds |v|² in stage 2, shift-input in stage 3, sqrtbl[index] in stage 4) + * r_lzcr : LZCR value, alive across stage 3 (srav path needs `24 - LZCR`) + * r_shift : (31 - LZCR & ~1) >> 1 — final srav amount (stages 3-4) + * r_tmp : scratch (shift count, branch target, lookup addr, table base) + * + * GPR ccount peak: 9. + * Pipeline: clobbers IR0..3, MAC1..3, LZCS, LZCR. + * Words: ~35 (pending re-gen; matches libgte 0x800160a0..0x8001615c at +/- 0-2 words for BD-slot reshuffling). + * Sqrtbl: hardcoded to 0x800185B4 (libgte msc02.rel.data). Note: swapped to local. */ +I_ Slice_MipsCode ac_normalize_v3s4(U4 r_sx, U4 r_sy, U4 r_sz, U4 r_sq_y, U4 r_sq_z, U4 r_recip_est, U4 r_lzcr, U4 r_shift, U4 r_tmp) +atom_dbg_skip MipsAtomComp_Proc_(ac_normalize_v3s4, { + /* 9-arg signature — must be on one line so the metaprogram captures the full arg list. + * r_sx, r_sy, r_sz : in/out — src components, overwritten with normalized + * r_sq_y, r_sq_z : scratch — MAC2, MAC3 → die after stage 2 accumulate (r_sq_y may alias r_lzcr post-stage-2) + * r_recip_est : ≡ r_sqmag — multi-purpose (|v|² → shift-input → sqrtbl entry) + * r_lzcr : LZCR value (alive across stage 3 srav path) + * r_shift : (31 - LZCR & ~1) / 2 — final srav amount (stages 3-4) + * r_tmp : scratch — shift count, branch target, lookup addr, table base + * + * GPR ccount peak: 9. + * Pipeline: clobbers IR0..3, MAC1..3, LZCS, LZCR. + * Words: ~35 (pending re-gen; matches libgte 0x800160a0..0x8001615c at +/- 0-2 words). + * + * Sqrtbl address: link-time constant `>e_normalize_sqrtbl`, split via >>16 and &0xFFFF. */ + + // ─── Stage 1: mtc2 src → IR1/2/3, SQR fires (MAC1/2/3 = IR², IR ← MAC saturated) ─── + // Componentized equivalent: mac_gte_sqr_v3(r_sx, r_sy, r_sz, r_sq_x, r_sq_y, r_sq_z). + // We inline for GPR-pressure reasons (see file-level comment). + gte_mv_to_data_r(r_sx, C2_IR1), + gte_mv_to_data_r(r_sy, C2_IR2), + gte_mv_to_data_r(r_sz, C2_IR3), + nop, gte_cmdw_sqr, + + // ─── Stage 2: mfc2 MAC1/2/3, sum, mtc2 LZCS ─── + // Note: r_recip_est first used as the sum accumulator (= |v|²), which is also what LZCS needs. + gte_mv_from_data_r(r_sq_y, C2_MAC1), /* r_sq_y = MAC1 = sx² */ + gte_mv_from_data_r(r_sq_z, C2_MAC2), /* r_sq_z = MAC2 = sy² */ + gte_mv_from_data_r(r_recip_est, C2_MAC3), /* r_recip_est = MAC3 = sz² */ + nop, /* MFC2→GPR load delay (1 slot) */ + add_u(r_recip_est, r_recip_est, r_sq_z), /* r_recip_est += sy² */ + add_u(r_recip_est, r_recip_est, r_sq_y), /* r_recip_est += sx² (sum = |v|²) */ + gte_mv_to_data_r( r_recip_est, C2_LZCS), /* LZCS = |v|² */ + nop2, + gte_mv_from_data_r(r_lzcr, C2_LZCR), /* r_lzcr = LZCR (count of leading bits) */ + nop, /* MFC2→GPR load delay (1 slot) */ + + // ─── Stage 3: compute shift amount, align |v|² to bit 24, lookup 1/|v| ─── + // Matches libgte `bltz +0x10 ; nop ; b +0x14 ; sllv t4,v0,t3` pattern: + // - bltz TAKEN → nop (BD), jump to srav_path; sllv SKIPPED + // - bltz !TAKEN → nop (BD), b +0x14 jumps to aligned_done; sllv (BD of b) executes + and_i( r_lzcr, r_lzcr, -2), /* r_lzcr &= ~1 (force even for halving) */ + li_s( r_shift, 31), /* r_shift = 31 */ + sub_s( r_shift, r_shift, r_lzcr), /* r_shift = 31 - LZCR */ + shift_aright( r_shift, r_shift, 1), /* r_shift = (31 - LZCR) / 2 */ + add_si( r_tmp, r_lzcr, -24), /* r_tmp = LZCR - 24 (signed, for branch) */ + branch_lt_zero(r_tmp, atom_offset(srav_path, aligned_done)), nop, + jump_rel( atom_offset(aligned_done, srav_path)), + shift_lleft_var(r_recip_est, r_recip_est, r_tmp), /* BD-slot of branch_equal: r_recip_est = |v|² << (LZCR - 24) */ + atom_label(srav_path) /* SRAV path: |v|² is small (top bit < bit 24) */ + li_s( r_tmp, 24), + sub_s( r_tmp, r_tmp, r_lzcr), /* r_tmp = 24 - LZCR */ + shift_aright_var(r_recip_est, r_recip_est, r_tmp), /* r_recip_est = |v|² >> (24 - LZCR) */ +atom_label(aligned_done) /* Both paths converge here with |v|² aligned to bit 24 */ + /* r_recip_est now holds |v|² aligned to bit 24 — convert to byte offset, -64 to skip zero pad. */ + add_si( r_recip_est, r_recip_est, -64), + shift_lleft( r_recip_est, r_recip_est, 1), /* r_recip_est *= 2 (half-word index) */ + /* Reference OUR local sqrtbl via &-address split. Compiler/linker resolves both halves. */ + load_upper_i( r_tmp, u4_hi(& gte_normalize_sqr_tbl)), /* lui */ + or_i_self( r_tmp, u4_lo(& gte_normalize_sqr_tbl)), /* ori */ + add_u( r_tmp, r_tmp, r_recip_est), /* r_tmp = sqrtbl base + byte offset (matches libgte 0x80016118: addu t5,t5,t4) */ + load_half( r_recip_est, r_tmp, 0), /* r_recip_est = sqrtbl[r_recip_est] = 1/|v| estimate */ + nop, /* retire load_half before MTC2 (matches libgte 0x80016120: nop) */ + + // ─── Stage 4: mtc2 IR0..3, GPF (MAC = IR0*IR), mfc2 MAC, srav finalize ─── + // Componentized equivalent: mac_gte_gpf_scale. + gte_mv_to_data_r(r_recip_est, C2_IR0), /* IR0 = 1/|v| estimate */ + gte_mv_to_data_r(r_sx, C2_IR1), /* IR1 = src.x */ + gte_mv_to_data_r(r_sy, C2_IR2), /* IR2 = src.y */ + gte_mv_to_data_r(r_sz, C2_IR3), /* IR3 = src.z */ + nop2, /* COP2 transfer latency (2 slots) */ + gte_cmdw_gpf, + gte_mv_from_data_r(r_sx, C2_MAC1), /* MAC1 → r_sx (overwrites src.x with raw reciprocal-scaled) */ + gte_mv_from_data_r(r_sy, C2_MAC2), + gte_mv_from_data_r(r_sz, C2_MAC3), + shift_aright_var(r_sx, r_sx, r_shift), + shift_aright_var(r_sy, r_sy, r_shift), + shift_aright_var(r_sz, r_sz, r_shift), +}) + #pragma endregion MACs (Mips Atom Components) #pragma region Bsked Atoms diff --git a/code/duffle/gte.h b/code/duffle/gte.h index 30c264b..e3b4a21 100644 --- a/code/duffle/gte.h +++ b/code/duffle/gte.h @@ -161,6 +161,8 @@ enum { gte_cmd_nclip = 0x06, /* Normal Clipping (Backface culling) */ gte_cmd_op = 0x0C, /* Outer Product */ gte_cmd_mvmva = 0x12, /* Matrix Vector Multiply & Add (Custom math) */ + gte_cmd_sqr = 0x28, /* Square vector — MAC[i] = IR[i]²; IR[i] ← MAC[i] saturated */ + gte_cmd_gpf = 0x3D, /* General-purpose Interpolation — MAC[i] = IR0 * IR[i] */ /* --- GTE Command Bit-Field Layout --- * A GTE command word (sent to COP2 with RS=1) is laid out as: @@ -171,17 +173,22 @@ enum { * +------------+--+-----+------+------+------+------+---+--------+----------+ * \_____ GTE_PAYLOAD _____/ \__ GTE_CMD __/ * - * Shifts/masks below are the *bit positions* and *bit widths* of each - * configurable field, used by the ENC_GTE_CMD encoder. + * Shifts/masks below are the *bit positions* and *bit widths* of each configurable field, used by the ENC_GTE_CMD encoder. * Mirrors the OPCODE_SHIFT / RS_SHIFT convention used in mips.h. */ gte_shift_sf = 19, gte_width_sf = 1, gte_mask_sf = 0x1, gte_shift_mx = 17, gte_width_mx = 2, gte_mask_mx = 0x3, gte_shift_v = 15, gte_width_v = 2, gte_mask_v = 0x3, - gte_shift_cv = 13, gte_width_cv = 2, gte_mask_cv = 0x3, + gte_shift_cv = 13, gte_width_cv = 2, gte_mask_cv = 0x3, gte_shift_lm = 10, gte_width_lm = 1, gte_mask_lm = 0x1, gte_shift_cmd = 0, gte_width_cmd = 6, gte_mask_cmd = 0x3F, + + /* Fake command number (bits 24-20) — IGNORED by the GTE hardware per PSX-SPX `geometrytransformationenginegte.md` line 48. + * libgte's compiler emits non-zero values in this field as a disassembly signature. */ + gte_shift_fake_cmd = 20, + gte_width_fake_cmd = 5, + gte_mask_fake_cmd = 0x1F, }; /* --- GTE Control Register Indices (for ctc2/cfc2) --- @@ -243,10 +250,10 @@ enum { _C2_OPS_ = 0 * bit 1 (0x02): register class — 0 = data, 1 = control * bit 2 (0x04): direction — 0 = read, 1 = write * - * The values 0x00 (sub_mfc2) and 0x04 (sub_mtc2) are the same 5-bit numbers as the general MIPS `cop_mf` / `cop_mt` defined in mips.h + * The values 0x00 (sub_mfc2) and 0x04 (sub_mtc2) are the same 5-bit numbers as general MIPS `cop_mf` / `cop_mt` defined in mips.h * (which target the data register file on any coprocessor). * They are re-aliased here so the four-way table reads like the spec mnemonics (MFC2 / CFC2 / MTC2 / CTC2) - * and so the encoding lives next to its only consumer (this header). + * and so the encoding is next to its only consumer (this header). * * Vendor mnemonic aliases (gte_mfc2 / gte_mtc2 / gte_cfc2 / gte_ctc2) live in gte_vendor_sym.h. */ enum { _C2_TX_SUBS_ = 0 @@ -309,23 +316,24 @@ enum { _C2_TX_SUBS_ = 0 /* GTE Command Format * Opcode is always MIPS_OP_COP2, RS is always 1 (CO). - * The lower 25 bits are the GTE-specific command payload. + * Lower 25 bits are GTE-specific command payload. * - * The granular `enc_gte_(x)` macros below mirror the `enc_op`/`enc_rs` pattern in mips.h: + * The `enc_gte_(x)` macros below mirror the `enc_op`/`enc_rs` pattern in mips.h: * Each one self-masks and shifts its own field, so a caller can build up a GTE command piece by piece * (handy for state-driven MVMVA emitters that vary one field at a time). * - * `ENC_GTE_CMD` is the all-in-one convenience for emitting a full command word in one go. + * `ENC_GTE_CMD` is an all-in-one convenience for emitting a full command word. * It just ORs the per-field encoders together. */ #define gte_cmd_base (enc_op(op_cop2) | (1 << 25)) /* Per-field encoders. Each one does (value & mask) << shift on its own. */ -#define enc_gte_sf(sf) (((sf) & gte_mask_sf ) << gte_shift_sf ) -#define enc_gte_mx(mx) (((mx) & gte_mask_mx ) << gte_shift_mx ) -#define enc_gte_v(v) (((v) & gte_mask_v ) << gte_shift_v ) -#define enc_gte_cv(cv) (((cv) & gte_mask_cv ) << gte_shift_cv ) -#define enc_gte_lm(lm) (((lm) & gte_mask_lm ) << gte_shift_lm ) -#define enc_gte_cmd(cmd) (((cmd) & gte_mask_cmd) << gte_shift_cmd) +#define enc_gte_sf(sf) (((sf) & gte_mask_sf ) << gte_shift_sf ) +#define enc_gte_mx(mx) (((mx) & gte_mask_mx ) << gte_shift_mx ) +#define enc_gte_v(v) (((v) & gte_mask_v ) << gte_shift_v ) +#define enc_gte_cv(cv) (((cv) & gte_mask_cv ) << gte_shift_cv ) +#define enc_gte_lm(lm) (((lm) & gte_mask_lm ) << gte_shift_lm ) +#define enc_gte_cmd(cmd) (((cmd) & gte_mask_cmd ) << gte_shift_cmd ) +#define enc_gte_fake_cmd(x) (((x) & gte_mask_fake_cmd) << gte_shift_fake_cmd) /* Composite: all six GTE fields + the COP2/CO base. */ #define enc_gte_cmdw(sf, mx, v, cv, lm, cmd) ( \ @@ -363,11 +371,11 @@ enum { _C2_TX_SUBS_ = 0 * (the perspective divide happens regardless of `sf`). * * If we emit a strictly-spec-compliant word (`sf=0`, reserved bits clear), - * PCSX-Redux's GTE checks those bits more strictly than the silicon does and RTPT silently no-ops — - * the floor's screen coordinates come out as raw projection-of-rotation (Z never divided), + * PCSX-Redux's GTE checks those bits more strictly than the silicon does and RTPT silently no-ops. + * The floor's screen coordinates come out as raw projection-of-rotation (Z never divided), * `nclip` ends up wrong, and the triangle is culled. * - * So for RTPS and RTPT we OR-in the `0x28` "PsyQ compat" pattern to match the working bit pattern everyone has shipped for 25 years. + * So for RTPS and RTPT we OR-in the `0x28` "PsyQ compat" pattern to match the working bit pattern. * NCLIP / OP / MVMVA stay spec-clean — their reserved bits really are zero in the original PsyQ source. * -------------------------------------------------------------------------- */ @@ -383,6 +391,36 @@ enum { _C2_TX_SUBS_ = 0 * The wedge alias is the 3D complement interpretation of the same 3 scalars (MAC1..MAC3). */ #define gte_cmdw_mvmva (gte_cmd_base | enc_gte_cmd(gte_cmd_mvmva)) +/* SQR / GPF cosmetic-bits compat helpers. + * Each command's `_compat` macro ORs in the `fake_cmd` field value libgte happens to emit. + * The hardware ignores these bits (per PSX-SPX line 48). */ +#define gte_cmdw_sqr_fake_sig enc_gte_fake_cmd(0x0A) +#define gte_cmdw_gpf_fake_sig enc_gte_fake_cmd(0x19) + +/* SQR — Square Vector. + * PSX-SPX `geometrytransformationenginegte.md` §"SQR": + * [MAC1,MAC2,MAC3] = [IR1*IR1, IR2*IR2, IR3*IR3] SHR (sf*12) + * [IR1,IR2,IR3] = [MAC1,MAC2,MAC3] (saturated to 0x7FFF when lm=1) + * Sourced verbatim from libgte msc02 VectorNormal disassembly at 0x800160b0: + * 0x4AA00428 = gte_cmd_base | gte_cmdw_sqr_compat | enc_gte_lm(1) | enc_gte_cmd(0x28) + * bit 19 sf=0 + * bit 10 lm=1 + * bits 5-0 cmd=0x28=SQR + * bits 24-20 = 0x0A (libgte "nonsense SDK command number" signature) */ +#define gte_cmdw_sqr (gte_cmd_base | enc_gte_cmd(gte_cmd_sqr) | enc_gte_lm(1) | gte_cmdw_sqr_fake_sig) + +/* GPF — General-purpose Interpolation. + * PSX-SPX `geometrytransformationenginegte.md` §"GPF": + * [MAC1,MAC2,MAC3] = (([IR1,IR2,IR3] * IR0) + [MAC1,MAC2,MAC3]) SAR (sf*12) + * [IR1,IR2,IR3] = [MAC1,MAC2,MAC3] + * Sourced verbatim from libgte msc02 VectorNormal disassembly at 0x8001613c: + * 0x4B90003D = gte_cmd_base | gte_cmdw_gpf_compat | enc_gte_cmd(0x3D) + * bit 19 sf=0 + * bit 10 lm=0 + * bits 5-0 cmd=0x3D=GPF + * bits 24-20 = 0x19 (libgte "nonsense SDK command number" signature) */ +#define gte_cmdw_gpf (gte_cmd_base | enc_gte_cmd(gte_cmd_gpf) | gte_cmdw_gpf_fake_sig) + #define gte_cmdw_rotate_translate_perspective_single gte_cmdw_rtps #define gte_cmdw_rotate_translate_perspective_triple gte_cmdw_rtpt /* RGA(Lengyel): RTPS/RTPT consume the matrix expansion of a rigid transformation (rotation matrix + translation vector) loaded into the RT/TR control registers. @@ -437,7 +475,6 @@ enum { #define gte_lw_v2_z(base) enc_gte_lw(gte_in_v2_z, (base), GTE_Z_Offset) /* gte_load_vN(r_ptr, base) — placeholder-punned lwc2 loaders - * * Emits `.word` constants encoding `lwc2 $N, off()` for the chosen GTE vector register, where `` is the GPR number you pass in * (typically one of R_T4..R_T9 for the standard "3-pointer" pattern). * diff --git a/code/duffle/lottes_tape.h b/code/duffle/lottes_tape.h index 0faf298..8953c42 100644 --- a/code/duffle/lottes_tape.h +++ b/code/duffle/lottes_tape.h @@ -106,6 +106,12 @@ typedef Slice_(MipsCode); typedef U4 const MipsAtom; // Underlying type to an array of mips asm words that must terminate with an ac_yield. #define MipsAtom_(sym) MipsCode sym [] align_(4) = +// Used for atoms with value-args +// FI_ void ac_X(args) MipsAtomComp_Proc_(ac_X, { body }) +// expands to: +// FI_ void ac_X(args) { MipsCode ac_X[] align_(4) = { body }; return ac_X; } +#define MipsAtom_Proc_(sym, abuilder, ...) { MipsCode sym [] align_(4) = __VA_ARGS__; atombuilder_unroll(abuilder, slice_from_array(MipsCode, sym)); } + // Used for components with no args (e.g., ac_load_tri_indices) or identifier-args (hardcoded register names). // MipsAtomComp_(ac_X) { body } // expands to: @@ -118,12 +124,18 @@ typedef U4 const MipsAtom; // Underlying type to an array of mips asm words that // FI_ Slice_MipsCode ac_X(args) { MipsCode ac_X[] align_(4) = { body }; return slice_from_array(MipsCode, ac_X); } #define MipsAtomComp_Proc_(sym, ...) { MipsCode sym [] align_(4) = __VA_ARGS__; return slice_from_array(MipsCode, sym); } -/* Line-table anchor: gcc only adds a file to the .debug_line file table when the - file contains line-numbered content. Files containing only: - - `MipsAtomComp_` static-array declarations, or - - `MipsAtomComp_Proc_` (force-inline) function bodies whose line info gets - attributed to the call site at the include point are otherwise omitted from the file table, - which breaks the DWARF injection when it tries to resolve atom-component provenance paths. +// Used for components with value-args (e.g., ac_format_f3_color). +// FI_ Slice_MipsCode ac_X(args) MipsAtomComp_Proc_(ac_X, { body }) +// expands to: +// FI_ Slice_MipsCode ac_X(args) { MipsCode ac_X[] align_(4) = { body }; return slice_from_array(MipsCode, ac_X); } +// #define MipsAtomComp_Proc_(sym, abuilder, ...) { MipsCode sym [] align_(4) = __VA_ARGS__; atombuilder_unroll(abuilder, slice_from_array(MipsCode, sym)); } + +/* Line-table anchor: gcc only adds a file to the .debug_line file table when the contains line-numbered content. + Files containing only: + - `MipsAtomComp_` static-array declarations, or + - `MipsAtomComp_Proc_` (force-inline) function bodies whose line info gets + attributed to the call site at the include point are otherwise omitted from the file table, + which breaks the DWARF injection when it tries to resolve atom-component provenance paths. Place `ATOM_FILE_LINE_MARKER();` once at file scope in any `.atom.c` that defines atoms. The macro expands to a file-scope `internal U4 const` declaration keeps the file in the line table. @@ -181,11 +193,13 @@ FI_ void tb_init(TapeBuilder* tb, FArena* arena) { tb->ptr = arena->start FI_ TapeBuilder tb_make_old( FArena* arena) { return (TapeBuilder){ arena->start, 0 }; } FI_ TapeBuilder tb_make(Slice mem) { return (TapeBuilder){ mem.ptr, mem.len, 0 }; } -FI_ void tb_emit(TapeBuilder* tb, MipsCode* atom) { u4_r(tb->ptr)[tb->used] = u4_(atom); ++ tb->used; } +FI_ void tb_emit(TapeBuilder* tb, MipsAtom* atom) { u4_r(tb->ptr)[tb->used] = u4_(atom); ++ tb->used; } FI_ void tb_data(TapeBuilder* tb, U4 data) { u4_r(tb->ptr)[tb->used] = u4_(data); ++ tb->used; } #define tb_emit_(atom) tb_emit(& tb, atom) #define tb_data_(field, data) tb_data(& tb, u4_(data)) +FI_ void tb_emit_bundle(TapeBuilder_R tb, Slice_MipsAtom atoms) { mem_copy(u4_(tb->ptr), u4_(atoms.ptr), tb->used); tb->used += atoms.len; } + FI_ Tape tb_end (TapeBuilder* tb) { tb_emit(tb,tape_exit); return (Tape){ C_(U4*,tb->ptr), tb->used }; } FI_ Tape tb_slice(TapeBuilder tb) { return (Tape){ C_(U4*,tb.ptr), tb.used }; } #define tb_scope(tb) for(U4 tbs_once=0;tbs_once==0;++tbs_once,tb_emit(tb,tape_exit)) @@ -233,22 +247,49 @@ typedef Relative_(FArena) Struct_(MipsAtomBuilder) { U4 start; U4 capacity; U4 u // Whatever the builder is writting to should most likely coresspond // to something that can fit within instruction cache? -FI_ void atombuilder_unroll(MipsAtomBuilder_R ab, Slice_MipsCode_R code) { - assert(ab->capacity - ab->used - code->len); - mem_copy(ab->start, u4_(code->ptr), code->len); - mem_bump(ab->start, ab->capacity, & ab->used, code->len); +FI_ void atombuilder_unroll(MipsAtomBuilder_R ab, Slice_MipsCode code) { + assert(ab->capacity - ab->used - code.len); + mem_copy(ab->start, u4_(code.ptr), code.len); + mem_bump(ab->start, ab->capacity, & ab->used, code.len); } #define atombuilder_unroll_mac(ab, mac) atombuilder_unroll(ab, slice_arg_from_array(Slice_MipsCode, mac)) -// When done authoring, utilize this to cap-off the atom +// When done authoring, utilize this to cap-off the atom (if not utilizing a MipsAtom_Proc). FI_ void atombuilder_end(MipsAtomBuilder_R ab) { mem_copy(ab->start, u4_(ac_yield), S_(ac_yield)); mem_bump(ab->start, ab->capacity, & ab->used, S_(ac_yield)); } -#define mipsatom_from_builder(ab) (Slice_MipsCode){ab.start, ab.used} +#define mipsatom_from_builder(ab) C_(MipsAtom*, (ab).start) #pragma endregion Mips Atom Builder +#pragma region Mips Atom Procs + +#if 0 +typedef Struct_(Binds_SyncPrimitiveArena) { U4 used; U4 cursor; }; +FI_ void sync_prim_arean_proc_demo(MipsAtomBuilder_R ab, U4 r_extra, U4 add_amnt_extra) +MipsAtom_Proc_(sync_primitive_arena_proc_demo, ab, atom_info(atom_bind(Binds_SyncPrimitiveArena) + , atom_reads( R_TapePtr, R_PrimCursor) + , atom_writes(R_TapePtr) +){ + load_word(R_AT, R_TapePtr, O_(Binds_SyncPrimitiveArena,used)), + load_word(R_T0, R_TapePtr, O_(Binds_SyncPrimitiveArena,cursor)), + add_ui_self( R_TapePtr, S_(Binds_SyncPrimitiveArena)), + /* Calculate byte offset and store directly back to RAM */ + sub_u( R_T0, R_PrimCursor, R_T0), // R_T0 = R_PrimCursor - binds.cursor + store_word(R_T0, R_AT, 0), // R_AT[0] = R_T0 + add_ui_self(r_extra, add_amnt_extra), // extra op for demonstration purposes. + mac_yield() +}) + +void demo_make_make_and_emit_atom(TapeBuilder* tb, MipsAtomBuilder* ab){ + sync_prim_arean_proc_demo(ab, R_T4, 4); + tb_emit(tb, mipsatom_from_builder(ab[0])); +} +#endif + +#pragma endregion Mips Atom Procs + #pragma region Baked Mips Atoms // These atoms are resolved at compile time and are (usually) statically linked readonly data. diff --git a/code/duffle/mips.h b/code/duffle/mips.h index da633e2..dd62a7a 100644 --- a/code/duffle/mips.h +++ b/code/duffle/mips.h @@ -348,6 +348,12 @@ enum { _BitOffsets = 0 #define shift_lright(rd, rt, shamt) enc_r(op_special, R_0, (rt), (rd), (shamt), fc_srl) #define shift_aright(rd, rt, shamt) enc_r(op_special, R_0, (rt), (rd), (shamt), fc_sra) +/* Shift Variable — register-shift forms. + * shift_lleft_var(rd, rt, rs) → sllv rd, rt, rs (shamt in low 5 bits of rs) + * shift_aright_var(rd, rt, rs) → srav rd, rt, rs */ +#define shift_lleft_var(rd, rt, rs) enc_r(op_special, (rs), (rt), (rd), 0, fc_sllv) +#define shift_aright_var(rd, rt, rs) enc_r(op_special, (rs), (rt), (rd), 0, fc_srav) + #define shift_lleft_self(rd_rt, shamt) enc_r(op_special, R_0, (rd_rt), (rd_rt), (shamt), fc_sll) #define mask_upper(rd, rt, shamt) shift_lleft(rd, rt, shamt), shift_lright(rd, rt, shamt) @@ -366,20 +372,18 @@ enum { _BitOffsets = 0 * WARNING: `jump(off)` CANNOT BE USED for within-atom jumps in the current pipeline. * The MIPS j opcode encodes `(target_addr >> 2)` in its 26-bit immediate field; an ABSOLUTE byte address, not a relative word offset. * The metaprogram computes `off` as a relative word offset (`target_word_idx - branch_word_idx - 1`), which the assembler/linker does NOT resolve. - * * `jump(off)` is only safe when the BUILD PIPELINE owns the absolute position of the emitted code — i.e. when: s * - the build emits a symbol-relative `.word` expression that the linker resolvess via `R_MIPS_26`, OR * - the code is hand-assembled with explicit absolute targets, OR a custom post-build patcher resolves the 26-bit field. + * TODO(Ed): Review this.. technically we can resolve aboslute jumps on baked atoms? (Even proedurally generated ones...) */ #define jump(off) enc_i(op_j, R_0, R_0, (off)) /* jump_rel off — unconditional relative jump (the within-atom-safe `jump`). - * MIPS I R3000A has no "branch always" opcode. The idiom for an unconditional relative jump is `beq $0, $0, off`. - */ + * MIPS I R3000A has no "branch always" opcode. The idiom for an unconditional relative jump is `beq $0, $0, off`. */ #define jump_rel(off) branch_equal(R_0, R_0, (off)) /* call_addr off — jump-and-link to immediate address. - * * Same WARNING as `jump(off)` above: the jal opcode also encodes an absolute 26-bit target. * For within-atom calls, the current pipeline has no equivalent always-taken call-and-link idiom. * Workaround: `branch_link` (always-taken branch + explicit `la $ra, next_word_addr; jr $ra`), or just use `call_reg($tmp)` after loading the target into a register. @@ -397,13 +401,7 @@ enum { _BitOffsets = 0 * sub_s / sub_u → sub / subu * mult_s / mult_u → mult / multu (writes HI/LO; result in LO) * div_s / div_u → div / divu (LO = quot, HI = rem) - * - * NOTE: dsl.h defines `add_s`/`sub_s`/`mut_s`/`gt_s`/etc. as _Generic-based signed integer-arithmetic helpers for U1/U2/U4. - * Those live in a different conceptual layer (generic arithmetic on DSL types) and would collide with the instruction encoders here. - * The `#undef` below lets the gas-style names below win; if a file needs both, the dsl.h versions can be reached via their long forms - * (e.g. `def_signed_op`-style or the underlying `add_s1/s2/s4`). */ -#undef add_s -#undef sub_s + */ #define add_s(rd, rs, rt) enc_r(op_special, (rs), (rt), (rd), 0, fc_add) #define add_u(rd, rs, rt) enc_r(op_special, (rs), (rt), (rd), 0, fc_addu) #define sub_s(rd, rs, rt) enc_r(op_special, (rs), (rt), (rd), 0, fc_sub) @@ -458,6 +456,9 @@ enum { _BitOffsets = 0 #define nop shift_lleft(rdiscard, rdiscard, 0) #define nop2 nop, nop +// li_s — load signed 16-bit immediate into GPR (addiu rt, $0, imm — sign-extends). +#define li_s(rt, imm) add_ui((rt), R_0, (imm)) + #define load_imm_1w(rt, imm) add_ui((rt), R_0, (imm)) #define load_imm_1w_s0(rt, imm) add_si((rt)), R_0, (imm)) diff --git a/code/duffle/word_count.metadata.h b/code/duffle/word_count.metadata.h index 68ee122..cd75d30 100644 --- a/code/duffle/word_count.metadata.h +++ b/code/duffle/word_count.metadata.h @@ -54,6 +54,15 @@ WORD_COUNT(gte_sw, 1) WORD_COUNT(gte_cmdw_rtpt, 1) WORD_COUNT(gte_cmdw_nclip, 1) WORD_COUNT(gte_avg_sort_z3, 1) +WORD_COUNT(gte_cmdw_sqr, 1) +WORD_COUNT(gte_cmdw_gpf, 1) +WORD_COUNT(shift_lleft_var, 1) +WORD_COUNT(shift_aright_var, 1) +WORD_COUNT(li_s, 1) +WORD_COUNT(and_i, 1) +WORD_COUNT(add_si, 1) +WORD_COUNT(branch_lt_zero, 1) +WORD_COUNT(sub_s, 1) WORD_COUNT(sub_u, 1) WORD_COUNT(nop2, 2) diff --git a/code/hello_camera/hello_camera.atom.c b/code/hello_camera/hello_camera.atom.c index b0571b3..b8a0648 100644 --- a/code/hello_camera/hello_camera.atom.c +++ b/code/hello_camera/hello_camera.atom.c @@ -383,8 +383,14 @@ internal MipsAtom_(resolve_look_at) atom_info(atom_bind(Binds_ResolveLookAt)) { mac_load_v3s4(R_Eye_x, R_Eye_y, R_Eye_z, R_CamEye, 0), mac_sub_v3s4( R_LkAt_Fwdx, R_LkAt_Fwdy, R_LkAt_Fwdz, R_Eye_x, R_Eye_y, R_Eye_z), - - + + // ac_normalize_v3s4(9 args): in-place normalize direction → unit vector. + // Reg-aliasing across the 4 stages: R_T7 = r_sq_y → r_lzcr, R_T8 = r_sq_z → r_shift, + // R_V0 = r_recip_est (always), R_V1 = r_tmp. r_sx/r_sy/r_sz = R_LkAt_Fwdx/y/z (in-place). + // mac_normalize_v3s4(R_LkAt_Fwdx, R_LkAt_Fwdy, R_LkAt_Fwdz, + // R_T7, R_T8, + // R_V0, + // R_T7, R_T8, R_V1), mac_yield(), }; @@ -438,7 +444,7 @@ MipsAtom_(cube_g4_face) atom_info(atom_phase(cube_g4), branch_le_zero(R_T0, atom_offset(cull, cube_g4_face_exit)), /* BD-slot: write the prim tag (R_0=0; overwrites the legacy tag word in the prim_buffer). * If branch IS taken (face culled), the body is skipped and this 0-tag is stranded — - * harmless because the OT entry that points to this prim is created later, only on the body path. */ + * harmless because the OT entry that points to this prim is created later. */ store_word(R_0, R_PrimCursor, O_(Poly_G4, tag)), shift_lleft(R_AT, R_T3, v3s2_byteoff), add_u(R_AT, R_AT, R_VertBase), load_word(R_V0, R_AT, O_(V3_S2, x)), load_word(R_V1, R_AT, O_(V3_S2, z)), diff --git a/code/hello_camera/hello_camera.c b/code/hello_camera/hello_camera.c index 16dcb34..235fc36 100644 --- a/code/hello_camera/hello_camera.c +++ b/code/hello_camera/hello_camera.c @@ -172,12 +172,12 @@ void update(PrimitiveArena* pa, U4* ordering_buf) S4 flag; //???? // Camera Look at - if (1) + if (0) { camera_look_at_c11(& smem.cam, & smem.cube.pos, & v3s4(0, -fp_one, 0)); } // Camera look at (Tape) - if (0) + if (1) { MT3_S2S4* look_at = & smem.cam.look_at; P3_S4* eye = & smem.cam.pos; @@ -188,8 +188,36 @@ void update(PrimitiveArena* pa, U4* ordering_buf) V3_S4 pos, off; tb.used = 0; tb_scope_run(& tb) { - tb_emit_(resolve_look_at); - // tb_data_(); + // tb_emit_bundle(resolve_look_at); + { + tb_emit_(resolve_look_at); { + tb_data_(look_at, & smem.cam.look_at); + tb_data_(eye, & smem.cam.pos); + tb_data_(target, & smem.cube.pos); + tb_data_(up_in, up_in); + // tb_emit(a_normalize_v3s4(/*Todo: resolve dependent register allocation*/)); + // tb_data_(fwd_out); + } + #if 0 + { + tb_emit_(resolve_look_at__resolve_right); { + //... + tb_emit_(a_normalize_v3s4(...)); + tb_data_(right_out); + } + tb_emit(resolve_look_at__resolve_up); { + //... + tb_emit_(ac_normalize_v3s4(...)); + tb_data_(up_out); + } + tb_emit(world_to_cam_expand_mt3_s2s4(...)); { + tb_data(look_at, & smem.cam.look_at); + } + tb_emit_(resolve_look_at__final); { + } + } + #endif + } } // forward = target[0]; sub_v3s4(& forward, eye[0]); // RGA(Lengyel): Affine point - point = zero-weight direction. @@ -205,7 +233,7 @@ void update(PrimitiveArena* pa, U4* ordering_buf) pos = eye[0]; mul_v3s4(& pos, v3s4(-1,-1,-1)); // RGA(Lengyel): -eye in world coordinates (spatial bulk only; implicit weight is dropped). - // RGA(Lengyel): R * (-eye) is the full matrix translation column. + // RGA(Lengyel): R * (-eye) -- full matrix translation column. // Motor translator would store half this displacement in m.xyz; GTE consumes full column. mul_m3s2_v3s4(look_at, & pos, & off); trans_m3s2( look_at, & off); diff --git a/scripts/duffle.lua b/scripts/duffle.lua index aa5516a..b32a0c8 100644 --- a/scripts/duffle.lua +++ b/scripts/duffle.lua @@ -1053,6 +1053,8 @@ M.GTE_COMMAND_ALIASES = { -- gte_avg_sort_z3 / gte_avg_sort_z4 are the duffle-side aliases for AVSZ3/4. ["gte_avg_sort_z3"] = "gte_cmdw_avsz3", ["gte_avg_sort_z4"] = "gte_cmdw_avsz4", + ["gte_cmdw_sqr"] = "gte_cmdw_sqr", + ["gte_cmdw_gpf"] = "gte_cmdw_gpf", } -- GTE command input-set table. @@ -1136,6 +1138,14 @@ M.GTE_COMMAND_INPUTS = { "C2_SZ0", "C2_SZ1", "C2_SZ2", "C2_SZ3", "gte_cr_ZSF4", }, + -- SQR: reads IR1..IR3 (per PSX-SPX gte.md SQR section; libgte disassembly 0x800160b0). + ["gte_cmdw_sqr"] = { + "C2_IR1", "C2_IR2", "C2_IR3", + }, + -- GPF: reads IR0 + IR1..IR3 (per PSX-SPX gte.md GPF section; libgte disassembly 0x8001613c). + ["gte_cmdw_gpf"] = { + "C2_IR0", "C2_IR1", "C2_IR2", "C2_IR3", + }, } -- GTE command output-set + semantic role table. @@ -1208,6 +1218,22 @@ M.GTE_COMMAND_OUTPUTS = { { register = "C2_IR2", role = "latest_color" }, { register = "C2_IR3", role = "latest_color" }, }, + ["gte_cmdw_sqr"] = { + { register = "C2_MAC1", role = "mac_result" }, + { register = "C2_MAC2", role = "mac_result" }, + { register = "C2_MAC3", role = "mac_result" }, + { register = "C2_IR1", role = "latest_color" }, + { register = "C2_IR2", role = "latest_color" }, + { register = "C2_IR3", role = "latest_color" }, + }, + ["gte_cmdw_gpf"] = { + { register = "C2_MAC1", role = "mac_result" }, + { register = "C2_MAC2", role = "mac_result" }, + { register = "C2_MAC3", role = "mac_result" }, + { register = "C2_IR1", role = "latest_color" }, + { register = "C2_IR2", role = "latest_color" }, + { register = "C2_IR3", role = "latest_color" }, + }, } -- GTE command/post-command latch-window table. @@ -1270,6 +1296,22 @@ M.GTE_COMMAND_LATCH_WINDOWS = { { register = "C2_IR2", required = 4 }, { register = "C2_IR3", required = 4 }, }, + ["gte_cmdw_sqr"] = { + { register = "C2_MAC1", required = 4 }, + { register = "C2_MAC2", required = 4 }, + { register = "C2_MAC3", required = 4 }, + { register = "C2_IR1", required = 4 }, + { register = "C2_IR2", required = 4 }, + { register = "C2_IR3", required = 4 }, + }, + ["gte_cmdw_gpf"] = { + { register = "C2_MAC1", required = 4 }, + { register = "C2_MAC2", required = 4 }, + { register = "C2_MAC3", required = 4 }, + { register = "C2_IR1", required = 4 }, + { register = "C2_IR2", required = 4 }, + { register = "C2_IR3", required = 4 }, + }, } -- Operand-class table for the COP2->GPR load-delay check. @@ -1285,6 +1327,7 @@ M.GTE_COMMAND_LATCH_WINDOWS = { M.OPERAND_READ_POSITIONS = { -- CPU ALU with one or two GPR operands. Reads every GPR operand. ["add_ui"] = {1, 2}, + ["li_s"] = {1, 2}, -- rt (write), imm16 (immediate) ["add_ui_self"] = {1}, ["add_si"] = {1, 2}, ["add_u"] = {1, 2, 3}, @@ -1354,6 +1397,8 @@ M.OPERAND_READ_POSITIONS = { ["gte_mv_to_ctrl_r"] = {}, ["gte_lw"] = {}, ["gte_sw"] = {}, + ["shift_lleft_var"] = {1, 2, 3}, -- rd, rt, rs (variable shift amount) + ["shift_aright_var"] = {1, 2, 3}, } -- GP0 packet sizes (total words including the 1-word tag) per GP0 cmd byte. @@ -1435,8 +1480,10 @@ M.INSTRUCTION_LATENCY = { ["xor_i"] = 1, ["xor_u"] = 1, ["nor_u"] = 1, ["shift_lleft"] = 1, ["shift_lleft_self"] = 1, + ["shift_lleft_var"] = 1, -- sllv: 1 cycle ["shift_lright"] = 1, ["shift_aright"] = 1, + ["shift_aright_var"] = 1, -- srav: 1 cycle ["mask_upper"] = 1, ["mov_from_high"] = 2, -- mfhi: 2 cycles ["mov_from_low"] = 2, -- mflo: 2 cycles @@ -1454,6 +1501,7 @@ M.INSTRUCTION_LATENCY = { ["load_half_u"] = 1, ["load_half"] = 1, ["load_byte_u"] = 1, ["load_byte"] = 1, ["load_upper_i"] = 1, + ["li_s"] = 1, -- aliased to add_ui(rt, R_0, imm); 1 cycle -- 2-word loads (lui + ori) used for >16-bit immediates ["load_imm"] = 2, ["load_imm_1w"] = 1, @@ -1497,6 +1545,8 @@ M.INSTRUCTION_LATENCY = { ["gte_cmdw_op"] = 6, -- OP: 6 cycles (PSX-SPX) ["gte_cmdw_outer_product"] = 6, -- alias for OP ["gte_cmdw_wedge"] = 6, -- alias for OP + ["gte_cmdw_sqr"] = 5, -- SQR(sf): 5 cycles (PSX-SPX); +2 nops for pre-fill if sf=0/1 + ["gte_cmdw_gpf"] = 5, -- GPF(sf,lm): 5 cycles (PSX-SPX); +2 nops for pre-fill if needed -- Long-form aliases (same cycle cost as their short form) ["gte_cmdw_rotate_translate_perspective_single"] = 15, -- alias for rtps ["gte_cmdw_rotate_translate_perspective_triple"] = 23, -- alias for rtpt @@ -1777,6 +1827,7 @@ M.CU2_TRANSITION_POLICY = { M.INSTRUCTION_GPR_EFFECTS = { -- CPU ALU with one or two GPR operands. Reads every GPR operand position. add_ui = { reads = {1, 2}, writes = {1} }, + li_s = { reads = {1, 2}, writes = {1} }, -- RMW: rt is both read + written add_ui_self = { reads = {1}, writes = {1} }, add_si = { reads = {1, 2}, writes = {1} }, add_u = { reads = {2, 3}, writes = {1} }, @@ -1893,6 +1944,8 @@ M.INSTRUCTION_GPR_EFFECTS = { atom_writes = { reads = {}, writes = {} }, -- mac_yield transfers control to the next atom; zero GPR effects. mac_yield = { reads = {}, writes = {} }, + shift_lleft_var = { reads = {2, 3}, writes = {1} }, + shift_aright_var = { reads = {2, 3}, writes = {1} }, } -- Bounded GPR-value rules consumed by the same forward event walk as `INSTRUCTION_GPR_EFFECTS`. @@ -1903,18 +1956,19 @@ M.INSTRUCTION_GPR_EFFECTS = { -- * passes/static_analysis.lua::apply_gpr_effects -- No second `bounded_value_pass` is permitted. M.GPR_VALUE_RULES = { - load_upper_i = { op = "load_upper_i", dest = 1, immediate = 2, }, - add_ui = { op = "add_ui", dest = 1, source = 2, immediate = 3, }, - or_i = { op = "or_i", dest = 1, source = 2, immediate = 3, }, - and_i = { op = "and_i", dest = 1, source = 2, immediate = 3, }, - xor_i = { op = "xor_i", dest = 1, source = 2, immediate = 3, }, - add_ui_self = { op = "add_ui", dest = 1, source = 1, immediate = 2, }, - or_i_self = { op = "or_i", dest = 1, source = 1, immediate = 2, }, + load_upper_i = { op = "load_upper_i", dest = 1, immediate = 2, }, + add_ui = { op = "add_ui", dest = 1, source = 2, immediate = 3, }, + li_s = { op = "add_ui", dest = 1, source = 2, immediate = 3 }, -- R_0 + sign-ext(imm) folds into a constant + or_i = { op = "or_i", dest = 1, source = 2, immediate = 3, }, + and_i = { op = "and_i", dest = 1, source = 2, immediate = 3, }, + xor_i = { op = "xor_i", dest = 1, source = 2, immediate = 3, }, + add_ui_self = { op = "add_ui", dest = 1, source = 1, immediate = 2, }, + or_i_self = { op = "or_i", dest = 1, source = 1, immediate = 2, }, -- Present register-form self variants. They are included here so a -- known value is not needlessly lost when these encoders are used. - add_u_self = { op = "add_u", dest = 1, sources = {1, 2}, }, - or_u_self = { op = "or", dest = 1, sources = {1, 2}, }, - shift_lleft_self = { op = "shift_lleft", dest = 1, source = 1, immediate = 2, }, + add_u_self = { op = "add_u", dest = 1, sources = {1, 2}, }, + or_u_self = { op = "or", dest = 1, sources = {1, 2}, }, + shift_lleft_self = { op = "shift_lleft", dest = 1, source = 1, immediate = 2, }, } -- Control-transfer (branch/jump/call) delay-slot policy table. diff --git a/scripts/passes/components.lua b/scripts/passes/components.lua index 87c7085..8a81f7c 100644 --- a/scripts/passes/components.lua +++ b/scripts/passes/components.lua @@ -299,7 +299,9 @@ local function word_count_rec(name, comp_by_name, wc, cache) local trimmed = t.tok if trimmed ~= "" then local lookup = strip_mac_prefix(duffle.read_ident(trimmed, 1)) - if lookup and comp_by_name[lookup] then + if lookup == "atom_label" or lookup == "atom_offset" then + -- Pure metaprogram anchors; emit zero words. + elseif lookup and comp_by_name[lookup] then -- It's a `mac_X(...)` call. Recurse. n = n + word_count_rec(lookup, comp_by_name, wc, cache) elseif lookup and wc and wc[lookup] then diff --git a/scripts/passes/static_analysis.lua b/scripts/passes/static_analysis.lua index 846b36b..c28db0e 100644 --- a/scripts/passes/static_analysis.lua +++ b/scripts/passes/static_analysis.lua @@ -256,8 +256,21 @@ local BRANCH_PATTERN = "^branch_[%w_]+%s*%(" -- The C preprocessor expands it BEFORE the metaprogram sees the source, but for source-level metadata consistency we still match it here and classify it as a branch_equal. -- This keeps `consuming_encoder` canonical for any downstream tooling that consults the metadata field. local JUMP_REL_PATTERN = "^jump_rel%s*%(" -local UNCOND_JUMP_PATTERN = "^%f[%w](jump|call_addr)%f[%W]" -local TERMINAL_JUMP_PATTERN = "^%f[%w](jump_reg|call_reg|jump_link)%f[%W]" +local UNCOND_JUMP_PATTERNS = { + "^%f[%w]jump%f[%W]", + "^%f[%w]call_addr%f[%W]", +} +local TERMINAL_JUMP_PATTERNS = { + "^%f[%w]jump_reg%f[%W]", + "^%f[%w]call_reg%f[%W]", + "^%f[%w]jump_link%f[%W]", +} +local function matches_any(tok, patterns) + for i = 1, #patterns do + if tok:match(patterns[i]) then return true end + end + return false +end local function classify_tokens(tokens) local n = #tokens @@ -301,13 +314,13 @@ local function classify_tokens(tokens) -- Both encode a 16-bit signed relative word offset. is_branch = true branch_label = tok:match("atom_offset%s*%([^,]+,%s*([%w_]+)%s*%)") or false - elseif tok:match(UNCOND_JUMP_PATTERN) then + elseif matches_any(tok, UNCOND_JUMP_PATTERNS) then -- Unconditional absolute jump / call: `jump(off)` / `call_addr(off)`. -- One immediate offset field; can carry an `atom_offset(F, T)` marker (the offsets pass dispatches on `consuming_encoder` — see `passes/offsets.lua::compute_offsets`). is_branch = true is_unconditional_jump = true branch_label = tok:match("atom_offset%s*%([^,]+,%s*([%w_]+)%s*%)") or false - elseif tok:match(TERMINAL_JUMP_PATTERN) then + elseif matches_any(tok, TERMINAL_JUMP_PATTERNS) then -- Register-form jump / call: no offset field; `atom_offset` is invalid here (the offsets pass will error if one is supplied). -- Transfers control OUT of the current atom — the CFG treats this as a path terminator. is_terminal_jump = true @@ -567,13 +580,31 @@ local function evaluate_gpr_value_rule(rule, ev_args, gpr_values) return shift_left_u4(immediate % 0x10000, 16) end - local source = nil + -- Encoders that take `R_0` implicitly (e.g. `li_s(rt, imm)` which is `add_ui(rt, R_0, imm)`) have a non-GPR operand at the source position. + -- Fall back to R_0 = 0. + -- The implicit-R_0 macros also use a different immediate position (e.g. `li_s`'s `add_ui` rule has source = 2 / immediate = 3 + -- but the macro takes 2 args); when the configured immediate position is out of bounds. + -- Fall back instead to scanning the macro's args for the first integer literal and use that as the immediate. + local source = 0 if rule.source then - source = constant_for_operand(gpr_values, ev_args[rule.source]) - if source == nil then return nil end + if is_gpr_operand(ev_args[rule.source]) then + source = constant_for_operand(gpr_values, ev_args[rule.source]) + if source == nil then return nil end + end + -- Non-GPR at source position = implicit R_0; source stays 0. + end + local immediate = nil + if rule.immediate and ev_args[rule.immediate] ~= nil then + immediate = parse_integer_literal(ev_args[rule.immediate]) + if immediate == nil then return nil end + elseif rule.immediate then + -- Immediate position out of bounds: scan for the first integer literal in the args. + for _, arg in ipairs(ev_args) do + immediate = parse_integer_literal(arg) + if immediate ~= nil then break end + end + if immediate == nil then return nil end end - local immediate = rule.immediate and parse_integer_literal(ev_args[rule.immediate]) or nil - if rule.immediate and immediate == nil then return nil end if operation == "add_ui" then return wrap_u4( source + sign_extend_i16(immediate)) elseif operation == "or_i" then return bit_binary( source, immediate % 0x10000, "or") elseif operation == "and_i" then return bit_binary( source, immediate % 0x10000, "and") @@ -1433,17 +1464,20 @@ end --- The register becomes non-volatile again at word N+2 (the load has retired), OR sooner if a non-load instruction overwrites the register --- (the overwriter's write is the fresh producer; the load's value is shadowed and never observed by any reader). --- ---- Runtime-helper atoms / components (`debug_skip == true`) are exempt: their internal load-then-use sequences ---- are part of the fixed handshake (e.g. `ac_load_tri_indices` loads into R_T0..R_T2, but those are caller-supplied). +--- Runtime-helper atoms / components (`debug_skip == true`) are exempt from some checks, but load-delay +--- safety applies to their emitted instructions as well. --- --- The walker reads `duffle.OPERAND_READ_POSITIONS[event.encoder]` to determine which args are read-source --- (the destination of a load is in `writes`, not `reads` — see `duffle.INSTRUCTION_GPR_EFFECTS`). ---- The check is purely structural; it does not consult the GPR-value lattice (no constant propagation needed for load-delay detection — the volatility window is unconditional). +--- The check is purely structural; it does not consult the GPR-value lattice +--- (no constant propagation needed for load-delay detection — the volatility window is unconditional). local function check_load_delay_slots(atom, pipe_ctx, findings) + -- The load-delay check applies to every atom and component body, including debug-skipped components (`ac_*` and `atom_dbg_skip MipsAtom_(...)`). + -- The `atom_dbg_skip` marker controls debugger stepping, not instruction safety. + local p = atom.paths or {} if atom.kind ~= "atom" then return end - local events = atom.paths.word_events or {} + local events = p.word_events or {} if #events == 0 then return end - if is_runtime_helper(atom) then return end local gpr_effects = duffle.INSTRUCTION_GPR_EFFECTS or {} local read_positions = duffle.OPERAND_READ_POSITIONS or {} @@ -1656,21 +1690,39 @@ local function check_yield_load_tail_pairing(atom, _pipe_ctx, findings) return atom.line + line_in_body[tokens[idx].rel] end - -- ── Rule 1: every `mac_yield_load()` must be in a branch BD-slot. + -- ── Rule 1: every `mac_yield_load()` must be in a branch BD-slot, OR sit between two `atom_label`s (natural fall-through load pattern). + -- When the pattern is satisfied, the check stays silent; only violations emit findings. 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), - } + local prev_tc = (tok_idx >= 2) and tc[tok_idx - 1] or nil + -- Look for the next `atom_label()` token (skip `atom_offset` markers; check immediately-adjacent first). + local next_label_tc = (tok_idx + 1 <= n) and tc[tok_idx + 1] or nil + if next_label_tc and next_label_tc.ident ~= "atom_label" then + next_label_tc = nil + for j = tok_idx + 1, n do + local t = tc[j] + if t.ident == "atom_label" then + next_label_tc = t + break + end + end + end + local natural_fallthrough = prev_tc and prev_tc.is_atom_label and next_label_tc ~= nil + if not natural_fallthrough then + if tok_idx < 2 or not prev_tc.is_branch then + local prev_ident = prev_tc and (prev_tc.ident or "?") or "" + local next_ident = next_label_tc and (next_label_tc.ident .. "(" .. (next_label_tc.label_name 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 — and the next `atom_label()` token is `%s` — `mac_yield_load()` must fill a branch BD-slot or sit between two `atom_label`s for the natural fall-through load." + , atom.name, tok_idx >= 2 and line_for(tok_idx) or atom.line, tok_idx, prev_ident, next_ident), + } + end end end end @@ -2015,8 +2067,9 @@ local function analyze_atom_paths(atom, pipe_ctx) succ[#succ + 1] = label_pos + 1 end end - -- For literal-offset jumps (label == false), the target is a non-tracked address; conservatively omit. - return succ, nil + -- For literal-offset jumps (label == false), control transfers out unconditionally. + -- Treat as a terminator so the path is recorded (NOT as a silent fall-through to the next token, which is unreachable in this atom's execution). + return {}, tok_idx end -- Conditional branch: BD slot absorbed; two successors — fall-through (tok_idx+2) + taken (if known). if tok_idx + 2 <= n then @@ -2032,9 +2085,11 @@ local function analyze_atom_paths(atom, pipe_ctx) -- Return (succ, nil), the second value is the terminator marker (nil = not a terminator). return succ, nil end - -- Normal token: just the next one + -- Normal token: just the next one. + -- The final ordinary word of the body has no successor and terminates the path; + -- record it as an implicit endpoint so the cycle budget for non-yield components is not silently zeroed. if tok_idx + 1 <= n then return { tok_idx + 1 }, nil end - return {}, nil + return {}, tok_idx end -- DFS through all paths. Track the current cycle sum, a visited set scoped to the current path (to detect loops), and a count of paths.