diff --git a/code/duffle/gte.atom.c b/code/duffle/gte.atom.c index cb007ce..8e6a328 100644 --- a/code/duffle/gte.atom.c +++ b/code/duffle/gte.atom.c @@ -83,6 +83,10 @@ FI_ Slice_MipsCode ac_gte_gpf_scale(MipsAtomBuilder_R ab, U4 r_sx, U4 r_sy, U4 r shift_aright_var(r_dz, r_dz, r_shift), }) +#pragma endregion MACs (Mips Atom Components) + +#pragma region Atom Procs + /* ─── 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 @@ -188,14 +192,14 @@ typedef Struct_(Binds_NormalizeV3S4) { * gte.atom.c is the GENERIC GTE primitives file — it exposes only the parameter-style normalize_v3s4_proc for any future caller. */ I_ void normalize_v3s4_proc( MipsAtomBuilder_R ab - , U4 r_src /* GPR code: scratch base carrier (wave-context, e.g., R_T4) */ - , U4 r_dst /* GPR code: scratch dst pointer carrier (wave-context, e.g., R_T5) */ - , U4 r_sx, U4 r_sy, U4 r_sz /* GPR codes: src.x/y/z scratch (atom-local) */ - , U4 r_sq_y, U4 r_sq_z /* GPR codes: MAC1/2 scratch (atom-local) */ - , U4 r_recip_est /* GPR code: |v|² sum + shift-input + sqrtbl[index] (atom-local) */ - , U4 r_lzcr /* GPR code: LZCR value (atom-local) */ - , U4 r_shift /* GPR code: final srav amount (atom-local) */ - , U4 r_tmp /* GPR code: scratch (shift count, branch target, lookup addr, table base) */ + , U4 r_src /* GPR code: scratch base carrier (wave-context, e.g., R_T4) */ + , U4 r_dst /* GPR code: scratch dst pointer carrier (wave-context, e.g., R_T5) */ + , U4 r_sx, U4 r_sy, U4 r_sz /* GPR codes: src.x/y/z scratch (atom-local) */ + , U4 r_sq_y, U4 r_sq_z /* GPR codes: MAC1/2 scratch (atom-local) */ + , U4 r_recip_est /* GPR code: |v|² sum + shift-input + sqrtbl[index] (atom-local) */ + , U4 r_lzcr /* GPR code: LZCR value (atom-local) */ + , U4 r_shift /* GPR code: final srav amount (atom-local) */ + , U4 r_tmp /* GPR code: scratch (shift count, branch target, lookup addr, table base) */ ) /* MipsAtom_Proc_ wrapper: declares the static MipsCode[] body, then calls atombuilder_unroll(ab, ...) to copy the encoded instructions into the caller's MipsAtomBuilder arena. */ MipsAtom_Proc_(normalize_v3s4, ab, { @@ -271,9 +275,9 @@ atom_label(aligned_done) /* Both paths converge h mac_yield() }) -#pragma endregion MACs (Mips Atom Components) +#pragma endregion Atom Procs -#pragma region Bsked Atoms +#pragma region Baked Atoms typedef Struct_(Binds_SetGteMT3S2S4) { MT3_S2S4* transform; diff --git a/code/hello_camera/gen/auto_reg.h b/code/hello_camera/gen/auto_reg.h new file mode 100644 index 0000000..b6cc875 --- /dev/null +++ b/code/hello_camera/gen/auto_reg.h @@ -0,0 +1,13 @@ +#ifdef INTELLISENSE_DIRECTIVES +#pragma once +#endif +// Auto-generated by ps1_meta.lua (passes/auto_reg.lua) — DO NOT EDIT +// Directory: C:\projects\Pikuma\ps1\code\hello_camera +// source: C:/projects/Pikuma/ps1/code/hello_camera/hello_camera.c +// source: C:/projects/Pikuma/ps1/code/hello_camera/hello_camera.h +// source: C:/projects/Pikuma/ps1/code/hello_camera/hello_camera.atom.c +// Per-phase register allocations resolved by the lua pass. +// R__Code = for every marker in this directory. + +#define R_GpTmp_Code R_V0_Code + diff --git a/code/hello_camera/gen/macs.h b/code/hello_camera/gen/macs.h index 3f6e2a8..9ebc717 100644 --- a/code/hello_camera/gen/macs.h +++ b/code/hello_camera/gen/macs.h @@ -86,7 +86,7 @@ WORD_COUNT(mac_resolve_look_at__input_and_sub, 34) , load_word(r_a, r_g, O_(V3_S4,x)) \ , load_word(r_b, r_g, O_(V3_S4,y)) \ , load_word(r_c, r_g, O_(V3_S4,z)) \ -, nop /* Load b (up_in).x/y/z into r_d + R_AT/R_V0 (hardcoded; reusing the +, nop /* Load b (up_in).x/y/z into r_d + R_AT/R_V0 (hardcoded; reusing the * body's last two loads is fine because the load-delay slot is the nop * after the third load, and mtc2 below doesn't read these regs). */ \ , load_word(r_d, r_h, O_(V3_S4,x)) \ diff --git a/code/hello_camera/hello_camera.atom.c b/code/hello_camera/hello_camera.atom.c index f0cbca8..9f36ef2 100644 --- a/code/hello_camera/hello_camera.atom.c +++ b/code/hello_camera/hello_camera.atom.c @@ -17,6 +17,7 @@ # include "duffle/psyq.atom.c" # include "gen/offsets.h" # include "gen/macs.h" +# include "gen/auto_reg.h" # include "hello_camera.h" #endif @@ -50,18 +51,18 @@ MipsAtomComp_Proc_(ac_put_draw_env, ab, { * (binary; the PutDrawEnv implementation builds the 16-word DR_ENV from the user's DRAWENV struct and emits it via GP0 GPU commands.) * * Word indices (libpsyx PutDrawEnv / SetDrawEnv order): - * tag = (length << 24) | addr — 16-word packet (1 tag + 15 code) - * code[0] = DrawMode (dfe=1, dtd=0, tpage=0) — must come first per libpsyx - * code[1] = TextureWindow (tw=(0,0)) — bare-cmd word; GPU uses current state - * code[2] = DrawArea top-left (clip.x=0, clip.y=240) - * code[3] = DrawArea bottom-right (clip.x+w=320, clip.y+h=480) - * code[4] = DrawOffset (ofs=(0,0)) — bare-cmd word - * code[5] = Mask (dtd=0, dfe=1, isbg=1) — 0xE6 cmd + isbg bit - * code[6] = Initial-bg-color (isbg=1, r=7, g=7, b=7) - * code[7] = DrawMode (isbg=1, tpage=0) — re-asserts DrawMode with isbg - * code[8..10] = padding (NOP) — 3 words to fill the packet - * code[11..12] = TextureWindow bottom-right — defaults to (0,0,0,0) - * code[13..14] = padding (NOP) — completes the 16-word packet + * tag = (length << 24) | addr — 16-word packet (1 tag + 15 code) + * code[0] = DrawMode (dfe=1, dtd=0, tpage=0) — must come first per libpsyx + * code[1] = TextureWindow (tw=(0,0)) — bare-cmd word; GPU uses current state + * code[2] = DrawArea top-left (clip.x=0, clip.y=240) + * code[3] = DrawArea bottom-right (clip.x+w=320, clip.y+h=480) + * code[4] = DrawOffset (ofs=(0,0)) — bare-cmd word + * code[5] = Mask (dtd=0, dfe=1, isbg=1) — 0xE6 cmd + isbg bit + * code[6] = Initial-bg-color (isbg=1, r=7, g=7, b=7) + * code[7] = DrawMode (isbg=1, tpage=0) — re-asserts DrawMode with isbg + * code[8..10] = padding (NOP) — 3 words to fill the packet + * code[11..12] = TextureWindow bottom-right — defaults to (0,0,0,0) + * code[13..14] = padding (NOP) — completes the 16-word packet */ mac_gcmd_push(gp0_dr_env_tag, reg_transfer, reg_base, port), /* tag (length=15 << 24, addr=0) — packet header for the DR_ENV sequence. The GPU needs this to recognize the next 15 words as a DR_ENV packet and trigger the isbg auto-clear. */ mac_gcmd_push(gp0_word_draw_mode_drawing_allowed, reg_transfer, reg_base, port), /* code[0] DrawMode (dfe=1, dtd=0, tpage=0) */ @@ -93,16 +94,35 @@ MipsAtomComp_Proc_(ac_put_draw_env, ab, { #pragma region Atom Procs // Modular Atoms +/* Scratchpad layout for the resolve_look_at bundle. + * The chain atoms communicate entirely via the wave-context GPR carrier R_ResolveScratch (R_T4) + hardcoded offsets into smem.scratchpad + * (PS1 hardware scratchpad at 0x1F800000). + * + * Atom 0 (input_and_sub) STAGES the C-side inputs (eye, up_in) into the scratchpad; + * AT THE SAME TIME it computes fwd = target - eye and stores it at scratch+0. + * Atoms 1-6 then read/write specific scratchpad offsets internally using + * `r_scratch + hardcoded_offset` — no tape-data pointers are passed between atoms. + * +0 fwd (atom 0 writes; atom 1 reads) + * +16 uz (atom 1 writes; atoms 2 + 4 read) + * +32 right (atom 2 writes; atom 3 reads) + * +48 ux (atom 3 writes; atoms 4 + 6 read) + * +64 up (atom 4 writes; atom 5 reads) + * +80 uy (atom 5 writes; atom 6 reads) + * +96 eye (atom 0 stages from C-side pointer; atom 6 reads) + * +128 up_in (atom 0 stages from C-side pointer; atom 2 reads) + */ + +// enum { +// R_LookAt = R_T0 atom_reg atom_type(MT3_S2S4*), +// R_CamEye = R_T1 atom_reg atom_type(P3_S4*), +// R_CamTarget = R_T2 atom_reg atom_type(P3_S4*), +// R_WorldUp = R_T3 atom_reg atom_type(V3_S4*), +// }; + enum { /* Wave-context GPR carrier for the resolve_look_at bundle: the scratch base. - * Set by atom 0 (popped from tape), read by atoms 1-6 (used as pointer base). - * Type is U4* — this holds the scratch base address (smem.scratchpad value). - * - * Other wave-context carriers (R_ResolveUzPtr / UxPtr / UyPtr) used in the - * prior design were dropped: the new chain atoms compute their src/dst - * addresses internally from R_ResolveScratch + hardcoded_offset. */ + * Set by atom 0 (popped from tape), read by atoms 1-6 (used as pointer base). */ R_ResolveScratch = R_T4 atom_reg atom_type(U4*), -#define R_ResolveScratch_Code R_T4_Code }; typedef Struct_(Binds_ResolveLookAt) { MT3_S2S4* look_at; @@ -111,11 +131,7 @@ typedef Struct_(Binds_ResolveLookAt) { V3_S4* up_in; }; -/* Per-atom bind-pop structs for the resolve_look_at bundle. - * Atom 0 (input_and_sub) is the ONLY atom that touches the C-side pointers + - * scratch base. Atoms 1-6 use scratch + hardcoded offsets internally. - * Field types are U4 (raw pointer value) because the structs are populated - * by the frame-time bundle helper with the literal C-side pointer values. */ +/* Per-atom bind-pop structs for the resolve_look_at bundle. */ typedef Struct_(Binds_ResolveLookAtScratch) { U4 scratch_base; /* U4 (scratch base address — populated by helper with u4_(smem.scratchpad)) */ }; @@ -125,30 +141,25 @@ typedef Struct_(Binds_ResolveLookAtScratch) { * * Each slot is 16 bytes: V3_S4 is already 16 bytes (4 × S4 = x/y/z/pad). * The struct fields are contiguous — slot i starts at offset i*16. - * Used by the assembly via O_(ResolveLookAtScratch, fld.x/y/z) which resolves - * to a compile-time byte offset. NOT a runtime struct — the struct is purely - * a schema for offsets; the assembly uses `r_scratch + O_(...)` to compute - * slot addresses at runtime. + * Used by the assembly via O_(ResolveLookAtScratch, fld.x/y/z) which resolves to a compile-time byte offset. + * NOT a runtime struct — the struct is purely a schema for offsets; the assembly uses `r_scratch + O_(...)` to compute slot addresses at runtime. * * Slot producers/consumers (referenced by the resolve_look_at chain atoms): - * - * +0 fwd atom 0 writes (target - eye); atom 1 (normalize) reads - * +16 uz atom 1 writes (normalize fwd); atoms 2 + 4 read (cross operands) - * +32 right atom 2 writes (cross uz x up_in); atom 3 (normalize) reads - * +48 ux atom 3 writes (normalize right); atoms 4 + 6 read - * +64 up atom 4 writes (cross uz x ux); atom 5 (normalize) reads - * +80 uy atom 5 writes (normalize up); atom 6 reads - * +96 eye atom 0 stages (C-side input); atom 6 reads (translation column) - * +112 target reserved (currently written nowhere — kept for symmetry w/ eye) - * +128 up_in atom 0 stages (C-side input); atom 2 reads (cross operand) + * +0 fwd 0 writes (target - eye); atom 1 (normalize) reads + * +16 uz 1 writes (normalize fwd); atoms 2 + 4 read (cross operands) + * +32 right 2 writes (cross uz x up_in); atom 3 (normalize) reads + * +48 ux 3 writes (normalize right); atoms 4 + 6 read + * +64 up 4 writes (cross uz x ux); atom 5 (normalize) reads + * +80 uy 5 writes (normalize up); atom 6 reads + * +96 eye 0 stages (C-side input); atom 6 reads (translation column) + * +112 target reserved (currently written nowhere — kept for symmetry w/ eye) + * +128 up_in 0 stages (C-side input); atom 2 reads (cross operand) * * Fields use P3_S4 (point) for eye/target (RGA: affine point, implicit weight 1); - * V3_S4 (vector) for fwd/uz/right/ux/up/uy/up_in (RGA: Euclidean vector). P3_S4 - * is a storage alias of V3_S4 (see math.h comment: "Storage alias of V3_S4. + * V3_S4 (vector) for fwd/uz/right/ux/up/uy/up_in (RGA: Euclidean vector). + * P3_S4 is a storage alias of V3_S4 (see math.h comment: "Storage alias of V3_S4. * Use P3_S4 when the value is a point.") — both are 16 bytes. - * - * Moved from gte.atom.c (Task 12.11): gte.atom.c is the GENERIC GTE primitives - * file and must not know about any specific atom bundle's scratch layout. */ + */ typedef Struct_(ResolveLookAtScratch) { V3_S4 fwd; /* offset +0 (16 bytes — 4 S4 fields incl. internal pad) */ V3_S4 uz; /* offset +16 (16 bytes) */ @@ -161,38 +172,25 @@ typedef Struct_(ResolveLookAtScratch) { V3_S4 up_in; /* offset +128 (16 bytes) */ }; -/* ─── resolve_look_at bundle chain atoms (Task 5) ──────────────────────────── - * 7 unique atom procs in the resolve_look_at bundle (4 chain atoms + 3 normalize - * variants). All 7 are runtime-built MipsAtom_Proc_ atoms: each function declares - * a static MipsCode[] body, then calls atombuilder_unroll() to append it to the - * caller's MipsAtomBuilder arena. Task 6's resolve_look_at_init() uses this pattern - * to pre-build the bundle into the static arena (smem.resolve_look_at_arena). +/* ─── resolve_look_at bundle chain atoms ──────────────────────────── + * 7 unique atom procs in the resolve_look_at bundle (4 chain atoms + 3 normalize variants). + * All 7 are runtime-built MipsAtom_Proc_ atoms: each function declares a static MipsCode[] body, + * then calls atombuilder_unroll() to append it to the caller's MipsAtomBuilder arena. + * resolve_look_at_init() uses this pattern to pre-build the bundle into the static arena (smem.resolve_look_at_arena). * - * Atom roster (positions 0-6 in the bundle): - * Atom 0: resolve_look_at__input_and_sub (chain atom) - * Atom 1: resolve_look_at__normalize_fwd_to_uz (normalize wrapper) - * Atom 2: resolve_look_at__cross_uz_up_in_to_right (chain atom) - * Atom 3: resolve_look_at__normalize_right_to_ux (normalize wrapper) - * Atom 4: resolve_look_at__cross_uz_ux_to_up (chain atom) - * Atom 5: resolve_look_at__normalize_up_to_uy (normalize wrapper) - * Atom 6: resolve_look_at__populate_and_translate (chain atom) + * Atom roster: + * 0: resolve_look_at__input_and_sub (chain atom) + * 1: resolve_look_at__normalize_fwd_to_uz (normalize wrapper) + * 2: resolve_look_at__cross_uz_up_in_to_right (chain atom) + * 3: resolve_look_at__normalize_right_to_ux (normalize wrapper) + * 4: resolve_look_at__cross_uz_ux_to_up (chain atom) + * 5: resolve_look_at__normalize_up_to_uy (normalize wrapper) + * 6: resolve_look_at__populate_and_translate (chain atom) * - * The 3 normalize wrappers are CHAIN-SPECIFIC — they hardcode src/dst scratch - * offsets in the body (computed via r_scratch + O_(ResolveLookAtScratch, fld)). - * The generic normalize_v3s4_proc (in gte.atom.c) takes src/dst as GPR parameters - * and is NOT used by this bundle. (Layering rule: gte.atom.c contains only - * generic GTE primitives; bundle-specific code lives in this file.) - * - * The 3 normalize procs were moved from gte.atom.c to this file in Task 12.11 - * (user feedback: "normalize is not supposed to be aware of a specific scratch - * for one atom bundle"). The procs were renamed to resolve_look_at__normalize_*_proc - * to make their bundle-specific nature clear. - * - * Lua metaprogram support (Task 12.10): the metaprogram auto-emits - * `atom_offset__X__Y` defs in gen/offsets.h for each atom_label/atom_offset pair - * in the body. The 3 normalize procs each have internal branches (srav_path / - * aligned_done variants) and get their per-proc-instance defs (e.g., - * `atom_offset_srav_path_fwd_to_uz_aligned_done_fwd_to_uz`). + * The 3 normalize wrappers are CHAIN-SPECIFIC — they hardcode src/dst scratch offsets in the body + * (computed via r_scratch + O_(ResolveLookAtScratch, fld)). + * The generic normalize_v3s4_proc (in gte.atom.c) takes src/dst as GPR parameters and is NOT used by this bundle. + * (Layering rule: gte.atom.c contains only generic GTE primitives; bundle-specific code are within this file.) */ typedef Struct_(Binds_ResolveLookAtSub) { @@ -201,23 +199,19 @@ typedef Struct_(Binds_ResolveLookAtSub) { U4 up_in; /* U4 (C-side V3_S4* — read by atom 0 directly; staged into scratchpad by atom 0) */ }; -/* Atom 0 in the bundle: input_and_sub. Stages C-side inputs into the scratchpad - * and computes fwd = target - eye. - * - * Inputs (C-side pointers popped from the tape; NOT scratchpad addresses): +/* Atom 0 in the bundle: input_and_sub. Stages C-side inputs into the scratchpad and computes fwd = target - eye. + * Inputs (C-side pointers popped from the tape): * r_target_ptr : P3_S4* (C-side struct; atom 0 reads target.x/y/z directly) * r_eye_ptr : P3_S4* (C-side struct; staged into scratchpad at +96/+100/+104) * r_up_in_ptr : V3_S4* (C-side struct; staged into scratchpad at +128/+132/+136) - * * Wave-context output: * r_scratch : R_ResolveScratch (R_T4) — scratch base, read by atoms 1-6 - * + * * Bind-pop layout: - * Binds_ResolveLookAtSub = 12 bytes (target + eye + up_in ptrs) - * Binds_ResolveLookAtScratch = 4 bytes (scratch_base) - * + * Binds_ResolveLookAtSub = 12 bytes (target + eye + up_in ptrs) + * Binds_ResolveLookAtScratch = 4 bytes (scratch_base) * Staging work: - * * Stage eye.x/y/z → scratch+96/+100/+104 (for atom 6's translation column) + * * Stage eye.x/y/z → scratch+96/+100/+104 (for atom 6's translation column) * * Stage up_in.x/y/z → scratch+128/+132/+136 (for atom 2's outer-product operand) * * Compute fwd = target - eye, store fwd.x/y/z → scratch+0/+4/+8 (for atom 1) * @@ -235,20 +229,17 @@ typedef Struct_(Binds_ResolveLookAtSub) { * * Pool cost: 8 GPRs + R_T4 (carrier) + R_AT + R_V0 (hardcoded) = 11 GPRs. */ -I_ void resolve_look_at__input_and_sub_proc(MipsAtomBuilder_R ab - , U4 r_target_ptr - , U4 r_eye_ptr - , U4 r_up_in_ptr - , U4 r_scratch - , U4 r_tmp0, U4 r_tmp1, U4 r_tmp2, U4 r_tmp3 +I_ void resolve_look_at__input_and_sub_proc(MipsAtomBuilder_R ab, U4 r_scratch + , U4 r_target_ptr,U4 r_eye_ptr, U4 r_up_in_ptr + , U4 r_tmp0, U4 r_tmp1, U4 r_tmp2, U4 r_tmp3 ) MipsAtom_Proc_(resolve_look_at__input_and_sub, ab, { /* Pop the 3 C-side pointers + scratch_base from the tape. */ load_word(r_target_ptr, R_TapePtr, O_(Binds_ResolveLookAtSub,target)), load_word(r_eye_ptr, R_TapePtr, O_(Binds_ResolveLookAtSub,eye)), load_word(r_up_in_ptr, R_TapePtr, O_(Binds_ResolveLookAtSub,up_in)), add_ui_self( R_TapePtr, S_(Binds_ResolveLookAtSub)), - load_word(r_scratch, R_TapePtr, O_(Binds_ResolveLookAtScratch,scratch_base)), - add_ui_self( R_TapePtr, S_(Binds_ResolveLookAtScratch)), + load_word(r_scratch, R_TapePtr, O_(Binds_ResolveLookAtScratch,scratch_base)), + add_ui_self( R_TapePtr, S_(Binds_ResolveLookAtScratch)), /* Stage eye.x/y/z into the scratchpad (atom 6 reads these for the translation * column). Reuse r_tmp0/r_tmp1/r_tmp2. Offsets via O_(ResolveLookAtScratch,*). */ @@ -291,32 +282,30 @@ I_ void resolve_look_at__input_and_sub_proc(MipsAtomBuilder_R ab }) /* Atoms 2 + 4 in the bundle: out = a × b (GTE outer product on IR/D vectors). - * No bind pop — the three operand pointers (a, b, out) are derived in-body - * from r_scratch + hardcoded_offset. Each atom has its own variant because - * the offsets are baked into the body and each atom uses unique GPRs. + * No bind pop — the three operand pointers (a, b, out) are derived in-body from r_scratch + hardcoded_offset. + * Each atom has its own variant because the offsets are baked into the body and each atom uses unique GPRs. * * GTE register layout (per PSX-SPX + duffle gte.h): - * IR1/2/3 = a.x/y/z (mtc2) - * VXY0 = b.x (mtc2) - * VZ0 = b.y (mtc2) - * VXY1 = b.z (mtc2) - * OP = outer product + * IR1/2/3 = a.x/y/z (mtc2) + * VXY0 = b.x (mtc2) + * VZ0 = b.y (mtc2) + * VXY1 = b.z (mtc2) + * OP = outer product * MAC1/2/3 = out.x/y/z (mfc2) * * Pool cost: r_scratch (R_T4 carrier) + 7 body GPRs + R_AT + R_V0 (hardcoded) = 10 GPRs. */ /* Atom 2: cross uz × up_in → right. */ -I_ void resolve_look_at__cross_uz_up_in_to_right_proc(MipsAtomBuilder_R ab - , U4 r_scratch - , U4 r_a, U4 r_b, U4 r_c /* load a.x/y/z; result out.x/y/z */ - , U4 r_d /* load b.x */ - , U4 r_f, U4 r_g, U4 r_h /* r_f = &right (out ptr), r_g = &uz, r_h = &up_in */ +I_ void resolve_look_at__cross_uz_up_in_to_right_proc(MipsAtomBuilder_R ab, U4 r_scratch + , U4 r_a, U4 r_b, U4 r_c /* load a.x/y/z; result out.x/y/z */ + , U4 r_d /* load b.x */ + , U4 r_f, U4 r_g, U4 r_h /* r_f = &right (out ptr), r_g = &uz, r_h = &up_in */ ) MipsAtom_Proc_(resolve_look_at__cross_uz_up_in_to_right, ab, { /* Compute the three scratch pointers from r_scratch. */ - add_si(r_g, r_scratch, O_(ResolveLookAtScratch,uz)), /* r_g = &uz */ - add_si(r_h, r_scratch, O_(ResolveLookAtScratch,up_in)), /* r_h = &up_in */ - add_si(r_f, r_scratch, O_(ResolveLookAtScratch,right)), /* r_f = &right (out) */ + add_si(r_g, r_scratch, O_(ResolveLookAtScratch,uz)), /* r_g = &uz */ + add_si(r_h, r_scratch, O_(ResolveLookAtScratch,up_in)), /* r_h = &up_in */ + add_si(r_f, r_scratch, O_(ResolveLookAtScratch,right)), /* r_f = &right (out) */ nop, /* Load a (uz).x/y/z into r_a/r_b/r_c. */ @@ -325,9 +314,9 @@ I_ void resolve_look_at__cross_uz_up_in_to_right_proc(MipsAtomBuilder_R ab load_word(r_c, r_g, O_(V3_S4,z)), nop, - /* Load b (up_in).x/y/z into r_d + R_AT/R_V0 (hardcoded; reusing the - * body's last two loads is fine because the load-delay slot is the nop - * after the third load, and mtc2 below doesn't read these regs). */ + /* Load b (up_in).x/y/z into r_d + R_AT/R_V0 + (hardcoded; reusing the body's last two loads is fine because the load-delay slot is the nop after the third load, + and mtc2 below doesn't read these regs). */ load_word(r_d, r_h, O_(V3_S4,x)), load_word(R_AT, r_h, O_(V3_S4,y)), load_word(R_V0, r_h, O_(V3_S4,z)), @@ -359,8 +348,7 @@ I_ void resolve_look_at__cross_uz_up_in_to_right_proc(MipsAtomBuilder_R ab }) /* Atom 4: cross uz × ux → up. */ -I_ void resolve_look_at__cross_uz_ux_to_up_proc(MipsAtomBuilder_R ab - , U4 r_scratch +I_ void resolve_look_at__cross_uz_ux_to_up_proc(MipsAtomBuilder_R ab, U4 r_scratch , U4 r_a, U4 r_b, U4 r_c /* load a.x/y/z; result out.x/y/z */ , U4 r_d /* load b.x */ , U4 r_f, U4 r_g, U4 r_h /* r_f = &up (out ptr), r_g = &uz, r_h = &ux */ @@ -404,26 +392,22 @@ I_ void resolve_look_at__cross_uz_ux_to_up_proc(MipsAtomBuilder_R ab mac_yield() }) -/* Atoms 1, 3, 5 in the bundle: chain-specific normalize wrappers around the - * generic normalize_v3s4_proc (gte.atom.c). The generic proc takes src/dst as - * GPR parameters; these wrappers HARDCODE src/dst via r_scratch + O_(ResolveLookAtScratch, fld) - * so the C-side bundle helper doesn't need to push scratchpad addresses via - * tb_data between atoms. (Task 12.8 fix: eliminate magic offsets.) - * - * The 4-stage normalize body (SQR → mfc2 → LZCS → GPF → srav) is identical to - * the generic version (GPR-renamed); cycle counts match. The only per-atom - * difference is the (src, dst) scratch offsets and the per-proc atom_label - * suffixes (srav_path_fwd_to_uz, srav_path_right_to_ux, srav_path_up_to_uy) so - * the per-proc-instance offsets are emitted disjointly in gen/offsets.h. +/* Atoms 1, 3, 5 in the bundle: chain-specific normalize wrappers around the generic normalize_v3s4_proc (gte.atom.c). + * The generic proc takes src/dst as GPR parameters; these wrappers HARDCODE src/dst via r_scratch + O_(ResolveLookAtScratch, fld) + * so the C-side bundle helper doesn't need to push scratchpad addresses via tb_data between atoms. + * + * The 4-stage normalize body (SQR → mfc2 → LZCS → GPF → srav) is identical to the generic version (GPR-renamed); cycle counts match. + * The only difference is the (src, dst) scratch offsets and the per-proc atom_label suffixes + * (srav_path_fwd_to_uz, srav_path_right_to_ux, srav_path_up_to_uy) so the per-proc-instance offsets are emitted disjointly in gen/offsets.h. * * GPR pool (10 free regs: R_T0..R_T3, R_T5..R_T7, R_V0, R_V1, R_AT; R_T4 reserved for R_ResolveScratch): - * r_a : src ptr (overlaps with r_recip_est carrier after the 3 src-loads) - * r_b : dst ptr (saved throughout) + * r_a : src ptr (overlaps with r_recip_est carrier after the 3 src-loads) + * r_b : dst ptr (saved throughout) * r_e/r_f/r_i : src.x/y/z → result.x/y/z (preserved across stages 1-2 via r_d/r_g/r_recip_est scratch) - * r_d/r_g : MAC1/2 scratch (dead after stage 2) - * r_h : LZCR (saved across stages 3-4) + * r_d/r_g : MAC1/2 scratch (dead after stage 2) + * r_h : LZCR (saved across stages 3-4) * r_recip_est : |v|² accumulator + sqrtbl[index] + 1/|v| (saved throughout) - * r_shift : final srav amount (saved across stages 3-4) + * r_shift : final srav amount (saved across stages 3-4) * * The Lua metaprogram (Task 12.10) auto-emits: * - `mac_resolve_look_at__normalize__to_` alias in gen/macs.h @@ -431,12 +415,11 @@ I_ void resolve_look_at__cross_uz_ux_to_up_proc(MipsAtomBuilder_R ab */ /* Atom 1: normalize fwd (scratch+0) → uz (scratch+16). */ -I_ void resolve_look_at__normalize_fwd_to_uz_proc(MipsAtomBuilder_R ab - , U4 r_scratch - , U4 r_a, U4 r_b /* src/dst scratch pointers */ - , U4 r_e, U4 r_f, U4 r_i /* src.x/y/z → result.x/y/z */ - , U4 r_d, U4 r_g /* MAC1/2 scratch (dead after stage 2) */ - , U4 r_h /* LZCR */ +I_ void resolve_look_at__normalize_fwd_to_uz_proc(MipsAtomBuilder_R ab, U4 r_scratch + , U4 r_a, U4 r_b /* src/dst scratch pointers */ + , U4 r_e, U4 r_f, U4 r_i /* src.x/y/z → result.x/y/z */ + , U4 r_d, U4 r_g /* MAC1/2 scratch (dead after stage 2) */ + , U4 r_h /* LZCR */ , U4 r_recip_est , U4 r_shift ) MipsAtom_Proc_(resolve_look_at__normalize_fwd_to_uz, ab, { @@ -515,8 +498,7 @@ I_ void resolve_look_at__normalize_fwd_to_uz_proc(MipsAtomBuilder_R ab }) /* Atom 3: normalize right (scratch+32) → ux (scratch+48). */ -I_ void resolve_look_at__normalize_right_to_ux_proc(MipsAtomBuilder_R ab - , U4 r_scratch +I_ void resolve_look_at__normalize_right_to_ux_proc(MipsAtomBuilder_R ab, U4 r_scratch , U4 r_a, U4 r_b , U4 r_e, U4 r_f, U4 r_i , U4 r_d, U4 r_g @@ -655,8 +637,7 @@ I_ void resolve_look_at__normalize_up_to_uy_proc(MipsAtomBuilder_R ab typedef Struct_(Binds_ResolveLookAtPopAndTrans) { U4 look_at; /* U4 (MT3_S2S4* — destination matrix address) */ }; -/* Atom 6 in the bundle: write look_at->m[][] from ux/uy/uz, then compute - * the translation column t[] = R * (-eye). +/* Atom 6 in the bundle: write look_at->m[][] from ux/uy/uz, then compute the translation column t[] = R * (-eye). * * GPR codes (assigned by resolve_look_at_init): * r_look_at : MT3_S2S4* (popped from tape; output matrix destination) @@ -666,14 +647,15 @@ typedef Struct_(Binds_ResolveLookAtPopAndTrans) { * r_peye : pointer to eye (offset O_(ResolveLookAtScratch,eye)) * r_tmp0/1/2 : atom-local scratch (load + MVMVA + store temps) * - * The 4 pointer regs (r_pux/r_puy/r_puz/r_peye) are DEDICATED — they hold the scratch addresses for the entire body. + * 4 pointer regs (r_pux/r_puy/r_puz/r_peye) are DEDICATED — they hold the scratch addresses for the entire body. * They are computed in-body via `add_si(r_px, r_scratch, O_(ResolveLookAtScratch, field))` so no tape-data pointer is needed. * * Struct layout (per duffle/math.h): * MT3_S2S4 { A3x3_S2 m; A3_S4 t; } → m[][] is S2 packed (9 × 2 = 18 bytes at offset 0) * t[0/1/2] is S4 (3 × 4 = 12 bytes at offset 18) * - * Translation column: GTE MVMVA with the world rotation matrix pre-set (helper emits set_gte_world before the bundle, per the bundle design). + * Translation column: GTE MVMVA with the world rotation matrix pre-set + * (helper emits set_gte_world before the bundle, per the bundle design). * MVMVA computes R * pos (with cv=0/mx=0/sf=0/v=0); MAC1/2/3 = R * (-eye). * Pool cost: r_look_at (1) + r_scratch (R_T4 carrier) + 4 ptr regs + 3 tmp regs = 9 GPRs. */ @@ -737,16 +719,14 @@ I_ void resolve_look_at__populate_and_translate_proc(MipsAtomBuilder_R ab gte_mv_to_data_r(r_tmp2, C2_IR3), nop2, - /* MVMVA: MAC1/2/3 = R * IR with cv=0 (no TR vector), mx=0 (rotation matrix), - * sf=0 (no shift), v=0 (V0 = IR1/2/3, no far-plane clipping). The pre-set - * rotation matrix is the one set by the preceding set_gte_world atom. + /* MVMVA: MAC1/2/3 = R * IR with cv=0 (no TR vector), mx=0 (rotation matrix), sf=0 (no shift), v=0 (V0 = IR1/2/3, no far-plane clipping). + * The pre-set rotation matrix is the one set by the preceding set_gte_world atom. * gte_cmdw_mvmva is parameterless and defaults to cv=0/mx=0/sf=0/v=0. */ gte_cmdw_mvmva, nop, /* GTE interlock */ /* mfc2 MAC1/2/3 → r_tmp0/r_tmp1/r_tmp2 (sign-extended into 32-bit GPRs). - * MAC1/2/3 hold R*v with no TR add and no perspective divide — exactly the - * 3 distinct world-space translation values we need for t[0..2]. */ + * MAC1/2/3 hold R*v with no TR add and no perspective divide — exactly the 3 distinct world-space translation values we need for t[0..2]. */ gte_mv_from_data_r(r_tmp0, C2_MAC1), gte_mv_from_data_r(r_tmp1, C2_MAC2), gte_mv_from_data_r(r_tmp2, C2_MAC3), @@ -821,32 +801,52 @@ internal MipsAtom_(screen_env_init) atom_info(atom_phase(screen_init) mac_yield(), }; +/* gp_screen_init's GPR setup. Tests the mixed user-pinning + auto-reg pattern: + * - R_IO_BaseAddr = R_T4 (user-pinned via atom_reg; pre-existing) + * - R_GP1_Offset = R_T2 (user-pinned via atom_reg; NEW -- for GPIO_PORT1_OFFSET) + * - R_ScreenX = R_T5 (user-pinned via atom_reg; used as a transfer and GTE setup reg) + * - R_GpTmp = auto-allocated by the lua pass and used for several GPU transfers; + * the C preprocessor resolves it to the chosen free pool GPR. + * + * For gp_screen_init, the auto-reg pool exclusions are: + * user_pinned (from the corpus register_alias_registry) : R_T0..R_T7 (all 8 user-pinned across hello_camera.atom.c) + * body-parsed physical registers : aliases resolve through the registry; + * the body uses R_ScreenX, not raw R_T5 + * source_pool after both subtractions : {R_V0, R_V1} only + * R_GpTmp gets R_V0 (the first-fit choice). Its repeated GPU-transfer use proves that the + * auto-reg allocation is active while the R_ScreenX references prove the pinned alias is used. + * R_TapePtr (R_T9), R_AtomJmp (R_T8), R_AT are excluded from the POOL by construction in + * passes/auto_reg.lua -- see the "obvious exclusions" comment block at the top of that file. + */ enum { R_IO_BaseAddr = R_T4 atom_reg, /* Caller-pinned: IO_BASE_ADDR = 0x1F800000 */ + R_GP1_Offset = R_T2 atom_reg, /* Caller-pinned: GPIO_PORT1_OFFSET = 0x10 */ + atom_auto_reg(gp_screen_init, R_GpTmp), /* Auto-allocated scratch; resolved to a free pool GPR by the lua pass. C-preprocessor expands to R_GpTmp = R_GpTmp_Code with an atom_auto_reg trailing comment. */ #define R_IO_BaseAddr_Code R_T4_Code +#define R_GP1_Offset_Code R_T2_Code }; internal MipsAtom_(gp_screen_init) atom_info(atom_phase(screen_init), atom_reads(R_IO_BaseAddr)) { store_word(R_0, R_IO_BaseAddr, GPIO_PORT1_OFFSET), /* GP1(00h) Reset */ - mac_gcmd_push(gp1_word_ResetCmdBuffer(), R_T5, R_IO_BaseAddr, GPIO_PORT1_OFFSET), /* GP1(01h) ClearFIFO */ - mac_gcmd_push(gp1_word_AcknowledgeIRQ(), R_T5, R_IO_BaseAddr, GPIO_PORT1_OFFSET), /* GP1(02h) AckIRQ */ - mac_gcmd_push(gp1_word_DisplayOn(), R_T5, R_IO_BaseAddr, GPIO_PORT1_OFFSET), /* GP1(03h) Display ON */ - mac_gcmd_push(gp1_word_dma_to_gpu(), R_T5, R_IO_BaseAddr, GPIO_PORT1_OFFSET), /* GP1(04h) DMADirection=2 (CPU→GPU). libpsyx's per-frame PutDrawEnv/DrawOTag use DMA2; without this the DMA queue never drains. */ - mac_gcmd_push(gp1_word_StartDisplayArea(), R_T5, R_IO_BaseAddr, GPIO_PORT1_OFFSET), /* GP1(05h) StartDisplayArea (X=0, Y=0) */ + mac_gcmd_push(gp1_word_ResetCmdBuffer(), R_ScreenX, R_IO_BaseAddr, GPIO_PORT1_OFFSET), /* GP1(01h) ClearFIFO; uses pinned R_ScreenX as the transfer reg. */ + mac_gcmd_push(gp1_word_AcknowledgeIRQ(), R_ScreenX, R_IO_BaseAddr, GPIO_PORT1_OFFSET), /* GP1(02h) AckIRQ; uses pinned R_ScreenX as the transfer reg. */ + mac_gcmd_push(gp1_word_DisplayOn(), R_ScreenX, R_IO_BaseAddr, GPIO_PORT1_OFFSET), /* GP1(03h) Display ON; uses pinned R_ScreenX as the transfer reg. */ + mac_gcmd_push(gp1_word_dma_to_gpu(), R_GpTmp, R_IO_BaseAddr, GPIO_PORT1_OFFSET), /* GP1(04h) DMADirection=2 (CPU->GPU). libpsyx's per-frame PutDrawEnv/DrawOTag use DMA2; without this the DMA queue never drains. Uses auto-allocated R_GpTmp. */ + mac_gcmd_push(gp1_word_StartDisplayArea(), R_GpTmp, R_IO_BaseAddr, GPIO_PORT1_OFFSET), /* GP1(05h) StartDisplayArea (X=0, Y=0); uses auto-allocated R_GpTmp. */ - /* GP1: DisplayMode + Display Ranges */ - mac_gcmd_push(gp1_word_display_mode_320x240_15bit_ntsc, R_T5, R_IO_BaseAddr, GPIO_PORT1_OFFSET), - mac_gcmd_push(gp1_word_horizontal_range_ntsc, R_T5, R_IO_BaseAddr, GPIO_PORT1_OFFSET), - mac_gcmd_push(gp1_word_vertical_range_ntsc, R_T5, R_IO_BaseAddr, GPIO_PORT1_OFFSET), + /* GP1: DisplayMode + Display Ranges. */ + mac_gcmd_push(gp1_word_display_mode_320x240_15bit_ntsc, R_ScreenX, R_IO_BaseAddr, GPIO_PORT1_OFFSET), + mac_gcmd_push(gp1_word_horizontal_range_ntsc, R_ScreenX, R_IO_BaseAddr, GPIO_PORT1_OFFSET), + mac_gcmd_push(gp1_word_vertical_range_ntsc, R_ScreenX, R_IO_BaseAddr, GPIO_PORT1_OFFSET), /* GTE: SetGeomOffset (OFX, OFY) — ScreenRes_CenterX, ScreenRes_CenterY. */ - load_upper_i(R_T5, ScreenRes_CenterX), gte_mv_to_ctrl_r(R_T5, gte_cr_OFX_Code), - load_upper_i(R_T5, ScreenRes_CenterY), gte_mv_to_ctrl_r(R_T5, gte_cr_OFY_Code), + load_upper_i(R_ScreenX, ScreenRes_CenterX), gte_mv_to_ctrl_r(R_ScreenX, gte_cr_OFX_Code), + load_upper_i(R_ScreenX, ScreenRes_CenterY), gte_mv_to_ctrl_r(R_ScreenX, gte_cr_OFY_Code), /* GTE: SetGeomScreen (H) — CR26 (per PSX-SPX / libpsyx), value is the raw projection-plane distance, NOT shifted. */ - add_ui(R_T5, R_0, ScreenZ), gte_mv_to_ctrl_r(R_T5, gte_cr_H_Code), + add_ui(R_ScreenX, R_0, ScreenZ), gte_mv_to_ctrl_r(R_ScreenX, gte_cr_H_Code), /* GP1: DisplayEnable — bit 0 = 0 (Display ON). */ - mac_gcmd_push(gp1_word_DisplayOn(), R_T5, R_IO_BaseAddr, GPIO_PORT1_OFFSET), + mac_gcmd_push(gp1_word_DisplayOn(), R_GpTmp, R_IO_BaseAddr, GPIO_PORT1_OFFSET), /* Uses auto-allocated R_GpTmp. */ mac_yield(), }; @@ -1015,43 +1015,8 @@ atom_label(exit_circle_z) mac_yield_tail(), }; -/* Scratchpad layout for the resolve_look_at bundle. - * The chain atoms communicate entirely via the wave-context GPR carrier - * R_ResolveScratch (R_T4) + hardcoded offsets into smem.scratchpad - * (PS1 hardware scratchpad at 0x1F800000). - * - * Atom 0 (input_and_sub) STAGES the C-side inputs (eye, up_in) into the scratchpad; - * AT THE SAME TIME it computes fwd = target - eye and stores it at scratch+0. - * Atoms 1-6 then read/write specific scratchpad offsets internally using - * `r_scratch + hardcoded_offset` — no tape-data pointers are passed between atoms. - * - * +0 fwd (atom 0 writes; atom 1 reads) - * +16 uz (atom 1 writes; atoms 2 + 4 read) - * +32 right (atom 2 writes; atom 3 reads) - * +48 ux (atom 3 writes; atoms 4 + 6 read) - * +64 up (atom 4 writes; atom 5 reads) - * +80 uy (atom 5 writes; atom 6 reads) - * +96 eye (atom 0 stages from C-side pointer; atom 6 reads) - * +128 up_in (atom 0 stages from C-side pointer; atom 2 reads) - * - * No struct view is required — the C-side bundle helper passes only C-side - * pointers (target, eye, up_in, look_at) and the scratch base address; - * the assembly hardcodes all inter-slot offsets. The original Task 12.7 magic - * offsets `& smem.scratchpad[N]` in the C-side helper were eliminated by this - * redesign; the user feedback was: "you didn't have to use magic offsets into - * the scratchpad memory. those are harcoded." */ - enum { - R_LookAt = R_T0 atom_reg atom_type(MT3_S2S4*), - R_CamEye = R_T1 atom_reg atom_type(P3_S4*), - R_CamTarget = R_T2 atom_reg atom_type(P3_S4*), - R_WorldUp = R_T3 atom_reg atom_type(V3_S4*), -}; - - - -enum { - R_PrimCursor = R_T7 atom_reg atom_type(U4*), /* VRAM output cursor (primitive buffer) */ + R_PrimCursor = R_T7 atom_reg atom_type(U4*), /* 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 */ R_VertBase = R_T5 atom_reg atom_type(V3_S2*), /* Base address of the vertex array */ R_OtBase = R_T6 atom_reg atom_type(U4*), /* Base address of the Ordering Table */ @@ -1060,7 +1025,6 @@ enum { #define R_VertBase_Code R_T5_Code #define R_OtBase_Code R_T6_Code }; - typedef Struct_(Binds_CubeTri) { U4 PrimCursor; V4_S2* FaceCursor; @@ -1097,7 +1061,7 @@ MipsAtom_(cube_g4_face) atom_info(atom_phase(cube_g4), gte_mv_from_data_r(R_T0, C2_MAC0), nop, 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). + /* 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. */ store_word(R_0, R_PrimCursor, O_(Poly_G4, tag)), diff --git a/code/hello_camera/hello_camera.c b/code/hello_camera/hello_camera.c index b98d8d4..e63a7f7 100644 --- a/code/hello_camera/hello_camera.c +++ b/code/hello_camera/hello_camera.c @@ -43,6 +43,7 @@ #pragma region Hello Camera Headers # include "gen/macs.h" # include "gen/offsets.h" +# include "gen/auto_reg.h" #include "hello_camera.h" #pragma endregion Hello Camera Headers @@ -161,15 +162,11 @@ internal void resolve_look_at_init(void) { * computes fwd = target - eye; binds R_ResolveScratch (R_T4) as the wave-context carrier for atoms 1-6. * The body hardcodes R_AT and R_V0 as eye.y/eye.z temps (the existing sub_u(eye.x, eye.y, eye.z) chain from the prior Task 12.7 design). */ smem.resolve_look_at_atom_addrs[0] = (MipsAtom*)u4_v(ab->start + ab->used * sizeof(U4)); - resolve_look_at__input_and_sub_proc(ab, - R_T0, /* r_target_ptr (popped from tape) */ - R_T1, /* r_eye_ptr (popped from tape) */ - R_T2, /* r_up_in_ptr (popped from tape) */ - R_ResolveScratch, /* r_scratch (wave-context carrier; popped from tape) */ - R_T3, /* r_tmp0 */ - R_T5, /* r_tmp1 */ - R_T6, /* r_tmp2 */ - R_T7); /* r_tmp3 */ + resolve_look_at__input_and_sub_proc(ab, R_ResolveScratch, + R_T0, /* r_target_ptr (popped from tape) */ + R_T1, /* r_eye_ptr (popped from tape) */ + R_T2, /* r_up_in_ptr (popped from tape) */ + R_T3, R_T5, R_T6, R_T7); /* r_tmp<0-3> */ /* Atom 1: resolve_look_at__normalize_fwd_to_uz — src=scratch+0, dst=scratch+16 (HARDCODED in body). * GPR pool: r_scratch (R_T4 carrier) + 10 body GPRs = 11. diff --git a/scripts/passes/auto_reg.lua b/scripts/passes/auto_reg.lua index 129f489..dc84097 100644 --- a/scripts/passes/auto_reg.lua +++ b/scripts/passes/auto_reg.lua @@ -4,22 +4,21 @@ --- Runs a deterministic first-fit allocator in the `R_T0..R_T7 + R_V0..R_V1` pool (10 physical GPRs). --- Emits one `#define R__Code R_Tn_Code` per marker into per-directory `gen/auto_reg.h`. --- ---- User-pinned GPRs (added 2026-08-10): The corpus's `register_alias_registry` is consulted to ---- exclude GPRs the user has pinned via `atom_reg` + `_Code` defs (e.g. wave-context carriers like ---- `R_ResolveScratch = R_T4 atom_reg`). These GPRs are unavailable to EVERY atom's source pool, ---- not just to atoms in the same phase — wave-context carriers are preserved across atoms by the ---- wave-context discipline and must never be reallocated. +--- User-pinned GPRs : The corpus's `register_alias_registry` is consulted to exclude GPRs the user has pinned via +--- `atom_reg` + `_Code` defs (e.g. carriers like `R_ResolveScratch = R_T4 atom_reg`). +--- These GPRs are unavailable to EVERY atom's source pool. +--- Carriers are preserved across atoms by context discipline and must never be reallocated. --- Per-atom body parsing also catches alias references (R_) and hardcoded R_Tn references, --- so the user can write either `R_T4` or `R_ResolveScratch` in an atom body and the pass will --- exclude R_T4 from that atom's pool. --- --- Conflict detection: If the user hardcodes `R_Tn` in an atom body that shares a phase with an auto-reg that picked `R_Tn`, ---- emit `phase_register_clash` as an info finding (no build stop). Should be unreachable after the ---- user-pinning + body-parsing fix above; kept as a defensive safety net. +--- emit `phase_register_clash` as an info finding (no build stop). +--- Should be unreachable after the user-pinning + body-parsing fix above; kept as a defensive safety net. --- ---- Pool exhaustion: if a phase declares more `R_` mappings than the 10-register pool can hold, +--- Pool exhaustion: If a phase declares more `R_` mappings than the 10-register pool can hold, --- emit `phase_register_pool_exhausted` as a build-stopping error. ---- + --- @class AutoRegResult --- @field outputs table[] -- {kind=, path=} entries --- @field errors table[] -- {line=, msg=} entries (build-stops) @@ -28,20 +27,57 @@ local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") --- The fixed allocation pool: 10 physical GPRs whose `R__Code` macros exist in mips.h (lines 92-107). --- Each pool entry is the PHYSICAL GPR ident (R_T0 etc.); --- `gpr .. "_Code"` resolves to the matching `R_Tn_Code` constant the source code references via `#define R_Load_Code R_T0_Code`. --- Excluded: R_AT (assembler temp per lottes_tape.h:86), R_T8 (deferred to ac_yield_load pattern), --- R_T9 (R_TapePtr; owned by the tape runtime). +--- ════════════════════════════════════════════════════════════════════════════ +--- THE GPR ALLOCATION POOL — what is allocatable, and (more importantly) WHY +--- ════════════════════════════════════════════════════════════════════════════ +--- +--- The auto-reg pass picks physical GPRs for `atom_auto_reg(...)` / `phase_auto_reg(...)` markers. +--- It allocates from a FIXED 10-register pool. +--- This comment block makes the inclusion AND exclusion criteria obvious so a reader doesn't have +--- to grep lottes_tape.h + mips.h to understand the design. +--- +--- ── WHAT'S IN THE POOL (10 GPRs, all caller-trash per the O32 ABI) ──────── +--- R_T0..R_T7 (GPR codes 8..15), R_V0..R_V1 (GPR codes 2..3) +--- The workhorse of every atom body. The uesr should be aware of atom allocation across atoms they chain. +--- If they have a collision it means either they didn't saturate the register file optimally for a phase, +--- or the may have made the workload to large for the run. +--- +--- ── WHAT'S NOT IN THE POOL — and WHY (the "obvious exclusions") ──────────── +--- R_T9 (GPR code 25) — R_TapePtr, the tape instruction stream pointer. +--- Owned by the tape runtime (in tape_run / tape_run_a02_s07). +--- `rgcc(R_TapePtr)` register-variable ties the C compiler's view to $t9 across the whole tape_run. +--- The auto-reg pass MUST NOT clobber this; doing so would desync the C-side tape pointer from the +--- hardware pointer and crash on the next tape_run. +--- +--- R_T8 (GPR code 24) — R_AtomJmp, the atom-jump register used by the 4-word yield handshake. +--- Every `mac_yield()` / `mac_yield_tail` does `load_word R_AtomJmp, R_TapePtr, 0` then +--- `jump_reg R_AtomJmp`. The auto-reg pass MUST NOT clobber this either, or the atom dispatcher breaks. +--- Owned by the tape runtime, same family as R_TapePtr. +--- +--- R_AT (GPR code 1) — Assembler temporary. Reserved by the MIPS O32 ABI for pseudoinstruction expansion +--- (lottes_tape.h:86, mips.h:93). The ISA's psuedo instructions use it as a scratch temporary. +--- +--- R_A0..A3 (codes 4..7) — Function arguments. Used in tape_run_a02_s07, see below. +--- R_S0..S7 (codes 16..23) — Callee-saved. Preserved across C-ABI calls by convention. +--- The `tape_run_a02_s07` variant clobbers them deliberately, but the default `tape_run` does NOT. +--- Kept out of POOL to preserve the conservative default. +--- Add them in a separate "big clobber" pool if/when needed. +--- +--- R_K0/K1 (codes 26..27) — Kernel / interrupt handler reserves. Never touched by user code; OS-internal. +--- R_GP/SP/FP/RA (codes 28..31) — Stack frame + return-address. Owned by the C compiler; never allocatable. +--- R_0 (code 0) — Hardwired zero. Cannot be written. +--- local POOL = { "R_T0", "R_T1", "R_T2", "R_T3", "R_T4", "R_T5", "R_T6", "R_T7", "R_V0", "R_V1", } --- Map from integer MIPS GPR code (the `code` field on AliasEntry) to the physical GPR ident --- in POOL. The standard MIPS O32 ABI register numbering matches mips.h's R_*_Code #defines --- (mips.h:92-123). Only the POOL entries matter for auto_reg — non-pool aliases are out of scope. +-- Map from integer MIPS GPR code (the `code` field on AliasEntry) to the physical GPR ident in POOL. +-- The standard MIPS O32 ABI register numbering matches mips.h's R_*_Code #defines (mips.h). +-- Only the POOL entries matter for auto_reg — non-pool aliases +-- (R_AT=1, R_A0..A3=4..7, R_T8=24, R_T9=25, R_K0/K1=26..27, R_GP/SP/FP/RA=28..31) +-- are deliberately omitted — see the comment block above for the WHY of each exclusion. local INT_CODE_TO_POOL_GPR = { [2] = "R_V0", [3] = "R_V1", [8] = "R_T0", [9] = "R_T1", [10] = "R_T2", [11] = "R_T3", @@ -71,9 +107,10 @@ local function allocate_phase(phase_label, decls) if not next_gpr then errors[#errors + 1] = { line = 0, - msg = string.format( - "phase_register_pool_exhausted: phase '%s' requested symbol '%s' but the pool has no remaining registers (max 10 per phase: R_T0..R_T7 + R_V0..R_V1). Split the phase or use hardcoded GPRs." - , phase_label, sym), + msg = string.format("phase_register_pool_exhausted: " + .. "phase '%s' requested symbol '%s' but the pool has no remaining registers " + .. "(max 10 per phase: R_T0..R_T7 + R_V0..R_V1). Split the phase or use hardcoded GPRs." + , phase_label, sym), } return result, errors end @@ -83,26 +120,22 @@ local function allocate_phase(phase_label, decls) end -- Build two projections from corpus.register_alias_registry: --- user_pinned -- { [physical_gpr_ident] = true } -- GPRs unavailable to auto_reg globally --- -- (wave-context carriers, file-scope pinned aliases) --- alias_to_gpr -- { [alias_ident] = physical_gpr_ident } -- for body parsing --- Both projections are derived from the same set of entries: every AliasEntry in --- register_alias_registry has `has_atom_reg = true` (only those entries are added to the --- registry; see passes/scan_source.lua parse_enum_entry). Each entry's `code` is the integer --- MIPS GPR number (0..31); INT_CODE_TO_POOL_GPR translates it back to the physical GPR ident. --- Aliases whose `code` points to a non-POOL GPR (e.g. R_S0, R_T8, R_K1) are ignored — they --- don't affect the auto_reg pool, and they're already excluded from POOL above. +-- user_pinned -- { [physical_gpr_ident] = true } -- GPRs unavailable to auto_reg globally (wave-context carriers, file-scope pinned aliases) +-- alias_to_gpr -- { [alias_ident] = physical_gpr_ident } -- for body parsing +-- Both projections are derived from the same set of entries: every AliasEntry in register_alias_registry has `has_atom_reg = true` +-- (only those entries are added to the registry; see passes/scan_source.lua parse_enum_entry). +-- Each entry's `code` is the integer MIPS GPR number (0..31); INT_CODE_TO_POOL_GPR translates it back to the physical GPR ident. +-- Aliases whose `code` points to a non-POOL GPR (e.g. R_S0, R_T8, R_K1) are ignored — +-- they don't affect the auto_reg pool, and they're already excluded from POOL above. local function build_user_pins(corpus) local user_pinned = {} local alias_to_gpr = {} - if not corpus.register_alias_registry then - return user_pinned, alias_to_gpr - end + if not corpus.register_alias_registry then return user_pinned, alias_to_gpr end for alias_name, alias_entry in pairs(corpus.register_alias_registry) do if alias_entry.has_atom_reg and alias_entry.code then local gpr = INT_CODE_TO_POOL_GPR[alias_entry.code] if gpr then - user_pinned[gpr] = true + user_pinned[gpr] = true alias_to_gpr[alias_name] = gpr end end @@ -161,8 +194,8 @@ local function emit_auto_reg_h(out_dir, dir, sources, mappings) lines[#lines + 1] = "// R__Code = for every marker in this directory." lines[#lines + 1] = "" for _, sym in ipairs(stable_sort_keys(mappings)) do - local gpr = mappings[sym] - local gpr_code = gpr .. "_Code" + local gpr = mappings[sym] + local gpr_code = gpr .. "_Code" lines[#lines + 1] = "#define " .. sym .. "_Code " .. gpr_code end lines[#lines + 1] = "" @@ -190,12 +223,10 @@ function M.run(ctx) end -- 0. Build the user-pinned GPR exclusion set + alias-to-GPR resolution map. - -- Wave-context carriers (e.g. `R_ResolveScratch = R_T4 atom_reg` in - -- hello_camera.atom.c) MUST NOT be allocated to any auto-reg marker — they're - -- preserved across atoms by the wave-context discipline. The corpus's - -- register_alias_registry is the source of truth for these opt-in pins. - -- Body references to those aliases (via alias_to_gpr) are also excluded on a - -- per-atom basis in step 2 below. + -- Wave-context carriers (e.g. `R_ResolveScratch = R_T4 atom_reg` in hello_camera.atom.c) + -- MUST NOT be allocated to any auto-reg marker — they're preserved across atoms by the wave-context discipline. + -- The corpus's register_alias_registry is the source of truth for these opt-in pins. + -- Body references to those aliases (via alias_to_gpr) are also excluded on a per-atom basis in step 2 below. local user_pinned, alias_to_gpr = build_user_pins(corpus) -- 1. Allocate phase pools first (phase declarations take precedence over per-atom declarations). @@ -213,8 +244,8 @@ function M.run(ctx) -- 2. Allocate per-atom auto-regs. If the atom scope matches a phase, reuse the phase pool. -- Otherwise, allocate a private pool for the atom. - -- The phase membership is in `corpus.atom_phases[phase_label].atoms` (an array of atom names declared via `atom_phase()` in the atom's `atom_info` line). - -- Build a reverse map `atom_name -> phase_label` so the lookup is O(1) per atom scope. + -- The phase membership is in `corpus.atom_phases[phase_label].atoms` (an array of atom names declared via `atom_phase()` + -- in the atom's `atom_info` line). Build a reverse map `atom_name -> phase_label` so the lookup is O(1) per atom scope. local atom_name_to_phase = {} for phase_label, entry in pairs(corpus.atom_phases or {}) do for _, atom_name in ipairs(entry.atoms or {}) do @@ -229,8 +260,7 @@ function M.run(ctx) -- (a) every GPR already committed (phase allocations + prior atom allocations) -- (b) every USER-PINNED GPR (wave-context carriers + file-scope pinned aliases) -- (c) every GPR referenced in the atom's body — either hardcoded R_X or alias R_Xxx - -- (the latter resolved via alias_to_gpr; this catches cases where the user - -- wrote R_ResolveScratch instead of R_T4 directly) + -- (the latter resolved via alias_to_gpr; this catches cases where the user wrote R_ResolveScratch instead of R_T4 directly) -- Atoms whose scope matches a phase share the global pool with the phase allocations; -- the original `source_pool = phase_allocations[phase_label]` form used the phase -- allocation MAP as a pool, but that map has no array part, so `table.remove(source_pool, 1)` @@ -239,7 +269,7 @@ function M.run(ctx) for _, m in pairs(phase_allocations) do for _, gpr in pairs(m) do used[gpr] = true end end for _, m in pairs(atom_allocations) do for _, gpr in pairs(m) do used[gpr] = true end end -- (c) Body references — scan the atom body for hardcoded + alias-resolved GPRs. - -- Folded into `used` so the source_pool exclusion is a single check. + -- Folded into `used` so the source_pool exclusion is a single check. local atom = corpus.atoms_by_name and corpus.atoms_by_name[atom_scope] if atom and atom.body then local body_used = find_used_gprs(atom.body, alias_to_gpr) @@ -259,7 +289,8 @@ function M.run(ctx) if not next_gpr then errors[#errors + 1] = { line = 0, - msg = string.format("phase_register_pool_exhausted: atom '%s' requested symbol '%s' but no free registers remain in its scope pool." + msg = string.format("phase_register_pool_exhausted: atom '%s' requested symbol '%s' " + .. "but no free registers remain in its scope pool." , atom_scope, sym), } else @@ -271,10 +302,10 @@ function M.run(ctx) -- 3. Conflict-with-hardcoded detection (defensive — should be unreachable now). -- The source_pool exclusion in step 2 (b) + (c) already accounts for both user-pinned GPRs - -- and body-referenced GPRs (hardcoded R_Tn OR alias R_). An auto-reg allocation that - -- matched an existing body reference would be impossible by construction. This warning is kept - -- as a defensive safety net for cases the body scanner might miss (e.g. macros that expand to - -- register references the scanner cannot resolve). + -- and body-referenced GPRs (hardcoded R_Tn OR alias R_). + -- An auto-reg allocation that matched an existing body reference would be impossible by construction. + -- This warning is kept as a defensive safety net for cases the body scanner might miss + -- (e.g. macros that expand to register references the scanner cannot resolve). -- For each resolved (scope, sym) -> R_Tn mapping, scan the atom body source for used GPRs. for atom_scope, decls in pairs(atom_allocations) do local atom = corpus.atoms_by_name and corpus.atoms_by_name[atom_scope] @@ -284,7 +315,8 @@ function M.run(ctx) if used_in_body[allocated_gpr] and used_in_body[allocated_gpr] > 0 then warnings[#warnings + 1] = { line = atom.line or 0, - msg = string.format("phase_register_clash: atom '%s' has hardcoded '%s' in its body AND an auto-reg marker '%s' that was allocated to '%s' (same phase). Resolve by removing the hardcoded reference or renaming the auto-reg." + msg = string.format("phase_register_clash: atom '%s' has hardcoded '%s' in its body AND an auto-reg marker '%s' " + .. "that was allocated to '%s' (same phase). Resolve by removing the hardcoded reference or renaming the auto-reg." , atom_scope, allocated_gpr, sym, allocated_gpr), } end @@ -300,7 +332,8 @@ function M.run(ctx) for _, src in ipairs(sources) do -- Collect every (sym -> gpr) entry that originated from a source in this directory. -- `src.scan.atom_auto_regs` is keyed by ATOM SCOPE NAME; `pairs(t)` iterates KEYS so `scope_name` here is the scope ident (e.g. "cube_g4_face"). - -- The previous `for _, scan_atom_auto` form silently assigned the VALUE (a `{sym = sym}` table) to the variable, which made `atom_allocations[scan_atom_auto]` a table-indexed lookup that never resolved. + -- The previous `for _, scan_atom_auto` form silently assigned the VALUE (a `{sym = sym}` table) to the variable, + -- which made `atom_allocations[scan_atom_auto]` a table-indexed lookup that never resolved. for scope_name in pairs(src.scan and src.scan.atom_auto_regs or {}) do for sym, gpr in pairs(atom_allocations[scope_name] or {}) do per_dir_mappings[sym] = gpr