Author SHA1 Message Date
ed 4fbf550d3c hot-reload attempt (unreviewed, not working) 2026-08-06 10:44:34 -04:00
47 changed files with 2299 additions and 3290 deletions
+33
View File
@@ -177,6 +177,39 @@
"tbreak main",
"continue"
]
},
{
"name": "Debug: Hello Camera! (attach only)",
"type": "gdb",
"request": "attach",
"target": "localhost:3333",
"remote": true,
"cwd": "${workspaceRoot}",
"valuesFormatting": "parseText",
"registerLimit": "1-32",
"frameFilters": false,
"showDevDebugOutput": false,
"printCalls": false,
"stopAtConnect": true,
"gdbpath": "gdb-multiarch",
"windows": {
"gdbpath": "gdb-multiarch.exe"
},
"osx": {
"gdbpath": "gdb"
},
"executable": "${workspaceRoot}/build/hello_camera.dwarf-injected.elf",
"setupCommands": [
{ "text": "set mi-async off" },
{ "text": "set remotetimeout 0" },
{ "text": "set logging file build/gen/hello_camera.gdb.log" },
{ "text": "set logging redirect on" }
],
"autorun": [
"source scripts/gdb/gdb_tape_atoms.gdb",
"tbreak hot_reload_entry",
"continue"
]
}
]
}
-14
View File
@@ -1,14 +0,0 @@
#ifdef INTELLISENSE_DIRECTIVES
# pragma once
#endif
enum {
bios_init_pad_2 = 0x12,
bios_start_pad_2 = 0x13,
bios_flushcache = 0x44,
bios_table_addr = 0xA0,
bios_btable_addr = 0xB0,
};
enum {
bios_pad_buffer_size = 0x22,
};
-20
View File
@@ -75,26 +75,6 @@
* ----------------------------------------------------------------------------*/
#define atom_reg /* atom_reg: opt the preceding enum entry into the DWARF registry */
// ----------------------------------------------------------------------------
// atom_auto_reg(atom, sym) — per-atom auto-allocated GPR binding.
// enum {
// atom_auto_reg(cube_g4_face, R_Fwdx), // expands to: R_Fwdx = R_Fwdx_Code /* atom_auto_reg: cube_g4_face */,
// atom_auto_reg(cube_g4_face, R_Eye_z) atom_type(S4), // atom_type chains after
// };
// (The macro IS the entire enum entry — no separate LHS=RHS. The `atom` scope is
// preserved in a trailing C-comment on the RHS so the Lua scanner can recover
// it after preprocessing strips the macro form. R_<Sym>_Code is resolved from gen/auto_reg.h which the .c file #include's before the enum declaration.)
#define atom_auto_reg(atom, sym) sym = sym ## _Code /* atom_auto_reg: atom */
// ----------------------------------------------------------------------------
// phase_auto_reg(phase, sym) — per-phase auto-allocated GPR binding.
// enum {
// phase_auto_reg(cube_g4, R_Temp0), // expands to: R_Temp0 = R_Temp0_Code /* phase_auto_reg: cube_g4 */,
// phase_auto_reg(cube_g4, R_Temp1),
// };
// (Same macro-as-enum-entry form as atom_auto_reg above; the `phase` scope is preserved in a trailing C-comment on the RHS for the Lua scanner to recover.)
#define phase_auto_reg(phase, sym) sym = sym ## _Code /* phase_auto_reg: phase */
/* ============================================================================
* atom_info :
* MipsAtom_(cube_tri) atom_info(
+3 -7
View File
@@ -28,9 +28,8 @@
#define internal static // internal
#define asm __asm__
#define A_(data) (& data)
#define align_(value) __attribute__((aligned (value))) // for easy alignment
#define align_(value) __attribute__((aligned (value))) // for easy alignment
#define C_(type,data) ((type)(data)) // for enforced precedence
#define expect_(x, y) __builtin_expect(x, y) // so compiler knows the common path
@@ -134,8 +133,8 @@ typedef __UINT32_TYPE__ TSet_(B4);
#define u4_v(value) C_(U4 V_*, value)
enum { false = 0, true = 1, true_overflow, };
#define u4_lo(value) (u4_(value) & 0xFFFFU)
#define u4_hi(value) (u4_(value) >> (S_(U2) * 8))
#define u4_lo(value) ((value) & 0xFFFFU)
#define u4_hi(value) ((value) >> 12)
typedef void Proc_(VoidFn) (void);
@@ -169,8 +168,6 @@ 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)
@@ -180,7 +177,6 @@ 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
+15 -78
View File
@@ -14,9 +14,7 @@
// source: C:\projects\Pikuma\ps1\code\duffle\pad.h
// source: C:\projects\Pikuma\ps1\code\duffle\dsl.atom.h
// source: C:\projects\Pikuma\ps1\code\duffle\lottes_tape.h
// source: C:\projects\Pikuma\ps1\code\duffle\bios.h
// source: C:\projects\Pikuma\ps1\code\duffle\psyq.h
// source: C:\projects\Pikuma\ps1\code\duffle\pad.c
// source: C:\projects\Pikuma\ps1\code\duffle\math.atom.c
// source: C:\projects\Pikuma\ps1\code\duffle\mips.atom.c
// source: C:\projects\Pikuma\ps1\code\duffle\gte.atom.c
@@ -70,27 +68,6 @@ WORD_COUNT(mac_load_v2s2, 2)
, store_half(rt_y, base, offset + O_(V2_S2,y))
WORD_COUNT(mac_store_v2s2, 2)
/* atom_dbg_skip */
#define mac_load_v3s4(rs_x, rs_y, rs_z, r_base, offset) \
load_word( rs_x, r_base, O_(V3_S4,x)) \
, load_word( rs_y, r_base, O_(V3_S4,y)) \
, load_word( rs_z, r_base, O_(V3_S4,z))
WORD_COUNT(mac_load_v3s4, 3)
/* atom_dbg_skip */
#define mac_store_v3s4(rt_x, rt_y, rt_z, base, offset) \
store_word(rt_x, base, offset + O_(V3_S4,x)) \
, store_word(rt_y, base, offset + O_(V3_S4,y)) \
, store_word(rt_z, base, offset + O_(V3_S4,z))
WORD_COUNT(mac_store_v3s4, 3)
/* atom_dbg_skip */
#define mac_sub_v3s4(rds_x, rds_y, rds_z, rt_x, rt_y, rt_z) \
sub_s(rds_x, rds_x, rt_x) \
, sub_s(rds_y, rds_y, rt_y) \
, sub_s(rds_z, rds_z, rt_z)
WORD_COUNT(mac_sub_v3s4, 3)
/* atom_dbg_skip */
#define mac_store_rects2(rt_x, rt_y, rt_width, rt_height, base, offset) \
store_half(rt_x, base, offset + O_(Rect_S2,x)) \
@@ -147,34 +124,6 @@ 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)
#define mac_gcmd_push(cmd, reg_transfer, reg_base, port) \
load_upper_i(reg_transfer, cmd >> 16) \
, or_i_self( reg_transfer, cmd & 0xFFFF) \
@@ -207,41 +156,29 @@ WORD_COUNT(mac_format_f3_color, 3)
, mac_pack_color_word(r_prim_cursor, O_(Poly_G4,c3), 0, r3,g3,b3)
WORD_COUNT(mac_format_g4_color, 12)
#define mac_insert_ot_tag(r_ot_base, r_prim_cursor, poly_size) \
#define mac_insert_ot_tag_f3(r_ot_base, r_prim_cursor) \
shift_lleft( R_T1, R_T1, S_(U4)/2) /* T1 = otz * S_(U4) (otz arg is implicit R_T1) */ \
, add_u_self( R_T1, r_ot_base) /* T1 = & OrderingTable[OTZ] */ \
, load_word( R_AT, R_T1, O_(PolyTag,code)) /* AT = old_ot_head */ \
, load_upper_i(R_V0, (poly_size/S_(U4) - S_(PolyTag)/S_(U4)) << PolyTag_len_bits) \
, load_upper_i(R_V0, (S_(Poly_F3)/S_(U4) - S_(PolyTag)/S_(U4)) << PolyTag_len_bits) /* V0 = (5 - 1) << 24 = 4 << 24 */ \
, mask_upper( R_AT, R_AT, S_(PolyTag_len_bits)) /* Strip upper 8 bits (length from prev cell) → keep only low 24 */ \
, or_u( R_AT, R_AT, R_V0) /* Merge length */ \
, store_word( R_AT, r_prim_cursor, O_(PolyTag,code)) /* prim->tag = packed(prim_length, old_addr) */ \
, shift_lleft( R_AT, r_prim_cursor, S_(PolyTag_len_bits)) /* AT = (prim_length << 24) | old_addr */ \
, shift_lright(R_AT, R_AT, S_(PolyTag_len_bits)) \
, store_word( R_AT, R_T1, O_(PolyTag,code)) /* OrderingTable[OTZ] = PrimCursor */
WORD_COUNT(mac_insert_ot_tag, 11)
WORD_COUNT(mac_insert_ot_tag_f3, 11)
/* atom_dbg_skip */
#define mac_pad_set_centered_axes(r_state, r_scratch) \
load_upper_i(r_scratch, (PadAxis_Centered_Word >> 16) & 0xFFFF) \
, or_i_self( r_scratch, PadAxis_Centered_Word & 0xFFFF) \
, store_word( r_scratch, r_state, O_(PadState,axes))
WORD_COUNT(mac_pad_set_centered_axes, 3)
/* atom_dbg_skip */
#define mac_pad_set_id_byte(r_state, r_id, id_value) \
add_ui( r_id, R_0, id_value) \
, store_byte(r_id, r_state, O_(PadState,id))
WORD_COUNT(mac_pad_set_id_byte, 2)
/* atom_dbg_skip */
#define mac_pad_set_status(r_tmp, r_state, pad_status) \
add_ui( r_tmp, R_0, pad_status) \
, store_word(r_tmp, r_state, O_(PadState,status))
WORD_COUNT(mac_pad_set_status, 2)
/* atom_dbg_skip */
#define mac_pad_store_inverted_buttons(r_buttons, r_pad_state) \
nor_u( r_buttons, r_buttons, R_0) \
, store_half( r_buttons, r_pad_state, O_(PadState, buttons))
WORD_COUNT(mac_pad_store_inverted_buttons, 2)
#define mac_insert_ot_tag_g4(r_ot_base, r_prim_cursor) \
shift_lleft( R_T1, R_T1, S_(U4)/2) /* T1 = otz * S_(U4) (otz arg is implicit R_T1) */ \
, add_u_self( R_T1, r_ot_base) /* T1 = & OrderingTable[OTZ] */ \
, load_word( R_AT, R_T1, O_(PolyTag,code)) /* AT = old_ot_head */ \
, load_upper_i(R_V0, (S_(Poly_G4)/S_(U4) - S_(PolyTag)/S_(U4)) << PolyTag_len_bits) /* V0 = (9 - 1) << 24 = 8 << 24 */ \
, mask_upper( R_AT, R_AT, S_(PolyTag_len_bits)) /* Strip upper 8 bits (length from prev cell) → keep only low 24 */ \
, or_u( R_AT, R_AT, R_V0) /* Merge length */ \
, store_word( R_AT, r_prim_cursor, O_(PolyTag,code)) /* prim->tag = packed(prim_length, old_addr) */ \
, shift_lleft( R_AT, r_prim_cursor, S_(PolyTag_len_bits)) /* AT = (prim_length << 24) | old_addr */ \
, shift_lright(R_AT, R_AT, S_(PolyTag_len_bits)) \
, store_word( R_AT, R_T1, O_(PolyTag,code)) /* OrderingTable[OTZ] = PrimCursor */
WORD_COUNT(mac_insert_ot_tag_g4, 11)
+10 -22
View File
@@ -11,9 +11,7 @@
// source: C:\projects\Pikuma\ps1\code\duffle\pad.h
// source: C:\projects\Pikuma\ps1\code\duffle\dsl.atom.h
// source: C:\projects\Pikuma\ps1\code\duffle\lottes_tape.h
// source: C:\projects\Pikuma\ps1\code\duffle\bios.h
// source: C:\projects\Pikuma\ps1\code\duffle\psyq.h
// source: C:\projects\Pikuma\ps1\code\duffle\pad.c
// source: C:\projects\Pikuma\ps1\code\duffle\math.atom.c
// source: C:\projects\Pikuma\ps1\code\duffle\mips.atom.c
// source: C:\projects\Pikuma\ps1\code\duffle\gte.atom.c
@@ -25,27 +23,17 @@
#pragma region duffle
// --- atom: normalize_v3s4 (63 words) ---
// --- atom: pad_bios_snapshot (78 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
#define _atom_offset_disconnected_snap_end 65
#define _atom_offset_case_2_id_dispatch 9
#define _atom_offset_pending_snap_end 54
#define _atom_offset_id_dispatch_try_analog_stick 12
#define _atom_offset_id_dispatch_snap_end 40
#define _atom_offset_try_analog_stick_try_analog_pad 13
#define _atom_offset_analog_stick_snap_end 25
#define _atom_offset_try_analog_pad_try_unsupported 12
#define _atom_offset_snap_root_skip_disconnected 8
#define _atom_offset_disconnected_snap_end 61
#define _atom_offset_case_2_id_dispatch 8
#define _atom_offset_pending_snap_end 51
#define _atom_offset_id_dispatch_try_analog_stick 11
#define _atom_offset_id_dispatch_snap_end 38
#define _atom_offset_try_analog_stick_try_analog_pad 12
#define _atom_offset_analog_stick_snap_end 24
#define _atom_offset_try_analog_pad_try_unsupported 11
#define _atom_offset_analog_pad_snap_end 10
enum {
+35 -13
View File
@@ -8,47 +8,54 @@ ATOM_FILE_DEBUGGER_LINE_MARKER(gp_atom_c);
#pragma region MACs (Mips Atom Components)
FI_ Slice_MipsCode ac_gcmd_push(AtomBuilder_R ab, U4 cmd, U4 reg_transfer, U4 reg_base, U2 port)
MipsAtomComp_Proc_(ac_gcmd_push, ab, {
FI_ Slice_MipsCode ac_gcmd_push(U4 cmd, U4 reg_transfer, U4 reg_base, U2 port)
MipsAtomComp_Proc_(ac_gcmd_push, {
load_upper_i(reg_transfer, cmd >> 16),
or_i_self( reg_transfer, cmd & 0xFFFF),
store_word( reg_transfer, reg_base, port),
})
FI_ Slice_MipsCode ac_store_rgb8(AtomBuilder_R ab, U1 rr, U1 rg, U1 rb, U4 base, U4 offset) atom_dbg_skip MipsAtomComp_Proc_(ac_store_rgb8, ab, {
FI_ Slice_MipsCode ac_store_rgb8(U1 rr, U1 rg, U1 rb, U4 base, U4 offset) atom_dbg_skip MipsAtomComp_Proc_(ac_store_rgb8, {
store_byte(rr, base, offset + O_(RGB8,r)),
store_byte(rg, base, offset + O_(RGB8,g)),
store_byte(rb, base, offset + O_(RGB8,b)),
store_byte(rb, base, offset + O_(RGB8,b)),
})
FI_ Slice_MipsCode ac_pack_color_word(AtomBuilder_R ab, U4 r_base, U4 off, U4 cmd, U1 r, U1 g, U1 b)
atom_dbg_skip MipsAtomComp_Proc_(ac_pack_color_word, ab, {
/* 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)),
or_i_self( R_AT, ((g) << 8) | (r)),
store_word( R_AT, r_base, (off)),
})
FI_ Slice_MipsCode ac_format_f3_color(AtomBuilder_R ab, U4 r_base, U1 r, U1 g, U1 b)
atom_dbg_skip MipsAtomComp_Proc_(ac_format_f3_color, ab, { mac_pack_color_word(r_base, O_(Poly_F3,color), gp0_cmd_poly_f3, r, g, b) })
/* 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) })
FI_ Slice_MipsCode ac_format_g4_color(AtomBuilder_R ab, U4 r_prim_cursor,
/* 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,
U1 r2, U1 g2, U1 b2,
U1 r3, U1 g3, U1 b3)
MipsAtomComp_Proc_(ac_format_g4_color, ab, {
MipsAtomComp_Proc_(ac_format_g4_color, {
mac_pack_color_word(r_prim_cursor, O_(Poly_G4,c0), gp0_cmd_poly_g4, r0,g0,b0),
mac_pack_color_word(r_prim_cursor, O_(Poly_G4,c1), 0, r1,g1,b1),
mac_pack_color_word(r_prim_cursor, O_(Poly_G4,c2), 0, r2,g2,b2),
mac_pack_color_word(r_prim_cursor, O_(Poly_G4,c3), 0, r3,g3,b3),
})
/* Words: 11; Correctly inserts a primitive into the Ordering Table linked list. */
I_ Slice_MipsCode ac_insert_ot_tag(AtomBuilder_R ab, U4 r_ot_base, U4 r_prim_cursor, U4 poly_size) MipsAtomComp_Proc_(ac_insert_ot_tag, ab, {
/* Words: 11; Correctly inserts a primitive into the Ordering Table linked list.
* Hardcoded for Poly_F3 (5 words). For Poly_G4, use ac_insert_ot_tag_g4. */
I_ Slice_MipsCode ac_insert_ot_tag_f3(U4 r_ot_base, U4 r_prim_cursor) MipsAtomComp_Proc_(ac_insert_ot_tag_f3, {
shift_lleft( R_T1, R_T1, S_(U4)/2), // T1 = otz * S_(U4) (otz arg is implicit R_T1)
add_u_self( R_T1, r_ot_base), // T1 = & OrderingTable[OTZ]
load_word( R_AT, R_T1, O_(PolyTag,code)), // AT = old_ot_head
load_upper_i(R_V0, (poly_size/S_(U4) - S_(PolyTag)/S_(U4)) << PolyTag_len_bits),
load_upper_i(R_V0, (S_(Poly_F3)/S_(U4) - S_(PolyTag)/S_(U4)) << PolyTag_len_bits), // V0 = (5 - 1) << 24 = 4 << 24
mask_upper( R_AT, R_AT, S_(PolyTag_len_bits)), // Strip upper 8 bits (length from prev cell) → keep only low 24
or_u( R_AT, R_AT, R_V0), // Merge length
store_word( R_AT, r_prim_cursor, O_(PolyTag,code)), // prim->tag = packed(prim_length, old_addr)
@@ -57,4 +64,19 @@ I_ Slice_MipsCode ac_insert_ot_tag(AtomBuilder_R ab, U4 r_ot_base, U4 r_prim_cur
store_word( R_AT, R_T1, O_(PolyTag,code)), // OrderingTable[OTZ] = PrimCursor
})
/* Words: 11; Correctly inserts a primitive into the Ordering Table linked list.
* Hardcoded for Poly_G4 (9 words). For Poly_F3, use ac_insert_ot_tag_f3. */
I_ Slice_MipsCode ac_insert_ot_tag_g4(U4 r_ot_base, U4 r_prim_cursor) MipsAtomComp_Proc_(ac_insert_ot_tag_g4, {
shift_lleft( R_T1, R_T1, S_(U4)/2), // T1 = otz * S_(U4) (otz arg is implicit R_T1)
add_u_self( R_T1, r_ot_base), // T1 = & OrderingTable[OTZ]
load_word( R_AT, R_T1, O_(PolyTag,code)), // AT = old_ot_head
load_upper_i(R_V0, (S_(Poly_G4)/S_(U4) - S_(PolyTag)/S_(U4)) << PolyTag_len_bits), // V0 = (9 - 1) << 24 = 8 << 24
mask_upper( R_AT, R_AT, S_(PolyTag_len_bits)), // Strip upper 8 bits (length from prev cell) → keep only low 24
or_u( R_AT, R_AT, R_V0), // Merge length
store_word( R_AT, r_prim_cursor, O_(PolyTag,code)), // prim->tag = packed(prim_length, old_addr)
shift_lleft( R_AT, r_prim_cursor, S_(PolyTag_len_bits)), // AT = (prim_length << 24) | old_addr
shift_lright(R_AT, R_AT, S_(PolyTag_len_bits)),
store_word( R_AT, R_T1, O_(PolyTag,code)), // OrderingTable[OTZ] = PrimCursor
})
#pragma endregion MACs (Mips Atom Components)
+13 -241
View File
@@ -11,7 +11,7 @@ ATOM_FILE_DEBUGGER_LINE_MARKER(gte_atom_c);
#pragma region MACs (Mips Atom Components)
/* Words: 3; Loads 3 S2 indices from the face array */
FI_ Slice_MipsCode ac_load_tri_indices(AtomBuilder_R ab, U4 r_face_cusor, U4 r_i0, U4 r_i1, U4 r_i2) atom_dbg_skip MipsAtomComp_Proc_(ac_load_tri_indices, ab, {
FI_ Slice_MipsCode ac_load_tri_indices(U4 r_face_cusor, U4 r_i0, U4 r_i1, U4 r_i2) atom_dbg_skip MipsAtomComp_Proc_(ac_load_tri_indices, {
load_half_u(r_i0, r_face_cusor, 0 * S_(S2)),
load_half_u(r_i1, r_face_cusor, 1 * S_(S2)),
load_half_u(r_i2, r_face_cusor, 2 * S_(S2)),
@@ -19,14 +19,14 @@ FI_ Slice_MipsCode ac_load_tri_indices(AtomBuilder_R ab, U4 r_face_cusor, U4 r_i
/* Words: 3; Stores the 3 transformed (V2_S2 screen) vertices to the F3.
* PIPELINE: post-RTPT (SXY0=v0.screen, SXY1=v1.screen, SXY2=v2.screen). */
FI_ Slice_MipsCode ac_gte_store_f3(AtomBuilder_R ab, U4 r_primitive_cursor) atom_dbg_skip MipsAtomComp_Proc_(ac_gte_store_f3, ab, {
FI_ Slice_MipsCode ac_gte_store_f3(U4 r_primitive_cursor) atom_dbg_skip MipsAtomComp_Proc_(ac_gte_store_f3, {
gte_sw(C2_SXY0, r_primitive_cursor, O_(Poly_F3,p0)),
gte_sw(C2_SXY1, r_primitive_cursor, O_(Poly_F3,p1)),
gte_sw(C2_SXY2, r_primitive_cursor, O_(Poly_F3,p2)),
})
/* Words: 18; Translates indices to vertex addresses and pushes them to GTE */
I_ Slice_MipsCode ac_gte_load_tri_verts(AtomBuilder_R ab, U4 r_vert_base, U4 r_v0, U4 r_v1, U4 r_v2) atom_dbg_skip MipsAtomComp_Proc_(ac_gte_load_tri_verts, ab, {
I_ Slice_MipsCode ac_gte_load_tri_verts(U4 r_vert_base, U4 r_v0, U4 r_v1, U4 r_v2) atom_dbg_skip MipsAtomComp_Proc_(ac_gte_load_tri_verts, {
shift_lleft(R_AT, r_v0, v3s2_byteoff), add_u_self(R_AT, r_vert_base), load_word(R_V0, R_AT, O_(V3_S2,x)), load_word(R_V1, R_AT, O_(V3_S2,z)), gte_mv_to_data_r(R_V0, C2_VXY0), gte_mv_to_data_r(R_V1, C2_VZ0),
shift_lleft(R_AT, r_v1, v3s2_byteoff), add_u_self(R_AT, r_vert_base), load_word(R_V0, R_AT, O_(V3_S2,x)), load_word(R_V1, R_AT, O_(V3_S2,z)), gte_mv_to_data_r(R_V0, C2_VXY1), gte_mv_to_data_r(R_V1, C2_VZ1),
shift_lleft(R_AT, r_v2, v3s2_byteoff), add_u_self(R_AT, r_vert_base), load_word(R_V0, R_AT, O_(V3_S2,x)), load_word(R_V1, R_AT, O_(V3_S2,z)), gte_mv_to_data_r(R_V0, C2_VXY2), gte_mv_to_data_r(R_V1, C2_VZ2),
@@ -34,10 +34,10 @@ I_ Slice_MipsCode ac_gte_load_tri_verts(AtomBuilder_R ab, U4 r_vert_base, U4 r_v
/* Words: 3; Stores the 3 transformed (V2_S2 screen) vertices of the
* G4 triangle portion to p0/p1/p2.
* PIPELINE: post-RTPT, pre-RTPS (SXY0=v0.screen, SXY1=v1.screen, SXY2=v2.screen).
* PIPELINE: post-RTPT, pre-RTPS (SXY0=v0.screen, SXY1=v1.screen, SXY2=v2.screen).
* MUST be called BEFORE V3-RTPS, otherwise SXY0/1/2 get overwritten with v3
* (RTPS writes only to SXY2, but to keep the three registers aligned with v0/v1/v2 you must store before RTPS). */
FI_ Slice_MipsCode ac_gte_store_g4_p012(AtomBuilder_R ab, U4 r_primitive_cursor) atom_dbg_skip MipsAtomComp_Proc_(ac_gte_store_g4_p012, ab, {
FI_ Slice_MipsCode ac_gte_store_g4_p012(U4 r_primitive_cursor) atom_dbg_skip MipsAtomComp_Proc_(ac_gte_store_g4_p012, {
gte_sw(C2_SXY0, r_primitive_cursor, O_(Poly_G4,p0)),
gte_sw(C2_SXY1, r_primitive_cursor, O_(Poly_G4,p1)),
gte_sw(C2_SXY2, r_primitive_cursor, O_(Poly_G4,p2)),
@@ -47,250 +47,22 @@ FI_ Slice_MipsCode ac_gte_store_g4_p012(AtomBuilder_R ab, U4 r_primitive_cursor)
* PIPELINE: post-RTPS (SXY2 holds v3.screen because RTPS writes its single-vertex result to SXY2;
* SXY0 still holds v0.screen from the earlier RTPT.
*/
FI_ Slice_MipsCode ac_gte_store_g4_p3(AtomBuilder_R ab, U4 r_primitive_cursor) atom_dbg_skip MipsAtomComp_Proc_(ac_gte_store_g4_p3, ab, { 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(AtomBuilder_R ab, 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, ab, {
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(AtomBuilder_R ab, 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, ab, {
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),
})
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)) })
#pragma endregion MACs (Mips Atom Components)
#pragma region Atom Procs
#pragma region Bsked Atoms
/* ─── 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,
typedef Struct_(Binds_SetGteWorld) {
M3_S2* transform;
};
/* ─── Full normalize (all 4 stages inline) ───
* Generic 4-stage GTE normalize (SQR → sum+LZCR → align+sqrtbl → GPF+srav).
*
* Parameterized by caller-provided scratch base + src/dst offsets.
* The caller passes r_src_offset and r_dst_offset as compile-time constants
* (typically derived from O_ macros in the caller's struct schema, e.g., `O_(CallerBundleScratch, fwd)`).
*
* This design lets any caller (with a scratch base + struct schema) use `normalize_v3s4_proc`
* without putting magic offsets in the C-side bundle helper — the offsets come from O_ macros at the call site.
*
* Body uses 9 GPRs (r_src_ptr..r_branch_tmp):
* r_src_ptr, r_dst_ptr : src/dst pointers (computed from r_scratch + caller offsets)
* r_tmp : src.x PRESERVED across stages 1-2 (NOT clobbered by mfc2 MAC2) → fed to IR1 in stage 4
* r_mac1_scratch : MAC1 result scratch (also holds aligned |v|² in stage 3)
* r_mac2_scratch : MAC2 result scratch → result.x after stage 4 sra
* r_recip_est : src.y PRESERVED across stages 1-2 → fed to IR2 in stage 4 → result.y
* r_lzcr : |v|² sum (stage 2) → shift count (stage 3) → 1/|v| (stage 4 IR0)
* r_shift : shift count SAVED in stage 3 → consumed by stage 4 srav
* r_branch_tmp : src.z PRESERVED across stages 1-2 → fed to IR3 in stage 4 → result.z (also sqrtbl base addr)
*
* Atom_labels are srav_path / aligned_done
* (NOT namespaced — they're internal to this proc;
* the metaprogram's per-atom-name enum emission handles any collision across different atoms/files that share the same labels).
*
* Pool cost: 11 GPRs (well within the 9-10 caller-trash GPR budget when r_scratch is a wave-context carrier).
*
* Direct port of PSYQ libgte msc02.rel.text VectorNormal disassembly (0x800160a0..0x8001615c).
* Words: ~59 (matches libgte 0x800160a0..0x8001615c at +/- 0-2 words for BD-slot reshuffling).
* Sqrtbl: hardcoded to 0x800185B4 (libgte msc02.rel.data). Note: swapped to local.
* Pipeline: clobbers IR0..3, MAC1..3, LZCS, LZCR.
*/
/* MipsAtom_Proc_ wrapper: declares the static MipsCode[] body, then calls atombuilder_unroll(ab, ...) to copy the encoded instructions into the caller's MipsAtomBuilder arena. */
I_ MipsAtom* normalize_v3s4_proc(AtomArena_R aa, U4 r_scratch /* GPR code: scratch base carrier (e.g., R_T4 = R_ResolveScratch) */
, U4 r_src_offset, U4 r_dst_offset /* GPR codes: PARAMETERIZED offsets (caller passes O_ macros) */
, U4 r_src_ptr, U4 r_dst_ptr, U4 r_tmp /* GPR codes: 3 scratch regs (src/dst computed + tmp) */
, U4 r_mac1_scratch, U4 r_mac2_scratch /* GPR codes: 2 more: MAC1/MAC2 scratch */
, U4 r_recip_est /* GPR code: |v|² sum + shift-input + sqrtbl[index] */
, U4 r_lzcr, U4 r_shift /* GPR codes: lzcr + final srav amount */
, U4 r_branch_tmp /* GPR code: scratch (shift count, branch target, lookup addr) */
)
MipsAtom_Proc_(normalize_v3s4, aa, {
add_si(r_src_ptr, r_scratch, r_src_offset), /* r_src_ptr = &src */
add_si(r_dst_ptr, r_scratch, r_dst_offset), /* r_dst_ptr = &dst */
nop,
/* Load src.x/y/z from r_src_ptr (caller-determined address) into r_tmp/r_recip_est/r_branch_tmp.
* r_tmp holds src.x throughout stages 1-2 — r_mac2_scratch is clobbered to MAC2 in stage 1.5 (line below). */
load_word(r_tmp, r_src_ptr, O_(V3_S4,x)),
load_word(r_recip_est, r_src_ptr, O_(V3_S4,y)),
load_word(r_branch_tmp, r_src_ptr, O_(V3_S4,z)),
nop, /* load-delay */
/* Stage 1: mtc2 src → IR1/2/3, SQR fires. */
gte_mv_to_data_r(r_tmp, C2_IR1),
gte_mv_to_data_r(r_recip_est, C2_IR2),
gte_mv_to_data_r(r_branch_tmp, C2_IR3),
nop, gte_cmdw_sqr,
/* Stage 2: mfc2 MAC1/2/3, sum, mtc2 LZCS. */
gte_mv_from_data_r(r_mac1_scratch, C2_MAC1),
gte_mv_from_data_r(r_mac2_scratch, C2_MAC2),
gte_mv_from_data_r(r_lzcr, C2_MAC3),
nop,
add_u(r_lzcr, r_lzcr, r_mac2_scratch),
add_u(r_lzcr, r_lzcr, r_mac1_scratch),
gte_mv_to_data_r(r_lzcr, C2_LZCS),
nop2,
gte_mv_from_data_r(r_shift, C2_LZCR),
nop,
/* Stage 3: compute srav amount (r_lzcr) + align |v|² to bit 24.
* IMPORTANT: the sllv/srav below writes the aligned |v|² to r_mac1_scratch (NOT r_lzcr),
* so r_lzcr retains the shift count all the way to the start of stage 4.
*/
and_i( r_shift, r_shift, -2),
or_u(r_mac1_scratch, r_lzcr, 0), /* FIX B: save sum before clobbering r_lzcr with shift count */
li_s( r_lzcr, 31),
sub_s( r_lzcr, r_lzcr, r_shift),
shift_aright(r_lzcr, r_lzcr, 1),
/* r_branch_tmp = LZCR - 24 (overwrites r_branch_tmp; src.z no longer needed after SQR) */
add_si( r_branch_tmp, r_shift, -24),
branch_lt_zero(r_branch_tmp, atom_offset(aligned_done, srav_path)), nop, /* FIX A: bltz → srav_path (LZCR<24 path) */
jump_rel(atom_offset(srav_path, aligned_done)), /* FIX A: b → aligned_done (LZCR>=24 path) */
shift_lleft_var(r_mac1_scratch, r_mac1_scratch, r_branch_tmp), /* FIX B: src=sum (r_mac1_scratch), dst=same */
atom_label(srav_path)
li_s( r_branch_tmp, 24),
sub_s( r_branch_tmp, r_branch_tmp, r_shift),
shift_aright_var(r_mac1_scratch, r_mac1_scratch, r_branch_tmp), /* FIX B: src=sum (r_mac1_scratch), dst=same */
atom_label(aligned_done)
/* Save the shift count to r_shift before the next 5 instructions overwrite r_lzcr
* (the sqrtbl lookup loads 1/|v| into r_lzcr, which becomes IR0 in stage 4). */
or_u(r_shift, r_lzcr, 0), /* r_shift ← shift count (preserved through stage 4) */
/* r_mac1_scratch holds |v|² aligned (top bit at bit 7). */
add_si( r_mac1_scratch, r_mac1_scratch, -64),
shift_lleft(r_mac1_scratch, r_mac1_scratch, 1),
load_upper_i(r_branch_tmp, u4_hi(& gte_normalize_sqr_tbl)),
or_i_self( r_branch_tmp, u4_lo(& gte_normalize_sqr_tbl)),
add_u(r_branch_tmp, r_branch_tmp, r_mac1_scratch),
load_half(r_lzcr, r_branch_tmp, 0), nop, /* r_lzcr = sqrtbl[aligned-64] = 1/|v| (IR0 in stage 4) */
/* FIX bug C: r_branch_tmp held the sqrtbl base+index, NOT src.z. Reload src.z from scratch now that r_branch_tmp is free. */
load_word(r_branch_tmp, r_src_ptr, O_(V3_S4,z)), nop, /* r_branch_tmp = src.z (for IR3 in stage 4) */
/* Stage 4: GPF + srav finalize (r_shift = shift count, r_lzcr = 1/|v|). */
gte_mv_to_data_r(r_lzcr, C2_IR0),
gte_mv_to_data_r(r_tmp, C2_IR1), /* IR1 = src.x (preserved in r_tmp — r_mac2_scratch was clobbered to MAC2 in stage 1.5) */
gte_mv_to_data_r(r_recip_est, C2_IR2),
gte_mv_to_data_r(r_branch_tmp, C2_IR3), /* IR3 = src.z (reloaded) */
nop2, gte_cmdw_gpf,
gte_mv_from_data_r(r_mac2_scratch, C2_MAC1),
gte_mv_from_data_r(r_recip_est, C2_MAC2),
gte_mv_from_data_r(r_branch_tmp, C2_MAC3),
shift_aright_var(r_mac2_scratch, r_mac2_scratch, r_shift), /* sra by r_shift = (31-LZCR)/2 (saved before sqrtbl lookup) */
shift_aright_var(r_recip_est, r_recip_est, r_shift),
shift_aright_var(r_branch_tmp, r_branch_tmp, r_shift),
/* Store result.x/y/z to r_dst_ptr (caller-determined dst address). */
store_word(r_mac2_scratch, r_dst_ptr, O_(V3_S4,x)),
store_word(r_recip_est, r_dst_ptr, O_(V3_S4,y)),
store_word(r_branch_tmp, r_dst_ptr, O_(V3_S4,z)),
mac_yield()
})
#pragma endregion Atom Procs
#pragma region Baked Atoms
typedef Struct_(Binds_SetGteMT3S2S4) {
MT3_S2S4* transform;
};
internal MipsAtom_(set_gte_mt3s2s4) atom_info(
atom_bind(Binds_SetGteMT3S2S4)
internal MipsAtom_(set_gte_world) atom_info(
atom_bind(Binds_SetGteWorld)
, atom_reads(R_TapePtr)
){
/* Pop matrix address from tape into R_T3 ($11) */
load_word(R_T3, R_TapePtr, O_(Binds_SetGteMT3S2S4,transform)),
add_ui_self( R_TapePtr, S_(Binds_SetGteMT3S2S4)),
load_word(R_T3, R_TapePtr, O_(Binds_SetGteWorld,transform)),
add_ui_self( R_TapePtr, S_(Binds_SetGteWorld)),
/* Load 3x3 Rotation + 3x1 Translation from R_T3 into GTE CONTROL Regs (ctc2) */
load_word(R_T0, R_T3, 0), load_word(R_T1, R_T3, 4),
gte_mv_to_ctrl_r(R_T0, gte_cr_RT11), gte_mv_to_ctrl_r(R_T1, gte_cr_RT12),
+19 -60
View File
@@ -161,8 +161,6 @@ 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:
@@ -173,22 +171,17 @@ 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) ---
@@ -250,10 +243,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 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 the 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 is next to its only consumer (this header).
* and so the encoding lives 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
@@ -316,24 +309,23 @@ enum { _C2_TX_SUBS_ = 0
/* GTE Command Format
* Opcode is always MIPS_OP_COP2, RS is always 1 (CO).
* Lower 25 bits are GTE-specific command payload.
* The lower 25 bits are the GTE-specific command payload.
*
* The `enc_gte_<field>(x)` macros below mirror the `enc_op`/`enc_rs` pattern in mips.h:
* The granular `enc_gte_<field>(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 an all-in-one convenience for emitting a full command word.
* `ENC_GTE_CMD` is the all-in-one convenience for emitting a full command word in one go.
* 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_fake_cmd(x) (((x) & gte_mask_fake_cmd) << gte_shift_fake_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)
/* Composite: all six GTE fields + the COP2/CO base. */
#define enc_gte_cmdw(sf, mx, v, cv, lm, cmd) ( \
@@ -371,11 +363,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.
* 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.
* NCLIP / OP / MVMVA stay spec-clean — their reserved bits really are zero in the original PsyQ source.
* --------------------------------------------------------------------------
*/
@@ -386,45 +378,11 @@ enum { _C2_TX_SUBS_ = 0
#define gte_cmdw_nclip (gte_cmd_base | enc_gte_cmd(gte_cmd_nclip))
#define gte_cmdw_op (gte_cmd_base | enc_gte_cmd(gte_cmd_op ))
#define gte_cmdw_outer_product gte_cmdw_op /* "outer product" -- NOCASH/Sdk terminology */
#define gte_cmdw_wedge gte_cmdw_op /* "wedge product" -- geometric-algebra terminology.
* RGA(Lengyel): the GTE OP is a 3D signed-16-bit D x IR cross, not a generic RGA exterior product.
* The wedge alias is the 3D complement interpretation of the same 3 scalars (MAC1..MAC3). */
#define gte_cmdw_wedge gte_cmdw_op /* "wedge product" -- geometric-algebra terminology */
#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.
* For unitized points the same result equals the motor antiproduct; the GTE executes the LA form, not a symbolic antiproduct. */
/* PsyQ compatibility bits for AVSZ3 (Bits 20, 22, 24 must be set) */
#define gte_cmdw_psyq_avsz3_compat (0x15 << 20)
@@ -475,6 +433,7 @@ 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(<base>)` for the chosen GTE vector register, where `<base>` is the GPR number you pass in
* (typically one of R_T4..R_T9 for the standard "3-pointer" pattern).
*
+86 -106
View File
@@ -12,57 +12,68 @@
#endif
#pragma region Tape Drive
/* -----------------------------------------------------------------------------------------------------------
/* -----------------------------------------------------------------------------
* TAPE DRIVE ABI
* -----------------------------------------------------------------------------------------------------------
* Note(Ed): One of the main purposes of this codebase is to help me learn this,
* as such the information below may not* be entirely realized or finalized conceptually.
* -----------------------------------------------------------------------------------------------------------
* This ABI and its associated legos were directly inspired by researching the work of
* Timothy Lottes and Onat Türkçüoğlu; along with many others. It's the simplest bootstrap of a
* directly executed chain of assemby arrays (Atoms) that terminate with a yield sequence to the next atom.
* These eventually lead to a terminal atom for the tape which is defined below as "tape_exit".
* -----------------------------------------------------------------------------
* Note(Ed): One of the main purposes of this codebase is to help me
* learn this, as such the information below may be entirely realized
* or finalized conceptually.
* -----------------------------------------------------------------------------
* This ABI and its associated legos were directly inspired by researching
* the work of Timothy Lottes and Onat Türkçüoğlu; along with many others.
* It's the simplest bootstrap of a a directly executed chain of assemby
* arrays (Atoms) that terminate with a yield sequence to the next atom.
* These eventually lead to a terminal atom for the tape which is defined
* below as "tape_exit".
*
* This behaves as one of the simplest runtime harnesses ontop of a host-enviornment's execution engine
* to author and compose programs with. From here various conventions can be further applied.
* To make things easier to understand it may be better to focus on what this ABI does not have.
* It does not have have any branching within the tape but relative branches within atoms or between atoms.
* Branching nearly is always downstream. Atuomatic stack usage is non-existent.
* Push/Pop, FIFO, or Arena/Bump data structures are used by atoms explicitly.
* In it's current form with the C11 macro DSL, the user also has fullfill manual register allocation per atom.
* This behaves as one of the simplest runtime harnesses ontop of a
* host-enviornment's execution engine to author and compose programs with.
* From here various conventions can be further applied.
* To make things easier to understand it may be better to focus on what this
* ABI does not have. It does not have have any branching within the tape but
* relative branches between atoms. Branching nearly is always downstream.
* Stack usage is non-existent. Push/Pop, FIFO, or Arena/Bump data structures
* are used by atoms explicitly. In it's current form withe C11 macro dsl,
* the user also has to do manual register allocation per atom.
*
* One of the remarkable things about utilizing this ABI is its essentially interopable with CPUs, GPUs, FPGA,
* or, basically anything from the 5th generation consoles and onward.
* The ABI directly reflects how all computational hardware must be architected in order to execute
* digital logic effectively on current era tech.
* On the PS1 we don't have access to a few features like multi-threading, speculative execution, or L3 cache;
* but, we can set the foundation for legoing whats required for eventually expanding this ABI's paradigm
* and core atoms to take those newer hardware features into account. For example, you can easily expand
* this to support wave-based execution model on a PS2 or PS3. Not having a stack or
* automatic register allocation means the user cannott ignore excessive argument shuffle across workload or
* waves and thier phases. Crossing ABI boundaries to other runtimes that do has obviouss penalties.
* One of the remarkable things about utilizing this abi is its essentially
* interopable with CPUs, GPUs, FPGA, or, basically anything
* from the 5th generation consoles and onward.
* The ABI directly reflects how all computational hardware must be architected
* in order to execute digital logic effectively on current era tech.
* On the PS1 we don't have access to a few features like multi-threading,
* speculative execution, or L3 cache; but, we can set the foundation for legoing
* whats required baseline wise for eventually expanding the harness and core atoms
* to take those newer hardware features into account. For example, you can easily
* expand this to support wave-based execution model on a PS2 or PS3.
* Not having a stack or automatic register allocation means the user can't ignore
* excessive argument shuffle across workload or waves and thier phases.
* Crossing ABI boundaries to other runtimes that do has an obviouss penalties.
*
* Learning data-oreinted code becomes a natural progression. Your not fighting a stack-based procedural
* paradigm that wants to argument shuffle. There is no ambiguity due to the lack of constraints, for example,
* on how the user may "call" a procedure in traditional random dispatch runtimes. The user does have to
* hammer down "rules" or patterns for massaging the compiler to dissolve those call frames; just to get
* the asesmbly into its desired form. The form is obvious, and once the user gets to author these compoonents
* it becomes a game of tetris.
* Learning data-oreinted code becomes a natural progression. Your not fighting
* a stack-based procedural paradigm that wants to argument shuffle on the stack
* by lack of constraints on how the user may "call" a procedure. The user doesn't
* have to hammer down "rules" or patterns to know how to massage the compiler
* to get the asesmbly into its natural form. The form is obvious, and once
* the user gets to author their compoonents it becomes a game of tetris.
*
* Another feature is this ABI is very compatible with bootstrapping and developing simple toolchains built off
* of bit-packed annotated command streams the user can directly author, maintatain, and immediately execute.
* That being like a color forth, or maybe something more familar like an immediate mode library
* for various systems such as GUIs. This can make the tetris less of a chore with some helpful policy
* generation for allocation of registers, helping to choose resuable components, designing DSL on the fly, etc.
* -----------------------------------------------------------------------------------------------------------
* TODO(Ed): We need pretty ascii diagrams and proper guides, articles, etc.
* -----------------------------------------------------------------------------------------------------------
* For now this ideation has just started functioning. I'm abusing C11 & a lua metaprogram to help establish
* a hybrid toolchain to ideate on a traditional text-based authoring UX for this paradigm.
* If pcsx-redux provides viable hot-reload and persistent data storage beyond save-states
* (just copying ram to filesystem), I can author a color forth to mess around with.
* With either an editor in-emulator or on the actual machine itself. Assembly is tedius,
* but I think this codebase most likely has a pretty ergonomic flavor worst case...
* Another feature is this ABI is very compatible with bootstrapping and developing
* simple toolchains built off of bit-packed annotated command streams the user can
* directly author, maintatain, and immediately execute. That being a color forth.
* This can make the tetris less of a chore with some helpful policy generation for
* allocation of registers, helping to choose resuable components, designing DSL on
* the fly, etc.
* -----------------------------------------------------------------------------
* TODO(Ed): We ned pretty ascii diagrams and proper guides, articles, etc.
* -----------------------------------------------------------------------------
* For now this thing is just functioning and I'm abusing C11 + a lua metaprogram
* to help establish a hybrid toolchain to ideate on a traditional text-based
* authoring UX for this paradigm.
* If pcsx-redux gets me viable hot-reload and persistent data storage beyond
* save-states (just copying ram to filesystem). I can author a color forth to
* mess around with, with an editor in-emulator or on the actual machine itself.
* Assembly is tedius, but I think this codebase most likely has some of the most,
* ergonomic you can come across..
* */
/* Register Allocation Info */
enum {
@@ -106,32 +117,25 @@ 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, aa, ...) { MipsCode sym [] align_(4) = __VA_ARGS__; return atomarena_push(aa, 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:
// MipsCode ac_X[] align_(4) = { body };
#define MipsAtomComp_(sym) MipsCode sym [] align_(4) =
// Used for components with value-args (mandatory `ab` (atom-builder) arg).
// FI_ void ac_X(MipsAtomBuilder_R ab, args) MipsAtomComp_Proc_(ac_X, ab, { body })
// 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_ void ac_X(MipsAtomBuilder_R ab, args) {
// MipsCode ac_X[] align_(4) = { body };
// atombuilder_unroll(ab, slice_from_array(MipsCode, ac_X));
// }
// The body must NOT include mac_yield() (the parent atom yields).
// Inline-only callers (the generated `mac_<name>` aliases) skip this arg via metaprogram filtering;
// escape callers (ac_<name> invoked as a function) pass a long-lived builder.
#define MipsAtomComp_Proc_(sym, ab, ...) { MipsCode sym [] align_(4) = __VA_ARGS__; atombuilder_push(ab, slice_from_array(MipsCode, sym)); }
// 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.
/* Line-table anchor: gcc only adds a file to the .debug_line file table when the contains line-numbered content.
Files containing only atoms and atom components.
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.
The constant is in `.rodata` and unreferenced; the linker may eliminate it.
@@ -186,15 +190,13 @@ FI_ void tape_run_a02_s07(Tape tape) { register U4* tape_ptr rgcc(R_TapePtr) = u
typedef Relative_(FArena) Struct_(TapeBuilder) { U4 ptr; U4 capacity; U4 used; };
FI_ void tb_init(TapeBuilder* tb, FArena* arena) { tb->ptr = arena->start; tb->used = 0; }
FI_ TapeBuilder tb_make_old( FArena* arena) { return (TapeBuilder){ arena->start, 0 }; }
FI_ TapeBuilder tb_make(Slice mem) { return (TapeBuilder){ u4_(mem.ptr), mem.len, 0 }; } /* capacity in elements (matches used units) */
FI_ TapeBuilder tb_make(Slice mem) { return (TapeBuilder){ mem.ptr, mem.len, 0 }; }
FI_ void tb_emit(TapeBuilder* tb, MipsAtom* atom) { u4_r(tb->ptr)[tb->used] = u4_(atom); ++ tb->used; }
FI_ void tb_emit(TapeBuilder* tb, MipsCode* 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), S_slice(atoms)); 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))
@@ -229,57 +231,35 @@ atom_dbg_skip MipsAtomComp_(ac_yield_tail) {
add_ui_self(R_TapePtr, S_(MipsCode)),
jump_reg( R_AtomJmp), nop,
};
#pragma endregion Macro Atom Components
#pragma region Atom Builder
#pragma region Mips Atom Builder
// This helps with runtime procedural authoring of mips atoms.
typedef Struct_(FMipsAtom512) { U4 data[512]; U4 used; };
// FArena Related
typedef Relative_(FArena) Struct_(AtomBuilder) { U4 start; U4 capacity; U4 used; };
typedef Relative_(FArena) Struct_(MipsAtomBuilder) { U4 start; U4 capacity; U4 used; };
// Whatever the builder is writting to should most likely coresspond
// to something that can fit within instruction cache?
// Usual way to resolve an atom after the bulder is done.
#define atom_from_atombuilder(ab) C_(MipsAtom*, (ab).start)
FI_ void atombuilder_push(AtomBuilder_R ab, Slice_MipsCode code) {
assert(ab->capacity - ab->used - code.len);
U4 dest = ab->start + ab->used * S_(MipsCode);
mem_copy(dest, u4_(code.ptr), S_slice(code));
mem_bump(ab->start, ab->capacity, & ab->used, code.len);
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);
}
#define atombuilder_push_mac(ab, mac) atombuilder_push(ab, slice_arg_from_array(Slice_MipsCode, mac))
#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 (if not utilizing a MipsAtom_Proc).
FI_ void atombuilder_end(AtomBuilder_R ab) { atombuilder_push(ab, slice_from_array(MipsCode, ac_yield)); }
// When done authoring, utilize this to cap-off the atom
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));
}
FI_ void tb_emit_atombuilder(TapeBuilder_R tb, AtomBuilder_R ab) { tb_emit(tb, atom_from_atombuilder(ab[0])); }
#define mipsatom_from_builder(ab) (Slice_MipsCode){ab.start, ab.used}
#pragma endregion Mips Atom Builder
#pragma region Atom Arena
typedef Relative_(FArena) Struct_(AtomArena) { U4 start; U4 capacity; U4 used; };
#define atomarena_unused_start(ab) ((ab).start + (ab).used * S_(MipsCode))
FI_ void atomarena_init(AtomArena_R arena, Slice mem) { assert(arena != nullptr);
arena->start = u4_(mem.ptr);
arena->capacity = mem.len;
arena->used = 0;
}
FI_ AtomArena atomarena_make(Slice mem) { AtomArena a; atomarena_init(& a, mem); return a; }
FI_ MipsAtom* atomarena_push(AtomArena_R aa, Slice_MipsCode code) {
assert(aa->capacity - aa->used - code.len);
U4 dest = atomarena_unused_start(aa[0]);
mem_copy(dest, u4_(code.ptr), S_slice(code));
mem_bump(aa->start, aa->capacity, & aa->used, code.len);
return C_(MipsAtom*, dest);
}
FI_ void atomarena_reset(AtomArena_R aa) { aa->used = 0; }
#pragma region Atom Arena
#pragma region Mips Atom Procs
#pragma endregion Mips Atom Procs
#pragma region Baked Mips Atoms
// These atoms are resolved at compile time and are (usually) statically linked readonly data.
+3 -21
View File
@@ -9,35 +9,17 @@ ATOM_FILE_DEBUGGER_LINE_MARKER(math_atom_c);
#pragma region MACs (Mips Atom Component)
FI_ Slice_MipsCode ac_load_v2s2(AtomBuilder_R ab, U4 rs_x, U4 rs_y, U4 r_base, U4 offset) atom_dbg_skip MipsAtomComp_Proc_(ac_load_v2s2, ab, {
FI_ Slice_MipsCode ac_load_v2s2(U4 rs_x, U4 rs_y, U4 r_base, U4 offset) atom_dbg_skip MipsAtomComp_Proc_(ac_load_v2s2, {
load_half( rs_x, r_base, O_(V3_S2,x)),
load_half( rs_y, r_base, O_(V3_S2,y)),
})
FI_ Slice_MipsCode ac_store_v2s2(AtomBuilder_R ab, U4 rt_x, U4 rt_y, U4 base, U4 offset) atom_dbg_skip MipsAtomComp_Proc_(ac_store_v2s2, ab, {
FI_ Slice_MipsCode ac_store_v2s2(U4 rt_x, U4 rt_y, U4 base, U4 offset) atom_dbg_skip MipsAtomComp_Proc_(ac_store_v2s2, {
store_half(rt_x, base, offset + O_(V2_S2,x)),
store_half(rt_y, base, offset + O_(V2_S2,y)),
})
FI_ Slice_MipsCode ac_load_v3s4(AtomBuilder_R ab, U4 rs_x, U4 rs_y, U4 rs_z, U4 r_base, U4 offset) atom_dbg_skip MipsAtomComp_Proc_(ac_load_v3s4, ab, {
load_word( rs_x, r_base, O_(V3_S4,x)),
load_word( rs_y, r_base, O_(V3_S4,y)),
load_word( rs_z, r_base, O_(V3_S4,z)),
})
FI_ Slice_MipsCode ac_store_v3s4(AtomBuilder_R ab, U4 rt_x, U4 rt_y, U4 rt_z, U4 base, U4 offset) atom_dbg_skip MipsAtomComp_Proc_(ac_store_v3s4, ab, {
store_word(rt_x, base, offset + O_(V3_S4,x)),
store_word(rt_y, base, offset + O_(V3_S4,y)),
store_word(rt_z, base, offset + O_(V3_S4,z)),
})
FI_ Slice_MipsCode ac_sub_v3s4(AtomBuilder_R ab, U4 rds_x, U4 rds_y, U4 rds_z, U4 rt_x, U4 rt_y, U4 rt_z) atom_dbg_skip MipsAtomComp_Proc_(ac_sub_v3s4, ab, {
sub_s(rds_x, rds_x, rt_x),
sub_s(rds_y, rds_y, rt_y),
sub_s(rds_z, rds_z, rt_z),
})
FI_ Slice_MipsCode ac_store_rects2(AtomBuilder_R ab, U4 rt_x, U4 rt_y, U4 rt_width, U4 rt_height, U4 base, U4 offset) atom_dbg_skip MipsAtomComp_Proc_(ac_store_rects2, ab, {
FI_ Slice_MipsCode ac_store_rects2(U4 rt_x, U4 rt_y, U4 rt_width, U4 rt_height, U4 base, U4 offset) atom_dbg_skip MipsAtomComp_Proc_(ac_store_rects2, {
store_half(rt_x, base, offset + O_(Rect_S2,x)),
store_half(rt_y, base, offset + O_(Rect_S2,y)),
store_half(rt_width, base, offset + O_(Rect_S2,width)),
+5 -55
View File
@@ -7,18 +7,6 @@
#define max(A, B) (((A) > (B)) ? (A) : (B))
#define clamp_bot(X, B) max(X, B)
/* Convention
<Type> ## <Width> _ <Component Type> ## <Component Width>
For types with compound data (Ex: Rotation Matrix & Translation):
<TypeA> ## <TypeB> ## <Width> _ <ComponentTypeA> ## <ComponentWidthA> ## <ComponentTypeB> ## <ComponentWidthB>
A: Array
V: Vector
R: Range
M: Matrix
T: Translation
*/
enum {
v3s2_byteoff = 3, // log2(8), used with shift_left_logical op for index via byte offset.
};
@@ -38,38 +26,23 @@ typedef Struct_(Extent2_S4) { S4 width; S4 height; };
typedef Struct_(V2_U1) { U1 x; U1 y; };
typedef Struct_(V2_S2) { S2 x; S2 y; };
typedef Struct_(V2_S4) { S4 x; S4 y; };
typedef Struct_(V3_S2) { S2 x; S2 y; S2 z; S2 pad; }; // PSY-Q: SVECTOR
typedef Struct_(V3_S4) { S4 x; S4 y; S4 z; S4 pad; }; // PSY-Q: VECTOR. RGA(Lengyel): Euclidean vector or direction. A zero-weight RGA point is stored as a V3_S4 with the implicit weight dropped.
typedef Struct_(V3_S2) { S2 x; S2 y; S2 z; S2 pad; };
typedef Struct_(V3_S4) { S4 x; S4 y; S4 z; S4 pad; };
typedef Struct_(V4_S2) { S2 x; S2 y; S2 z; S2 w; };
typedef Struct_(V4_S4) { S4 x; S4 y; S4 z; S4 w; };
// typedef Struct_(P3_S4) { S4 x; S4 y; S4 z; S4 w1; }; // RGA(Lengyel): Affine point with implicit weight one. Storage alias of V3_S4. Use P3_S4 when the value is a point.
typedef V3_S4 P3_S4;
typedef Struct_(R2_S2) { V2_S2 p0; V2_S2 p1; }; // Range-2 Signed 2-Byte (16-bit)
typedef Struct_(R2_S4) { V2_S4 p0; V2_S4 p1; }; // Range-2 Signed 4-Byte (32-bit)
typedef Struct_(R2_S2) { V2_S2 p0; V2_S2 p1; };
typedef Struct_(R2_S4) { V2_S4 p0; V2_S4 p1; };
typedef Struct_(Rect_S2) { S2 x; S2 y; S2 width; S2 height; };
typedef Struct_(Rect_S4) { S4 x; S4 y; S4 width; S4 height; };
typedef Struct_(MT3_S2S4) { A3x3_S2 m; A3_S4 t; }; // PSY-Q: MATRIX. RGA(Lengyel): Matrix expansion of a rigid transformation. GTE utilizes this representation; corresponding motor not constructed here.
typedef Struct_(M3_S2) { A3x3_S2 m; A3_S4 t; };
/* RGA(Lengyel) reserved names (deferred):
* P4_S4 - future flat point with explicit weight (Lengyel/TML FlatPoint3D analog).
* B3_S4 - future 3D bivector (callers store a Complement(Wedge(...)) as a V3_S4).
* Mo8_S4 - future motor. Not introduced until a course operation actually needs composition, interpolation, or inversion. */
typedef Array_(V2_U1, 2);
typedef Array_(V2_S2, 2);
typedef Array_(V2_S2, 3);
typedef Array_(V2_S2, 4);
enum {
fp_one = (1 << 12),
};
#define v3s4_fp_one() v3s4(fp_one, fp_one, fp_one)
#define v2s2(x,y) (V2_S2){x,y}
#define v3s2(x,y,z) (V3_S2){x,y,z,0}
#define v3s4(x,y,z) (V3_S4){x,y,z,0}
@@ -88,28 +61,5 @@ FI_ void add_a3s4_fp(A3_S4_R out_a, A3_S4 b) {
(out_a[0])[2] += b[2] >> 1;
}
FI_ void sub_a3s4(A3_S4_R out_a, A3_S4 b) {
(out_a[0])[0] -= b[0];
(out_a[0])[1] -= b[1];
(out_a[0])[2] -= b[2];
}
FI_ void sub_a3s4_fp(A3_S4_R out_a, A3_S4 b) {
(out_a[0])[0] -= b[0] >> 1;
(out_a[0])[1] -= b[1] >> 1;
(out_a[0])[2] -= b[2] >> 1;
}
FI_ void mul_a3s4(A3_S4_R out_a, A3_S4 b) {
(out_a[0])[0] *= b[0];
(out_a[0])[1] *= b[1];
(out_a[0])[2] *= b[2];
}
FI_ void add_v3s4 (V3_S4_R out_a, V3_S4 b) { add_a3s4 (pcast(A3_S4_R, out_a), pcast(A3_S4, b)); }
FI_ void add_v3s4_fp(V3_S4_R out_a, V3_S4 b) { add_a3s4_fp(pcast(A3_S4_R, out_a), pcast(A3_S4, b)); }
FI_ void sub_v3s4 (V3_S4_R out_a, V3_S4 b) { sub_a3s4 (pcast(A3_S4_R, out_a), pcast(A3_S4, b)); }
FI_ void sub_v3s4_fp(V3_S4_R out_a, V3_S4 b) { sub_a3s4_fp(pcast(A3_S4_R, out_a), pcast(A3_S4, b)); }
FI_ void mul_v3s4 (V3_S4_R out_a, V3_S4 b) { mul_a3s4 (pcast(A3_S4_R, out_a), pcast(A3_S4, b)); }
+10 -12
View File
@@ -58,13 +58,13 @@ typedef Struct_(Str8) { UTF8* ptr; U4 len; };
typedef Struct_(Slice_Str8) { Str8* ptr; U4 len; };
#define slit(string_literal) (Str8){ (UTF8*) string_literal, S_(string_literal) - 1 }
typedef Struct_(Slice) { B1* ptr; U4 len; }; // Untyped Slice (byte-addressable; .len in elements)
FI_ Slice slice_ut_(U4 ptr, U4 len) { return (Slice){(B1*)ptr, len}; }
typedef Struct_(Slice) { U4 ptr, len; }; // Untyped Slice
FI_ Slice slice_ut_(U4 ptr, U4 len) { return (Slice){ptr, len}; }
#define Slice_(type) Struct_(tmpl(Slice,type)) { type* ptr; U4 len; }
typedef Slice_(B1);
#define slice_assert(s) do { assert((s).ptr != 0); assert((s).len > 0); } while(0)
#define slice_end(slice) ((slice).ptr + S_slice(slice) / S_(B1)) /* byte-ptr arithmetic; .len is in elements per slice convention */
#define slice_end(slice) ((slice).ptr + (slice).len)
#define S_slice(s) ((s).len * S_((s).ptr[0]))
#define slice_ut(ptr,len) slice_ut_(u4_(ptr), u4_(len))
@@ -73,23 +73,22 @@ typedef Slice_(B1);
#define slice_iter(container, iter) (T_((container).ptr) iter = (container).ptr; iter != slice_end(container); ++ iter)
#define slice_arg_from_array(type, ...) & (tmpl(Slice,type)) { .ptr = array_decl(type,__VA_ARGS__), .len = array_len( array_decl(type,__VA_ARGS__)) }
#define slice_from_array(type, array) (tmpl(Slice,type)) { .ptr = array, .len = S_(array) / S_(type) }
#define slice_from_array(type, array) (tmpl(Slice,type)) { .ptr = array, .len = S_(array) }
FI_ void slice_zero_(Slice s) { slice_assert(s); mem_zero(u4_(s.ptr), s.len); }
FI_ void slice_zero_(Slice s) { slice_assert(s); mem_zero(s.ptr, s.len); }
#define slice_zero(s) slice_zero_(slice_to_ut(s))
FI_ void slice_copy_(Slice dest, Slice src) {
assert(S_slice(dest) >= S_slice(src));
assert(dest.len >= src.len);
slice_assert(dest);
slice_assert(src);
mem_copy(u4_(dest.ptr), u4_(src.ptr), S_slice(src));
mem_copy(dest.ptr, src.ptr, src.len);
}
#define slice_copy(dest, src) do { \
static_assert(T_same(dest, src)); \
slice_copy_(slice_to_ut(dest), slice_to_ut(src)); \
} while(0)
typedef Slice_(U1);
typedef Slice_(U4);
#pragma endregion Slice
@@ -99,7 +98,7 @@ typedef Slice_(U4);
typedef Opt_(farena) { U4 alignment, type_width; };
typedef Struct_(FArena) { U4 start, capacity, used; };
FI_ void farena_init(FArena_R arena, Slice mem) { assert(arena != nullptr);
arena->start = u4_(mem.ptr);
arena->start = mem.ptr;
arena->capacity = mem.len;
arena->used = 0;
}
@@ -110,15 +109,14 @@ I_ Slice farena_push(FArena_R arena, U4 amount, Opt_farena o) {
U4 to_commit = align_pow2(desired, o.alignment ? o.alignment : MEM_ALIGNMENT_DEFAULT);
U4 ptr = arena->start + arena->used;
mem_bump(arena->start, arena->capacity, & arena->used, to_commit);
return (Slice){ (B1*)ptr, to_commit };
return (Slice){ ptr, to_commit };
}
FI_ void farena_reset (FArena_R arena) { arena->used = 0; }
FI_ void farena_rewind(FArena_R arena, U4 save_point) {
U4 end = arena->start + arena->used; assert_bounds(save_point, arena->start, end);
arena->used -= save_point - arena->start;
}
FI_ U4 farena_save(FArena arena) { return arena.used; }
FI_ U4 farena_unused_start(FArena arena) { return arena.start + arena.used; }
FI_ U4 farena_save(FArena arena) { return arena.used; }
#define farena_push_(arena, amount, ...) farena_push((arena), (amount), opt_(farena, __VA_ARGS__))
#define farena_push_type(arena, type, ...) C_(type*, farena_push((arena), 1, opt_(farena, .type_width=S_(type), __VA_ARGS__)).ptr)
#define farena_push_array(arena, type, amount, ...) (tmpl(Slice,type)){ C_(type*, farena_push((arena), (amount), opt_(farena, .type_width=S_(type), __VA_ARGS__)).ptr), (amount) }
+5 -1
View File
@@ -1,7 +1,6 @@
#ifdef INTELLISENSE_DIRECTIVES
# include "gen/macs.h"
# include "gen/offsets.h"
# include "bios.h"
# include "lottes_tape.h"
#endif
@@ -9,6 +8,11 @@ ATOM_FILE_DEBUGGER_LINE_MARKER(mips_atom_c);
#pragma region Baked Atoms
enum {
bios_flushcache = 0x44,
bios_table_addr = 0xA0,
};
/* Flushes the Instruction Cache (PSX A-function 0x44 via BIOS stub at 0xA0).
* Sequence (per MIPS ABI; arguments in arg registers, RA pushed to stack):
* 1. sp -= 8; sw $ra, 4($sp) ; save RA
+11 -12
View File
@@ -348,12 +348,6 @@ 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)
@@ -372,18 +366,20 @@ 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.
@@ -401,7 +397,13 @@ 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)
@@ -456,9 +458,6 @@ 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))
+73 -84
View File
@@ -9,34 +9,6 @@
ATOM_FILE_DEBUGGER_LINE_MARKER(pad_atom_c);
#pragma region MACs (Mips Atom Components)
FI_ Slice_MipsCode ac_pad_set_centered_axes(AtomBuilder_R ab, U4 r_state, U4 r_scratch) atom_dbg_skip MipsAtomComp_Proc_(ac_pad_set_centered_axes, ab, {
load_upper_i(r_scratch, (PadAxis_Centered_Word >> 16) & 0xFFFF),
or_i_self( r_scratch, PadAxis_Centered_Word & 0xFFFF),
store_word( r_scratch, r_state, O_(PadState,axes)),
})
FI_ Slice_MipsCode ac_pad_set_id_byte(AtomBuilder_R ab, U1 r_state, U1 r_id, U1 id_value) atom_dbg_skip MipsAtomComp_Proc_(ac_pad_set_id_byte, ab, {
add_ui( r_id, R_0, id_value),
store_byte(r_id, r_state, O_(PadState,id)),
})
FI_ Slice_MipsCode ac_pad_set_status(AtomBuilder_R ab, U4 r_tmp, U1 r_state, U4 pad_status) atom_dbg_skip MipsAtomComp_Proc_(ac_pad_set_status, ab, {
add_ui( r_tmp, R_0, pad_status),
store_word(r_tmp, r_state, O_(PadState,status)),
})
/* Invert r_buttons (active-low → active-high) and store to PadState.buttons.
* r_buttons must already be loaded (the caller is responsible for filling the load-delay slot of
* the preceding load_half_u with an instruction that doesn't read r_buttons). */
FI_ Slice_MipsCode ac_pad_store_inverted_buttons(AtomBuilder_R ab, U1 r_buttons, U1 r_pad_state) atom_dbg_skip MipsAtomComp_Proc_(ac_pad_store_inverted_buttons, ab, {
nor_u( r_buttons, r_buttons, R_0),
store_half( r_buttons, r_pad_state, O_(PadState, buttons)),
})
#pragma endregion MACs (Mips Atom Components)
#pragma region Baked Atoms
/* ----- pad_bios_snapshot -----
@@ -63,7 +35,7 @@ FI_ Slice_MipsCode ac_pad_store_inverted_buttons(AtomBuilder_R ab, U1 r_buttons,
*/
enum {
R_PadRaw = R_T0 atom_reg atom_type(U1),
R_PadState = R_T1 atom_reg atom_type(PadState*),
R_PadState = R_T1 atom_reg,
R_RawStatus = R_T2 atom_reg,
R_RawId = R_T3 atom_reg,
};
@@ -72,8 +44,8 @@ typedef Struct_(Binds_PadBiosSnapshot) {
PadState* state;
};
internal MipsAtom_(pad_bios_snapshot) atom_info(atom_bind(Binds_PadBiosSnapshot)
, atom_reads( R_PadRaw, R_PadState, R_RawStatus, R_RawId)
, atom_writes(R_PadRaw, R_PadState, R_RawStatus, R_RawId)
, atom_reads( R_PadRaw, R_PadState, R_RawStatus, R_RawId, R_T4, R_T5, R_TapePtr)
, atom_writes(R_PadRaw, R_PadState, R_RawStatus, R_RawId, R_T4, R_T5, R_TapePtr)
) {
/* === Bind consumption: T0 = raw, T1 = state, advance R_TapePtr by 8. */
load_word(R_PadRaw, R_TapePtr, O_(Binds_PadBiosSnapshot,raw)),
@@ -81,97 +53,111 @@ internal MipsAtom_(pad_bios_snapshot) atom_info(atom_bind(Binds_PadBiosSnapshot)
add_ui_self( R_TapePtr, S_(Binds_PadBiosSnapshot)),
/* === Read raw[0] (status) + raw[1] (id) */
load_byte_u(R_RawStatus, R_PadRaw, O_(PadBiosRaw,status)),
load_byte_u(R_RawId, R_PadRaw, O_(PadBiosRaw,id)),
load_byte_u(R_RawStatus, R_PadRaw, 0),
load_byte_u(R_RawId, R_PadRaw, 1),
atom_label(snap_root) /* === Case 1: Disconnected (status == 0xFF). */
add_ui(R_T4, R_0, PadRawStatus_Timeout), branch_ne(R_RawStatus, R_T4, atom_offset(snap_root, skip_disconnected)),
add_ui(R_T4, R_0, 0xFF), branch_ne(R_RawStatus, R_T4, atom_offset(snap_root, skip_disconnected)),
/* BD-slot: pre-compute PadStatus_Disconnected. Branch reads R_T4=0xFF in EX before this WB completes.
* If branch NOT taken (fall through to pending/id_dispatch), R_T4 is overwritten by the next case body's add_ui — harmless. */
atom_label(disconnected) /* === Disconnected body. */
mac_pad_set_status(R_T4, R_PadState, PadStatus_Disconnected),
store_half( R_0, R_PadState, O_(PadState,buttons)),
mac_pad_set_centered_axes(R_PadState, R_T4),
mac_pad_set_id_byte(R_PadState, R_RawId, PadRawStatus_Timeout),
/* R_T4 = PadStatus_Disconnected from snap_root BD-slot. */
store_word(R_T4, R_PadState, O_(PadState,status)),
store_half(R_0, R_PadState, O_(PadState,buttons)),
/* axes = 0x80808080 (centered) — single sw writes the 4-byte axes block at offset 8 (left_x, left_y, right_x, right_y). */
load_upper_i(R_T4, 0x8080), or_i_self(R_T4, 0x8080),
store_word( R_T4, R_PadState, O_(PadState,left_x)),
store_byte( R_RawId, R_PadState, O_(PadState,id)),
jump_rel(atom_offset(disconnected, snap_end)),
/* BD-slot: load next atom's entry point (replaces the nop).
* Always jumps to snap_end, where mac_yield_tail() transfers control to R_AtomJmp without re-loading it. */
* The unconditional branch always jumps to snap_end, where mac_yield_tail()
* transfers control to R_AtomJmp without re-loading it. */
mac_yield_load(),
atom_label(skip_disconnected)
/* === Case 2: Pending (status == 0 && id == 0)
* Combined check: if (status | id) != 0 then skip to id_dispatch. Falls through to the Pending case only when both are zero. */
* Combined check: if (status | id) != 0 then skip to id_dispatch.
* Falls through to the Pending case only when both are zero. */
or_u_self(R_RawStatus, R_RawId), branch_ne(R_RawStatus, R_0, atom_offset(case_2, id_dispatch)),
/* BD-slot: pre-compute PadStatus_Pending. Branch reads R_RawStatus in EX before this WB completes.
* If branch NOT taken (fall through to id_dispatch), R_T4 is overwritten by the digital/analog body add_ui - harmless. */
* If branch NOT taken (fall through to id_dispatch), R_T4 is overwritten by the digital/analog body add_ui harmless. */
atom_label(pending) /* === Pending body (status=0, id=0 — pre-IRQ-empty buffer). */
mac_pad_set_status(R_T4, R_PadState, PadStatus_Pending),
store_half( R_0, R_PadState, O_(PadState,buttons)),
mac_pad_set_centered_axes(R_PadState, R_T4),
store_byte(R_RawId, R_PadState, O_(PadState,id)),
atom_label(pending) /* === Pending body */
/* R_T4 = PadStatus_Pending from case_2 BD-slot. */
store_word(R_T4, R_PadState, O_(PadState,status)),
store_half(R_0, R_PadState, O_(PadState,buttons)),
/* axes = 0x80808080 (centered) — single sw writes the 4-byte axes block at offset 8 (left_x, left_y, right_x, right_y). */
load_upper_i(R_T4, 0x8080), or_i_self(R_T4, 0x8080),
store_word( R_T4, R_PadState, O_(PadState,left_x)),
store_byte( R_RawId, R_PadState, O_(PadState,id)),
jump_rel(atom_offset(pending, snap_end)),
mac_yield_load(),
atom_label(id_dispatch) /* === Case 3-6: ID dispatch */
add_ui(R_T4, R_0, PadRawId_Digital), branch_ne(R_RawId, R_T4, atom_offset(id_dispatch, try_analog_stick)),
add_ui(R_T4, R_0, 0x41), branch_ne(R_RawId, R_T4, atom_offset(id_dispatch, try_analog_stick)),
/* BD-slot: pre-compute PadStatus_Digital. Branch reads R_RawId in EX before this WB completes.
* If branch NOT taken (fall through to try_analog_stick), R_T4 is overwritten by the analog body add_ui. */
/* === Digital body (status, buttons normalize, axes=0x80, id, branch.
* R_T5 holds the 0x80808080 axes constant (loaded into the load-delay slot of the buttons-load).
* R_T5 is then "dead" — only consumed at the analog_pad range check downstream. */
mac_pad_set_status(R_T4, R_PadState, PadStatus_Digital),
load_half_u( R_T4, R_PadRaw, O_(PadBiosRaw, buttons)), /* R_T4 = raw_buttons; */
load_upper_i(R_T5, PadAxis_Centered_Hi), or_i_self(R_T5, PadAxis_Centered_Lo), /* fills the buttons-load's delay slot (doesn't read R_T4) */
mac_pad_store_inverted_buttons(R_T4, R_PadState), /* R_T4 settled: nor + sh writes ~raw_buttons to state.buttons */
store_word(R_T5, R_PadState, O_(PadState, axes)), /* single sw writes the 4-byte axes block at offset 8 (left_x, left_y, right_x, right_y) */
mac_pad_set_id_byte(R_PadState, R_T4, PadRawId_Digital),
/* === Digital body (status, buttons normalize, axes=0x80, id, branch. */
/* R_T4 = PadStatus_Digital from id_dispatch BD-slot. */
store_word( R_T4, R_PadState, O_(PadState,status)),
load_half_u(R_T4, R_PadRaw, 2 * S_(U1)),
/* Fill R_T4's load-delay slot with the 0x80808080 axes constant into R_T5
* (R_T5 is dead on this path; it's only consumed at the analog_pad range check). */
load_upper_i(R_T5, 0x8080), or_i_self(R_T5, 0x8080),
nor_u( R_T4, R_T4, R_0), /* raw_buttons is already in host bit order; no swap needed */
store_half( R_T4, R_PadState, O_(PadState,buttons)),
/* axes = 0x80808080 (centered) — single sw writes the 4-byte axes block at offset 8 (left_x, left_y, right_x, right_y). */
store_word( R_T5, R_PadState, O_(PadState,left_x)),
add_ui( R_T4, R_0, 0x41),
store_byte( R_T4, R_PadState, O_(PadState,id)),
jump_rel(atom_offset(id_dispatch, snap_end)),
mac_yield_load(),
atom_label(try_analog_stick) /* === Case 4: AnalogStick (id == 0x53)*/
add_ui(R_T4, R_0, PadRawId_AnalogStick), branch_ne(R_RawId, R_T4, atom_offset(try_analog_stick, try_analog_pad)),
add_ui(R_T4, R_0, 0x53), branch_ne(R_RawId, R_T4, atom_offset(try_analog_stick, try_analog_pad)),
/* BD-slot: pre-compute PadStatus_AnalogStick. Branch reads R_RawId in EX before this WB completes.
* If branch NOT taken (fall through to try_analog_pad), R_T4 is overwritten by the analog_pad body add_ui. */
atom_label(analog_stick) /* === AnalogStick body
* R_T5 holds left_xy (loaded into the load-delay slot of the buttons-load via the left-axis load_half_u).
* R_T4 holds right_xy (loaded into the load-delay slot of the left-load).
* R_T5 is then "dead" — reused for the id-byte value load in mac_pad_write_id_byte.
* The buttons invert+store happens BEFORE R_T4 is overwritten by the right_xy load. */
mac_pad_set_status(R_T4, R_PadState, PadStatus_AnalogStick),
load_half_u( R_T4, R_PadRaw, O_(PadBiosRaw,buttons)), /* R_T4 = raw_buttons; delay slot at the next instruction */
load_half_u( R_T5, R_PadRaw, O_(PadBiosRaw,left)), /* fills the buttons-load's delay slot (doesn't read R_T4) */
mac_pad_store_inverted_buttons(R_T4, R_PadState), /* R_T4 settled: nor + sh writes ~raw_buttons to state.buttons */
load_half_u( R_T4, R_PadRaw, O_(PadBiosRaw,right)), /* fills R_T5's load-delay slot (doesn't read R_T5); overwrites R_T4 (was buttons) with right_xy */
store_half( R_T5, R_PadState, O_(PadState, left)),
store_half( R_T4, R_PadState, O_(PadState, right)),
mac_pad_set_id_byte(R_PadState, R_T5, PadRawId_AnalogStick),
* Axes are loaded as two halfwords: raw[6..7] → left_xy (sh at offset 8), raw[4..5] → right_xy (sh at offset 10).
* R_T5 holds left_xy / id-value in turn (it's dead on this path — only consumed at the analog_pad range check). */
/* R_T4 = PadStatus_AnalogStick from try_analog_stick BD-slot. */
store_word( R_T4, R_PadState, O_(PadState,status)),
load_half_u( R_T4, R_PadRaw, 2 * S_(U1)), /* R_T4 = raw_buttons */
load_half_u( R_T5, R_PadRaw, 6 * S_(U1)), /* R_T5 = left_xy; fills R_T4's load-delay slot (doesn't read R_T4) */
nor_u( R_T4, R_T4, R_0), /* R_T4 = ~raw_buttons */
store_half( R_T4, R_PadState, O_(PadState,buttons)),
load_half_u( R_T4, R_PadRaw, 4 * S_(U1)), /* R_T4 = right_xy; fills R_T5's load-delay slot */
store_half( R_T5, R_PadState, O_(PadState,left_x)), /* R_T5 settled, store left_xy */
store_half( R_T4, R_PadState, O_(PadState,right_x)),
add_ui( R_T5, R_0, 0x53), /* R_T5 = id value (clobbers left_xy, already stored) */
store_byte( R_T5, R_PadState, O_(PadState,id)),
jump_rel(atom_offset(analog_stick, snap_end)),
mac_yield_load(),
atom_label(try_analog_pad) /* === Case 5-6: AnalogPad (id & 0xF0 == 0x70) */
and_i( R_T4, R_RawId, PadRawId_AnalogPadMask),
add_ui( R_T5, R_0, PadRawId_AnalogPadValue),
and_i( R_T4, R_RawId, 0xF0),
add_ui( R_T5, R_0, 0x70),
branch_ne(R_T4, R_T5, atom_offset(try_analog_pad, try_unsupported)),
/* BD-slot: pre-compute PadStatus_AnalogPad. Branch reads R_T4 in EX before this WB completes.
* If branch NOT taken (fall through to try_unsupported), R_T4 is overwritten by the unsupported body add_ui. */
atom_label(analog_pad) /* === AnalogPad body
* Same shape as AnalogStick with AnalogPad status. R_T5 holds left_xy (it's dead on this path).
* The id byte is raw id from the BIOS buffer (R_RawId already holds raw[1]).
* Buttons invert + store happens before R_T4 is overwritten by the right_xy load. */
mac_pad_set_status(R_T4, R_PadState, PadStatus_AnalogPad),
load_half_u( R_T4, R_PadRaw, O_(PadBiosRaw,buttons)), /* R_T4 = raw_buttons; delay slot at the next instruction */
load_half_u( R_T5, R_PadRaw, O_(PadBiosRaw,left)), /* fills the buttons-load's delay slot (doesn't read R_T4) */
mac_pad_store_inverted_buttons(R_T4, R_PadState), /* R_T4 settled: nor + sh writes ~raw_buttons to state.buttons */
load_half_u(R_T4, R_PadRaw, O_(PadBiosRaw,right)), /* fills R_T5's load-delay slot (doesn't read R_T5); overwrites R_T4 with right_xy */
store_half( R_T5, R_PadState, O_(PadState, left)),
store_half( R_T4, R_PadState, O_(PadState, right)),
store_byte( R_RawId, R_PadState, O_(PadState, id)),
* Same shape as AnalogStick with AnalogPad status. R_T5 holds left_xy (it's dead on this path). */
/* R_T4 = PadStatus_AnalogPad from try_analog_pad BD-slot. */
store_word( R_T4, R_PadState, O_(PadState,status)),
load_half_u(R_T4, R_PadRaw, 2 * S_(U1)), /* R_T4 = raw_buttons */
load_half_u(R_T5, R_PadRaw, 6 * S_(U1)), /* R_T5 = left_xy; fills R_T4's load-delay slot */
nor_u( R_T4, R_T4, R_0), /* R_T4 = ~raw_buttons */
store_half( R_T4, R_PadState, O_(PadState,buttons)),
load_half_u(R_T4, R_PadRaw, 4 * S_(U1)), /* R_T4 = right_xy; fills R_T5's load-delay slot */
store_half( R_T5, R_PadState, O_(PadState,left_x)), /* R_T5 settled, store left_xy */
store_half( R_T4, R_PadState, O_(PadState,right_x)),
store_byte( R_RawId, R_PadState, O_(PadState,id)),
jump_rel(atom_offset(analog_pad, snap_end)),
mac_yield_load(),
@@ -180,8 +166,11 @@ atom_label(try_unsupported) /* === Case 7: Unsupported — fall through from the
add_ui( R_T4, R_0, PadStatus_Unsupported),
store_word(R_T4, R_PadState, O_(PadState,status)),
store_half(R_0, R_PadState, O_(PadState,buttons)),
mac_pad_set_centered_axes(R_PadState, R_T4),
mac_pad_set_id_byte(R_PadState, R_RawId, PadUnknownId_Sentinel),
/* axes = 0x80808080 (centered) — single sw writes the 4-byte axes block at offset 8 (left_x, left_y, right_x, right_y). */
load_upper_i(R_T4, 0x8080), or_i_self(R_T4, 0x8080),
store_word( R_T4, R_PadState, O_(PadState,left_x)),
add_ui( R_T4, R_0, 0xFF), /* 0xFF sentinel: "unknown id" */
store_byte( R_T4, R_PadState, O_(PadState,id)),
/* Fall through to snap_end. */
atom_label(no_jump_fallthrough)
-78
View File
@@ -1,78 +0,0 @@
#ifdef INTELLISENSE_DIRECTIVES
# include "dsl.h"
# include "gcc_asm.h"
# include "mips.h"
# include "bios.h"
# include "pad.h"
#endif
/* Uses ONE 8-byte frame allocated via the compiler's standard prologue.
* 4 wasted-arg words for B(12h) InitPAD2 are at [SP+0..15] but are not explicitly allocated.
* Compiler handles the MIPS O32 "wasted stack" convention for us by treating the B-call as a 4-arg call.
*
* The buffer pointers are passed as arguments so the compiler keeps them in callee-saved registers;
* The B(12h) asm volatile block does NOT clobber those registers (it clobbers only the volatile GPRs + B-table arg registers explicitly).
* The C-level writes after the call re-load the pointers from their callee-saved homes.
*
* The clobber list for both B-calls names the full BIOS destroy set documented in kernelbios.md:167-174 (R1..R15, R24..R25, R31, HI/LO).
* The kernel-ABI "volatile GPRs" subset is clb_mem_drain; the rest of the destroy set is enumerated explicitly here. */
NI_ void pad_bios_init_start(PadBiosRaw* raw0, PadBiosRaw* raw1)
{
/* Pin raw0 + raw1 to $a0 + $a1 via rgcc; the B(12h) call uses these directly.
* The `(void)` casts mark them as unread after the call so the compiler doesn't need to move them back. */
register PadBiosRaw* p0 rgcc(R_A0) = raw0;
register PadBiosRaw* p1 rgcc(R_A1) = raw1;
(void)p0; (void)p1;
// TODO(Ed): Properly annotate the raw values in the inline asm instructions.
// Use enums.
/* B(12h) InitPAD2(raw0, 0x22, raw1, 0x22)
* $a0 = raw0 (rgcc-bound; survives the sequence below)
* $a1 = raw1 (preserved into $a2 before $a1 is overwritten)
* $a2 = raw1 (moved from $a1; survives $a1's overwrite)
* $a3 = 0x22 (immediate)
* $t1 = 0x12 (function number)
* $t2 = 0xB0 (BIOS B-table address) */
asm volatile(
asm_words(
or_u( rarg_2, rarg_1, rdiscard), /* $a2 = $a1 = raw1 */
add_ui( rarg_1, rdiscard, bios_pad_buffer_size), /* $a1 = 0x22 */
add_ui( rarg_3, rdiscard, bios_pad_buffer_size), /* $a3 = 0x22 */
add_ui( rtmp_1, rdiscard, bios_init_pad_2), /* $t1 = 0x12 */
add_ui( rtmp_2, rdiscard, bios_btable_addr), /* $t2 = 0xB0 */
call_reg(rtmp_2), /* jalr $t2, $ra */
nop /* BD slot */
)
asm_rpins, r_use(p0), r_use(p1)
asm_clobber:
rlit(R_AT),
rlit(R_V0), rlit(R_V1),
rlit(R_T0), rlit(R_T1), rlit(R_T2), rlit(R_T3), rlit(R_T4),
rlit(R_T5), rlit(R_T6), rlit(R_T7), rlit(R_T8), rlit(R_T9),
rlit(R_RA),
clb_mem_drain
);
/* The C-level writes re-load the pointers via the parameter names and write 0xFF to each
* buffer's status byte to mark the initial-state hazard documented in kernelbios.md:1621-1624. */
u1_v(raw0)[0] = 0xFF;
u1_v(raw1)[0] = 0xFF;
/* B(13h) StartPAD2() — no args. The BIOS preserves $sp. */
asm volatile(
asm_words(
add_ui( rtmp_1, rdiscard, bios_start_pad_2), /* $t1 = 0x13 */
add_ui( rtmp_2, rdiscard, bios_btable_addr), /* $t2 = 0xB0 (re-load) */
call_reg(rtmp_2), /* jalr $t2, $ra */
nop /* BD slot */
)
asm_clobber:
rlit(R_AT),
rlit(R_V0), rlit(R_V1),
rlit(R_T0), rlit(R_T1), rlit(R_T2), rlit(R_T3), rlit(R_T4),
rlit(R_T5), rlit(R_T6), rlit(R_T7), rlit(R_T8), rlit(R_T9),
rlit(R_RA),
clb_mem_drain
);
}
+36 -78
View File
@@ -1,30 +1,28 @@
#ifdef INTELLISENSE_DIRECTIVES
# pragma once
# include "dsl.h"
# include "math.h"
#endif
/* PSX button bit positions — 1:1 with PSX-SPX docs at docs/psx-spx/docs/controllersandmemorycards.md:405-421.
* Wire is active-low (0 = pressed).
* The decoder atom computes buttons = (~raw_buttons) & 0xFFFF;
* active-low-to-active-high inversion is applied bit-by-bit. */
typedef Enum_(U2, PadBtns) {
Bit_(Pad_Select, 0),
Bit_(Pad_L3, 1),
Bit_(Pad_R3, 2),
Bit_(Pad_Start, 3),
Bit_(Pad_Up, 4),
Bit_(Pad_Right, 5),
Bit_(Pad_Down, 6),
Bit_(Pad_Left, 7),
Bit_(Pad_L2, 8),
Bit_(Pad_R2, 9),
Bit_(Pad_L1, 10),
Bit_(Pad_R1, 11),
* The decoder atom computes buttons = (~raw_buttons) & 0xFFFF; the active-low-to-active-high inversion is applied bit-by-bit. */
enum {
Bit_(Pad_Select, 0),
Bit_(Pad_L3, 1),
Bit_(Pad_R3, 2),
Bit_(Pad_Start, 3),
Bit_(Pad_Up, 4),
Bit_(Pad_Right, 5),
Bit_(Pad_Down, 6),
Bit_(Pad_Left, 7),
Bit_(Pad_L2, 8),
Bit_(Pad_R2, 9),
Bit_(Pad_L1, 10),
Bit_(Pad_R1, 11),
Bit_(Pad_Triangle, 12),
Bit_(Pad_Circle, 13),
Bit_(Pad_Cross, 14),
Bit_(Pad_Square, 15),
Bit_(Pad_Circle, 13),
Bit_(Pad_Cross, 14),
Bit_(Pad_Square, 15),
};
enum {
@@ -34,22 +32,18 @@ enum {
Pad1 = 1 << PadId_Offset,
};
/* =============================================================================
#define pad0_(btn_id) (btn_id << Pad0)
#define pad1_(btn_id) (btn_id << Pad1)
/* ============================================================
* BIOS pad-buffer subsystem: docs/psx-spx/docs/kernelbios.md (B(12h) + B(13h))
* ============================================================================= */
* ============================================================ */
enum {
PAD_BIOS_RAW_SIZE = 0x22,
};
// BIOS pad buffer layout (docs/psx-spx/docs/kernelbios.md (InitPAD2 returns 0x22 = 34 bytes per port)).
// Bytes 0..7 are the named snapshot region; bytes 8..33 are reserved (the BIOS writes the buffer raw; we only read bytes 0..7 via O_(PadBiosRaw, ...)).
typedef Struct_(PadBiosRaw) {
U1 status; /* offset 0 (PadRawStatus_Ok / PadRawStatus_Timeout) */
U1 id; /* offset 1 (PadRawId_Digital / PadRawId_AnalogStick / 0x7x AnalogPad) */
U2 buttons; /* offset 2-3 (active-low 16-bit button map) */
V2_U1 right; /* offset 4-5 (right stick x, y) */
V2_U1 left; /* offset 6-7 (left stick x, y) */
U1 reserved[PAD_BIOS_RAW_SIZE - 8]; /* offset 8..33 */
U1 bytes[PAD_BIOS_RAW_SIZE];
};
typedef Enum_(U4, PadStatus) {
@@ -62,54 +56,18 @@ typedef Enum_(U4, PadStatus) {
PadStatus_Invalid,
};
/* Distinct from the game-facing PadStatus enum: PadRawStatus_Ok and PadRawStatus_Timeout are raw BIOS values;
* PadStatus_* are game-facing post-decode states. PadUnknownId_Sentinel is written by the decoder
* when the controller id does not match any known controller type.
* PadAxisCentered_Word: Four-byte 0x80 pattern used to clear / center
* four byte axes at PadState.left_x through PadState.right_y. */
typedef Enum_(U1, PadRawStatus) {
PadRawStatus_Ok = 0x00,
PadRawStatus_Timeout = 0xFF,
};
typedef Enum_(U1, PadRawId) {
PadRawId_Digital = 0x41,
PadRawId_AnalogStick = 0x53,
PadRawId_AnalogPadMask = 0xF0,
PadRawId_AnalogPadValue = 0x70,
};
typedef Enum_(U1, PadUnknownId) {
PadUnknownId_Sentinel = 0xFF,
};
typedef Enum_(U4, PadAxisCentered) {
PadAxis_Centered_Hi = 0x8080,
PadAxis_Centered_Lo = 0x8080,
PadAxis_Centered_Word = 0x80808080U,
};
typedef Enum_(U1, PadDeadZone) {
PadDeadZone_LowBound = 0x70, /* left_x < LowBound → active; delta = 0x80 - left_x > 0 (rightward pull) */
PadDeadZone_Center = 0x80, /* analog rest position; left_x == Center → delta = 0 (no rotation) */
PadDeadZone_HighBound = 0x90, /* left_x > HighBound → active; delta = 0x80 - left_x < 0 (leftward pull) */
};
typedef Struct_(PadAxes) {
V2_U1 left; /* offset 8-9 */
V2_U1 right; /* offset 10-11 */
};
// Field order is chosen so that the 4 axes (left_x, left_y, right_x, right_y)
// form a contiguous 4-byte block at offset 8, allowing a single `store_word` to clear-or-write all 4 axes in one MIPS instruction.
/* PadState — per-port normalized runtime state.
* Field order is chosen so that the 4 axes (left_x, left_y, right_x, right_y)
* form a contiguous 4-byte block at offset 8, allowing a single `store_word` to clear-or-write all 4 axes in one MIPS instruction.
* The struct size stays 12 bytes (unchanged from the prior order,
* which left the C compiler to insert 1 byte of trailing pad to reach the 4-byte struct alignment). */
typedef Struct_(PadState) {
PadStatus status; /* offset 0, (U4) */
PadBtns buttons; /* offset 4, */
U1 id; /* offset 6, */
byte_pad(1); /* offset 7, explicit pad to align the axes block */
union {
A2_V2_U1 axes; /* offset 8-11 store_target (4-byte aligned)*/
struct {
V2_U1 left; /* offset 8-9 */
V2_U1 right; /* offset 10-11 */
};
};
PadStatus status; /* offset 0, size 4 (U4) */
U2 buttons; /* offset 4, size 2 */
U1 id; /* offset 6, size 1 */
U1 pad; /* offset 7, size 1 — explicit pad to align the axes block */
U1 left_x; /* offset 8, size 1 — store_word target (4-byte aligned) */
U1 left_y; /* offset 9, size 1 */
U1 right_x; /* offset 10, size 1 */
U1 right_y; /* offset 11, size 1 */
};
internal void pad_bios_init_start(PadBiosRaw* raw0, PadBiosRaw* raw1);
+5 -23
View File
@@ -64,9 +64,9 @@ typedef Struct_(Tile) {
Linear Algebra
*/
MT3_S2S4* mt3s2s4_rotation (V3_S2* vec, MT3_S2S4* mat) asm("RotMatrix");
MT3_S2S4* mt3s2s4_translation(MT3_S2S4* mat, V3_S4* vec) asm("TransMatrix");
MT3_S2S4* mt3s2s4_scale (MT3_S2S4* mat, V3_S4* vec) asm("ScaleMatrix");
M3_S2* m3s2_rotation (V3_S2* vec, M3_S2* mat) asm("RotMatrix");
M3_S2* m3s2_translation(M3_S2* mat, V3_S4* vec) asm("TransMatrix");
M3_S2* m3s2_scale (M3_S2* mat, V3_S4* vec) asm("ScaleMatrix");
// Rotation, Translation, Perspective
@@ -99,23 +99,5 @@ FI_ S4 rtp_avg_nclip_a4_v3s2(
);
}
void gte_matrix_set_rotation (MT3_S2S4* mat) asm("SetRotMatrix");
void gte_matrix_set_translation(MT3_S2S4* mat) asm("SetTransMatrix");
// Einheit, Metrication to unit vector. "Normalization", not Orthogonal "Normal, Normalis". Directionalization.
// RGA(Lengyel): Normalize the bulk of a zero-weight direction. This is not finite-point unitization (which forces w=1).
S4 normalize_v3s4(V3_S4* v0, V3_S4* v1) asm("VectorNormal");
// RGA(Lengyel): Apply the matrix expansion of a rigid transformation.
// Motor antiproduct is equivalent for unitized points; LA form is what GTE consumes.
V3_S4* mul_m3s2_v3s4(MT3_S2S4* m, V3_S4* v, V3_S4* result) asm("ApplyMatrixLV");
// RGA(Lengyel): Store the full translation column. The motor translator would store half this displacement in m.xyz.
MT3_S2S4* trans_m3s2(MT3_S2S4* m, V3_S4* off) asm("TransMatrix");
MT3_S2S4* gte_comp_coord_m3s2(MT3_S2S4* m0, MT3_S2S4* m1, MT3_S2S4* result) asm("CompMatrixLV");
// RGA(Lengyel): Complement(Wedge(a,b)), i.e. the Euclidean 3D complement of the exterior product, stored as a V3_S4.
// The underlying GTE OP is a specialized signed-16-bit D x IR command; the wedge interpretation is a 3D dual of the same 3 scalars.
void cross_v3s4(V3_S4* v0, V3_S4* v1, V3_S4* result) asm("OuterProduct12");
void gte_matrix_set_rotation (M3_S2* mat) asm("SetRotMatrix");
void gte_matrix_set_translation(M3_S2* mat) asm("SetTransMatrix");
-11
View File
@@ -15,8 +15,6 @@
#define WORD_COUNT(name, count) enum { words_##name = (count) };
WORD_COUNT(nop, 1)
WORD_COUNT(atom_label, 0)
WORD_COUNT(atom_offset, 0)
WORD_COUNT(load_upper_i, 1)
WORD_COUNT(jump_reg, 1)
WORD_COUNT(jump_link, 1)
@@ -56,15 +54,6 @@ 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)
-13
View File
@@ -1,13 +0,0 @@
#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_<Sym>_Code = <chosen GPR's _Code constant> for every marker in this directory.
#define R_GpTmp_Code R_V0_Code
+1 -19
View File
@@ -8,7 +8,7 @@
#pragma region hello_camera
// --- atom: pad_input_cube_rotation (60 words) ---
// --- atom: pad_apply_input (60 words) ---
#define _atom_offset_dpad_left_exit_dpad_left 6
#define _atom_offset_dpad_right_exit_dpad_right 6
@@ -26,24 +26,6 @@ enum {
atom_offset_end_low_exit_stick = _atom_offset_end_low_exit_stick,
};
// --- atom: pad_input_cam (40 words) ---
#define _atom_offset_left_x_exit_left_x 3
#define _atom_offset_right_x_exit_right_x 3
#define _atom_offset_up_y_exit_up_y 3
#define _atom_offset_down_y_exit_down_y 3
#define _atom_offset_cross_z_exit_cross_z 3
#define _atom_offset_circle_z_exit_circle_z 3
enum {
atom_offset_left_x_exit_left_x = _atom_offset_left_x_exit_left_x,
atom_offset_right_x_exit_right_x = _atom_offset_right_x_exit_right_x,
atom_offset_up_y_exit_up_y = _atom_offset_up_y_exit_up_y,
atom_offset_down_y_exit_down_y = _atom_offset_down_y_exit_down_y,
atom_offset_cross_z_exit_cross_z = _atom_offset_cross_z_exit_cross_z,
atom_offset_circle_z_exit_circle_z = _atom_offset_circle_z_exit_circle_z,
};
// --- atom: cube_g4_face (76 words) ---
#define _atom_offset_cull_cube_g4_face_exit 41
+77 -526
View File
@@ -17,7 +17,6 @@
# include "duffle/psyq.atom.c"
# include "gen/offsets.h"
# include "gen/macs.h"
# include "gen/auto_reg.h"
# include "hello_camera.h"
#endif
@@ -25,8 +24,8 @@ ATOM_FILE_DEBUGGER_LINE_MARKER(hello_joypad_atom_c);
#pragma region MACs (Mips Atom components)
FI_ Slice_MipsCode ac_put_disp_env(AtomBuilder_R ab, U4 reg_transfer, U4 reg_base, U2 port)
MipsAtomComp_Proc_(ac_put_disp_env, ab, {
FI_ Slice_MipsCode ac_put_disp_env(U4 reg_transfer, U4 reg_base, U2 port)
MipsAtomComp_Proc_(ac_put_disp_env, {
// Emits 5 GP0 commands for buffer 0 (display_area = (0,0,320,240)).
// Sequence per libpsyx PutDispEnv: DrawArea TL → DrawArea BR → Mask → DrawArea TL → DrawArea BR
mac_gcmd_push(gp0_word_draw_area_top_left_origin, reg_transfer, reg_base, port),
@@ -36,8 +35,8 @@ MipsAtomComp_Proc_(ac_put_disp_env, ab, {
mac_gcmd_push(gp0_word_draw_area_bottom_right_320x240, reg_transfer, reg_base, port),
})
FI_ Slice_MipsCode ac_put_draw_env(AtomBuilder_R ab, U4 reg_transfer, U4 reg_base, U2 port)
MipsAtomComp_Proc_(ac_put_draw_env, ab, {
FI_ Slice_MipsCode ac_put_draw_env(U4 reg_transfer, U4 reg_base, U2 port)
MipsAtomComp_Proc_(ac_put_draw_env, {
/*
* ORIGIN: each code word corresponds to the EXACT value libpsyx's PutDrawEnv function would compute for the same DrawEnv settings.
* References:
@@ -51,18 +50,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) */
@@ -91,404 +90,6 @@ MipsAtomComp_Proc_(ac_put_draw_env, ab, {
#pragma endregion MACs
#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). */
R_ResolveScratch = R_T4 atom_reg atom_type(U4*),
};
typedef Struct_(Binds_ResolveLookAt) {
MT3_S2S4* look_at;
P3_S4* eye;
P3_S4* target;
V3_S4* up_in;
};
/* ─── ResolveLookAtScratch — offset schema for the resolve_look_at bundle's
* scratchpad slots (PS1 hardware scratchpad at 0x1F800000).
*
* 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.
*
* Slot producers/consumers (referenced by the resolve_look_at chain atoms):
* +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.
* Use P3_S4 when the value is a point.") — both are 16 bytes.
*/
typedef Struct_(ResolveLookAtScratch) {
V3_S4 fwd; /* offset +0 (16 bytes — 4 S4 fields incl. internal pad) */
V3_S4 uz; /* offset +16 (16 bytes) */
V3_S4 right; /* offset +32 (16 bytes) */
V3_S4 ux; /* offset +48 (16 bytes) */
V3_S4 up; /* offset +64 (16 bytes) */
V3_S4 uy; /* offset +80 (16 bytes) */
P3_S4 eye; /* offset +96 (16 bytes; storage alias of V3_S4) */
P3_S4 target; /* offset +112 (16 bytes; storage alias of V3_S4) */
V3_S4 up_in; /* offset +128 (16 bytes) */
};
/* ─── resolve_look_at bundle chain atoms ────────────────────────────
* 4 unique atom procs in the resolve_look_at bundle (4 chain atoms + 3 calls to generic normalize_v3s4_proc).
* All 4 chain atoms 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:
* 0: resolve_look_at__input_and_sub (chain atom)
* 1: normalize_v3s4_proc (gte.atom.c) (generic normalize; called for fwd→uz)
* 2: resolve_look_at__cross_uz_up_in_to_right (chain atom)
* 3: normalize_v3s4_proc (gte.atom.c) (generic normalize; called for right→ux)
* 4: resolve_look_at__cross_uz_ux_to_up (chain atom)
* 5: normalize_v3s4_proc (gte.atom.c) (generic normalize; called for up→uy)
* 6: resolve_look_at__populate_and_translate (chain atom)
*
* The generic normalize_v3s4_proc is a parameterized 4-stage GTE normalize (SQR → mfc2 → LZCS → GPF → srav);
* it accepts scratch base + offset args so any caller (with a scratch base + struct schema) can use it.
*/
typedef Struct_(Binds_ResolveLookAtSub) {
U4 target; /* U4 (C-side P3_S4* — read by atom 0 directly; NOT a scratchpad address) */
U4 eye; /* U4 (C-side P3_S4* — read by atom 0 directly; staged into scratchpad by atom 0) */
U4 up_in; /* U4 (C-side V3_S4* — read by atom 0 directly; staged into scratchpad by atom 0) */
U4 scratchpad;
};
/* 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)
* r_scratch : R_ResolveScratch (R_T4) — scratch base, read by atoms 1-6
*
* Bind-pop layout:
* Binds_ResolveLookAtSub
* Staging work:
* * Stage eye.x/y/z → scratch (for atom 6's translation column)
* * Stage up_in.x/y/z → scratch (for atom 2's outer-product operand)
* * Compute fwd = target - eye, store fwd.x/y/z → scratch+0/+4/+8 (for atom 1)
*
* GPR codes (assigned by resolve_look_at_init):
* r_target_ptr : R_T0
* r_eye_ptr : R_T1
* r_up_in_ptr : R_T2
* r_scratch : R_T4 (R_ResolveScratch; wave-context carrier)
* r_tmp0 : R_T3 (stage eye/up_in + load eye.y)
* r_tmp1 : R_T5 (stage eye/up_in + load eye.z)
* r_tmp2 : R_T6 (stage eye/up_in + load target.x)
* r_tmp3 : R_T7 (stage eye/up_in + load target.y)
* R_AT : hardcoded (load eye.y / eye.z / target.z)
* R_V0 : hardcoded (load eye.z / target.z)
*
* Pool cost: 8 GPRs + R_T4 (carrier) + R_AT + R_V0 (hardcoded) = 11 GPRs.
*/
I_ MipsAtom* resolve_look_at__input_and_sub_proc(AtomArena_R aa, 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, aa, {
/* 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)),
load_word(r_scratch, R_TapePtr, O_(Binds_ResolveLookAtSub,scratchpad)),
add_ui_self( R_TapePtr, S_(Binds_ResolveLookAtSub)),
/* 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,*). */
load_word(r_tmp0, r_eye_ptr, O_(P3_S4,x)),
load_word(r_tmp1, r_eye_ptr, O_(P3_S4,y)),
load_word(r_tmp2, r_eye_ptr, O_(P3_S4,z)),
nop, /* load-delay */
store_word(r_tmp0, r_scratch, O_(ResolveLookAtScratch,eye.x)),
store_word(r_tmp1, r_scratch, O_(ResolveLookAtScratch,eye.y)),
store_word(r_tmp2, r_scratch, O_(ResolveLookAtScratch,eye.z)),
/* Stage up_in.x/y/z into the scratchpad (atom 2 reads these for the outer
* product with uz). Reuse r_tmp0/r_tmp1/r_tmp2. */
load_word(r_tmp0, r_up_in_ptr, O_(V3_S4,x)),
load_word(r_tmp1, r_up_in_ptr, O_(V3_S4,y)),
load_word(r_tmp2, r_up_in_ptr, O_(V3_S4,z)),
nop, /* load-delay */
store_word(r_tmp0, r_scratch, O_(ResolveLookAtScratch,up_in.x)),
store_word(r_tmp1, r_scratch, O_(ResolveLookAtScratch,up_in.y)),
store_word(r_tmp2, r_scratch, O_(ResolveLookAtScratch,up_in.z)),
/* Compute fwd = target - eye. */
load_word(r_tmp0, r_target_ptr, O_(P3_S4,x)),
load_word(r_tmp1, r_target_ptr, O_(P3_S4,y)),
load_word(r_tmp2, r_target_ptr, O_(P3_S4,z)),
load_word(r_tmp3, r_eye_ptr, O_(P3_S4,x)),
load_word(R_AT, r_eye_ptr, O_(P3_S4,y)),
load_word(R_V0, r_eye_ptr, O_(P3_S4,z)),
nop, /* load-delay */
sub_u(r_tmp0, r_tmp0, r_tmp3),
sub_u(r_tmp1, r_tmp1, R_AT),
sub_u(r_tmp2, r_tmp2, R_V0),
/* Store fwd.x/y/z (atom 1 reads these as the normalize src). */
store_word(r_tmp0, r_scratch, O_(ResolveLookAtScratch,fwd.x)),
store_word(r_tmp1, r_scratch, O_(ResolveLookAtScratch,fwd.y)),
store_word(r_tmp2, r_scratch, O_(ResolveLookAtScratch,fwd.z)),
mac_yield()
})
/* 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.
*
* 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
* 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_ MipsAtom* resolve_look_at__cross_uz_up_in_to_right_proc(AtomArena_R aa, 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, aa, {
/* 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) */
nop,
/* Load a (uz).x/y/z into r_a/r_b/r_c. */
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 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)),
nop,
/* mtc2 a → IR1/2/3, b → D1/2/3 (VXY0/VZ0/VXY1). */
gte_mv_to_data_r(r_a, C2_IR1),
gte_mv_to_data_r(r_b, C2_IR2),
gte_mv_to_data_r(r_c, C2_IR3),
gte_mv_to_data_r(r_d, C2_VXY0), /* D1 = b.x */
gte_mv_to_data_r(R_AT, C2_VZ0), /* D2 = b.y */
gte_mv_to_data_r(R_V0, C2_VXY1), /* D3 = b.z */
nop2, /* MTC2 retirement (CPU→COP2 2-slot delay) */
gte_cmdw_outer_product, /* OP fires; MAC1/2/3 = a × b */
/* mfc2 MAC1/2/3 → r_a/r_b/r_c (out.x/y/z). */
gte_mv_from_data_r(r_a, C2_MAC1),
gte_mv_from_data_r(r_b, C2_MAC2),
gte_mv_from_data_r(r_c, C2_MAC3),
nop, /* MFC2 retirement */
/* Store out.x/y/z to r_f (out ptr = scratch+32). */
store_word(r_a, r_f, O_(V3_S4,x)),
store_word(r_b, r_f, O_(V3_S4,y)),
store_word(r_c, r_f, O_(V3_S4,z)),
mac_yield()
})
/* Atom 4: cross uz × ux → up. */
I_ MipsAtom* resolve_look_at__cross_uz_ux_to_up_proc(AtomArena_R aa, 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 */
) MipsAtom_Proc_(resolve_look_at__cross_uz_ux_to_up, aa, {
/* 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,ux)), /* r_h = &ux */
add_si(r_f, r_scratch, O_(ResolveLookAtScratch,up)), /* r_f = &up (out) */
nop,
/* Load a (uz).x/y/z into r_a/r_b/r_c. */
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 (ux).x/y/z into r_d + R_AT/R_V0. */
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)),
nop,
/* mtc2 a → IR1/2/3, b → D1/2/3 (VXY0/VZ0/VXY1). */
gte_mv_to_data_r(r_a, C2_IR1),
gte_mv_to_data_r(r_b, C2_IR2),
gte_mv_to_data_r(r_c, C2_IR3),
gte_mv_to_data_r(r_d, C2_VXY0),
gte_mv_to_data_r(R_AT, C2_VZ0),
gte_mv_to_data_r(R_V0, C2_VXY1),
nop2,
gte_cmdw_outer_product,
gte_mv_from_data_r(r_a, C2_MAC1),
gte_mv_from_data_r(r_b, C2_MAC2),
gte_mv_from_data_r(r_c, C2_MAC3),
nop,
store_word(r_a, r_f, O_(V3_S4,x)),
store_word(r_b, r_f, O_(V3_S4,y)),
store_word(r_c, r_f, O_(V3_S4,z)),
mac_yield()
})
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).
*
* GPR codes (assigned by resolve_look_at_init):
* r_look_at : MT3_S2S4* (popped from tape; output matrix destination)
* r_pux : pointer to ux (offset O_(ResolveLookAtScratch,ux))
* r_puy : pointer to uy (offset O_(ResolveLookAtScratch,uy))
* r_puz : pointer to uz (offset O_(ResolveLookAtScratch,uz))
* r_peye : pointer to eye (offset O_(ResolveLookAtScratch,eye))
* r_tmp0/1/2 : atom-local scratch (load + MVMVA + store temps)
*
* 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).
* 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.
*/
I_ MipsAtom* resolve_look_at__populate_and_translate_proc(AtomArena_R aa
, U4 r_look_at
, U4 r_scratch
, U4 r_pux, U4 r_puy, U4 r_puz, U4 r_peye /* 4 dedicated pointer regs */
, U4 r_tmp0, U4 r_tmp1, U4 r_tmp2 /* 3 atom-local scratch regs */
) MipsAtom_Proc_(resolve_look_at__populate_and_translate, aa, {
/* Pop look_at* (the matrix output) — advance R_TapePtr by 4 bytes. */
load_word(r_look_at, R_TapePtr, O_(Binds_ResolveLookAtPopAndTrans,look_at)),
add_ui_self( R_TapePtr, S_(Binds_ResolveLookAtPopAndTrans)),
/* Compute the 4 scratch pointers in their dedicated GPRs. */
add_si(r_pux, r_scratch, O_(ResolveLookAtScratch,ux)), /* r_pux = &ux */
add_si(r_puy, r_scratch, O_(ResolveLookAtScratch,uy)), /* r_puy = &uy */
add_si(r_puz, r_scratch, O_(ResolveLookAtScratch,uz)), /* r_puz = &uz */
add_si(r_peye, r_scratch, O_(ResolveLookAtScratch,eye)), /* r_peye = &eye */
nop,
/* ── m[0] = (S2)ux ── */
load_word(r_tmp0, r_pux, O_(V3_S4,x)),
load_word(r_tmp1, r_pux, O_(V3_S4,y)),
load_word(r_tmp2, r_pux, O_(V3_S4,z)),
nop,
store_half(r_tmp0, r_look_at, O_(MT3_S2S4,m[0][0])),
store_half(r_tmp1, r_look_at, O_(MT3_S2S4,m[0][1])),
store_half(r_tmp2, r_look_at, O_(MT3_S2S4,m[0][2])),
/* ── m[1] = (S2)uy ── */
load_word(r_tmp0, r_puy, O_(V3_S4,x)),
load_word(r_tmp1, r_puy, O_(V3_S4,y)),
load_word(r_tmp2, r_puy, O_(V3_S4,z)),
nop,
store_half(r_tmp0, r_look_at, O_(MT3_S2S4,m[1][0])),
store_half(r_tmp1, r_look_at, O_(MT3_S2S4,m[1][1])),
store_half(r_tmp2, r_look_at, O_(MT3_S2S4,m[1][2])),
/* ── m[2] = (S2)uz ── */
load_word(r_tmp0, r_puz, O_(V3_S4,x)),
load_word(r_tmp1, r_puz, O_(V3_S4,y)),
load_word(r_tmp2, r_puz, O_(V3_S4,z)),
nop,
store_half(r_tmp0, r_look_at, O_(MT3_S2S4,m[2][0])),
store_half(r_tmp1, r_look_at, O_(MT3_S2S4,m[2][1])),
store_half(r_tmp2, r_look_at, O_(MT3_S2S4,m[2][2])),
/* ── Translation column t[i] = R * (-eye) ─────────────────────────────
* pos = -eye: load eye.x/y/z from r_peye, negate via sub_u from R_0. */
load_word(r_tmp0, r_peye, O_(P3_S4,x)),
load_word(r_tmp1, r_peye, O_(P3_S4,y)),
load_word(r_tmp2, r_peye, O_(P3_S4,z)),
nop,
sub_u(r_tmp0, R_0, r_tmp0), /* pos.x = -eye.x */
sub_u(r_tmp1, R_0, r_tmp1),
sub_u(r_tmp2, R_0, r_tmp2),
/* mtc2 IR1/2/3 = pos (for MVMVA — input vector registers). */
gte_mv_to_data_r(r_tmp0, C2_IR1),
gte_mv_to_data_r(r_tmp1, C2_IR2),
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.
* 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]. */
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),
nop,
store_word(r_tmp0, r_look_at, O_(MT3_S2S4,t[0])),
store_word(r_tmp1, r_look_at, O_(MT3_S2S4,t[1])),
store_word(r_tmp2, r_look_at, O_(MT3_S2S4,t[2])),
mac_yield()
})
#pragma endregion Atom Procs
#pragma region Baked Atoms
enum {
@@ -528,7 +129,7 @@ internal MipsAtom_(screen_env_init) atom_info(atom_phase(screen_init)
store_word(R_0, R_ScreenBuf, O_(DrawEnv,texture_window.width) + OA_(DoubleBuffer,draw,1)),
/* draw[0].texture_page = 10 (gp0_tpage_default). C11 SetDefDrawEnv at C11_only.elf:0x8001273C writes the same 0x0A. . */
add_ui(R_T0, R_0, gp0_tpage_default),
add_ui(R_T0, R_0, gp0_tpage_default),
store_half(R_T0, R_ScreenBuf, O_(DrawEnv,texture_page) + OA_(DoubleBuffer,draw,0)),
store_half(R_T0, R_ScreenBuf, O_(DrawEnv,texture_page) + OA_(DoubleBuffer,draw,1)),
@@ -543,73 +144,73 @@ internal MipsAtom_(screen_env_init) atom_info(atom_phase(screen_init)
store_byte(R_T0, R_ScreenBuf, O_(DrawEnv,enable_auto_clear) + OA_(DoubleBuffer,draw,1)),
/* draw[0].initial_bg_color = (r=7, g=7, b=7). */
add_ui(R_T0, R_0, 7),
add_ui(R_T0, R_0, 7),
mac_store_rgb8(R_T0,R_T0,R_T0, R_ScreenBuf, O_(DrawEnv,initial_bg_color) + OA_(DoubleBuffer,draw,0)),
mac_store_rgb8(R_T0,R_T0,R_T0, R_ScreenBuf, O_(DrawEnv,initial_bg_color) + OA_(DoubleBuffer,draw,1)),
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_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. */
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 (CPUGPU). 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) */
/* 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),
/* 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),
/* GTE: SetGeomOffset (OFX, OFY) — ScreenRes_CenterX, ScreenRes_CenterY. */
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),
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),
/* GTE: SetGeomScreen (H) — CR26 (per PSX-SPX / libpsyx), value is the raw projection-plane distance, NOT shifted. */
add_ui(R_ScreenX, R_0, ScreenZ), gte_mv_to_ctrl_r(R_ScreenX, gte_cr_H_Code),
add_ui(R_T5, R_0, ScreenZ), gte_mv_to_ctrl_r(R_T5, gte_cr_H_Code),
/* GP1: DisplayEnable — bit 0 = 0 (Display ON). */
mac_gcmd_push(gp1_word_DisplayOn(), R_GpTmp, R_IO_BaseAddr, GPIO_PORT1_OFFSET), /* Uses auto-allocated R_GpTmp. */
mac_gcmd_push(gp1_word_DisplayOn(), R_T5, R_IO_BaseAddr, GPIO_PORT1_OFFSET),
mac_yield(),
};
/* ----- pad_apply_input -----
* Reads pad[0].buttons + pad[0].left_x;
* Applies the input-semantics deltas to cube_rot.y + floor_rot.y:
* - D-pad Left: cube_rot.y += 30, floor_rot.y += 5
* - D-pad Right: cube_rot.y -= 30, floor_rot.y -= 5
* - Analog stick X (dead zone 0x70..0x90):
* cube delta = (0x80 - left_x) >> 2 (range approx -32..+32)
* floor delta = (0x80 - left_x) >> 5 (range approx -4..+4)
* - D-pad + analog deltas add when used together.
*
* Convention:
* pad_state = 0 means no buttons active.
* The fail-safe zero-button value flows through unchanged, so a disconnected/fresh pad produces no rotation.
* The branch_le_zero pattern below matches the existing pad_input_demo convention (atom body lines 248/257).
*
* Signed-delta trick:
* load_byte_u zero-extends left_x to 32 bits; sub_u from 0x80 wraps to a SIGNED two's-complement value in the negative range;
* shift_aright (sra) then correctly sign-extends the shift for both positive (left_x < 0x80) and negative (left_x > 0x80) cases.
* Digital pads publish left_x = 0x80 → delta = 0 → no rotation, so the analog step is naturally a no-op for digital controllers.
*/
typedef Struct_(Binds_PadApplyInput) {
PadState* state;
V3_S2* cube_rot;
V3_S2* floor_rot;
PadState* state;
V3_S2* cube_rot;
V3_S2* floor_rot;
};
enum {
R_PadStateT5 = R_T5 atom_reg,
R_CubeRot = R_T1 atom_reg,
R_FloorRot = R_T2 atom_reg,
};
internal MipsAtom_(pad_input_cube_rotation) atom_info(atom_bind(Binds_PadApplyInput)
internal MipsAtom_(pad_apply_input) atom_info(atom_bind(Binds_PadApplyInput)
, atom_reads(R_T0, R_CubeRot, R_FloorRot, R_T3, R_T4, R_PadStateT5, R_TapePtr)
, atom_writes( R_CubeRot, R_FloorRot)
) {
@@ -624,7 +225,7 @@ internal MipsAtom_(pad_input_cube_rotation) atom_info(atom_bind(Binds_PadApplyIn
// Note(Ed): Potential op with delay slot?
/* D-pad Left: cube_rot.y += 30, floor_rot.y += 5. */
and_i(R_T3, R_T0, Pad_Left), branch_le_zero(R_T3, atom_offset(dpad_left, exit_dpad_left)),
and_i(R_T3, R_T0, pad0_(Pad_Left)), branch_le_zero(R_T3, atom_offset(dpad_left, exit_dpad_left)),
load_half( R_T4, R_CubeRot, O_(V3_S2,y)), /* BD-slot */
load_half( R_T3, R_FloorRot, O_(V3_S2,y)),
add_si( R_T4, R_T4, 30),
@@ -634,7 +235,7 @@ internal MipsAtom_(pad_input_cube_rotation) atom_info(atom_bind(Binds_PadApplyIn
atom_label(exit_dpad_left)
/* D-pad Right: cube_rot.y -= 30, floor_rot.y -= 5. */
and_i(R_T3, R_T0, Pad_Right), branch_le_zero(R_T3, atom_offset(dpad_right, exit_dpad_right)),
and_i(R_T3, R_T0, pad0_(Pad_Right)), branch_le_zero(R_T3, atom_offset(dpad_right, exit_dpad_right)),
load_half( R_T4, R_CubeRot, O_(V3_S2,y)), /* BD-slot */
load_half( R_T3, R_FloorRot, O_(V3_S2,y)),
add_si( R_T4, R_T4, -30),
@@ -645,21 +246,21 @@ internal MipsAtom_(pad_input_cube_rotation) atom_info(atom_bind(Binds_PadApplyIn
/* Analog left-stick X: dead zone 0x70..0x90.
* Cube delta = (0x80 - left_x) >> 2; floor delta = (0x80 - left_x) >> 5. */
load_byte_u(R_T3, R_PadStateT5, O_(PadState,left.x)),
load_byte_u(R_T3, R_PadStateT5, O_(PadState,left_x)),
/* Dead-zone check: skip analog if left_x in [0x70, 0x90] inclusive. Outside dead zone on LOW side: left_x < 0x70 (strictly).
* set_lt_u(R_T4, R_T3, R_T4=0x70) → R_T4 = (left_x < 0x70) ? 1 : 0. */
add_ui(R_T4, R_0, PadDeadZone_HighBound), set_lt_u(R_T4, R_T3, R_T4), branch_ne(R_T4, R_0, atom_offset(dead_zone_low_check, dead_low_active)),
add_ui(R_T4, R_0, PadDeadZone_Center), /* BD-slot: pre-load 0x80 for dead_low_active */
add_ui(R_T4, R_0, 0x70), set_lt_u(R_T4, R_T3, R_T4), branch_ne(R_T4, R_0, atom_offset(dead_zone_low_check, dead_low_active)),
add_ui(R_T4, R_0, 0x80), /* BD-slot: pre-load 0x80 for dead_low_active */
atom_label(dead_check_upper)
/* left_x >= 0x70 → check upper bound. */
load_byte_u(R_T3, R_PadStateT5, O_(PadState,left.x)), /* reload */
add_ui( R_T4, R_0, PadDeadZone_HighBound),
load_byte_u(R_T3, R_PadStateT5, O_(PadState,left_x)), /* reload */
add_ui( R_T4, R_0, 0x90),
/* R_T4 = (0x90 < left_x) ? 1 : 0 → (left_x > 0x90) ? 1 : 0 */
set_lt_u(R_T4, R_T4, R_T3), branch_ne(R_T4, R_0, atom_offset(dead_zone_high_check, dead_high_active)),
add_ui( R_T4, R_0, PadDeadZone_Center), /* BD-slot: pre-load 0x80 for dead_high_active */
add_ui( R_T4, R_0, 0x80), /* BD-slot: pre-load 0x80 for dead_high_active */
jump_rel(atom_offset(dead_zone_skip, exit_stick)),
mac_yield_load(),
@@ -672,7 +273,8 @@ atom_label(dead_low_active)
/* R_T4 = cube_delta */
shift_aright(R_T4, R_T3, 2),
load_half( R_T0, R_CubeRot, O_(V3_S2,y)), nop,
load_half( R_T0, R_CubeRot, O_(V3_S2,y)),
nop,
add_u( R_T0, R_T0, R_T4),
store_half( R_T0, R_CubeRot, O_(V3_S2,y)),
/* R_T4 = floor_delta — moved into the load-delay slot of the floor load below (fills the 1-instruction gap;
@@ -693,7 +295,8 @@ atom_label(dead_high_active)
/* delta = 0x80 - left_x (signed negative). */
shift_aright(R_T4, R_T3, 2), /* R_T4 = cube_delta (signed) */
load_half( R_T0, R_CubeRot, O_(V3_S2,y)), nop,
load_half( R_T0, R_CubeRot, O_(V3_S2,y)),
nop,
add_u( R_T0, R_T0, R_T4),
store_half( R_T0, R_CubeRot, O_(V3_S2,y)),
@@ -712,60 +315,7 @@ atom_label(exit_stick)
};
enum {
R_Cam = R_T4 atom_reg,
R_CamPadState = R_T5 atom_reg,
};
typedef Struct_(Binds_PadInputCam) {
PadState* state;
Camera* cam;
};
internal MipsAtom_(pad_input_cam) atom_info(atom_bind(Binds_PadInputCam)
, atom_reads( R_Cam, R_CamPadState, R_TapePtr)
, atom_writes(R_Cam)
) {
/* Bind pop: state → R_CamPadState (R_T5), cam → R_Cam (R_T4), advance R_TapePtr by 8. */
load_word(R_CamPadState, R_TapePtr, O_(Binds_PadInputCam,state)),
load_word(R_Cam, R_TapePtr, O_(Binds_PadInputCam,cam)),
add_ui_self( R_TapePtr, S_(Binds_PadInputCam)),
/* Load pad[0].buttons into R_T0; nop fills the load-delay slot. */
load_word(R_T0, R_CamPadState, O_(PadState,buttons)),
load_word(R_T1, R_Cam, O_(Camera,pos.x)), // BD-Slot.
// D-pad Left → cam.pos.x -= 50. and_i fulfills BD-slot for load on R_Cam.
and_i(R_T3, R_T0, Pad_Left), branch_le_zero(R_T3, atom_offset(left_x, exit_left_x)), mac_yield_load(),
add_si(R_T1, R_T1, -50), store_word(R_T1, R_Cam, O_(Camera,pos.x)),
atom_label(exit_left_x)
/* D-pad Right → cam.pos.x += 50. Reuses R_T1 from Left. */
and_i(R_T3, R_T0, Pad_Right), branch_le_zero(R_T3, atom_offset(right_x, exit_right_x)), nop,
add_si(R_T1, R_T1, 50), store_word(R_T1, R_Cam, O_(Camera,pos.x)),
atom_label(exit_right_x)
/* D-pad Up → cam.pos.y -= 50. Load pos.y BEFORE the andi. */
load_word(R_T1, R_Cam, O_(Camera,pos.y)),
and_i(R_T3, R_T0, Pad_Up), branch_le_zero(R_T3, atom_offset(up_y, exit_up_y)), nop,
add_si(R_T1, R_T1, -50), store_word(R_T1, R_Cam, O_(Camera,pos.y)),
atom_label(exit_up_y)
/* D-pad Down → cam.pos.y += 50. Reuses R_T1 from Up. */
and_i(R_T3, R_T0, Pad_Down), branch_le_zero(R_T3, atom_offset(down_y, exit_down_y)), nop,
add_si(R_T1, R_T1, 50), store_word(R_T1, R_Cam, O_(Camera,pos.y)),
atom_label(exit_down_y)
/* D-pad Cross → cam.pos.z -= 50. Load pos.z BEFORE the andi. */
load_word(R_T1, R_Cam, O_(Camera,pos.z)),
and_i(R_T3, R_T0, Pad_Cross), branch_le_zero(R_T3, atom_offset(cross_z, exit_cross_z)), nop,
add_si(R_T1, R_T1, -50), store_word(R_T1, R_Cam, O_(Camera,pos.z)),
atom_label(exit_cross_z)
/* D-pad Circle → cam.pos.z += 50. Reuses R_T1 from Cross. */
and_i(R_T3, R_T0, Pad_Circle), branch_le_zero(R_T3, atom_offset(circle_z, exit_circle_z)), nop,
add_si(R_T1, R_T1, 50), store_word(R_T1, R_Cam, O_(Camera,pos.z)),
atom_label(exit_circle_z)
mac_yield_tail(),
};
enum {
R_PrimCursor = R_T7 atom_reg atom_type(U4*), /* Output cursor (primitive buffer) */
R_PrimCursor = R_T7 atom_reg atom_type(U4*), /* VRAM output cursor (primitive buffer) */
R_FaceCursor = R_T4 atom_reg atom_type(V4_S2*), /* Cube face-index cursor (V4_S2*); floor context switches to V3_S2* via atom_phase */
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 */
@@ -774,6 +324,7 @@ enum {
#define R_VertBase_Code R_T5_Code
#define R_OtBase_Code R_T6_Code
};
typedef Struct_(Binds_CubeTri) {
U4 PrimCursor;
V4_S2* FaceCursor;
@@ -793,7 +344,7 @@ internal MipsAtom_(rbind_cube_g4_face) atom_info(atom_bind(Binds_CubeTri), atom_
mac_yield()
};
// cube_g4_face — Draw one cube face (Gouraud-shaded quad) via the GTE tape pipeline
// cube_g4_face — Draw one cube face (Gouraud-shaded quad) via the GTE tape pipeline
internal
MipsAtom_(cube_g4_face) atom_info(atom_phase(cube_g4),
atom_reads( R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase),
@@ -810,9 +361,9 @@ 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).
* 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. */
/* 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. */
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)),
@@ -828,7 +379,7 @@ MipsAtom_(cube_g4_face) atom_info(atom_phase(cube_g4),
set_lt_u( R_AT, R_T1, R_AT),
branch_equal(R_AT, R_0, atom_offset(bounds_chk, cube_g4_face_exit)), nop,
mac_insert_ot_tag(R_OtBase, R_PrimCursor, S_(Poly_G4)),
mac_insert_ot_tag_g4(R_OtBase, R_PrimCursor),
mac_format_g4_color(R_PrimCursor,
/* c0 magenta */ 0xFF, 0x00, 0xFF,
/* c1 yellow */ 0xFF, 0xFF, 0x00,
@@ -869,14 +420,14 @@ MipsAtom_(floor_f3_face) atom_info(atom_phase(floor_f3)
, atom_reads( R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase)
, atom_writes(R_PrimCursor, R_FaceCursor)
) {
mac_load_tri_indices(R_FaceCursor, R_T0, R_T1, R_T2),
mac_load_tri_indices( R_FaceCursor, R_T0, R_T1, R_T2),
mac_gte_load_tri_verts(R_VertBase, R_T0, R_T1, R_T2),
nop2, gte_cmdw_rotate_translate_perspective_triple, // 2 nops retire the final cpu -> gte writes before RTPT
gte_cmdw_nclip,
/* Culling (Branch forward if Backface) */
gte_mv_from_data_r(R_T0, C2_MAC0),
nop, branch_le_zero(R_T0, atom_offset(culling, floor_f3_face_exit)), nop, // required gte -> cpu load-delay slot.
nop, branch_le_zero(R_T0, atom_offset(culling, floor_f3_face_exit)), nop, // required gte -> cpu load-delay slot.
/* Format Primitive */
mac_gte_store_f3(R_PrimCursor),
@@ -888,7 +439,7 @@ MipsAtom_(floor_f3_face) atom_info(atom_phase(floor_f3)
set_lt_u( R_AT, R_T1, R_AT),
branch_equal(R_AT, R_0, atom_offset(bounds_chk, floor_f3_face_exit)), nop,
mac_format_f3_color(R_PrimCursor, 0xFF, 0xFF, 0xFF), // RGB-form (R=FF, G=FF, B=FF = white)
mac_insert_ot_tag(R_OtBase, R_PrimCursor, S_(Poly_F3)), /* Insert into Ordering Table Linked List */
mac_insert_ot_tag_f3(R_OtBase, R_PrimCursor), /* Insert into Ordering Table Linked List */
add_ui_self(R_PrimCursor, S_(Poly_F3)), /* Advance Prim Cursor (5 words) */
// Note(Ed): No bounds checking, should be checked before atom runs.
// end: branch(bounds_chk)
+105 -291
View File
@@ -26,12 +26,10 @@
#include "duffle/dsl.atom.h"
#include "duffle/lottes_tape.h"
#include "duffle/bios.h"
#include "duffle/psyq.h"
#pragma endregion Duffle Headers
#pragma region Duffle TUs
#include "duffle/pad.c"
#include "duffle/math.atom.c"
#include "duffle/mips.atom.c"
#include "duffle/gte.atom.c"
@@ -43,7 +41,6 @@
#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
@@ -52,15 +49,9 @@
#include "hello_camera.atom.c"
#pragma endregion Hello Joypad TUs
enum {
Scratchpad_Loc = 0x1F800000,
};
#define C_scratch(type) C_(type, Scratchpad_Loc)
enum {
Scratchpad_Len = 1024,
MemTape_Len = 512,
ResolveLookAtArena_Words = 512,
enum {
Scratchpad_Len = 1024,
MemTape_Len = 512,
};
typedef Struct_(SMemory) {
PrimitiveArena primitives;
@@ -70,10 +61,7 @@ typedef Struct_(SMemory) {
U4 MemTape[MemTape_Len];
MT3_S2S4 tform_world;
MT3_S2S4 tform_view;
Camera cam;
M3_S2 tform_world;
Ent_Cube cube;
Ent_Floor floor;
@@ -81,18 +69,11 @@ typedef Struct_(SMemory) {
PadBiosRaw pad_raw[2];
PadState pad[2];
// TODO(Ed): We don't need this we can just cast at any point an address to a desired view of scratchpad, we have the address.
U4_V scratchpad; // d-cache
U4 resolve_look_at_mem[ResolveLookAtArena_Words];
MipsAtom* resolve_look_at_atom_addrs[7];
};
global SMemory smem;
extern SMemory smem;
#define pad0_btn_(btn) btn & smem.pad[0].buttons
#define pad1_btn_(btn) btn & smem.pad[1].buttons
I_ B1* prim__alloc(U4 type_width, Str8 type_name) {
gknown PrimitiveArena* pa = & smem.primitives;
gknown B1* buf = (B1*) r_(smem.primitives.buf)[smem.active_buf_id];
@@ -103,218 +84,97 @@ I_ B1* prim__alloc(U4 type_width, Str8 type_name) {
}
#define prim_alloc(type) (type*)prim__alloc(S_(type), slit( stringify(type)))
void
resolve_look_at_c11(MT3_S2S4* look_at, P3_S4* eye, P3_S4* target, V3_S4* up_in) {
// RGA(Lengyel): Build matrix expansion of a rigid transformation. Corresponding motor is not constructed; we write the LA form for GTE.
// Preconditions: eye != target, up_in not collinear with (target - eye).
V3_S4 right, up, forward;
V3_S4 ux, uy, uz;
V3_S4 pos, off;
/* Uses ONE 8-byte frame allocated via the compiler's standard prologue.
* The 4 wasted-arg words for B(12h) InitPAD2 live at [SP+0..15] but are not explicitly allocated.
* The compiler handles the MIPS O32 "wasted stack" convention for us by treating the B-call as a 4-arg call.
*
* The buffer pointers are passed as arguments so the compiler keeps them in callee-saved registers;
* The B(12h) asm volatile block does NOT clobber those registers (it clobbers only the volatile GPRs + the B-table arg registers explicitly).
* The C-level writes after the call re-load the pointers from their callee-saved homes.
*
* The clobber list for both B-calls names the full BIOS destroy set documented in kernelbios.md:167-174 (R1..R15, R24..R25, R31, HI/LO).
* The kernel-ABI "volatile GPRs" subset is clb_system; the rest of the destroy set is enumerated explicitly here. */
NI_ void pad_bios_init_start(PadBiosRaw* raw0, PadBiosRaw* raw1)
{
/* Pin raw0 + raw1 to $a0 + $a1 via rgcc; the B(12h) call uses these directly.
* The `(void)` casts mark them as unread after the call so the compiler doesn't need to move them back. */
register PadBiosRaw* p0 rgcc(R_A0) = raw0;
register PadBiosRaw* p1 rgcc(R_A1) = raw1;
(void)p0; (void)p1;
forward = target[0]; sub_v3s4(& forward, eye[0]); // RGA(Lengyel): Affine point - point = zero-weight direction.
normalize_v3s4(& forward, & uz); // RGA(Lengyel): Normalize the direction bulk. Not finite-point unitization.
// TODO(Ed): Properly annotate the raw values in the inline asm instructions.
// Use enums.
cross_v3s4(& uz, up_in, & right); normalize_v3s4(& right, & ux); // RGA(Lengyel): Complement(Wedge(forward, up_in)) -> right axis.
cross_v3s4(& uz, & ux, & up); normalize_v3s4(& up, & uy); // RGA(Lengyel): Complement(Wedge(forward, right)) -> up axis.
/* B(12h) InitPAD2(raw0, 0x22, raw1, 0x22)
* $a0 = raw0 (rgcc-bound; survives the sequence below)
* $a1 = raw1 (preserved into $a2 before $a1 is overwritten)
* $a2 = raw1 (moved from $a1; survives $a1's overwrite)
* $a3 = 0x22 (immediate)
* $t1 = 0x12 (function number)
* $t2 = 0xB0 (BIOS B-table address) */
asm volatile(
asm_words(
or_u( rarg_2, rarg_1, rdiscard), /* $a2 = $a1 = raw1 */
add_ui( rarg_1, rdiscard, 0x22), /* $a1 = 0x22 */
add_ui( rarg_3, rdiscard, 0x22), /* $a3 = 0x22 */
add_ui( rtmp_1, rdiscard, 0x12), /* $t1 = 0x12 */
add_ui( rtmp_2, rdiscard, 0xB0), /* $t2 = 0xB0 */
call_reg(rtmp_2), /* jalr $t2, $ra */
nop /* BD slot */
)
asm_rpins, r_use(p0), r_use(p1)
asm_clobber:
rlit(R_AT),
rlit(R_V0), rlit(R_V1),
rlit(R_T0), rlit(R_T1), rlit(R_T2), rlit(R_T3), rlit(R_T4),
rlit(R_T5), rlit(R_T6), rlit(R_T7), rlit(R_T8), rlit(R_T9),
rlit(R_RA),
clb_mem_drain
);
// RGA(Lengyel): matrix expansion of the world-to-camera rotation (basis rows).
look_at->m[0][0] = ux.x; look_at->m[0][1] = ux.y; look_at->m[0][2] = ux.z;
look_at->m[1][0] = uy.x; look_at->m[1][1] = uy.y; look_at->m[1][2] = uy.z;
look_at->m[2][0] = uz.x; look_at->m[2][1] = uz.y; look_at->m[2][2] = uz.z;
/* The C-level writes re-load the pointers via the parameter names and write 0xFF to each
* buffer's status byte to mark the initial-state hazard documented in kernelbios.md:1621-1624. */
u1_v(raw0)[0] = 0xFF;
u1_v(raw1)[0] = 0xFF;
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.
// 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);
/* B(13h) StartPAD2() — no args. The BIOS preserves $sp. */
asm volatile(
asm_words(
add_ui( rtmp_1, rdiscard, 0x13), /* $t1 = 0x13 */
add_ui( rtmp_2, rdiscard, 0xB0), /* $t2 = 0xB0 (re-load) */
call_reg(rtmp_2), /* jalr $t2, $ra */
nop /* BD slot */
)
asm_clobber:
rlit(R_AT),
rlit(R_V0), rlit(R_V1),
rlit(R_T0), rlit(R_T1), rlit(R_T2), rlit(R_T3), rlit(R_T4),
rlit(R_T5), rlit(R_T6), rlit(R_T7), rlit(R_T8), rlit(R_T9),
rlit(R_RA),
clb_mem_drain
);
}
/* Pre-build all 7 chain atoms of the resolve_look_at bundle into the static arena.
* Called ONCE from main() before the frame loop.
* After this returns, the smem.resolve_look_at_atom_addrs[] array contains valid MIPS atom pointers
* for the frame-time bundle helper to emit via tb_emit(tb, captured_addr).
*
* 4 unique procs in hello_camera.atom.c (chain atoms 0, 2, 4, 6); atoms 1, 3, 5
* share the GENERIC normalize_v3s4_proc from gte.atom.c (called 3x with different
* O_(ResolveLookAtScratch,...) offsets):
* 0: resolve_look_at__input_and_sub_proc
* 1: normalize_v3s4_proc (fwd → uz; offsets 0, 16)
* 2: resolve_look_at__cross_uz_up_in_to_right_proc
* 3: normalize_v3s4_proc (right → ux; offsets 32, 48)
* 4: resolve_look_at__cross_uz_ux_to_up_proc
* 5: normalize_v3s4_proc (up → uy; offsets 64, 80)
* 6: resolve_look_at__populate_and_translate_proc
*
* Task 12.16 promotion: the bundle-specific resolve_look_at__chain_normalize_proc
* has been promoted to the generic normalize_v3s4_proc (gte.atom.c), which now
* takes r_scratch + r_src_offset + r_dst_offset as U4 parameters. The 3 callers
* pass O_(ResolveLookAtScratch,...) macros as offset args. The metaprogram emits
* one set of `atom_offset__normalize_v3s4__srav_path__aligned_done` defs
* (namespaced by atom name) in duffle/gen/offsets.h, shared by all 3 callers.
*
* GPR pool per atom: 10 free GPRs (R_T0..R_T3 + R_T5..R_T7 + R_V0 + R_V1 + R_AT).
* R_T4 is reserved as the wave-context carrier (R_ResolveScratch).
*/
internal void resolve_look_at_init(void) {
/* Wrap the static arena in a MipsAtomBuilder. */
AtomArena ab = atomarena_make(slice_ut_arr(smem.resolve_look_at_mem));
/* Atom 0: resolve_look_at__input_and_sub — stages eye/up_in into scratchpad,
* 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] = 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: normalize_v3s4_proc
* The proc takes r_src_offset + r_dst_offset as U4 PARAMETERS — we pass the O_(...) macros here (evaluating to numeric literals 0 and 16).
* Body is identical across the 3 call sites (atoms 1, 3, 5); only the offset args differ.
* GPR pool: r_scratch (R_T4 carrier) + 9 body GPRs = 10.
* r_src_ptr (R_T0) : src ptr
* r_dst_ptr (R_T1) : dst ptr
* r_tmp (R_T2) : src.x PRESERVED (NOT clobbered by mfc2 MAC2) → fed to IR1 in stage 4
* r_mac1_scratch (R_T3) : MAC1 scratch + aligned |v|² in stage 3
* r_mac2_scratch (R_T5) : MAC2 scratch → result.x after stage 4 sra
* r_recip_est (R_T6) : src.y → result.y
* r_lzcr (R_T7) : |v|² accumulator + shift count + 1/|v| (overwritten across stages 2-4)
* r_shift (R_V0) : shift count (saved in stage 3) → sra amount in stage 4
* r_branch_tmp (R_V1) : src.z → result.z (reused after stage 1)
*/
ab.start = ab.start + ab.used;
smem.resolve_look_at_atom_addrs[1] = normalize_v3s4_proc(& ab, R_ResolveScratch,
O_(ResolveLookAtScratch, fwd), /* r_src_offset = 0 */
O_(ResolveLookAtScratch, uz), /* r_dst_offset = 16 */
R_T0, R_T1, R_T2, /* r_src_ptr, r_dst_ptr, r_tmp */
R_T3, /* r_mac1_scratch */
R_T5, /* r_mac2_scratch */
R_T6, /* r_recip_est */
R_T7, /* r_lzcr */
R_V0, /* r_shift */
R_V1); /* r_branch_tmp */
/* Atom 2: resolve_look_at__cross_uz_up_in_to_right
* out=scratch+32 (HARDCODED in body). GPR pool: r_scratch + 7 body + R_AT + R_V0 = 10. */
smem.resolve_look_at_atom_addrs[2] = resolve_look_at__cross_uz_up_in_to_right_proc(& ab, R_ResolveScratch,
R_T0, R_T1, R_T2, /* r_a, r_b, r_c (a.x/y/z → out.x/y/z) */
R_T3, /* r_d (b.x) */
R_T5, /* r_f (out ptr = scratch+32) */
R_T6, /* r_g (a ptr = scratch+16) */
R_T7); /* r_h (b ptr = scratch+128) */
/* Atom 3: normalize_v3s4_proc. */
smem.resolve_look_at_atom_addrs[3] = normalize_v3s4_proc(& ab, R_ResolveScratch,
O_(ResolveLookAtScratch, right), /* r_src_offset = 32 */
O_(ResolveLookAtScratch, ux), /* r_dst_offset = 48 */
R_T0, R_T1, R_T2,
R_T3,
R_T5,
R_T6,
R_T7,
R_V0,
R_V1);
/* Atom 4: resolve_look_at__cross_uz_ux_to_up — a=scratch+16, b=scratch+48, out=scratch+64 (HARDCODED). */
smem.resolve_look_at_atom_addrs[4] = resolve_look_at__cross_uz_ux_to_up_proc(& ab, R_ResolveScratch,
R_T0, R_T1, R_T2,
R_T3,
R_T5, /* r_f (out ptr = scratch+64) */
R_T6, /* r_g (a ptr = scratch+16) */
R_T7); /* r_h (b ptr = scratch+48) */
/* Atom 5: normalize_v3s4_proc (generic, from gte.atom.c) — src=scratch+64=up, dst=scratch+80=uy. */
smem.resolve_look_at_atom_addrs[5] = normalize_v3s4_proc(& ab, R_ResolveScratch,
O_(ResolveLookAtScratch, up), /* r_src_offset = 64 */
O_(ResolveLookAtScratch, uy), /* r_dst_offset = 80 */
R_T0, R_T1, R_T2,
R_T3,
R_T5,
R_T6,
R_T7,
R_V0,
R_V1);
/* Atom 6: resolve_look_at__populate_and_translate — write look_at->m[][] from ux/uy/uz (computed from r_scratch+offset internally),
then compute translation column t[] = R * (-eye). GPR pool: r_look_at + r_scratch + 4 ptr regs + 3 tmp regs = 9. */
smem.resolve_look_at_atom_addrs[6] = resolve_look_at__populate_and_translate_proc(& ab,
R_T0, /* r_look_at (popped from tape; MT3_S2S4*) */
R_ResolveScratch, /* r_scratch (wave-context carrier) */
R_T1, R_T3, R_T5, R_T7, /* r_pux, r_puy, r_puz, r_peye */
R_T2, R_T6, R_V0); /* r_tmp0, r_tmp1, r_tmp2 */
/* Sanity check: arena didn't overflow. */
assert(ab.used <= ResolveLookAtArena_Words);
}
/* Emit the resolve_look_at bundle into the tape. Called once per frame from update().
* The 7 chain atoms are pre-built at init time (resolve_look_at_init) and referenced by address via smem.resolve_look_at_atom_addrs[].
* Per-frame work: 7 tb_emit (atom pointer emissions) + 5 tb_data (C-side pointers for atom 0 + look_at for atom 6).
*
* Binds_ contract (the field-name labels are for human readability):
* Atom 0 input_and_sub target(4) eye(4) up_in(4) scratch_base(4) = 4 words
* Atoms 1-5 (no tape data — atom uses r_scratch + offset internally)
* Atom 6 populate_and_translate look_at(4) = 1 word
* ----
* 5 tb_data words total per frame.
*/
I_ void resolve_look_at(
TapeBuilder_R tb
, MT3_S2S4* look_at
, P3_S4* eye
, P3_S4* target
, V3_S4* up_in
){
// tb_emit_bundle(tb, slice_from_array(MipsAtom, smem.resolve_look_at_atom_addrs));
/* Atom 0: input_and_sub — stages eye/up_in into scratchpad + computes fwd. */
tb_emit(tb, smem.resolve_look_at_atom_addrs[0]); {
tb_data(tb, u4_(target)); /* Binds_ResolveLookAtSub.target (C-side P3_S4*) */
tb_data(tb, u4_(eye)); /* Binds_ResolveLookAtSub.eye (C-side P3_S4*) */
tb_data(tb, u4_(up_in)); /* Binds_ResolveLookAtSub.up_in (C-side V3_S4*) */
tb_data(tb, u4_(smem.scratchpad)); /* Binds_ResolveLookAtScratch.scratch_base */
}
/* Atoms 1-5: disabled (atom 1 verification below) */
tb_emit(tb, smem.resolve_look_at_atom_addrs[1]); { }
// tb_emit(tb, smem.resolve_look_at_atom_addrs[2]); { }
// tb_emit(tb, smem.resolve_look_at_atom_addrs[3]); { }
// tb_emit(tb, smem.resolve_look_at_atom_addrs[4]); { }
// tb_emit(tb, smem.resolve_look_at_atom_addrs[5]); { }
// /* Atom 6: populate_and_translate — only output pointer is the matrix destination. */
// tb_emit(tb, smem.resolve_look_at_atom_addrs[6]); {
// tb_data(tb, u4_(look_at)); /* Binds_ResolveLookAtPopAndTrans.look_at (MT3_S2S4*) */
// }
}
FI_ void camera_look_at_c11(Camera* c, P3_S4* target, V3_S4* up_in) { resolve_look_at_c11(& c->look_at, & c->pos, target, up_in); }
GCC_OPTIMIZATION_DISABLE
void update(PrimitiveArena* pa, U4* ordering_buf)
{
TapeBuilder tb = tb_make(slice_ut_arr(smem.MemTape));
// Pad Input
if (1) // Pad Input
{
tb.used = 0; tb_scope_run(& tb) {
// Grab latest state from bios.
/* BIOS-owned polling: per-frame snapshot of both ports. */
tb_emit_(pad_bios_snapshot);
tb_data_(raw, & smem.pad_raw[0]);
tb_data_(raw, & smem.pad_raw[0]);
tb_data_(state, & smem.pad[0]);
tb_emit_(pad_bios_snapshot);
tb_data_(raw, & smem.pad_raw[1]);
tb_data_(state, & smem.pad[1]);
tb_emit_(pad_input_cam);
tb_data_(state, & smem.pad[0]);
tb_data_(cam, & smem.cam);
// tb_emit_(pad_input_cube_rotation);
// tb_data_(state, & smem.pad[0]);
// tb_data_(cube_rot, & smem.cube.rot);
// tb_data_(floor_rot, & smem.floor.rot);
/* Per-frame rotation apply: consume pad[0].buttons + pad[0].left_x */
tb_emit_(pad_apply_input);
tb_data_(state, & smem.pad[0]);
tb_data_(cube_rot, & smem.cube.rot);
tb_data_(floor_rot, & smem.floor.rot);
}
}
@@ -341,54 +201,15 @@ void update(PrimitiveArena* pa, U4* ordering_buf)
A2_S2 p; //???
S4 flag; //????
if (0) {
camera_look_at_c11(& smem.cam, & smem.cube.pos, & v3s4(0, -fp_one, 0));
}
if (1)
{
tb.used = 0; tb_scope_run(& tb) {
resolve_look_at(& tb, & smem.cam.look_at, & smem.cam.pos, & smem.cube.pos, & v3s4(0, -fp_one, 0));
}
V3_S4 right, up, forward;
V3_S4 ux, uy, uz;
V3_S4 pos, off;
ResolveLookAtScratch_V scratch = C_scratch(ResolveLookAtScratch_V);
// Atom 0: Works (tape emits fwd to scratch+0; C-side reads it back)
forward = scratch->fwd;
// C-side normalize fallback (atom 1 disabled)
// normalize_v3s4(& forward, & uz);
uz = scratch->uz; /* tape-side: enable after verifying atom 1 fix */
cross_v3s4(& uz, & v3s4(0, -fp_one, 0), & right); normalize_v3s4(& right, & ux);
cross_v3s4(& uz, & ux, & up); normalize_v3s4(& up, & uy);
smem.cam.look_at.m[0][0] = ux.x; smem.cam.look_at.m[0][1] = ux.y; smem.cam.look_at.m[0][2] = ux.z;
smem.cam.look_at.m[1][0] = uy.x; smem.cam.look_at.m[1][1] = uy.y; smem.cam.look_at.m[1][2] = uy.z;
smem.cam.look_at.m[2][0] = uz.x; smem.cam.look_at.m[2][1] = uz.y; smem.cam.look_at.m[2][2] = uz.z;
pos = smem.cam.pos; mul_v3s4(& pos, v3s4(-1,-1,-1)); // RGA(Lengyel): -eye in world coordinates (spatial bulk only; implicit weight is dropped).
mul_m3s2_v3s4(& smem.cam.look_at, & pos, & off);
trans_m3s2( & smem.cam.look_at, & off);
}
// Draw cube
if (1)
{
mt3s2s4_rotation (& smem.cube.rot, & smem.tform_world);
mt3s2s4_translation(& smem.tform_world, & smem.cube.pos);
mt3s2s4_scale (& smem.tform_world, & smem.cube.scale);
// Combine world and look_at matrix.
gte_comp_coord_m3s2(& smem.cam.look_at, & smem.tform_world, & smem.tform_view);
gte_matrix_set_rotation (& smem.tform_view);
gte_matrix_set_translation(& smem.tform_view);
// gte_matrix_set_rotation (& smem.tform_world);
// gte_matrix_set_translation(& smem.tform_world);
m3s2_rotation (& smem.cube.rot, & smem.tform_world);
m3s2_translation(& smem.tform_world, & smem.cube.pos);
m3s2_scale (& smem.tform_world, & smem.cube.scale);
gte_matrix_set_rotation (& smem.tform_world);
gte_matrix_set_translation(& smem.tform_world);
U4 prim_base = u4_(pa->buf[smem.active_buf_id]);
U4 prim_cursor = prim_base + pa->used;
@@ -409,22 +230,16 @@ void update(PrimitiveArena* pa, U4* ordering_buf)
tb_data(& tb, u4_(& pa->used));
tb_data(& tb, prim_base);
}
tape_run_a02_s07(tb_slice(tb));// Fire off the tape (bigger-clobber variant).
tape_run(tb_slice(tb));
// smem.cube.rot.y += 30;
}
// Draw floor
if (1)
{
mt3s2s4_rotation (& smem.floor.rot, & smem.tform_world);
mt3s2s4_translation(& smem.tform_world, & smem.floor.pos);
mt3s2s4_scale (& smem.tform_world, & smem.floor.scale);
// Combine world and look_at matrix.
gte_comp_coord_m3s2(& smem.cam.look_at, & smem.tform_world, & smem.tform_view);
gte_matrix_set_rotation (& smem.tform_view);
gte_matrix_set_translation(& smem.tform_view);
m3s2_rotation (& smem.floor.rot, & smem.tform_world);
m3s2_translation(& smem.tform_world, & smem.floor.pos);
m3s2_scale (& smem.tform_world, & smem.floor.scale);
U4 prim_base = u4_(pa->buf[smem.active_buf_id]);
U4 prim_cursor = prim_base + pa->used;
@@ -434,11 +249,11 @@ void update(PrimitiveArena* pa, U4* ordering_buf)
// Prepare the tape. (Push protocol to tape)
tb.used = 0; tb_scope(& tb) {
// tb_emit(& tb, set_gte_mt3s2s4);
// tb_data(& tb, u4_(& smem.tform_view));
tb_emit(& tb, set_gte_world);
tb_data(& tb, u4_(& smem.tform_world));
tb_emit(& tb, rbind_floor_f3_face);
// TODO(Ed): Just use a single context struct ref?
// TODO(Ed): Just use a single context struct ref
tb_data(& tb, prim_cursor);
tb_data(& tb, u4_(smem.floor.faces));
tb_data(& tb, u4_(smem.floor.verts));
@@ -451,7 +266,7 @@ void update(PrimitiveArena* pa, U4* ordering_buf)
tb_data(& tb, u4_(& pa->used));
tb_data(& tb, prim_base);
}
tape_run_a02_s07(tb_slice(tb));// Fire off the tape (bigger-clobber variant).
tape_run(tb_slice(tb));// Fire off the tape.
// C-side state (pa->used) has already been updated by the tape!
// smem.floor.rot.y += 5;
@@ -475,14 +290,25 @@ void gp_display_frame(DoubleBuffer* screen_buf, S4* active_buf_id, U4* ordering_
}
GCC_OPTIMIZATION_DISABLE
void hot_reload_entry(void)
{
smem.primitives.used = 0;
while (1) {
gknown S4* active_buf_id = & smem.active_buf_id;
gknown U4* ordering_buf = r_(smem.ordering_tbl)[active_buf_id[0]];
gknown PrimitiveArena* pa = & smem.primitives;
update(pa, ordering_buf);
render();
gp_display_frame(& smem.screen_buf, active_buf_id, ordering_buf, pa);
}
}
int main(void)
{
smem = (SMemory){0};
// TODO(Ed): remove this field we don't need it in smem.
smem.scratchpad = C_(U4_V, Scratchpad_Loc);
smem.scratchpad = C_(U4_V, 0x1F800000);
// smem.primitives.used = 0;
// smem.active_buf_id = 0;
smem.cam.pos = v3s4(500, -1000, -1500);
/*Persistent Entity Setup*/{
ent_cube128_init(& smem.cube.verts, & smem.cube.faces); {
Ent_Cube* cube = & smem.cube;
@@ -502,10 +328,6 @@ int main(void)
reset_graph(0);
/* Direct BIOS: poll both ports during VBlank. */
pad_bios_init_start(& smem.pad_raw[0], & smem.pad_raw[1]);
/* Pre-build the resolve_look_at bundle atoms into the static arena. */
resolve_look_at_init();
/* Pinned registers for the GPU init atom. */
register U4* io_base_addr rgcc(R_IO_BaseAddr) = u4_r(IO_BASE_ADDR);
register DoubleBuffer* screen_buf rgcc(R_ScreenBuf) = & smem.screen_buf;
@@ -514,15 +336,7 @@ int main(void)
tb_emit(& tb, gp_screen_init);
}
}
while (1) {
gknown S4* active_buf_id = & smem.active_buf_id;
gknown U4* ordering_buf = r_(smem.ordering_tbl)[active_buf_id[0]];
gknown PrimitiveArena* pa = & smem.primitives;
update(pa, ordering_buf);
render();
gp_display_frame(& smem.screen_buf, active_buf_id, ordering_buf, pa);
};
hot_reload_entry();
return 0;
}
GCC_OPTIMIZATION_ENABLE
+8 -8
View File
@@ -21,6 +21,12 @@ enum {
ScreenRes_CenterY = (ScreenRes_Y >> 1),
};
enum {
fp_one = (1 << 12),
};
#define v3s4_fp_one() v3s4(fp_one, fp_one, fp_one)
typedef U4 OrderingTable_Buffer[OrderingTbl_Len];
typedef Array_(OrderingTable_Buffer, 2);
@@ -61,7 +67,7 @@ I_ void ent_cube128_init(A8_V3_S2* verts, A6_V4_S2* faces) {
typedef Struct_(Ent_Cube) {
V3_S4 accel;
V3_S4 vel;
V3_S4 pos; // RGA(Lengyel): affine point with implicit weight one. Storage alias of V3_S4.
V3_S4 pos;
V3_S4 scale;
V3_S2 rot;
A8_V3_S2 verts;
@@ -88,15 +94,9 @@ I_ void ent_floor_init(A4_V3_S2* verts, A2_V3_S2* faces) {
};
typedef Struct_(Ent_Floor) {
V3_S4 accel;
V3_S4 pos; // RGA(Lengyel): affine point with implicit weight one. Storage alias of V3_S4.
V3_S4 pos;
V3_S4 scale;
V3_S2 rot;
A4_V3_S2 verts;
A2_V3_S2 faces;
};
typedef Struct_(Camera) {
P3_S4 pos; // RGA(Lengyel): affine point with implicit weight one. Storage alias of V3_S4.
V3_S2 rot;
MT3_S2S4 look_at;
};
+10 -10
View File
@@ -24,8 +24,8 @@ ATOM_FILE_DEBUGGER_LINE_MARKER(hello_joypad_atom_c);
#pragma region MACs (Mips Atom components)
FI_ Slice_MipsCode ac_put_disp_env(MipsAtomBuilder_R ab, U4 reg_transfer, U4 reg_base, U2 port)
MipsAtomComp_Proc_(ac_put_disp_env, ab, {
FI_ Slice_MipsCode ac_put_disp_env(U4 reg_transfer, U4 reg_base, U2 port)
MipsAtomComp_Proc_(ac_put_disp_env, {
// Emits 5 GP0 commands for buffer 0 (display_area = (0,0,320,240)).
// Sequence per libpsyx PutDispEnv: DrawArea TL → DrawArea BR → Mask → DrawArea TL → DrawArea BR
mac_gcmd_push(gp0_word_draw_area_top_left_origin, reg_transfer, reg_base, port),
@@ -35,8 +35,8 @@ MipsAtomComp_Proc_(ac_put_disp_env, ab, {
mac_gcmd_push(gp0_word_draw_area_bottom_right_320x240, reg_transfer, reg_base, port),
})
FI_ Slice_MipsCode ac_put_draw_env(MipsAtomBuilder_R ab, U4 reg_transfer, U4 reg_base, U2 port)
MipsAtomComp_Proc_(ac_put_draw_env, ab, {
FI_ Slice_MipsCode ac_put_draw_env(U4 reg_transfer, U4 reg_base, U2 port)
MipsAtomComp_Proc_(ac_put_draw_env, {
/*
* ORIGIN: each code word corresponds to the EXACT value libpsyx's PutDrawEnv function would compute for the same DrawEnv settings.
* References:
@@ -116,7 +116,7 @@ internal MipsAtom_(screen_env_init) atom_info(atom_phase(screen_init)
store_word(R_0, R_ScreenBuf, O_(DisplayEnv,vinterlace) + OA_(DoubleBuffer,display,1)),
mac_store_rects2(R_0, R_ScreenY, R_ScreenX, R_ScreenY, R_ScreenBuf, O_(DrawEnv,clip_area) + OA_(DoubleBuffer,draw,0)), /* draw[0].clip_area = (0, 240, 320, 240). C11's SetDefDrawEnv writes clip.y = y_arg. */
mac_store_v2s2(R_0, R_ScreenY, R_ScreenBuf, O_(DrawEnv,drawing_offset[0]) + OA_(DoubleBuffer,draw,0)), /* draw[0].drawing_offset[0] = (0, 240); C11 passes y_arg as ofs. */
mac_store_v2s2( R_0, R_ScreenY, R_ScreenBuf, O_(DrawEnv,drawing_offset[0]) + OA_(DoubleBuffer,draw,0)), /* draw[0].drawing_offset[0] = (0, 240); C11 passes y_arg as ofs. */
mac_store_v2s2(R_ScreenX, R_ScreenY, R_ScreenBuf, O_(DrawEnv,clip_area.width) + OA_(DoubleBuffer,draw,1)),
@@ -129,7 +129,7 @@ internal MipsAtom_(screen_env_init) atom_info(atom_phase(screen_init)
store_word(R_0, R_ScreenBuf, O_(DrawEnv,texture_window.width) + OA_(DoubleBuffer,draw,1)),
/* draw[0].texture_page = 10 (gp0_tpage_default). C11 SetDefDrawEnv at C11_only.elf:0x8001273C writes the same 0x0A. . */
add_ui(R_T0, R_0, gp0_tpage_default),
add_ui(R_T0, R_0, gp0_tpage_default),
store_half(R_T0, R_ScreenBuf, O_(DrawEnv,texture_page) + OA_(DoubleBuffer,draw,0)),
store_half(R_T0, R_ScreenBuf, O_(DrawEnv,texture_page) + OA_(DoubleBuffer,draw,1)),
@@ -144,7 +144,7 @@ internal MipsAtom_(screen_env_init) atom_info(atom_phase(screen_init)
store_byte(R_T0, R_ScreenBuf, O_(DrawEnv,enable_auto_clear) + OA_(DoubleBuffer,draw,1)),
/* draw[0].initial_bg_color = (r=7, g=7, b=7). */
add_ui(R_T0, R_0, 7),
add_ui(R_T0, R_0, 7),
mac_store_rgb8(R_T0,R_T0,R_T0, R_ScreenBuf, O_(DrawEnv,initial_bg_color) + OA_(DoubleBuffer,draw,0)),
mac_store_rgb8(R_T0,R_T0,R_T0, R_ScreenBuf, O_(DrawEnv,initial_bg_color) + OA_(DoubleBuffer,draw,1)),
@@ -228,7 +228,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).
* If branch IS taken (face culled), the body is skipped and this 0-tag is stranded —
* 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. */
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),
@@ -286,14 +286,14 @@ MipsAtom_(floor_f3_face) atom_info(atom_phase(floor_f3)
, atom_reads( R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase)
, atom_writes(R_PrimCursor, R_FaceCursor)
) {
mac_load_tri_indices(R_FaceCursor, R_T0, R_T1, R_T2),
mac_load_tri_indices( R_FaceCursor, R_T0, R_T1, R_T2),
mac_gte_load_tri_verts(R_VertBase, R_T0, R_T1, R_T2),
nop2, gte_cmdw_rotate_translate_perspective_triple, // 2 nops retire the final cpu -> gte writes before RTPT
gte_cmdw_nclip,
/* Culling (Branch forward if Backface) */
gte_mv_from_data_r(R_T0, C2_MAC0),
nop, branch_le_zero(R_T0, atom_offset(culling, floor_f3_face_exit)), nop, // required gte -> cpu load-delay slot.
nop, branch_le_zero(R_T0, atom_offset(culling, floor_f3_face_exit)), nop, // required gte -> cpu load-delay slot.
/* Format Primitive */
mac_gte_store_f3(R_PrimCursor),
+2 -2
View File
@@ -24,8 +24,8 @@
* Emits 9 instructions (status/buttons/axes/attempt stores plus the
* two-instruction zero-extended buttons load).
*/
FI_ Slice_MipsCode ac_pad_sio_write_pad_state(MipsAtomBuilder_R ab, U4 status_val, U4 state_ptr_reg, U4 scratch_reg)
MipsAtomComp_Proc_(ac_pad_sio_write_pad_state, ab, {
FI_ Slice_MipsCode ac_pad_sio_write_pad_state(U4 status_val, U4 state_ptr_reg, U4 scratch_reg)
MipsAtomComp_Proc_(ac_pad_sio_write_pad_state, {
add_ui(scratch_reg, R_0, status_val),
store_word(scratch_reg, state_ptr_reg, O_(PadState,status)),
/* FIX 2026-08-02: buttons = 0x0000FFFF = "no buttons pressed" in
+271 -51
View File
@@ -1,3 +1,13 @@
# --- Parameter Surface (Task 8) -----------------------------------------
# -Reload : After a successful build, invoke reload.ps1 as a child pwsh and propagate its exit code.
# -HelperZipOnly : Skip the build entirely; regenerate the helper zip and exit. Honors -HelperZipOutput for out-of-tree paths.
# -HelperZipOutput: When -HelperZipOnly is set, writes the archive to this path instead of the scripts/pcsx_debug_helper.zip.
param(
[switch]$Reload,
[switch]$HelperZipOnly,
[string]$HelperZipOutput = ''
)
$path_root = split-path -Path $PSScriptRoot -Parent
$path_build = join-path $path_root 'build'
$path_code = join-path $path_root 'code'
@@ -8,6 +18,98 @@ if ((test-path $path_build) -eq $false) {
new-item -itemtype directory -path $path_build
}
# --- HelperZipOnly short-circuit ----------------------------------------
# Must run before any compile/link work.
# Inlines the same logic as Make-HelperZip below to avoid an extra pwsh process spawn (~200 ms).
#The helper zip is small and the BCL call is in-process; cold ~14 ms, warm ~10 ms (assembly load + tiny zip write).
if ($HelperZipOnly) {
$zipDest = if ([string]::IsNullOrEmpty($HelperZipOutput)) {
join-path $path_scripts 'pcsx_debug_helper.zip'
}
else {
$HelperZipOutput
}
$HelperDir = join-path $path_scripts 'pcsx_debug_helper'
$elf32Src = join-path $path_scripts 'elf32.lua'
$elf32Dest = join-path $HelperDir 'elf32.lua'
if (-not (test-path -LiteralPath $HelperDir)) {
write-error "helper dir not found: $HelperDir"
exit 1
}
if (-not (test-path -LiteralPath $elf32Src)) {
write-error "elf32.lua not found at $elf32Src"
exit 1
}
write-host "[build] HelperZipOnly mode -> $zipDest"
# --- Timestamp gate (Fix 1) -------------------------------------------
# PCSX-Redux holds pcsx_debug_helper.zip open via -archive at startup.
# The zip is consumed once at startup; the reload endpoint reads it
# from package.loaded on subsequent calls. Writing it on every build
# is dead work that fights the file lock. Skip the rewrite when the
# three sources (autoexec.lua, reload.lua, elf32.lua) are all older
# than the existing zip.
$sources = @(
(join-path $HelperDir 'autoexec.lua'),
(join-path $HelperDir 'reload.lua'),
$elf32Src
)
$zipMtime = $null
if (test-path -LiteralPath $zipDest) {
$zipMtime = (Get-Item -LiteralPath $zipDest).LastWriteTime
}
$needsRewrite = $false
if ($null -eq $zipMtime) {
$needsRewrite = $true
}
else {
foreach ($s in $sources) {
if (-not (test-path -LiteralPath $s)) { continue }
if ((Get-Item -LiteralPath $s).LastWriteTime -gt $zipMtime) {
$needsRewrite = $true
break
}
}
}
if (-not $needsRewrite) {
$sz = (Get-Item -LiteralPath $zipDest).Length
Write-Host "[build] helper zip up to date: $zipDest ($sz bytes); skipping"
return
}
Copy-Item -LiteralPath $elf32Src -Destination $elf32Dest -Force
try {
# Force the inode release so CreateFromDirectory can write fresh.
# ZipFile.CreateFromDirectory throws if the destination exists.
# If PCSX-Redux holds the file open, Remove-Item raises — fall
# back to writing pcsx_debug_helper.zip.new alongside. The next
# PCSX-Redux restart will read the canonical path; the .new file
# is a hint for the optional launch-script patch in fix 3.
if (test-path -LiteralPath $zipDest) {
try {
# -ErrorAction Stop is required so the catch below fires.
# Remove-Item raises a non-terminating error by default
# (ErrorActionPreference=Continue), which bypasses catch.
Remove-Item -LiteralPath $zipDest -Force -ErrorAction Stop
}
catch {
$zipDest = [System.IO.Path]::ChangeExtension($zipDest, '.zip.new')
Write-Warning "[build] canonical helper zip is locked; writing to $zipDest instead"
}
}
Add-Type -AssemblyName System.IO.Compression.FileSystem
[System.IO.Compression.ZipFile]::CreateFromDirectory(
$HelperDir, $zipDest,
[System.IO.Compression.CompressionLevel]::Optimal, $false) | Out-Null
$sz = (Get-Item -LiteralPath $zipDest).Length
Write-Host "[build] wrote $sz bytes to $zipDest"
}
finally {
if (test-path -LiteralPath $elf32Dest) { Remove-Item -LiteralPath $elf32Dest -Force }
}
return
}
# --- Toolchain Definition ---
# Assumes 'mipsel-none-elf' toolchain is in your system's PATH.
$Prefix = "mipsel-none-elf"
@@ -180,9 +282,12 @@ function link-modules { param([string[]]$link_modules, [string] $elf, [string[]
$link_args += ($f_link_pass_through_prefix + $f_link_mapfile + $map)
$link_args += ($f_link_pass_through_prefix + $f_link_start_group)
# 16 removed entries (c2, card, cd, comb, ds, gs, gun, hmd, math, mcrd, mcx, press, sio, snd, spu, tap)
# had LOAD lines in the map but ZERO .o files pulled in — they were unused.
# 5 kept libraries (api, c, etc, gpu, gte) are required by the C-side calls in hello_joypad.c (reset_graph, draw_sync, vsync, etc.).
# raw_sio_pad_poll_20260802 — Task 5.1c surgical library-list trim.
# The 16 removed entries (c2, card, cd, comb, ds, gs, gun, hmd, math,
# mcrd, mcx, press, sio, snd, spu, tap) had LOAD lines in the map but
# ZERO .o files pulled in — they were unused. The 5 kept libraries
# (api, c, etc, gpu, gte) are required by the C-side calls in
# hello_joypad.c (reset_graph, draw_sync, vsync, etc.).
$libraries = @(
"api",
"c",
@@ -211,32 +316,68 @@ function link-modules { param([string[]]$link_modules, [string] $elf, [string[]
function make-binary { param([string]$elf, [string]$exe)
Write-Host "--- Creating Binary ---" -ForegroundColor Cyan
write-host "Converting $elf to PS-EXE -> '$exe'"
$objcopy_args = ($f_objcopy_format + "binary"), $elf, $exe
$objcopy_args = ($f_objcopy_format + "binary"), $elf, $exe
& $Objcopy $objcopy_args
if ($LASTEXITCODE -ne 0) { Write-Error "Objcopy failed. Aborting."; exit 1 }
}
function ps1-meta { param(
[string]$unity_root,
[string] $unity_root,
[string[]]$sources,
[Parameter(Mandatory=$true)][string]$metadata,
[string]$out_root = (join-path $path_build 'gen'),
[string[]]$passes = @('--pre-link'),
[string] $out_root = (join-path $path_build 'gen'),
[string[]]$passes = @('--pre-link'),
[string[]]$extra_args = @()
)
# `--unity-root` and `--source` are mutually exclusive. Exactly one of `$unity_root` / `$sources` must be supplied; the other must be absent.
# `--unity-root` and `--source` are
# mutually exclusive. Exactly one of `$unity_root` / `$sources` must
# be supplied; the other must be absent.
if ($null -ne $unity_root -and $unity_root -ne '')
{
if ($null -ne $sources -and $sources.Count -gt 0) {
write-error 'ps1-meta: -unity_root and -sources are mutually exclusive'
exit 2
}
}
}
elseif ($null -eq $sources -or $sources.Count -eq 0) {
write-error 'ps1-meta: either -unity_root <file> or -sources <file...> is required'
exit 2
}
# --- Defensive attribute clear on tracked gen files ------------------------
# Git tracks code/<dir>/gen/*.h files and Windows keeps the Archive bit set
# on them. Combined with transient editor locks or co-running processes,
# this can make io.open(path, "wb") fail with Access Denied / Sharing
# Violation even though Get-ChildItem shows IsReadOnly = False. Clearing
# the Read-only + Archive bits locally is safe; git re-asserts them on
# the next operation but the metaprogram write always wins.
#
# Derived from the caller's parameters: $metadata lives in $path_duffle
# (so its parent is the duffle dir), and $unity_root / $sources[0] lives
# in $path_module (so its parent is the module dir).
$pathToDuffle = split-path -Path $metadata -Parent
$pathToModule = $null
if ($null -ne $unity_root -and $unity_root -ne '') {
$pathToModule = split-path -Path $unity_root -Parent
}
elseif ($null -ne $sources -and $sources.Count -gt 0) {
$pathToModule = split-path -Path $sources[0] -Parent
}
$genFiles = @(
join-path $pathToDuffle 'gen\macs.h'
join-path $pathToDuffle 'gen\offsets.h'
)
if ($null -ne $pathToModule) {
$genFiles += join-path $pathToModule 'gen\macs.h'
$genFiles += join-path $pathToModule 'gen\offsets.h'
}
foreach ($f in $genFiles) {
if (test-path -LiteralPath $f) {
attrib -R $f 2>&1 | Out-Null
attrib -A $f 2>&1 | Out-Null
}
}
$script = join-path $path_scripts 'ps1_meta.lua'
$input_summary = if ($null -ne $unity_root -and $unity_root -ne '') {
"unity=$unity_root"
@@ -265,14 +406,14 @@ function inject-dwarf { param(
[string]$path_gen
)
$base_name = [System.IO.Path]::GetFileNameWithoutExtension($elf)
$path_dwarf_line_bin = join-path $path_gen "$base_name.dwarf_line.bin"
$path_dwarf_aranges_bin = join-path $path_gen "$base_name.dwarf_aranges.bin"
$path_dwarf_rnglists_bin = join-path $path_gen "$base_name.dwarf_rnglists.bin"
$path_dwarf_info_bin = join-path $path_gen "$base_name.dwarf_info.bin"
$path_dwarf_abbrev_bin = join-path $path_gen "$base_name.dwarf_abbrev.bin"
$path_dwarf_str_bin = join-path $path_gen "$base_name.dwarf_str.bin"
$path_dwarf_loc_bin = join-path $path_gen "$base_name.dwarf_loc.bin"
$path_dwarf_loclists_bin = join-path $path_gen "$base_name.dwarf_loclists.bin"
$path_dwarf_line_bin = join-path $path_gen "$base_name.dwarf_line.bin"
$path_dwarf_aranges_bin = join-path $path_gen "$base_name.dwarf_aranges.bin"
$path_dwarf_rnglists_bin = join-path $path_gen "$base_name.dwarf_rnglists.bin"
$path_dwarf_info_bin = join-path $path_gen "$base_name.dwarf_info.bin"
$path_dwarf_abbrev_bin = join-path $path_gen "$base_name.dwarf_abbrev.bin"
$path_dwarf_str_bin = join-path $path_gen "$base_name.dwarf_str.bin"
$path_dwarf_loc_bin = join-path $path_gen "$base_name.dwarf_loc.bin"
$path_dwarf_loclists_bin = join-path $path_gen "$base_name.dwarf_loclists.bin"
$path_inject_elf = join-path $path_build "$base_name.dwarf-injected.elf"
if (-not (Test-Path $path_dwarf_line_bin)) { return }
@@ -517,7 +658,7 @@ function build-hello_camera {
$path_build_gen = join-path $path_build 'gen'
$src_c = join-path $path_module 'hello_camera.c'
ps1-meta -unity_root $src_c -metadata $path_atom_metadata -out_root $path_build_gen -passes @('--pre-link')
ps1-meta -unity_root $src_c -metadata $path_atom_metadata -out_root $path_build_gen
$assemble_args = @()
$assemble_args += $f_debug
@@ -552,50 +693,129 @@ function build-hello_camera {
link-modules $link_modules $elf $link_args
make-binary $elf $exe
# Post-link: gdb-runtime + dwarf-injection in a single Lua invocation (one luajit cold start).
ps1-meta -unity_root $src_c -metadata $path_atom_metadata -out_root $path_build_gen -passes @('--post-link') ` -extra_args @('--elf', $elf)
inject-dwarf $elf $path_build_gen
}
build-hello_camera
# NO idea if this works yet...
function Send-ToEmulator { param( [string]$exePath )
$uri = "http://localhost:8080/api/v1/load-exec"
# ── Helper-zip + reload helpers (Task 8) ──
# Defined right after the final build-hello_camera function so they're in scope for the post-build calls below.
# The Make-HelperZip function is also reused by the -HelperZipOnly short-circuit at the top of this script.
# Both call the in-process BCL CreateFromDirectory rather than spawning a child pwsh to avoid the ~200 ms process-spawn overhead.
function Make-HelperZip {
param([string]$OutputPath = '')
# Absolute path is safest for the emulator web server
$absolutePath = [System.IO.Path]::GetFullPath($exePath)
$dest = if ([string]::IsNullOrEmpty($OutputPath)) {
join-path $path_scripts 'pcsx_debug_helper.zip'
}
else {
$OutputPath
}
# Create JSON payload pointing to your compiled .ps-exe
$body = @{ filename = $absolutePath } | ConvertTo-Json
$HelperDir = join-path $path_scripts 'pcsx_debug_helper'
$elf32Src = join-path $path_scripts 'elf32.lua'
$elf32Dest = join-path $HelperDir 'elf32.lua'
if (-not (test-path -LiteralPath $HelperDir)) {
write-warning "[build] helper dir not found: $HelperDir; skipping helper zip"
return
}
if (-not (test-path -LiteralPath $elf32Src)) {
write-warning "[build] elf32.lua not found at $elf32Src; skipping helper zip"
return
}
Write-Host "Pushing hot-reload to PCSX-Redux..." -ForegroundColor Magenta
try {
$response = Invoke-RestMethod -Uri $uri -Method Post -Body $body -ContentType "application/json"
Write-Host "Hot-reload successful!" -ForegroundColor Green
} catch {
Write-Warning "Could not connect to PCSX-Redux web server. Ensure the emulator is running and Web Server is enabled."
}
# --- Timestamp gate (Fix 1) -------------------------------------------
# PCSX-Redux holds pcsx_debug_helper.zip open via -archive at startup.
# The zip is consumed once at startup; the reload endpoint reads it
# from package.loaded on subsequent calls. Writing it on every build
# is dead work that fights the file lock. Skip the rewrite when the
# three sources (autoexec.lua, reload.lua, elf32.lua) are all older
# than the existing zip.
$sources = @(
(join-path $HelperDir 'autoexec.lua'),
(join-path $HelperDir 'reload.lua'),
$elf32Src
)
$zipMtime = $null
if (test-path -LiteralPath $dest) {
$zipMtime = (Get-Item -LiteralPath $dest).LastWriteTime
}
$needsRewrite = $false
if ($null -eq $zipMtime) {
$needsRewrite = $true
}
else {
foreach ($s in $sources) {
if (-not (test-path -LiteralPath $s)) { continue }
if ((Get-Item -LiteralPath $s).LastWriteTime -gt $zipMtime) {
$needsRewrite = $true
break
}
}
}
if (-not $needsRewrite) {
$sz = (Get-Item -LiteralPath $dest).Length
Write-Host "[build] helper zip up to date: $dest ($sz bytes); skipping"
return
}
write-host "[build] regenerating helper zip -> $dest"
Copy-Item -LiteralPath $elf32Src -Destination $elf32Dest -Force
try {
# Force the inode release so CreateFromDirectory can write fresh.
# ZipFile.CreateFromDirectory throws if the destination exists.
# If PCSX-Redux holds the file open, Remove-Item raises — fall
# back to writing pcsx_debug_helper.zip.new alongside. The next
# PCSX-Redux restart will read the canonical path; the .new file
# is a hint for the optional launch-script patch in fix 3.
if (test-path -LiteralPath $dest) {
try {
# -ErrorAction Stop is required so the catch below fires.
# Remove-Item raises a non-terminating error by default
# (ErrorActionPreference=Continue), which bypasses catch.
Remove-Item -LiteralPath $dest -Force -ErrorAction Stop
}
catch {
$dest = [System.IO.Path]::ChangeExtension($dest, '.zip.new')
Write-Warning "[build] canonical helper zip is locked; writing to $dest instead"
}
}
Add-Type -AssemblyName System.IO.Compression.FileSystem
[System.IO.Compression.ZipFile]::CreateFromDirectory(
$HelperDir, $dest,
[System.IO.Compression.CompressionLevel]::Optimal, $false) | Out-Null
$sz = (Get-Item -LiteralPath $dest).Length
Write-Host "[build] wrote $sz bytes to $dest"
}
finally {
if (test-path -LiteralPath $elf32Dest) { Remove-Item -LiteralPath $elf32Dest -Force }
}
}
# # Automatically hot-reloads it into the running emulator
# Send-ToEmulator (join-path $path_build 'hello_gte.ps-exe')
# Invokes reload.ps1 as a child pwsh instead of POSTing to the nonexistent /api/v1/load-exec endpoint.
# Exit code is propagated so the build fails loud if the reload fails.
function Send-ToEmulator {
param([string]$ElfPath = (join-path $path_build 'hello_camera.elf'))
# --- Hot Reload via PCSX-Redux Web Server ---
# $exe_path = join-path $path_build 'hello_gte.ps-exe'
# $absolute_path = [System.IO.Path]::GetFullPath($exe_path)
$reloadScript = join-path $path_scripts 'reload.ps1'
if (-not (test-path -LiteralPath $reloadScript)) {
write-error "[build] reload.ps1 not found at $reloadScript"
exit 1
}
# PCSX-Redux expects the file location in the URL query string?
# We URL-encode the path to ensure backslashes and spaces don't break the HTTP request?
# $encoded_path = [uri]::EscapeDataString($absolute_path)
# $uri = "http://localhost:8080/api/v1/load-exec?path=$encoded_path"
write-host "[build] hot-reloading $ElfPath via reload.ps1" -ForegroundColor Magenta
& pwsh -NoProfile -File $reloadScript -Mode elf -Target hello_camera -ElfPath $ElfPath
if ($LASTEXITCODE -ne 0) {
write-error "[build] reload.ps1 failed (exit $LASTEXITCODE)"
exit $LASTEXITCODE
}
}
# Write-Host "Pushing hot-reload to PCSX-Redux..." -ForegroundColor Magenta
# try {
# # Send the request with the query string included
# Invoke-RestMethod -Uri $uri -Method Post
# Write-Host "Hot-reload successful!" -ForegroundColor Green
# } catch {
# Write-Host "Failed to hot-reload." -ForegroundColor Red
# # This will print the *actual* HTTP error instead of our generic warning
# Write-Host $_.Exception.Message -ForegroundColor Yellow
# }
# Post-build: Regenerate the helper zip (canonical output) and, if -Reload was passed, kick a hot-reload against the just-built ELF.
# Any future targets compiled by this script should add their own Make-HelperZip call after their build step; today's only target is hello_camera.
Make-HelperZip
if ($Reload) {
Send-ToEmulator
}
+14 -68
View File
@@ -217,7 +217,7 @@ local function parse_path_root(input)
if not server_end or server_end == server_start then
error("UNC path requires //server/share: " .. input, 3)
end
local server = input:sub(server_start, server_end - 1)
local server = input:sub(server_start, server_end - 1)
local share_start = server_end + 1
while input:sub(share_start, share_start) == "/" do
share_start = share_start + 1
@@ -515,7 +515,8 @@ local function splice_c_lines(source)
local splice_len = nil
if byte == BYTE_BACKSLASH and source:byte(pos + 1) == BYTE_NEWLINE then
splice_len = 2
elseif byte == BYTE_BACKSLASH and source:byte(pos + 1) == BYTE_CR and source:byte(pos + 2) == BYTE_NEWLINE then
elseif byte == BYTE_BACKSLASH and source:byte(pos + 1) == BYTE_CR
and source:byte(pos + 2) == BYTE_NEWLINE then
splice_len = 3
end
@@ -1052,8 +1053,6 @@ 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.
@@ -1137,14 +1136,6 @@ 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.
@@ -1217,22 +1208,6 @@ 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.
@@ -1295,22 +1270,6 @@ 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.
@@ -1326,7 +1285,6 @@ 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},
@@ -1396,8 +1354,6 @@ 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.
@@ -1479,10 +1435,8 @@ 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
@@ -1500,7 +1454,6 @@ 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,
@@ -1544,8 +1497,6 @@ 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
@@ -1826,7 +1777,6 @@ 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} },
@@ -1943,8 +1893,6 @@ 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`.
@@ -1955,19 +1903,18 @@ 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, },
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, },
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, },
-- 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.
@@ -2094,8 +2041,7 @@ local E_MAC_PREFIX_LEN = 4
--- * Unknown `mac_X` (not in `component_index`): fall back to `word_counts[ident]` if present; otherwise emit one opaque event so the cycle budget accounts for the word.
--- * Marker Tokens (`atom_label(...)` / `atom_offset(...)`): Zero events (they are pure metaprogram hints).
---
--- Cycle protection: a per-expansion `visiting` set tracks components currently on the expansion stack;
--- a re-entry produces a deterministic `{kind = "cycle", ...}` error and aborts that branch (does NOT hang, does NOT recurse).
--- Cycle protection: a per-expansion `visiting` set tracks components currently on the expansion stack; a re-entry produces a deterministic `{kind = "cycle", ...}` error and aborts that branch (does NOT hang, does NOT recurse).
---
--- Pure: reads `body_entry` / `component_index` / `word_counts`. Memoization is the caller's responsibility.
--- Callers wanting `word_events` / `word_event_errors` precomputed for many atoms should memoize them per atom.
+3 -2
View File
@@ -47,14 +47,15 @@ local function find_repo_root()
return root
end
--- Set `package.path` (for `require("duffle")` + `require("passes.X")`) and `package.cpath` (for `lpeg.dll`).
--- Set `package.path` (for `require("duffle")` + `require("passes.X")`) and
--- `package.cpath` (for `lpeg.dll`).
---
--- This script does NOT touch the OS environment: no `os.setenv`, no `os.putenv`, no `$PATH` mods.
--- It just sets `package.path` and `package.cpath` (the standard Lua way to register module search dirs).
--- lpeg is built by `update_deps.ps1` to `toolchain/lpeg/`,
--- which we wire into `package.cpath` here (so `require("lpeg")` from `duffle.lua` resolves without any global state).
function M.setup()
local repo_root = find_repo_root()
local repo_root = find_repo_root()
if not repo_root then
-- Unreachable in practice: find_repo_root() derives the repo root from this script's
-- own source path via debug.getinfo(1, "S").source (no subprocess, no git CLI, <1ms).
-418
View File
@@ -1,418 +0,0 @@
-- elf32.lua — Pure-Lua ELF32 format helpers with no lfs / no lpeg dependency.
-- The reload helper's `parse_manifest` (scripts/pcsx_debug_helper/reload.lua)
-- and the metaprogram's `read_elf_sections` + `read_nm` (scripts/elf_dwarf.lua)
-- both parsed ELF32 headers from wire bytes.
--
-- This module contains the format constants and the byte-level walker.
--- The metaprogram side keeps `read_u32_le` / `read_u16_le` as local forwarders; the helper side calls `E.*` directly.
--
-- **Adapter contract (explicit pass style):**
-- The helper VM's `Support.File` exposes byte-read methods that require `self` (fileffi.lua:225-227),
-- so callers wrap once in a 1-line adapter that strips `self`.
-- The parsers here operate on the unwrapped form.
-- Reads are flat function calls — `E.read_u8(adapter, off)`, `E.read_u32(adapter, off)`, `E.size(adapter)`.
-- read_u8(adapter, off) -> integer | nil
-- read_u16(adapter, off) -> integer | nil
-- read_u32(adapter, off) -> integer | nil
-- size(adapter) -> integer
--
-- **Convention:** every offset in the constants tables is a zero-based wire offset.
-- The `+ 1` conversion happens only at the `string.byte` boundary inside the readers.
--
-- spec: System V ABI gABI v1.2 §"ELF Header" (Table 1) + §"Section Header Table"
-- spec: System V ABI gABI v1.2 §"Symbol Table" (Elf32_Sym layout)
local M = {}
-- ════════════════════════════════════════════════════════════════════════════
-- Little-endian readers (bit-weighted accumulator, math.floor only)
-- ════════════════════════════════════════════════════════════════════════════
--- Read a 4-byte little-endian unsigned integer from `adapter` at zero-based wire offset `off`.
---
--- Bit weights are written as `0x100`, `0x10000`, `0x1000000` (i.e. 2^8, 2^16, 2^24) so the LE byte positions are visually explicit:
--- byte 0 contributes its value directly;
--- byte 1 is shifted left by 8; byte 2 by 16; byte 3 by 24.
---
--- math.floor (not LuaJIT's `>>`) keeps the body portable across LuaJIT 2.0/2.1 and plain Lua 5.x. `string.byte` receives `+ 1` at the boundary.
---
--- **Call form:** explicit-pass. The reader receives `adapter` as the first positional argument and the offset as the second; no `self` is passed.
--- Test fixtures declare `function(offset) ... end` and the parsers call them via dot syntax `adapter.read_u8_at(off)`.
--- The colon form `adapter:read_u8_at(off)` would prepend the adapter table as `offset` and break the contract.
--- @param adapter table
--- @param off integer -- zero-based wire offset
--- @return integer|nil
function M.read_u32(adapter, off)
return adapter.read_u8_at(off)
+ adapter.read_u8_at(off + 0x01) * 0x00000100
+ adapter.read_u8_at(off + 0x02) * 0x00010000
+ adapter.read_u8_at(off + 0x03) * 0x01000000
end
--- Read a 2-byte little-endian unsigned integer from `adapter` at zero-based wire offset `off`.
--- @param adapter table
--- @param off integer -- zero-based wire offset
--- @return integer|nil
function M.read_u16(adapter, off)
return adapter.read_u8_at(off)
+ adapter.read_u8_at(off + 0x01) * 0x00000100
end
--- Read a 1-byte unsigned integer from `adapter` at zero-based wire offset `off`.
--- @param adapter table
--- @param off integer -- zero-based wire offset
--- @return integer|nil
function M.read_u8(adapter, off)
return adapter.read_u8_at(off)
end
--- Total adapter byte length.
--- @param adapter table
--- @return integer
function M.size(adapter)
return adapter.read_size()
end
--- Forwarders kept for backward compat with scripts/elf_dwarf.lua.
--- The metaprogram side keeps `read_u32_le` / `read_u16_le`;
--- both layers now use the same byte-level helpers under the hood.
function M.read_u32_le(buf, off)
local byte_off = off + 1
return buf:byte(byte_off)
+ buf:byte(byte_off + 0x01) * 0x00000100
+ buf:byte(byte_off + 0x02) * 0x00010000
+ buf:byte(byte_off + 0x03) * 0x01000000
end
--- Read a 2-byte little-endian unsigned integer from `buf` at zero-based wire offset `off`.
--- @param buf string
--- @param off integer -- zero-based wire offset
--- @return integer
function M.read_u16_le(buf, off)
local byte_off = off + 1
return buf:byte(byte_off) + buf:byte(byte_off + 0x01) * 0x00000100
end
-- ════════════════════════════════════════════════════════════════════════════
-- Format constants
-- ════════════════════════════════════════════════════════════════════════════
-- ELF format constants (System V ABI gABI v1.2).
M.ELFCLASS32 = 1 -- spec: gABI v1.2 §"ELF Header" — EI_CLASS byte
M.ELFDATA2LSB = 1 -- spec: gABI v1.2 §"ELF Header" — EI_DATA byte
M.EM_MIPS = 8 -- spec: gABI v1.2 §"Machine Information" — MIPS architecture
-- Section type constants (System V ABI gABI v1.2 §"Section Header Table").
M.SHT_SYMTAB = 2 -- spec: gABI v1.2 §"Section Types" — symbol table
M.SHT_STRTAB = 3 -- spec: gABI v1.2 §"Section Types" — string table
M.SHT_NOBITS = 8 -- spec: gABI v1.2 §"Section Types" — no space in file
-- Section flag constants (System V ABI gABI v1.2 §"Section Header Table").
M.SHF_WRITE = 0x1 -- spec: gABI v1.2 §"Section Attributes" — writable
M.SHF_ALLOC = 0x2 -- spec: gABI v1.2 §"Section Attributes" — occupies memory
M.SHF_EXECINSTR = 0x4 -- spec: gABI v1.2 §"Section Attributes" — executable
-- ---------------------------------------------------------------------------
-- ELF32 header layout (System V ABI gABI v1.2 §"ELF Header" Table 1)
-- ---------------------------------------------------------------------------
-- All offsets are zero-based wire offsets. The header is 52 bytes total (header_bytes = 0x34 = 52).
M.ELF32_HEADER = {
magic_offset = 0x00, -- 4 bytes; expected "\127ELF"
magic = "\127ELF",
class_offset = 0x04, -- 1 byte; 1 = ELF32, 2 = ELF64
endian_offset = 0x05, -- 1 byte; 1 = little-endian, 2 = big-endian
header_bytes = 0x34, -- ELF32 header is 52 bytes total
e_entry_offset = 0x18, -- 4-byte LE; entry-point virtual address
e_shoff_offset = 0x20, -- 4-byte LE; section-header table file offset
e_shentsize_offset = 0x2E, -- 2-byte LE; section-header entry size in bytes
e_shnum_offset = 0x30, -- 2-byte LE; number of section headers
e_shstrndx_offset = 0x32, -- 2-byte LE; index of section-name string table
}
-- ---------------------------------------------------------------------------
-- ELF32 section-header layout (System V ABI gABI v1.2 §"Section Header Table")
-- ---------------------------------------------------------------------------
-- Each entry is 40 bytes (sh_entsize_bytes = 0x28 = 40);
-- zero-based, field offsets relative to the start of the entry.
M.ELF32_SECTION = {
sh_name_offset = 0x00, -- 4-byte LE; offset into .shstrtab
sh_type_offset = 0x04, -- 4-byte LE; section type (SHT_*)
sh_flags_offset = 0x08, -- 4-byte LE; section flags (SHF_*)
sh_addr_offset = 0x0C, -- 4-byte LE; virtual address at execution
sh_offset_offset = 0x10, -- 4-byte LE; section's file offset
sh_size_offset = 0x14, -- 4-byte LE; section's size in bytes
sh_link_offset = 0x18, -- 4-byte LE; link to a related section
sh_entsize_bytes = 0x28, -- spec: gABI v1.2 §"Section Header Table" — 40 bytes per entry
}
-- ---------------------------------------------------------------------------
-- ELF32 symbol-table entry layout (System V ABI gABI v1.2 §"Symbol Table")
-- ---------------------------------------------------------------------------
-- Each entry is 16 bytes (sym_entry_bytes = 0x10 = 16);
-- zero-based, field offsets relative to the start of the entry.
M.ELF32_SYM = {
st_name = 0x00, -- 4-byte LE; offset into the linked string table
st_value = 0x04, -- 4-byte LE; symbol value (address / absolute)
st_size = 0x08, -- 4-byte LE; symbol size in bytes
st_info = 0x0C, -- 1 byte; binding (high nibble) + type (low nibble)
sym_entry_bytes = 0x10, -- spec: gABI v1.2 §"Symbol Table" — 16 bytes per entry
}
-- DWARF32 initial-length terminator (DWARF4 §7.4) — kept here so the metaprogram's elf_dwarf.lua can drop its own copy of the same constant.
M.dw_dwarf32_terminator = 0xFFFFFFFF
-- ════════════════════════════════════════════════════════════════════════════
-- Adapter validation
-- ════════════════════════════════════════════════════════════════════════════
--- Validate that `adapter` exposes the byte-read surface.
--- Returns true on success, false + a stable error code on failure.
--- The helper side calls this before parse_manifest to reject callers before any byte is read.
--- @param adapter any
--- @return boolean, string|nil
function M.validate_adapter(adapter)
if type(adapter) ~= "table" then return false, "bad_file_adapter" end
if type(adapter.read_u8_at) ~= "function" then return false, "bad_file_adapter" end
if type(adapter.read_u16_at) ~= "function" then return false, "bad_file_adapter" end
if type(adapter.read_u32_at) ~= "function" then return false, "bad_file_adapter" end
if type(adapter.read_size) ~= "function" then return false, "bad_file_adapter" end
return true, nil
end
-- ════════════════════════════════════════════════════════════════════════════
-- String-table reader
-- ════════════════════════════════════════════════════════════════════════════
--- Extract a NUL-terminated C string from `strtab` at zero-based offset `off`.
--- Returns nil if `off` is out of range or the string is not NUL-terminated.
--- @param strtab string
--- @param off integer
--- @return string|nil
function M.get_str(strtab, off)
if off < 0 or off >= #strtab then return nil end
local end_pos = strtab:find("\0", off + 1, true)
if not end_pos then return nil end
return strtab:sub(off + 1, end_pos - 1)
end
-- ════════════════════════════════════════════════════════════════════════════
-- Header / section / symbol walkers
-- ════════════════════════════════════════════════════════════════════════════
--- Read the ELF32 header through `adapter` and validate the magic, class, and data encoding.
--- Returns a table on success:
--- { e_entry, e_shoff, e_shentsize, e_shnum, e_shstrndx, error = nil }
--- On failure returns nil + a stable error code:
--- bad_magic, unsupported_elf_class, unsupported_elf_data, truncated_header
--- The header's machine field is NOT validated here — callers (e.g. the helper's prime path) decide whether to require EM_MIPS before symbol reads.
--- @param adapter table
--- @return table|nil, string|nil
function M.parse_elf32_headers(adapter)
local ok, err = M.validate_adapter(adapter)
if not ok then return nil, err end
-- 4-byte magic: 0x7F 'E' 'L' 'F'.
-- The byte readers take the adapter explicitly.
-- The production `Support.File` adapter is wrapped by the caller to drop its implicit `self` so the parser shape is flat pass-style.
local b1 = M.read_u8(adapter, 0)
local b2 = M.read_u8(adapter, 1)
local b3 = M.read_u8(adapter, 2)
local b4 = M.read_u8(adapter, 3)
if not (b1 and b2 and b3 and b4)
or not (b1 == 0x7f and b2 == 0x45 and b3 == 0x4c and b4 == 0x46) then
return nil, "bad_magic"
end
local class = M.read_u8(adapter, M.ELF32_HEADER.class_offset)
if class ~= M.ELFCLASS32 then
return nil, "unsupported_elf_class"
end
local data = M.read_u8(adapter, M.ELF32_HEADER.endian_offset)
if data ~= M.ELFDATA2LSB then
return nil, "unsupported_elf_data"
end
local e_entry = M.read_u32(adapter, M.ELF32_HEADER.e_entry_offset)
local e_shoff = M.read_u32(adapter, M.ELF32_HEADER.e_shoff_offset)
local e_shentsize = M.read_u16(adapter, M.ELF32_HEADER.e_shentsize_offset)
local e_shnum = M.read_u16(adapter, M.ELF32_HEADER.e_shnum_offset)
local e_shstrndx = M.read_u16(adapter, M.ELF32_HEADER.e_shstrndx_offset)
if not (e_entry and e_shoff and e_shentsize and e_shnum and e_shstrndx) then
return nil, "truncated_header"
end
return {
e_entry = e_entry,
e_shoff = e_shoff,
e_shentsize = e_shentsize,
e_shnum = e_shnum,
e_shstrndx = e_shstrndx,
error = nil,
}
end
--- Read one section-header entry from `adapter` at `sh_off`.
--- Returns a table with the wire fields plus a (yet-unresolved) `name` field.
--- @param adapter table
--- @param sh_off integer
--- @return table|nil, string|nil -- entry, error
local function read_section_entry(adapter, sh_off)
local entry = {
sh_name = M.read_u32(adapter, sh_off + M.ELF32_SECTION.sh_name_offset),
sh_type = M.read_u32(adapter, sh_off + M.ELF32_SECTION.sh_type_offset),
sh_flags = M.read_u32(adapter, sh_off + M.ELF32_SECTION.sh_flags_offset),
sh_addr = M.read_u32(adapter, sh_off + M.ELF32_SECTION.sh_addr_offset),
sh_offset = M.read_u32(adapter, sh_off + M.ELF32_SECTION.sh_offset_offset),
sh_size = M.read_u32(adapter, sh_off + M.ELF32_SECTION.sh_size_offset),
sh_link = M.read_u32(adapter, sh_off + M.ELF32_SECTION.sh_link_offset),
name = "",
}
if not (entry.sh_name and entry.sh_type and entry.sh_flags and entry.sh_addr
and entry.sh_offset and entry.sh_size and entry.sh_link) then
return nil, "truncated_section_headers"
end
return entry, nil
end
--- Walk every section header in `hdr` and return a 1-based array of entries
--- (the section at logical index 0 is at array position 1, etc.).
--- Each entry has the wire fields plus a resolved `name` derived from `.shstrtab`.
--- Returns nil + a stable error code on failure: truncated_section_headers, missing_shstrtab, truncated_strtab
--- @param adapter table
--- @param hdr table -- the table returned by parse_elf32_headers
--- @return table|nil, string|nil
function M.walk_sections(adapter, hdr)
if not hdr or hdr.error then return nil, hdr and hdr.error or "truncated_section_headers" end
local file_size = M.size(adapter)
if hdr.e_shoff + hdr.e_shnum * hdr.e_shentsize > file_size then
return nil, "truncated_section_headers"
end
-- Read every section header first; we need .shstrtab to resolve names.
local sections = {}
for i = 0, hdr.e_shnum - 1 do
local sh_off = hdr.e_shoff + i * hdr.e_shentsize
local entry, err = read_section_entry(adapter, sh_off)
if not entry then return nil, err end
sections[i + 1] = entry
end
if hdr.e_shstrndx >= hdr.e_shnum then
return nil, "missing_shstrtab"
end
local shstrtab = sections[hdr.e_shstrndx + 1]
if not shstrtab or shstrtab.sh_type ~= M.SHT_STRTAB then
return nil, "missing_shstrtab"
end
if shstrtab.sh_offset + shstrtab.sh_size > file_size then
return nil, "truncated_section_headers"
end
local shstrtab_bytes = M.read_section_bytes(adapter, shstrtab)
if not shstrtab_bytes then return nil, "truncated_section_headers" end
for _, s in ipairs(sections) do
s.name = M.get_str(shstrtab_bytes, s.sh_name) or ""
end
return sections, nil
end
--- Read the bytes of one section. Returns a string, or nil if the adapter returns nil for any byte (out-of-bounds).
--- The caller is responsible fors sizing the buffer (the section's sh_offset + sh_size must fit in adapter.size).
--- @param adapter table
--- @param section table -- one entry from walk_sections
--- @return string|nil
function M.read_section_bytes(adapter, section)
local size = section.sh_size
if size == 0 then return "" end
local out = {}
for i = 0, size - 1 do
local b = M.read_u8(adapter, section.sh_offset + i)
if b == nil then return nil end
out[#out + 1] = string.char(b)
end
return table.concat(out)
end
--- Convenience: walk sections, then look up the named section, then read its bytes.
--- Returns nil + a stable error code if the section is absent or out-of-bounds.
--- @param adapter table
--- @param sections table -- 1-based array from walk_sections
--- @param name string
--- @return string|nil, string|nil
function M.read_named_section(adapter, sections, name)
if not sections then return nil, "missing_section" end
for _, s in ipairs(sections) do
if s.name == name then
local bytes = M.read_section_bytes(adapter, s)
if not bytes then return nil, "truncated_section_data" end
return bytes, nil
end
end
return nil, "missing_section"
end
--- Walk every SHT_SYMTAB section in `sections` and accumulate symbols by name.
--- Each stored entry is `{ value = st_value, size = st_size, info = st_info, shndx = st_shndx }`.
--- Both STB_LOCAL and STB_GLOBAL symbols are included; the live ELF stores `smem` as a local symbol.
--- Returns nil + a stable error code on failure: missing_symtab_strtab, truncated_section_headers
--- @param adapter table
--- @param sections table
--- @return table|nil, string|nil
function M.collect_symbols(adapter, sections)
if not sections then return nil, "missing_sections" end
local symbols = {}
local file_size = M.size(adapter)
for _, s in ipairs(sections) do
if s.sh_type == M.SHT_SYMTAB then
local strtab = sections[s.sh_link + 1]
if not strtab or strtab.sh_type ~= M.SHT_STRTAB then
return nil, "missing_symtab_strtab"
end
if strtab.sh_offset + strtab.sh_size > file_size then
return nil, "truncated_section_headers"
end
local strtab_bytes = M.read_section_bytes(adapter, strtab)
if not strtab_bytes then return nil, "truncated_section_headers" end
if s.sh_offset + s.sh_size > file_size then
return nil, "truncated_section_headers"
end
local symtab_bytes = M.read_section_bytes(adapter, s)
if not symtab_bytes then return nil, "truncated_section_headers" end
local n = #symtab_bytes / M.ELF32_SYM.sym_entry_bytes
for j = 0, n - 1 do
local e = s.sh_offset + j * M.ELF32_SYM.sym_entry_bytes
local st_name = M.read_u32(adapter, e + M.ELF32_SYM.st_name)
if st_name then
local st_value = M.read_u32(adapter, e + M.ELF32_SYM.st_value)
local st_size = M.read_u32(adapter, e + M.ELF32_SYM.st_size)
local st_info = M.read_u8(adapter, e + M.ELF32_SYM.st_info)
-- st_shndx is at offset 14 (2 bytes) — derived from the layout
-- the metaprogram reads too. Inline the read to keep the
-- adapter as the only I/O surface.
local b1 = M.read_u8(adapter, e + 14)
local b2 = M.read_u8(adapter, e + 15)
if not (b1 and b2) then
return nil, "truncated_section_headers"
end
local st_shndx = b1 + b2 * 0x100
local name = M.get_str(strtab_bytes, st_name) or ""
if name ~= "" then
symbols[name] = {
value = st_value,
size = st_size,
info = st_info,
shndx = st_shndx,
}
end
end
end
end
end
return symbols, nil
end
return M
+140 -153
View File
@@ -11,11 +11,6 @@
-- lfs is wired into package.cpath by `duffle_paths.lua` (vendored under `toolchain/lfs/lfs.dll`).
local lfs = require("lfs")
-- scripts/elf32.lua contains format-constant tables + the byte-level walker.
-- The this file re-exports `read_u32_le` / `read_u16_le` (and the DWARF32 terminator).
-- TODO(Ed): Remove re-export.
local E = require("elf32")
local M = {}
-- ════════════════════════════════════════════════════════════════════════════
@@ -107,13 +102,27 @@ M.MIPS_BYTES_PER_WORD = 0x04
--- **Wire-offset contract:** format offsets, fixed-width reader offsets, LEB/parser cursors, and section-relative values are zero-based wire offsets.
--- Only Lua string APIs receive a `+ 1` conversion at their boundary (`byte`, `sub`, and `find`).
--- ELF/DWARF field offsets are expressed in hex so they map directly to the zero-based byte positions in the binary file.
---
--- The ELF32 header / section / sym layout tables are within scripts/elf32.lua.
--- The metaprogram re-exports the DWARF32 initial-length terminator.
--- spec: DWARF4 spec §7.4 — 32-bit DWARF initial-length terminator
M.dw_dwarf32_terminator = E.dw_dwarf32_terminator
-- TODO(Ed): Remove re-export.
--- spec: System V ABI gABI v1.2 §"ELF Header" (Table 1) + §"Section Header Table"
M.ELF32 = {
magic_offset = 0x00, -- 4-byte magic "\127ELF" at file offset 0x00
magic = "\127ELF",
class_offset = 0x04, -- 1-byte; 1 = ELF32, 2 = ELF64
class_elf32 = 1,
endian_offset = 0x05, -- 1-byte; 1 = little-endian, 2 = big-endian
endian_little = 1,
header_bytes = 0x34, -- spec: gABI v1.2 §"ELF Header" — ELF32 header is 52 bytes total
e_shoff_offset = 0x20, -- 4-byte LE; section-header table file offset
e_shentsize_offset = 0x2E, -- 2-byte LE; section-header entry size in bytes
e_shnum_offset = 0x30, -- 2-byte LE; number of section headers
e_shstrndx_offset = 0x32, -- 2-byte LE; index of section-name string table
sh_size_bytes = 0x28, -- spec: gABI v1.2 §"Section Header Table" — each entry is 40 bytes
sh_name_offset = 0x00, -- 4-byte LE; offset into .shstrtab
sh_type_offset = 0x04, -- 4-byte LE; section type (SHT_*)
sh_offset_offset = 0x10, -- 4-byte LE; section's file offset
sh_size_offset = 0x14, -- 4-byte LE; section's size in bytes
dw_dwarf32_terminator = 0xFFFFFFFF, -- spec: DWARF4 spec §7.4 — 32-bit DWARF initial-length terminator
}
-- ----------------------------------------------------------------------------
-- DWARF4 .debug_aranges (per DWARF5 spec §7.4 — Address Range Table)
@@ -232,24 +241,27 @@ M.DWARF5_DEBUG_LINE = {
--- (which has partial `string.unpack` coverage).
--- **Convention:** `off` is a zero-based wire offset; `+ 1` is applied only at the `string.byte` boundary.
---
--- Thin forwarder: the canonical implementation lives in scripts/elf32.lua.
--- The "second caller lifts" pattern keeps the metaprogram side fluent
--- (`M.read_u32_le(buf, off)`) while the body is deduped.
--- **Byte weights** are written as `0x100`, `0x10000`, `0x1000000` (i.e. 2^8, 2^16, 2^24) so the LE byte positions are visually explicit:
--- byte 0 contributes its value directly; byte 1 is shifted left by 8 (= 0x100); byte 2 by 16 (= 0x10000); byte 3 by 24 (= 0x1000000).
--- @param buf string
--- @param off integer -- zero-based wire offset
--- @return integer
function M.read_u32_le(buf, off)
return E.read_u32_le(buf, off)
local byte_off = off + 1
return buf:byte(byte_off)
+ buf:byte(byte_off + 0x01) * 0x00000100
+ buf:byte(byte_off + 0x02) * 0x00010000
+ buf:byte(byte_off + 0x03) * 0x01000000
end
--- Read a 2-byte little-endian unsigned integer from `buf` at zero-based wire offset `off`.
--- (`off` is zero-based; `+ 1` is applied only at the `string.byte` boundary.)
--- Thin forwarder — see `M.read_u32_le` for the rationale.
--- @param buf string
--- @param off integer -- zero-based wire offset
--- @return integer
function M.read_u16_le(buf, off)
return E.read_u16_le(buf, off)
local byte_off = off + 1
return buf:byte(byte_off) + buf:byte(byte_off + 0x01) * 0x00000100
end
-- Pure-Lua 5.3 LEB128 readers (no `bit` library). `2^shift` arithmetic matches the existing parser.
@@ -430,20 +442,20 @@ function M.read_ref_sig8(buf, pos)
return M.read_u32_le(buf, pos), M.read_u32_le(buf, pos + 4), pos + 8
end
--- DWARF5 §7.5.6 (Type Entries).
--- Walk all units in `info` and return the 0-based offset of the first unit whose `DW_AT_type_signature`
--- (8-byte value at the end of the unit header) equals `target_sig`.
--- The signature is interpreted as two 32-bit halves (low/high) per the read_ref_sig8 contract;
--- we match both halves (i.e. the 8-byte value as a whole). Returns nil if no matching unit exists.
---
--- Unit header layout (from pos 0):
--- unit_length(4) + version(2) + unit_type(1) + address_size(1) + debug_abbrev_offset(4)
--- followed by type_unit_specific fields: type_signature(8) + type_offset(4)
--- The type_signature is at byte offset 8 of the body (right after debug_abbrev_offset).
--- @param info string -- the .debug_info section bytes
--- @param target_sig_lo integer -- low 4 bytes (LE) of the desired signature
--- @param target_sig_hi integer -- high 4 bytes (LE) of the desired signature
--- @return integer|nil, integer|nil -- unit offset, type_offset within the unit
-- DWARF5 §7.5.6 (Type Entries).
-- Walk all units in `info` and return the 0-based offset of the first unit
-- whose `DW_AT_type_signature` (8-byte value at the end of the unit header) equals `target_sig`.
-- The signature is interpreted as two 32-bit halves (low/high) per the read_ref_sig8 contract;
-- we match both halves (i.e. the 8-byte value as a whole). Returns nil if no matching unit exists.
--
-- Unit header layout (from pos 0):
-- unit_length(4) + version(2) + unit_type(1) + address_size(1) + debug_abbrev_offset(4)
-- followed by type_unit_specific fields: type_signature(8) + type_offset(4)
-- The type_signature is at byte offset 8 of the body (right after debug_abbrev_offset).
-- @param info string -- the .debug_info section bytes
-- @param target_sig_lo integer -- low 4 bytes (LE) of the desired signature
-- @param target_sig_hi integer -- high 4 bytes (LE) of the desired signature
-- @return integer|nil, integer|nil -- unit offset, type_offset within the unit
function M.find_type_unit_by_signature(info, target_sig_lo, target_sig_hi)
local pos = 0
local section_len = #info
@@ -527,7 +539,7 @@ end
--- (we walk all `e_shnum` headers regardless of how many names are requested, to find the .shstrtab first).
--- For frequent callers, pass the union of all needed sections in one call.
-- Can add `.debug_info` + `.debug_loc` + `.debug_str_offsets` to the list without writing a 2nd ELF walker.
--- @param elf_path Path
--- @param elf_path Path
--- @param section_names string[] -- list of section names to read
--- @return table<string, string>
function M.read_elf_sections(elf_path, section_names)
@@ -552,58 +564,69 @@ function M.read_elf_sections(elf_path, section_names)
return result
end
local file_size
do
f:seek("end", 0)
file_size = f:seek("cur", 0)
end
local adapter = {
read_u8_at = function(offset)
f:seek("set", offset)
local b = f:read(1)
if not b then return nil end
return b:byte()
end,
read_u16_at = function(offset)
f:seek("set", offset)
local b1 = f:read(1)
local b2 = f:read(1)
if not b1 or not b2 then return nil end
return b1:byte() + b2:byte() * 0x100
end,
read_u32_at = function(offset)
f:seek("set", offset)
local b1 = f:read(1)
local b2 = f:read(1)
local b3 = f:read(1)
local b4 = f:read(1)
if not b1 or not b2 or not b3 or not b4 then return nil end
return b1:byte() + b2:byte() * 0x100
+ b3:byte() * 0x10000 + b4:byte() * 0x1000000
end,
read_size = function() return file_size end,
}
-- Delegate the header parse + section walk to E.*.
local hdr, hdr_err = E.parse_elf32_headers(adapter)
if not hdr then
io.stderr:write(string.format("[elf_dwarf.read_elf_sections] header parse failed: %s\n", tostring(hdr_err)))
-- Read the ELF32 header.
local header = f:read(M.ELF32.header_bytes)
if not header or #header < M.ELF32.header_bytes then
io.stderr:write("[elf_dwarf.read_elf_sections] ELF too small for ELF32 header\n")
f:close()
return result
end
local sections, walk_err = E.walk_sections(adapter, hdr)
if not sections then
io.stderr:write(string.format("[elf_dwarf.read_elf_sections] section walk failed: %s\n", tostring(walk_err)))
-- Sanity-check magic + class + endianness.
if header:sub(M.ELF32.magic_offset + 1, M.ELF32.magic_offset + 0x04) ~= M.ELF32.magic then
io.stderr:write("[elf_dwarf.read_elf_sections] not an ELF file\n")
f:close()
return result
end
if header:byte(M.ELF32.class_offset + 1) ~= M.ELF32.class_elf32 then
io.stderr:write(string.format("[elf_dwarf.read_elf_sections] not ELF32 (class=%d)\n", header:byte(M.ELF32.class_offset + 1)))
f:close()
return result
end
if header:byte(M.ELF32.endian_offset + 1) ~= M.ELF32.endian_little then
io.stderr:write("[elf_dwarf.read_elf_sections] not little-endian; unsupported\n")
f:close()
return result
end
-- Resolve the requested sections.
for _, s in ipairs(sections) do
if wanted[s.name] then
local bytes = E.read_section_bytes(adapter, s)
if bytes then result[s.name] = bytes end
-- Parse section-header table location + dimensions from the header.
local e_shoff = M.read_u32_le(header, M.ELF32.e_shoff_offset)
local e_shentsize = M.read_u16_le(header, M.ELF32.e_shentsize_offset)
local e_shnum = M.read_u16_le(header, M.ELF32.e_shnum_offset)
local e_shstrndx = M.read_u16_le(header, M.ELF32.e_shstrndx_offset)
-- Read the section-header string table (.shstrtab) so we can resolve section names from their `sh_name` offsets.
f:seek("set", e_shoff + e_shstrndx * e_shentsize)
local strtab_hdr = f:read(e_shentsize)
if not strtab_hdr or #strtab_hdr < e_shentsize then
io.stderr:write("[elf_dwarf.read_elf_sections] could not read .shstrtab header\n")
f:close()
return result
end
local strtab_offset = M.read_u32_le(strtab_hdr, M.ELF32.sh_offset_offset)
local strtab_size = M.read_u32_le(strtab_hdr, M.ELF32.sh_size_offset)
f:seek("set", strtab_offset)
local strtab = f:read(strtab_size) or ""
-- Walk all section headers; collect (offset, size) for the wanted names.
local function read_section_bytes(sh_offset, sh_size)
f:seek("set", sh_offset)
return f:read(sh_size) or ""
end
for sh_idx = 0, e_shnum - 1 do
f:seek("set", e_shoff + sh_idx * e_shentsize)
local sh = f:read(e_shentsize)
if not sh or #sh < e_shentsize then break end
local sh_name = M.read_u32_le(sh, M.ELF32.sh_name_offset)
local sh_offset = M.read_u32_le(sh, M.ELF32.sh_offset_offset)
local sh_size = M.read_u32_le(sh, M.ELF32.sh_size_offset)
-- Extract the name (null-terminated C string in strtab).
local name_end = strtab:find("\0", sh_name + 1, true) or (sh_name + 1)
local name = strtab:sub(sh_name + 1, name_end - 1)
if wanted[name] then
result[name] = read_section_bytes(sh_offset, sh_size)
end
end
@@ -620,87 +643,48 @@ end
--- - We filter on STB_GLOBAL (high nibble of st_info = 1) to match `nm`'s default (external symbols only). STB_WEAK excluded.
--- - The `code_` prefix is stripped (MipsAtom_ macros emit bare atom names, no `code_` prefix).
--- - `st_size > 0` filter excludes undefined/imported symbols.
---
--- @param elf_path Path
--- @return table<string, {integer, integer}>
function M.read_nm(elf_path)
local addrs = {}
-- Existence check first; an empty or missing ELF returns an empty map.
if lfs.attributes(elf_path, "mode") ~= "file" then
-- Read .symtab + .strtab via the existing ELF walker (no subprocess).
local sections = M.read_elf_sections(elf_path, {".symtab", ".strtab"})
local symtab = sections[".symtab"]
local strtab = sections[".strtab"]
if not symtab or not strtab or #symtab == 0 or #strtab == 0 then
-- No symbol table (e.g. stripped ELF). Return empty.
return addrs
end
local f = io.open(elf_path, "rb")
if not f then
return addrs
end
-- Build the file adapter for E.*.
local file_size
do
f:seek("end", 0)
file_size = f:seek("cur", 0)
end
local adapter = {
read_u8_at = function(offset)
f:seek("set", offset)
local b = f:read(1)
if not b then return nil end
return b:byte()
end,
read_u16_at = function(offset)
f:seek("set", offset)
local b1 = f:read(1)
local b2 = f:read(1)
if not b1 or not b2 then return nil end
return b1:byte() + b2:byte() * 0x100
end,
read_u32_at = function(offset)
f:seek("set", offset)
local b1 = f:read(1)
local b2 = f:read(1)
local b3 = f:read(1)
local b4 = f:read(1)
if not b1 or not b2 or not b3 or not b4 then return nil end
return b1:byte() + b2:byte() * 0x100
+ b3:byte() * 0x10000 + b4:byte() * 0x1000000
end,
read_size = function() return file_size end,
}
-- Delegate the header + section walk to E.*.
local hdr, hdr_err = E.parse_elf32_headers(adapter)
if not hdr then
io.stderr:write(string.format("[elf_dwarf.read_nm] header parse failed: %s\n", tostring(hdr_err)))
f:close()
return addrs
end
local sections, walk_err = E.walk_sections(adapter, hdr)
if not sections then
io.stderr:write(string.format("[elf_dwarf.read_nm] section walk failed: %s\n", tostring(walk_err)))
f:close()
return addrs
end
-- E.collect_symbols returns every defined symbol (no binding filter).
-- The metaprogram then applies its STB_LOCAL / STB_GLOBAL + size>0 filter, matching `nm`'s default (external symbols only).
local symbols, sym_err = E.collect_symbols(adapter, sections)
if not symbols then
io.stderr:write(string.format("[elf_dwarf.read_nm] symbol collection failed: %s\n", tostring(sym_err)))
f:close()
return addrs
end
f:close()
for name, entry in pairs(symbols) do
-- High nibble of st_info = binding (STB_LOCAL=0, STB_GLOBAL=1, STB_WEAK=2).
-- math.floor(/16) is portable across LuaJIT 2.0/2.1 and plain Lua 5.x.
local binding = math.floor(entry.info / 16)
if (binding == 0 or binding == 1) and entry.size > 0 then
addrs[name] = { entry.value, entry.size }
-- Iterate the 16-byte ELF32 symtab entries.
-- Each entry (zero-based): st_name at 0, st_value at 4, st_size at 8, st_info at 12, st_other at 13, st_shndx at 14.
local SYM_ENTRY_BYTES = 0x10
local SYM_ST_NAME = 0x00
local SYM_ST_VALUE = 0x04
local SYM_ST_SIZE = 0x08
local SYM_ST_INFO = 0x0C
local n_syms = #symtab / SYM_ENTRY_BYTES
for i = 0, n_syms - 1 do
local entry_off = i * SYM_ENTRY_BYTES
local st_info = symtab:byte(entry_off + SYM_ST_INFO + 1)
-- High nibble = binding (STB_LOCAL=0, STB_GLOBAL=1, STB_WEAK=2).
-- Use math.floor(/16) instead of bit.rshift for LuaJIT 2.1 compat (LuaJIT's `>>` is 5.3+, but math.floor(x/16) works on all versions).
local binding = math.floor(st_info / 16)
if binding == 0 or binding == 1 then -- STB_LOCAL or STB_GLOBAL
local st_size = M.read_u32_le(symtab, entry_off + SYM_ST_SIZE)
if st_size > 0 then
local st_name_off = M.read_u32_le(symtab, entry_off + SYM_ST_NAME)
-- Extract the name from .strtab (null-terminated C string).
local name_end = strtab:find("\0", st_name_off + 1, true) or (st_name_off + 1)
local name = strtab:sub(st_name_off + 1, name_end - 1)
-- Filter: keep all symbol-table symbols (atoms emit their name as the bare `<name>` — MipsAtom_ macros strip the `code_` prefix).
-- The atoms_source_map pass already filters out non-atom symbols via the source-map.txt cross-ref.
if name and #name > 0 then
local st_value = M.read_u32_le(symtab, entry_off + SYM_ST_VALUE)
addrs[name] = { st_value, st_size }
end
end
end
end
@@ -838,11 +822,12 @@ end
--- * The `.debug_line` section may contain MULTIPLE line-program units
--- File indices are 1-based, **per unit**; we concatenate all units and the index ranges from 1..N₁ in unit 1, N₁+1..N₁+N₂ in unit 2, etc.
--- Per-unit indices (the way gcc emits them, and the way `DW_LNS_set_file` references them in the line program)
--- are returned via the `basename_to_index` map only when the unit boundary happens to align with the metaprogram's per-atom `inv.call_file`
--- are returned via the `basename_to_index` map only when the unit boundary happens to align with the metaprogram's per-atom
--- `inv.call_file` (true today for hello_joypad — the C unit is the LAST unit, and atom-side file indices fit 1-based).
--- * Per spec, the `.debug_line_str` section (DWARF5 §7.5.6) holds the strings referenced by `DW_FORM_line_strp`.
--- The legacy DWARF3 format embeds strings directly with null terminators. This helper handles BOTH.
--- * File entries may have multiple forms (gcc -gdwarf-5 with `DW_LNCT_directory_index` emits 2 forms: path + dir_index).
--- The helper supports:
--- * File entries may have multiple forms (gcc -gdwarf-5 with `DW_LNCT_directory_index`
--- emits 2 forms: path + dir_index). The helper supports:
--- - DW_FORM_line_strp (DWARF5; offset into .debug_line_str)
--- - DW_FORM_string (DWARF4-compat; inline null-terminated in .debug_line)
--- - DW_FORM_udata (ULEB128)
@@ -852,7 +837,9 @@ end
---
--- Behavior on failure: writes to stderr and returns nil.
--- Helpers consumed by `passes/dwarf_injection.lua::init_file_index_lookup(elf_path)` calls this once at pass start to populate the module-level `basename_to_index` map;
--- downstream `resolve_provenance_file_index(path)` consumers consult the map directly.
--- downstream `resolve_provenance_file_index(path)` consumers
--- (which replaced the former hardcoded `ATOM_SOURCE_FILE_INDEX` + `PROVENANCE_BASENAME_TO_FILE_INDEX` table per `conductor/tracks/dwarf_file_index_lookup_20260731/`)
--- consult the map directly.
---
--- @param elf_path string -- absolute path to the post-link ELF (typically the gcc-emitted `.elf` BEFORE dwarf_injector's splice; both shapes work since the splice preserves `.debug_line`)
--- @return table|nil, table|nil, table|nil
+96 -16
View File
@@ -1,41 +1,77 @@
# scripts/launch_pcsx_debug.ps1
#
# One-shot launcher for debug sessions: starts pcsx-redux with the .ps-exe
# loaded, the gdb stub enabled, AND the pcsx_debug_helper Lua plugin loaded
# so external CLI tools (gdb's `shell` command, etc.)
# can read GTE state via http://localhost:8080/api/v1/lua/gte
# (the gdb stub doesn't expose COP2 at all).
#
# usage:
# .\scripts\launch_pcsx_debug.ps1
# .\scripts\launch_pcsx_debug.ps1 -ExePath build\hello_gte.ps-exe
# .\scripts\launch_pcsx_debug.ps1 -HelperZip scripts\pcsx_debug_helper.zip
# loaded, the gdb stub enabled, the web server enabled, AND the
# pcsx_debug_helper Lua plugin loaded so external CLI tools can drive
# reloads via http://localhost:8080/api/v1/lua/reload.
#
# After launch:
# - gdb: target remote localhost:3333
# - web: curl http://localhost:8080/api/v1/lua/gte
# - web: POST http://localhost:8080/api/v1/lua/reload?mode=prime&...
#
# usage:
# .\scripts\launch_pcsx_debug.ps1
# .\scripts\launch_pcsx_debug.ps1 -ExePath build\hello_camera.ps-exe
# .\scripts\launch_pcsx_debug.ps1 -Cpu dynarec
# .\scripts\launch_pcsx_debug.ps1 -ElfPath build\hello_camera.elf
#
# Companion: scripts/debug_psyq.ps1 (bare launch — no .ps-exe, no helper).
[CmdletBinding()]
param(
[string]$PcsxPath = (Join-Path $PSScriptRoot '..\toolchain\pcsx-redux\vsprojects\x64\Release\pcsx-redux.exe'),
[string]$ExePath = (Join-Path $PSScriptRoot '..\build\hello_gte.ps-exe'),
[string]$ExePath = (Join-Path $PSScriptRoot '..\build\hello_camera.ps-exe'),
[string]$ElfPath = '',
[string]$HelperZip = (Join-Path $PSScriptRoot 'pcsx_debug_helper.zip'),
[int] $GdbPort = 3333,
[int] $WebPort = 8080
[int] $WebPort = 8080,
[ValidateSet('interpreter', 'dynarec')][string]$Cpu = 'interpreter'
)
$ErrorActionPreference = 'Stop'
# ── Derive -ElfPath when absent ──
# Convention: the .elf sits beside the .ps-exe with the same stem.
if ([string]::IsNullOrEmpty($ElfPath)) {
$exeFull = [System.IO.Path]::GetFullPath($ExePath)
$stem = [System.IO.Path]::GetFileNameWithoutExtension($exeFull)
$exeDir = [System.IO.Path]::GetDirectoryName($exeFull)
$ElfPath = Join-Path $exeDir "$stem.elf"
}
# ── Pre-checks ──
foreach ($p in @($PcsxPath, $ExePath, $HelperZip)) {
if (-not (Test-Path $p)) {
foreach ($p in @($PcsxPath, $ExePath, $ElfPath, $HelperZip)) {
if (-not (Test-Path -LiteralPath $p)) {
Write-Error "Missing: $p"
exit 1
}
}
# ── Reject a stale helper zip (Task 8) ──
# The helper zip must be newer than every .lua source that contributes
# to it. A stale zip means the running plugin does not match the on-disk
# source, which makes the reload contract meaningless.
$helperDir = Join-Path $PSScriptRoot 'pcsx_debug_helper'
$elf32Src = Join-Path $PSScriptRoot 'elf32.lua'
$sourceLuas = @(
(Join-Path $helperDir 'autoexec.lua'),
(Join-Path $helperDir 'reload.lua'),
$elf32Src
) | Where-Object { Test-Path -LiteralPath $_ }
$zipTime = (Get-Item -LiteralPath $HelperZip).LastWriteTime
$stale = $false
foreach ($src in $sourceLuas) {
$srcTime = (Get-Item -LiteralPath $src).LastWriteTime
if ($srcTime -gt $zipTime) {
Write-Error "helper zip is older than source: $src (zip=$($zipTime.ToString('o')) src=$($srcTime.ToString('o')); rerun build_psyq.ps1 to regenerate."
$stale = $true
}
}
if ($stale) {
exit 1
}
# Kill any existing pcsx-redux so the archive file isn't locked.
Get-Process pcsx-redux -ErrorAction SilentlyContinue | Stop-Process -Force
Start-Sleep -Seconds 2
@@ -44,17 +80,23 @@ Start-Sleep -Seconds 2
$absExe = [System.IO.Path]::GetFullPath($ExePath)
$absZip = [System.IO.Path]::GetFullPath($HelperZip)
$cpuFlag = if ($Cpu -eq 'dynarec') { '-dynarec' } else { '-interpreter' }
$args = @(
'-gdb', '-run'
'-loadexe', "`"$absExe`""
'-archive', "`"$absZip`""
'-webserver'
$cpuFlag
)
Write-Host "Launching pcsx-redux..." -ForegroundColor Cyan
Write-Host " ps-exe : $absExe"
Write-Host " elf : $ElfPath"
Write-Host " helper zip: $absZip"
Write-Host " gdb : localhost:$GdbPort"
Write-Host " web : localhost:$WebPort/api/v1/lua/gte"
Write-Host " web : localhost:$WebPort/api/v1/lua/reload"
Write-Host " cpu : $Cpu ($cpuFlag)"
Write-Host ""
Start-Process -FilePath $PcsxPath -ArgumentList $args | Out-Null
@@ -89,6 +131,44 @@ try {
Write-Host "Check the pcsx-redux Lua Console for debug cli messages." -ForegroundColor Yellow
}
# ── Prime the reload handler (Task 8) ──
# The reload handler keeps an internal ACTIVE manifest of the running
# ELF; reload requests fail with reload_not_primed until prime succeeds.
# We retry until the response carries ok=true or the launch deadline
# expires — the helper may not have finished registering handlers in the
# first web-poll cycle after the gte handler comes up.
$absElf = [System.IO.Path]::GetFullPath($ElfPath)
$encodedPath = [uri]::EscapeDataString($absElf)
$primeUri = "http://localhost:${WebPort}/api/v1/lua/reload?mode=prime&target=hello_camera&path=${encodedPath}"
Write-Host "Priming reload handler: $primeUri" -ForegroundColor Cyan
$primeDeadline = (Get-Date).AddSeconds(15)
$primeOk = $false
while ((Get-Date) -lt $primeDeadline) {
try {
$resp = Invoke-WebRequest -Method Post -Uri $primeUri -UseBasicParsing -TimeoutSec 5
$body = if ($resp.Content -is [byte[]]) {
[System.Text.Encoding]::UTF8.GetString([byte[]]$resp.Content)
} else {
[string]$resp.Content
}
$obj = $body | ConvertFrom-Json
if ($obj.ok) {
Write-Host "Prime OK: $(($obj | ConvertTo-Json -Compress))" -ForegroundColor Green
$primeOk = $true
break
} else {
Write-Host "Prime not yet ready: error=$($obj.error)" -ForegroundColor Yellow
}
} catch {
Write-Host "Prime request failed: $($_.Exception.Message)" -ForegroundColor Yellow
}
Start-Sleep -Milliseconds 500
}
if (-not $primeOk) {
Write-Warning "Prime did not return ok=true before the launch deadline. Reload requests will fail until the user primes manually."
}
Write-Host ""
Write-Host "pcsx-redux running. PIDs:" -ForegroundColor Cyan
Get-Process pcsx-redux | Select-Object Id, ProcessName | Format-Table
Get-Process pcsx-redux | Select-Object Id, ProcessName | Format-Table
+82
View File
@@ -0,0 +1,82 @@
# make_helper_zip.ps1
#
# Regenerate scripts/pcsx_debug_helper.zip from scripts/pcsx_debug_helper/.
# The archive contains exactly three entries at archive root:
#
# autoexec.lua
# elf32.lua (copied in from scripts/elf32.lua before packaging)
# reload.lua
#
# Determinism: CreateFromDirectory on the same set of files produces
# identical bytes. Verified by running the same command twice and
# asserting SHA-256 equality (see plan.md Task 6 Step 4).
#
# Performance: the implementation uses System.IO.Compression.ZipFile
# (BCL, in-process). Benchmarked: ~2 ms cold, ~2 ms warm on this
# workstation. Compress-Archive is rejected because its first call
# takes ~200 ms (assembly load) and subsequent calls take ~16 ms
# (process spawn per invocation). The 50 ms budget documented in
# plan.md Task 8 Step 3 excludes the compiler/assembler toolchain.
#
# Usage:
# pwsh -NoProfile -File scripts\make_helper_zip.ps1
#
# Optional -OutputPath switches the destination. Default is
# scripts/pcsx_debug_helper.zip next to the helper dir.
#
# Companion: scripts/pcsx_debug_helper/{autoexec,elf32,reload}.lua
# tests/reload_helper_zip_regen.ps1 (planned Task 8 verifier)
[CmdletBinding()]
param(
[string]$HelperDir = (Join-Path $PSScriptRoot 'pcsx_debug_helper'),
[string]$SourcesDir = $PSScriptRoot,
[string]$OutputPath = (Join-Path $PSScriptRoot 'pcsx_debug_helper.zip')
)
$ErrorActionPreference = 'Stop'
if (-not (Test-Path -LiteralPath $HelperDir)) {
throw "helper dir not found: $HelperDir"
}
# Stage elf32.lua into the helper dir so the in-process ZipFile walker
# picks it up alongside the helper-local files. elf32.lua is the shared
# ELF32 byte reader; the production reload.lua loads it through
# Support.extra.dofile("elf32.lua") at runtime.
$elf32Src = Join-Path $SourcesDir 'elf32.lua'
$elf32Dest = Join-Path $HelperDir 'elf32.lua'
if (-not (Test-Path -LiteralPath $elf32Src)) {
throw "elf32.lua not found at $elf32Src"
}
Copy-Item -LiteralPath $elf32Src -Destination $elf32Dest -Force
try {
# Remove any existing archive so CreateFromDirectory can write fresh.
# ZipFile.CreateFromDirectory throws if the destination exists.
if (Test-Path -LiteralPath $OutputPath) {
Remove-Item -LiteralPath $OutputPath -Force
}
# In-process zip; ~2 ms cold, ~2 ms warm. BCL compression matches
# Compress-Archive at CompressionLevel Optimal for these small files.
# Assembly is loaded once per pwsh.exe; the first run pays ~14 ms,
# subsequent runs pay ~0.2 ms.
Add-Type -AssemblyName System.IO.Compression.FileSystem
[System.IO.Compression.ZipFile]::CreateFromDirectory(
$HelperDir, $OutputPath,
[System.IO.Compression.CompressionLevel]::Optimal,
$false) | Out-Null
$sha = (Get-FileHash -LiteralPath $OutputPath -Algorithm SHA256).Hash
Write-Output ("[make_helper_zip] wrote {0} bytes, sha256={1}" -f `
(Get-Item -LiteralPath $OutputPath).Length, $sha)
Write-Output "[make_helper_zip] entries: autoexec.lua, elf32.lua, reload.lua"
}
finally {
# Remove the staged elf32.lua so the helper directory only contains
# the files the user expects to see there.
if (Test-Path -LiteralPath $elf32Dest) {
Remove-Item -LiteralPath $elf32Dest -Force
}
}
-355
View File
@@ -1,355 +0,0 @@
--- passes/auto_reg.lua — Per-phase automatic GPR allocator + gen/auto_reg.h emitter.
---
--- Reads the per-source + corpus-level `atom_auto_regs` + `phase_auto_regs` registries populated by `passes/scan_source.lua`.
--- Runs a deterministic first-fit allocator in the `R_T0..R_T7 + R_V0..R_V1` pool (10 physical GPRs).
--- Emits one `#define R_<Sym>_Code R_Tn_Code` per marker into per-directory `gen/auto_reg.h`.
---
--- 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_<Alias>) 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.
---
--- Pool exhaustion: If a phase declares more `R_<Sym>` 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)
--- @field warnings table[] -- {line=, msg=} entries (build-continues)
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./"
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
--- ════════════════════════════════════════════════════════════════════════════
--- 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).
-- 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",
[12] = "R_T4", [13] = "R_T5", [14] = "R_T6", [15] = "R_T7",
}
-- Stable sort for deterministic allocation order.
local function stable_sort_keys(tbl)
local keys = {}
for k in pairs(tbl) do keys[#keys + 1] = k end
table.sort(keys)
return keys
end
-- Allocate one phase's auto-reg mappings.
-- Returns (allocated_map, errors). On pool exhaustion, errors is populated and the function halts.
local function allocate_phase(phase_label, decls)
-- Deep-copy POOL into a fresh sequence table. The original `table.unpack and table.unpack(POOL) or { unpack(POOL) }`
-- idiom wraps the unpacked values in a single inner table under LuaJIT 5.1 (`table.unpack` is nil; the `or` returns one value),
-- which corrupts the pool into `{ {R_T0, R_T1, ...} }` — making `table.remove(pool, 1)` return the inner table on iteration.
local pool = {}
for i = 1, #POOL do pool[i] = POOL[i] end
local result = {}
local errors = {}
for _, sym in ipairs(stable_sort_keys(decls)) do
local next_gpr = table.remove(pool, 1)
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),
}
return result, errors
end
result[sym] = next_gpr
end
return result, errors
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.
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
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
alias_to_gpr[alias_name] = gpr
end
end
end
return user_pinned, alias_to_gpr
end
-- Find every physical GPR referenced in the atom body, via EITHER:
-- (a) A hardcoded physical GPR ident (R_T\d+|R_V\d+|R_A\d+|R_S\d+) — the existing regex;
-- (b) An alias ident (R_<Alias>) resolved via alias_to_gpr back to its physical GPR ident.
-- Returns { [physical_gpr_ident] = count }. Clash-detection and source-pool-exclusion logic
-- only needs the presence of each GPR (boolean test), but keeping count preserves the
-- original find_hardcoded_rn shape so callers can switch without churn.
-- The alias pattern is sorted lexicographically to keep the regex deterministic.
local function find_used_gprs(body_text, alias_to_gpr)
local found = {}
-- (a) Hardcoded physical GPRs (R_T0..R_T7, R_V0..R_V1, R_A0..R_A3, R_S0..R_S7).
for gpr in body_text:gmatch("(R_T%d+|R_V%d+|R_A%d+|R_S%d+)") do
found[gpr] = (found[gpr] or 0) + 1
end
-- (b) Alias references (R_<Alias>) resolved to physical GPRs via the registry.
-- Sorted by name so the regex is byte-stable across runs.
if alias_to_gpr and next(alias_to_gpr) then
local aliases = {}
for alias_name in pairs(alias_to_gpr) do
aliases[#aliases + 1] = alias_name
end
table.sort(aliases)
local pattern = "(" .. table.concat(aliases, "|") .. ")"
for alias_name in body_text:gmatch(pattern) do
local gpr = alias_to_gpr[alias_name]
if gpr and not found[gpr] then
found[gpr] = 1
end
end
end
return found
end
-- Emit one gen/auto_reg.h header per directory.
local function emit_auto_reg_h(out_dir, dir, sources, mappings)
if not mappings or next(mappings) == nil then return end
local out_path = out_dir .. "/" .. "auto_reg.h"
duffle.ensure_dir(out_dir)
local lines = {
"#ifdef INTELLISENSE_DIRECTIVES",
"#pragma once",
"#endif",
"// Auto-generated by ps1_meta.lua (passes/auto_reg.lua) — DO NOT EDIT",
"// Directory: " .. dir:gsub("/", "\\"),
}
for _, src in ipairs(sources) do
lines[#lines + 1] = "// source: " .. src.path
end
lines[#lines + 1] = "// Per-phase register allocations resolved by the lua pass."
lines[#lines + 1] = "// R_<Sym>_Code = <chosen GPR's _Code constant> 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"
lines[#lines + 1] = "#define " .. sym .. "_Code " .. gpr_code
end
lines[#lines + 1] = ""
duffle.write_file_lf(out_path, table.concat(lines, "\n") .. "\n")
print(" -> " .. out_path)
return out_path
end
-- ════════════════════════════════════════════════════════════════════════════
-- Pass entry
-- ════════════════════════════════════════════════════════════════════════════
local M = {}
--- @param ctx PassCtx
--- @return AutoRegResult
function M.run(ctx)
local outputs = {}
local errors = {}
local warnings = {}
local corpus = ctx.shared and ctx.shared.corpus
if type(corpus) ~= "table" then
error("auto_reg.run requires ctx.shared.corpus", 0)
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.
local user_pinned, alias_to_gpr = build_user_pins(corpus)
-- 1. Allocate phase pools first (phase declarations take precedence over per-atom declarations).
local phase_allocations = {}
for phase_label, decls in pairs(corpus.phase_auto_regs or {}) do
local mapping, errs = allocate_phase(phase_label, decls)
for sym, gpr in pairs(mapping) do
phase_allocations[phase_label] = phase_allocations[phase_label] or {}
phase_allocations[phase_label][sym] = gpr
end
for _, e in ipairs(errs) do
errors[#errors + 1] = e
end
end
-- 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(<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
atom_name_to_phase[atom_name] = phase_label
end
end
local atom_allocations = {}
for atom_scope, decls in pairs(corpus.atom_auto_regs or {}) do
local phase_label = atom_name_to_phase[atom_scope]
-- Build the atom's source pool: start with the full POOL, subtract:
-- (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)
-- 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)`
-- returned nil and every atom-with-phase marker errored with `phase_register_pool_exhausted`.
local used = {}
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.
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)
for gpr in pairs(body_used) do used[gpr] = true end
end
local source_pool = {}
for _, gpr in ipairs(POOL) do
-- Exclude (a) prior commitments, (b) USER-PINNED GPRs (wave-context carriers
-- declared via atom_reg + _Code defs, preserved across atoms globally).
if not used[gpr] and not user_pinned[gpr] then
source_pool[#source_pool + 1] = gpr
end
end
local result = {}
for _, sym in ipairs(stable_sort_keys(decls)) do
local next_gpr = table.remove(source_pool, 1)
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."
, atom_scope, sym),
}
else
result[sym] = next_gpr
end
end
atom_allocations[atom_scope] = result
end
-- 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_<Alias>).
-- 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]
if atom and atom.body then
local used_in_body = find_used_gprs(atom.body, alias_to_gpr)
for sym, allocated_gpr in pairs(decls) do
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."
, atom_scope, allocated_gpr, sym, allocated_gpr),
}
end
end
end
end
-- 4. Emit per-directory gen/auto_reg.h.
-- For each source directory that has atom_auto_regs or phase_auto_regs entries, emit one header.
local sources_by_dir = corpus.sources_by_dir or {}
for dir, sources in pairs(sources_by_dir) do
local per_dir_mappings = {}
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.
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
end
end
for scope_name in pairs(src.scan and src.scan.phase_auto_regs or {}) do
for sym, gpr in pairs(phase_allocations[scope_name] or {}) do
per_dir_mappings[sym] = gpr
end
end
end
local out_dir = dir .. "/gen"
local out_path = emit_auto_reg_h(out_dir, dir, sources, per_dir_mappings)
if out_path then outputs[#outputs + 1] = { auto_reg_h = out_path } end
end
return { outputs = outputs, errors = errors, warnings = warnings }
end
return M
+22 -48
View File
@@ -3,12 +3,9 @@
--- Ownership: `corpus.word_counts`, `corpus.components`, and `corpus.component_body_index`.
--- Scanner owns `declaration_comment` and `debug_skip` on each declaration record; this pass projects both forward.
---
--- Reads the pre-scanned SourceScan payload from `duffle.scan_source` for `MipsAtomComp_(ac_X)` and `MipsAtomComp_Proc_(ac_X, { body })` declarations (kind="comp_bare" / "comp_proc"),
--- Reads the pre-scanned SourceScan payload from `duffle.scan_source` for `MipsAtomComp_(ac_X)` and `MipsAtomComp_Proc_(ac_X, { body })` declarations,
--- then resolves the function-args string from the preceding `FI_ Slice_MipsCode ac_X(...)` declaration via a backward walk.
---
--- `MipsAtom_Proc_(X, ab, { body })` declarations (kind="atom_proc") are ATOMS, not components, and are deliberately excluded —
--- atoms get emitted via `tb_emit(tb, code_<name>)` linker symbols, not inlined as `mac_*` macros.
---
--- Emits one `gen/macs.h` per *immediate source directory* with `#define mac_X(sig) \` macros plus `WORD_COUNT(mac_X, N)` entries for downstream offset computation.
--- All sources inside the same directory contribute to the same file (per-directory aggregation).
--- The directory itself is the namespace, so the filename does not repeat the module name.
@@ -79,7 +76,7 @@ local MACS_FILENAME = "macs.h"
--- @field args string|nil -- Function-args string (function form only)
--- @field line integer -- Source line of the declaration
--- @field comment string|nil -- Scanner-owned `declaration_comment`; the components pass reads it from the scanner record
--- @field kind string -- "comp_bare" | "comp_proc" (atom_proc is NOT a component — see `project_components`)
--- @field kind string -- "comp_bare" | "comp_proc"
--- @field debug_skip boolean -- Mirror of `a.debug_skip` (scanner-owned); true iff a bare `atom_dbg_skip` marker immediately preceded the declaration
-- ════════════════════════════════════════════════════════════════════════════
@@ -203,16 +200,7 @@ end
local function project_components(source, scan)
local out = {}
for _, a in ipairs(scan.atoms) do
-- Only `MipsAtomComp_(ac_X)` (kind="comp_bare") and `MipsAtomComp_Proc_(ac_X, ...)` (kind="comp_proc")
-- are COMPONENTS — they get inlined via `mac_<name>` aliases inside atom bodies.
-- `MipsAtom_Proc_` (kind="atom_proc") is an ATOM (ends with `mac_yield()`); it gets emitted via
-- `tb_emit(tb, code_<name>)` (linker symbol), NOT inlined as a macro. Including `atom_proc` here
-- would incorrectly emit `mac_<name>` aliases for atoms, polluting `gen/macs.h`.
-- See `docs/duffle_dsl_primer.md` §"mac_* aliases" for the contract.
if a.kind == "comp_bare" or a.kind == "comp_proc" then
-- Function-args lookup is meaningful for `MipsAtomComp_Proc_` components
-- (the macro sits inside `FI_ Slice_MipsCode ac_X(...)`); the alias expansion
-- discards the `ab` (atom-builder) arg the same way both forms do.
local args = find_function_args_for(source, a.raw_name, a.ident_pos)
-- Comment ownership: scan_source.lua stamps `declaration_comment` on the record by walking backward past any associated bare marker.
-- The pass reads `declaration_comment` directly.
@@ -311,9 +299,7 @@ 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 == "atom_label" or lookup == "atom_offset" then
-- Pure metaprogram anchors; emit zero words.
elseif lookup and comp_by_name[lookup] then
if 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
@@ -404,7 +390,8 @@ local function cycle_cost_rec(name, comp_by_name, latency, cache)
end
--- (internal) Recursive GP0 prim-buffer contribution. Count `store_word` / `store_half` / `store_byte`
--- calls in the component body that target `R_PrimCursor` (these are the RAM-side prim-buffer words the macro contributes), recursing through nested `mac_*` calls.
--- calls in the component body that target `R_PrimCursor` (these are the
--- RAM-side prim-buffer words the macro contributes), recursing through nested `mac_*` calls.
--- Only `R_PrimCursor`-targeting stores count. Stores targeting other registers (e.g. `R_OtBase`, heap pointers) are not prim-buffer contributions.
--- @param name string
--- @param comp_by_name table<string, Component>
@@ -486,25 +473,12 @@ local function split_comment_lines(s)
end
--- Determine the macro signature: function-args list (function form) or variadic-ignored (bare form).
--- For `MipsAtomComp_Proc_` components, the leading `ab` (atom-builder) arg is dropped:
--- the generated `mac_<name>` macros are inline-expansion aliases for baked atoms; their bodies don't reference `ab`
--- (the builder is only consumed by the procedural `atombuilder_unroll` line that `MipsAtomComp_Proc_` appends after the body).
--- Inline callers therefore don't need to thread a builder context.
--- @param args_str string|nil
--- @return string
local function signature_from_args(args_str)
local arg_names = extract_arg_names(args_str)
if arg_names and #arg_names > 0 then
-- Drop the leading `ab` (atom-builder) first arg if present.
-- Convention: `MipsAtomComp_Proc_` components always declare `ab` as the first function-arg
-- (type `MipsAtomBuilder_R`), mirroring the macro signature in `lottes_tape.h`.
if arg_names[1] == "ab" then
table.remove(arg_names, 1)
end
if #arg_names > 0 then
return table.concat(arg_names, ", ")
end
return "..." -- `ab` was the only arg; fall through to variadic
return table.concat(arg_names, ", ")
end
return "..."
end
@@ -546,7 +520,7 @@ local function build_component_lines(c, counts)
-- Marker comment: emitted once for every skipped component.
-- The marker is scanner-owned (declared by `atom_dbg_skip` immediately before the declaration in the source);
-- This pass projects `c.debug_skip` and emits the marker as a generated comment.
-- the components pass projects `c.debug_skip` and emits the marker as a generated comment.
if c.debug_skip then
lines[#lines + 1] = "/* atom_dbg_skip */"
end
@@ -580,8 +554,8 @@ end
--- Build the boilerplate header lines (the `#ifdef INTELLISENSE_DIRECTIVES` block,
--- the `// Auto-generated` comment, the `// Source:` line, and the self-contained `WORD_COUNT` macro definition).
--- @param dir string -- Absolute source directory
--- @param sources SourceFile[] -- Sources contributing to this directory (for the header comment)
--- @param dir string -- the absolute source directory
--- @param sources SourceFile[] -- sources contributing to this directory (for the header comment)
--- @return string[]
local function header_boilerplate(dir, sources)
local source_lines = { "// Directory: " .. duffle.to_absolute_path(dir) .. "/" }
@@ -612,9 +586,9 @@ end
--- Compute the per-directory output path for `.macs.h`.
--- e.g. any source in `code/duffle/` produces `code/duffle/gen/macs.h` regardless of source filename.
--- The directory name is the namespace; the filename does not repeat it.
--- @param dir string -- Absolute source directory
--- @return string -- Output directory
--- @return string -- Full output path
--- @param dir string -- the absolute source directory
--- @return string -- the output directory
--- @return string -- the full output path
local function compute_macs_h_path(dir)
local out_dir = dir .. "/" .. GEN_SUBDIR
local out_path = out_dir .. "/" .. MACS_FILENAME
@@ -624,11 +598,11 @@ end
--- Emit a per-directory `.macs.h` header with the aggregated `mac_X` macros + `WORD_COUNT` entries.
--- Writes in BINARY mode so LF line endings are preserved (the git blob is LF; Windows text-mode would emit CRLF and break the byte-identical diff).
--- @param ctx PassCtx
--- @param dir string -- Absolute source directory
--- @param sources SourceFile[] -- Sources contributing to this directory (for the header comment)
--- @param components Component[] -- Aggregated components from all sources in this directory
--- @param counts table<string, integer> -- Precomputed word counts (from count_all_components)
--- @return string|nil -- Path to the written file (nil if no components)
--- @param dir string -- the absolute source directory
--- @param sources SourceFile[] -- sources contributing to this directory (for the header comment)
--- @param components Component[] -- aggregated components from all sources in this directory
--- @param counts table<string, integer> -- precomputed word counts (from count_all_components)
--- @return string|nil -- path to the written file (nil if no components)
local function emit_component_macros_h(ctx, dir, sources, components, counts)
if #components == 0 then return nil end
local out_dir, out_path = compute_macs_h_path(dir)
@@ -667,11 +641,11 @@ local function update_canonical_word_counts(corpus, components, counts)
end
--- @class ComponentDef
--- @field name string -- Bare name (without ac_/mac_ prefix)
--- @field line integer -- Definition source line (line of `MipsAtomComp_(ac_X)` / `MipsAtomComp_Proc_(ac_X, ...)`)
--- @field path string -- Absolute source path of the definition
--- @field kind string -- "comp_bare" | "comp_proc" (atom_proc is NOT a component)
--- @field debug_skip boolean -- Mirror of the scanner-owned `a.debug_skip`; consumers read this directly
--- @field name string -- bare name (without ac_/mac_ prefix)
--- @field line integer -- definition source line (line of `MipsAtomComp_(ac_X)` / `MipsAtomComp_Proc_(ac_X, ...)`)
--- @field path string -- absolute source path of the definition
--- @field kind string -- "comp_bare" | "comp_proc"
--- @field debug_skip boolean -- mirror of the scanner-owned `a.debug_skip`; consumers read this directly
--- (internal) Populate `corpus.components` with this source's components-by-name map.
--- First declaration wins; later declarations of the same bare name are dropped and recorded as a collision via `corpus.collisions` (kind = "component").
+19 -20
View File
@@ -703,9 +703,9 @@ end
--- `{comp_name, call_file, call_line, comp_file, comp_line, start_pos, end_pos, body_lines, debug_skip}`. `body_lines[k]`
--- is the k-th word's source line within the component body.
---
--- @param corpus table -- From `ctx.shared.corpus`
--- @param corpus table -- the corpus from `ctx.shared.corpus`
--- @param addrs table -- ELF symbols keyed by atom name from `elf_dwarf.read_nm`
--- @return table[] -- List of {name, addr, size_bytes, words, entries, invocations, debug_skip?}
--- @return table[] -- list of {name, addr, size_bytes, words, entries, invocations, debug_skip?}
local function build_atom_table(corpus, addrs)
-- Cross-ref: keep only atoms present in BOTH the nm symbol table AND `corpus.atoms_by_name`. Output is sorted by ascending addr.
local atoms_by_name = corpus.atoms_by_name or {}
@@ -834,10 +834,10 @@ end
--- This is intentional: silently falling back to a hardcoded GPR would mask the missing opt-in.
---
--- Pre-tokenized: `body_tokens` is the scan-source pass's pre-split list of top-level statements (each entry is a single `load_*` call or other statement).
--- @param body_tokens table[] -- The atom's pre-tokenized body statements (from atom.body_tokens)
--- @param binds_name string -- Expected Binds_X name (skip pairs with mismatching binds)
--- @param registries table -- Merged registries from collect_per_source_registries
--- @return table[] -- List of {reg = <MIPS index>, field = <field name>}
--- @param body_tokens table[] -- the atom's pre-tokenized body statements (from atom.body_tokens)
--- @param binds_name string -- expected Binds_X name (skip pairs with mismatching binds)
--- @param registries table -- merged registries from collect_per_source_registries
--- @return table[] -- list of {reg = <MIPS index>, field = <field name>}
local function parse_body_load_pairs(body_tokens, binds_name, registries)
local pairs = {}
local reg_index_by_name = (registries and registries.register_alias_registry) or {}
@@ -880,9 +880,9 @@ end
--- The piece chain uses (DW_OP_regN, DW_OP_piece, ULEB128(field_size)).
---
--- Binds fields come from `scan.binds`; the per-source `scan.binds[i].fields` already carries the typed-field record after the scan-source generalization.
--- @param corpus table -- From `ctx.shared.corpus`
--- @param atom_table table[] -- Cross-ref'd atom table from build_atom_table
--- @param registries table -- Merged registries from collect_per_source_registries
--- @param corpus table -- the corpus from `ctx.shared.corpus`
--- @param atom_table table[] -- the cross-ref'd atom table from build_atom_table
--- @param registries table -- merged registries from collect_per_source_registries
--- @return table, table -- (rbind_atoms, rbind_structs)
local function parse_rbind_atoms(corpus, atom_table, registries)
registries = registries or {}
@@ -944,7 +944,7 @@ local function parse_rbind_atoms(corpus, atom_table, registries)
binds = ai.binds,
fields = struct.fields, -- {name, offset} from scan.binds
bytes = struct.bytes,
regs = pairs, -- Ordered list of {reg, field}
regs = pairs, -- ordered list of {reg, field}
info_line = ai.info_line,
}
table.insert(struct.atom_names, atom_name)
@@ -992,7 +992,7 @@ local function build_dwarf_line_section(existing, atom_table)
while unit_pos < #existing do
if unit_pos + 4 > #existing then return existing end
local unit_length = elf_dwarf.read_u32_le(existing, unit_pos)
if unit_length == elf_dwarf.dw_dwarf32_terminator then return existing end
if unit_length == elf_dwarf.ELF32.dw_dwarf32_terminator then return existing end
local unit_end_excl = unit_pos + 4 + unit_length
if unit_end_excl > #existing then return existing end
last_pos, last_length, last_end = unit_pos, unit_length, unit_end_excl
@@ -1036,13 +1036,13 @@ local function build_dwarf_aranges_section(existing, atom_table)
-- We bump the unit's length field accordingly.
--
-- Unit structure (DWARF4 §7.21):
-- unit_length (4)
-- version (2)
-- unit_length (4)
-- version (2)
-- debug_info_offset (4) -- CU DIE offset in .debug_info
-- address_size (1)
-- segment_size (1)
-- entries... (4-byte addr + 4-byte length)
-- terminator (8 bytes: addr=0, length=0)
-- address_size (1)
-- segment_size (1)
-- entries... (4-byte addr + 4-byte length)
-- terminator (8 bytes: addr=0, length=0)
-- Walk all units and emit each one (preserving existing structure).
-- For the LAST unit, replace the terminator with my entries + new term.
@@ -1053,7 +1053,7 @@ local function build_dwarf_aranges_section(existing, atom_table)
while i < #existing do
-- Read this unit's length.
local ul = elf_dwarf.read_u32_le(existing, i)
if ul == elf_dwarf.dw_dwarf32_terminator then
if ul == elf_dwarf.ELF32.dw_dwarf32_terminator then
-- DWARF64 marker - not supported.
io.stderr:write("[dwarf_injection] WARN: .debug_aranges contains a DWARF64 marker (0xFFFFFFFF); the 64-bit extension is not supported by this metaprogram; passing through unchanged\n")
return existing
@@ -1780,8 +1780,7 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta
emit(uleb128(ABBREV_TYPED_VIEW_POINTER)) -- DW_TAG_pointer_type (abbrev 110; NOT 9; void chain target)
emit(elf_dwarf.write_u32_le(ref4_of(void_chain_offset))) -- 4-byte ref4: points at the void base_type's tag byte
-- type_chain_offsets["void|1"] is what step (f) of the per-RR_<R_Name> chain looks up.
type_chain_offsets["void|1"] = void_chain_offset -- both the base_type offset and the pointer_type are emitted consecutively; the OUTERMOST is the pointer_type.
-- The variable's DW_AT_type must reference the pointer_type, not the base_type. Patch below.
type_chain_offsets["void|1"] = void_chain_offset -- both the base_type offset and the pointer_type are emitted consecutively; the OUTERMOST is the pointer_type. The variable's DW_AT_type must reference the pointer_type, not the base_type. Patch below.
-- Capture the pointer_type's offset (the last-thing-emitted DIE start) and overwrite the lookup.
-- The pointer_type was emitted as: uleb(9) (1 byte) + 4-byte ref4 = 5 bytes. Its tag byte is at void_chain_offset + 8 (the base_type's 8 bytes: 1 tag + 5 name + 1 byte_size + 1 encoding).
local ptr_void_offset = void_chain_offset + 8
+3 -3
View File
@@ -188,11 +188,11 @@ function M.run(ctx)
if type(corpus.source_order) ~= "table" then error("emission_model: ctx.shared.corpus.source_order is required", 0) end
-- Project once, collect errors + warnings for one atom.
-- Kind must be one of: atom | atom_proc | raw_atom | comp_bare | comp_proc.
-- Kind must be one of: atom | raw_atom | comp_bare | comp_proc.
local function process_atom(atom, src)
if not (atom and atom.body) then return end
local kind = atom.kind
if kind ~= "atom" and kind ~= "atom_proc" and kind ~= "raw_atom" and kind ~= "comp_bare" and kind ~= "comp_proc" then
if kind ~= "atom" and kind ~= "raw_atom" and kind ~= "comp_bare" and kind ~= "comp_proc" then
return
end
local proj = project_atom(atom, src, corpus)
@@ -215,7 +215,7 @@ function M.run(ctx)
end
-- Walk `corpus.source_order`; within each source, visit atoms followed by raw_atoms.
-- Recognized kinds (atom | atom_proc | raw_atom | comp_bare | comp_proc) each receive the atom.paths projection via duffle.project_emission.
-- Recognized kinds (atom | raw_atom | comp_bare | comp_proc) each receive the atom.paths projection via duffle.project_emission.
-- Components are macros inlined into atom bodies; focused tests and isolated component analyses consume atom.paths directly.
for _, src in ipairs(corpus.source_order) do
local scan = src.scan or {}
-8
View File
@@ -4,17 +4,9 @@
--- for `MipsAtom_(name)` and `MipsCode code_<name>` declarations, computes the word offset
--- from each `atom_offset(F, T)` marker to its target `atom_label(T)` declaration, and emits
--- `gen/offsets.h` with one `#define _atom_offset_F_T = N` per branch.
---
--- Per-directory aggregation: every source in the same directory contributes to the same `gen/offsets.h`.
--- The directory itself is the namespace; the filename does not repeat the module name.
---
--- (Task 12.16 note: atom-namespaced enum names — e.g., `atom_offset__normalize_v3s4__srav_path__aligned_done` —
--- were considered to prevent cross-atom label collisions, but the C-side `atom_offset(F, T)` macro in
--- `code/duffle/dsl.atom.h` doesn't know the current atom_name at expansion time, so any namespacing
--- on the metaprogram side breaks the C build. Reverted. The C-side would need a per-atom
--- `CURRENT_ATOM` #define (set by `MipsAtom_`/`MipsAtom_Proc_` macros) plus an updated `atom_offset`
--- macro that uses it. That's a coordinated refactor — deferred to a future track.)
---
--- The offset is `target_word - branch_word - 1` (the standard MIPS branch-immediate encoding: branch_offset = relative_pc_in_words - 1).
-- ════════════════════════════════════════════════════════════════════════════
+13 -210
View File
@@ -3,7 +3,6 @@
--- Single source-walk pass that produces the fat `SourceScan` payload consumed by all downstream passes. Walks each corpus source record once,
--- extracting every construct type the metaprograms need:
--- MipsAtom_ (kind = "atom", with optional atom_info inner)
--- MipsAtom_Proc_ (kind = "atom_proc", body inside last {})
--- MipsAtomComp_ (kind = "comp_bare")
--- MipsAtomComp_Proc_ (kind = "comp_proc", body inside last {})
--- atom_dbg_skip — bare whole-atom/component debug-step marker; following declaration disambiguates
@@ -35,7 +34,7 @@ local parse_enum_int_literal
-- ════════════════════════════════════════════════════════════════════════════
--- @class SourceScan
--- @field atoms AtomEntry[] -- MipsAtom_ + MipsAtom_Proc_ + MipsAtomComp_ + MipsAtomComp_Proc_
--- @field atoms AtomEntry[] -- MipsAtom_ + MipsAtomComp_ + MipsAtomComp_Proc_
--- @field raw_atoms AtomEntry[] -- MipsCode code_<name> { body } (offsets pass only)
--- @field binds BindsEntry[] -- typedef Struct_(Binds_X) { fields } (fields pre-parsed)
--- @field atom_infos AtomInfoEntry[] -- MipsAtom_(name) atom_info(...) (sub-calls pre-parsed)
@@ -56,7 +55,7 @@ local parse_enum_int_literal
--- @field args string|nil -- Trimmed args inside the `(...)` (nil when has_parens is false)
--- @field pending boolean -- true while awaiting the following declaration
--- @field superseded_by_marker_line integer|nil -- set when a newer marker bumped this one out of the pending slot
--- @field target_kind string|nil -- "atom" | "atom_proc" | "comp_bare" | "comp_proc" | "unrelated" once observed (nil if no declaration ever followed)
--- @field target_kind string|nil -- "atom" | "comp_bare" | "comp_proc" | "unrelated" once observed (nil if no declaration ever followed)
--- @field proc_prelude boolean|nil -- true after the marker crossed an `FI_` prelude and awaits `MipsAtomComp_Proc_`
--- @class RegTypeDefault
@@ -112,7 +111,7 @@ local parse_enum_int_literal
--- @field name string -- Atom name (for components: without ac_ prefix)
--- @field body string -- Brace-delimited body (without the braces)
--- @field body_off integer -- Char offset of body[1] in source
--- @field kind string -- "atom" | "atom_proc" | "comp_bare" | "comp_proc" | "raw_atom"
--- @field kind string -- "atom" | "comp_bare" | "comp_proc" | "raw_atom"
--- @field raw_name string -- Un-stripped name (for components: with ac_ prefix)
--- @field ident_pos integer -- Position of the MipsAtom_/MipsAtomComp_ ident start
--- @field after_paren integer -- Position past the closing paren
@@ -269,7 +268,7 @@ end
--- marker_kind == "atom_dbg_skip" AND is_bare == true
--- Any other spelling or shape (parenthesized form, legacy name) is recorded as a raw marker for annotation validation but never stamps `debug_skip`.
--- @param out SourceScan
--- @param target_kind string|nil -- "atom" | "atom_proc" | "comp_bare" | "comp_proc" | "unrelated" once observed
--- @param target_kind string|nil -- "atom" | "comp_bare" | "comp_proc" | "unrelated" once observed
--- @return boolean|nil -- true iff the marker is the positive bare form
local function attach_debug_skip_marker(out, target_kind)
local markers = out.debug_skip_markers
@@ -800,11 +799,6 @@ local BYTE_x = 0x78 -- 'x'
local BYTE_X = 0x58 -- 'X'
local BYTE_OPEN_BRACE = 0x7B -- '{'
local BYTE_CLOSE_BRACE= 0x7D -- '}'
local BYTE_SLASH = 0x2F -- '/'
local BYTE_STAR = 0x2A -- '*'
local BYTE_SPACE = 0x20 -- ' '
local BYTE_TAB = 0x09 -- '\t'
local BYTE_CR = 0x0D -- '\r'
-- Maximum chain depth when resolving `R_*_Code` symbol RHS references.
-- Eight hops is enough for any production chain (R_TapePtr_Code -> R_T8_Code -> ...).
@@ -828,44 +822,6 @@ local function hex_digit_value(b)
return nil
end
-- Read one trailing C-comment that appears immediately after `pos` in `body`,
-- skipping horizontal whitespace and newlines first. Used by `parse_enum_entry` to
-- recover the `atom_auto_reg:` / `phase_auto_reg:` scope annotation embedded by
-- the `atom_auto_reg` / `phase_auto_reg` macros' RHS expansion
-- (`R_<Sym> = R_<Sym>_Code /* atom_auto_reg: <scope> */`).
-- Handles both block (`/* ... */`) and line (`// ...`) forms.
-- Returns the comment text (without delimiters), or nil if no comment is adjacent.
local function read_trailing_cmt_after(body, pos)
local body_len = #body
while pos <= body_len do
local b = body:byte(pos)
if b == BYTE_SPACE or b == BYTE_TAB or b == BYTE_NEWLINE or b == BYTE_CR then
pos = pos + 1
elseif b == BYTE_SLASH then
local b2 = body:byte(pos + 1)
if b2 == BYTE_STAR then
-- Block comment /* ... */
local i = pos + 2
while i < body_len do
if body:byte(i) == BYTE_STAR and body:byte(i + 1) == BYTE_SLASH then
return body:sub(pos + 2, i - 1)
end
i = i + 1
end
return nil -- unterminated; treat as no comment
elseif b2 == BYTE_SLASH then
-- Line comment // ... (strip the trailing newline)
local end_pos = duffle.find_byte(body, BYTE_NEWLINE, pos + 2) or (body_len + 1)
return body:sub(pos + 2, end_pos - 1)
end
return nil
else
return nil
end
end
return nil
end
--- Parse a decimal/negative-decimal/hex integer literal starting at byte position `start`.
--- Returns (value, end_pos) on success, or (nil, start) on failure / no match.
--- Accepts: 12, -1, 0, 0x10, 0X1F, -0x10.
@@ -1172,46 +1128,6 @@ local function parse_dbg_skip_marker(source, pos, ident_end, line_of, out)
return marker_end
end
--- Parse `atom_auto_reg(<atom>, R_<Sym>)` and `phase_auto_reg(<phase>, R_<Sym>)` markers.
---
--- The macros expand to `sym = sym##_Code` per their definition in dsl.atom.h.
--- After preprocessing, the marker renders as a full enum entry of the form `R_<Sym> = R_<Sym>_Code,`.
--- This parser detects the macro invocation site, extracts `(scope_name, sym)`, and stores it
--- in the per-source table (atom_auto_regs or phase_auto_regs) under the scope's name.
---
--- @param source string
--- @param pos integer
--- @param ident_end integer
--- @param line_of fun(pos: integer): integer
--- @param out SourceScan
--- @return integer
local function parse_auto_reg_marker(source, pos, ident_end, line_of, out)
local marker_kind = source:sub(pos, ident_end - 1) -- "atom_auto_reg" or "phase_auto_reg"
local scope_kind = marker_kind == "atom_auto_reg" and "atom" or "phase"
local inner, after_paren = read_parens_after(source, ident_end)
if not inner then return after_paren end
local args = duffle.split_top_level_commas(inner)
local scope_name = args[1] and duffle.trim(args[1]) or nil
local sym = args[2] and duffle.trim(args[2]) or nil
-- Filter: only accept `R_<Sym>` form (matches `^R_[%w_]+$`).
if scope_name and sym and sym:match("^R_[%w_]+$") then
if scope_kind == "atom" then
out.atom_auto_regs = out.atom_auto_regs or {}
out.atom_auto_regs[scope_name] = out.atom_auto_regs[scope_name] or {}
out.atom_auto_regs[scope_name][sym] = sym
else
out.phase_auto_regs = out.phase_auto_regs or {}
out.phase_auto_regs[scope_name] = out.phase_auto_regs[scope_name] or {}
out.phase_auto_regs[scope_name][sym] = sym
end
end
return after_paren
end
-- Parse `atom_dbg_reg_default(R_X, <type>...)`;
-- the second argument may be a `Type` or `Type*`/`Type**` chain. Records in `out.types[R_X]`.
local function parse_atom_dbg_reg_default(source, pos, ident_end, line_of, out)
@@ -1366,49 +1282,6 @@ local function parse_mips_atom_comp_proc(source, pos, ident_end, line_of, out)
return after_paren
end
--- Parse: `MipsAtom_Proc_(<name>, <abuilder>, { <body> })` — body is inside the LAST `{` in args.
--- Per Task 12.10: full support for the runtime-proc atom form. Registers the atom
--- with kind `"atom_proc"` so offsets.lua / components.lua can emit
--- * `mac_<name>` aliases in `gen/macs.h` (the components pass)
--- * `atom_offset__X__Y` defs in `gen/offsets.h` (the offsets pass)
--- The atom name is the FIRST ident of the args (the second arg `ab` is the
--- atom-builder, not the name). Unlike `MipsAtomComp_Proc_`, there is no `ac_`
--- prefix on the symbol — `MipsAtom_Proc_` is the runtime-proc wrapper, so the
--- symbol IS the bare atom name (e.g. `normalize_v3s4`, not `ac_normalize_v3s4`).
--- @param source string
--- @param pos integer
--- @param ident_end integer
--- @param line_of fun(pos: integer): integer
--- @param out SourceScan
--- @return integer
local function parse_mips_atom_proc(source, pos, ident_end, line_of, out)
local inner, after_paren, open_paren = read_parens_after(source, ident_end)
if not inner then return after_paren end
-- Find the LAST `{` in inner (the body brace, not any potential embedded braces in expressions).
local last_brace_pos = nil
for search_pos = #inner, 1, -1 do
if inner:sub(search_pos, search_pos) == "{" then last_brace_pos = search_pos; break end
end
if not last_brace_pos then return after_paren end
-- Use duffle.read_braces to find the matching close brace.
-- Uses `read_balanced` for delimiter-depth tracking.
-- If close_pos is past the end of inner, the brace didn't match (malformed input); skip.
local body, close_pos = duffle.read_braces(inner, last_brace_pos)
if close_pos > #inner + 1 then return after_paren end
-- The atom name is the FIRST ident of the args (matches MipsAtomComp_Proc_'s "first ident" rule).
-- MipsAtom_Proc_ has no `ac_` prefix; `strip_ac_prefix` is a no-op for unprefixed names.
local raw_name = inner:match("^%s*([%w_]+)") or "?"
local name = strip_ac_prefix(raw_name)
-- Position of body[1] in source = open_paren + 1 (start of inner) + last_brace_pos + 1 (past '{').
local body_off = open_paren + 2 + last_brace_pos
register_atom(out, "atom_proc", line_of(pos), name, body, body_off, raw_name, pos, after_paren, source)
return after_paren
end
--- Parse: `MipsCode code_<name> { <body> }` (raw atom form — offsets pass only).
--- @param source string
--- @param pos integer
@@ -1729,16 +1602,6 @@ local function parse_enum_entry(source, body, body_offset, line_of, out, entry_n
local value, value_end = parse_enum_value(body, after_ws, out)
if value == nil then return value_start end
-- Capture the trailing C-comment (if any) before `skip_ws_and_cmt` discards it.
-- The `atom_auto_reg(<scope>, <sym>)` macro expands to `R_<Sym> = R_<Sym>_Code /* atom_auto_reg: <scope> */`,
-- so the scope name lives in the comment after the RHS value. Routes through `out.atom_entry_comments`
-- for downstream `parse_enum` to split into `out.atom_auto_regs` / `out.phase_auto_regs`.
local trailing_cmt = read_trailing_cmt_after(body, value_end)
if trailing_cmt then
out.atom_entry_comments = out.atom_entry_comments or {}
out.atom_entry_comments[entry_name] = trailing_cmt
end
local after_value = duffle.skip_ws_and_cmt(body, value_end)
local has_atom_reg, end_after_atom_reg = check_bare_atom_reg(body, after_value)
@@ -1794,24 +1657,15 @@ local function parse_enum_body(source, body, body_offset, line_of, out)
else
local entry_name, name_end = duffle.read_ident(body, pos)
if entry_name then
-- In-enum `atom_auto_reg(<scope>, R_<Sym>)` / `phase_auto_reg(<scope>, R_<Sym>)` markers:
-- the C preprocessor expands them to `R_<Sym> = R_<Sym>_Code /* atom_auto_reg: <scope> */`,
-- but the metaprogram reads source-as-written so we must dispatch the parser here too.
-- Mirrors the top-level `DECL_PARSERS` entry for `atom_auto_reg` / `phase_auto_reg`.
if entry_name == "atom_auto_reg" or entry_name == "phase_auto_reg" then
local new_pos = parse_auto_reg_marker(body, pos, name_end, line_of, out)
if new_pos > pos then pos = new_pos else pos = name_end end
local after_name = duffle.skip_ws_and_cmt(body, name_end)
if body:byte(after_name) == BYTE_EQUAL then
local new_pos = parse_enum_entry(
source, body, body_offset, line_of, out,
entry_name, pos, after_name + 1
)
if new_pos > pos then pos = new_pos else pos = after_name + 1 end
else
local after_name = duffle.skip_ws_and_cmt(body, name_end)
if body:byte(after_name) == BYTE_EQUAL then
local new_pos = parse_enum_entry(
source, body, body_offset, line_of, out,
entry_name, pos, after_name + 1
)
if new_pos > pos then pos = new_pos else pos = after_name + 1 end
else
pos = name_end
end
pos = name_end
end
else
pos = pos + 1
@@ -1841,25 +1695,6 @@ local function parse_enum(source, pos, ident_end, line_of, out)
if not body then return after_brace end
parse_enum_body(source, body, body_off, line_of, out)
-- Route `atom_auto_reg:` / `phase_auto_reg:` markers discovered in trailing C-comments
-- into the per-source `atom_auto_regs` / `phase_auto_regs` projections.
-- Pattern matches the RHS expansion `R_<Sym> = R_<Sym>_Code /* <kind>_auto_reg: <scope> */`
-- emitted by the `atom_auto_reg` / `phase_auto_reg` macros in dsl.atom.h.
for entry_name, cmt_text in pairs(out.atom_entry_comments or {}) do
local atom_scope = cmt_text:match("atom_auto_reg:%s*([%w_]+)")
if atom_scope then
out.atom_auto_regs = out.atom_auto_regs or {}
out.atom_auto_regs[atom_scope] = out.atom_auto_regs[atom_scope] or {}
out.atom_auto_regs[atom_scope][entry_name] = entry_name
end
local phase_scope = cmt_text:match("phase_auto_reg:%s*([%w_]+)")
if phase_scope then
out.phase_auto_regs = out.phase_auto_regs or {}
out.phase_auto_regs[phase_scope] = out.phase_auto_regs[phase_scope] or {}
out.phase_auto_regs[phase_scope][entry_name] = entry_name
end
end
return after_brace
end
@@ -1873,18 +1708,12 @@ end
local DECL_PARSERS = {
MipsAtom_ = parse_mips_atom,
MipsAtom_Proc_ = parse_mips_atom_proc,
MipsAtomComp_ = parse_mips_atom_comp,
MipsAtomComp_Proc_ = parse_mips_atom_comp_proc,
-- `atom_dbg_skip` is the only debug-skip parser entry. Every other
-- identifier follows the ordinary unrelated-token path; there is no alias.
atom_dbg_skip = parse_dbg_skip_marker,
atom_dbg_reg_default = parse_atom_dbg_reg_default,
-- `atom_auto_reg(atom, R_<Sym>)` and `phase_auto_reg(phase, R_<Sym>)` populate per-source
-- `out.atom_auto_regs` / `out.phase_auto_regs`; the cross-source merge lands in
-- `corpus.atom_auto_regs` / `corpus.phase_auto_regs` (first-wins).
atom_auto_reg = parse_auto_reg_marker,
phase_auto_reg = parse_auto_reg_marker,
MipsCode = parse_mips_code,
typedef = parse_typedef_binds,
_Pragma = parse_pragma_macro,
@@ -1919,14 +1748,6 @@ local function scan_source(source, source_file, code_macros, code_macro_bodies)
debug_skip_markers = {},
types = {},
atom_views = {},
-- Per-source projection for `atom_auto_reg(<atom>, R_<Sym>)` markers.
-- Each entry is keyed by atom_name; the inner table maps `R_<Sym>` -> `R_<Sym>` (raw LHS sym).
-- Merged cross-source into `corpus.atom_auto_regs` (first-wins).
atom_auto_regs = {},
-- Per-source projection for `phase_auto_reg(<phase>, R_<Sym>)` markers.
-- Each entry is keyed by phase_label; the inner table maps `R_<Sym>` -> `R_<Sym>` (raw LHS sym).
-- Merged cross-source into `corpus.phase_auto_regs` (first-wins).
phase_auto_regs = {},
line_of = line_of,
-- Source-derived register-alias registry (atom_reg opt-in entries).
-- Keys are full R_* idents (never stripped); see parse_enum / parse_enum_body.
@@ -2166,8 +1987,6 @@ local function merge_corpus_registries(corpus)
corpus.atom_ctxs = corpus.atom_ctxs or {}
corpus.atom_phases = corpus.atom_phases or {}
corpus.atom_infos = corpus.atom_infos or {}
corpus.atom_auto_regs = corpus.atom_auto_regs or {}
corpus.phase_auto_regs = corpus.phase_auto_regs or {}
corpus.collisions = corpus.collisions or {}
-- Replace the existing corpus collections with empty tables so a re-run on the same corpus produces identical state (deterministic merge).
@@ -2211,7 +2030,7 @@ local function merge_corpus_registries(corpus)
corpus.collisions, "binds", bind_shape)
end
-- atoms_by_name: MipsAtom_(name) + MipsAtom_Proc_(name) + MipsAtomComp_(name) + MipsAtomComp_Proc_(name).
-- atoms_by_name: MipsAtom_(name) + MipsAtomComp_(name) + MipsAtomComp_Proc_(name).
-- Each atom carries `{line, name, body, body_off, kind, raw_name, ...}`.
-- Duplicate atom names across sources are first-wins + collision; see the atom_infos block below for the evidence list.
for _, atom_entry in ipairs(scan.atoms or {}) do
@@ -2246,22 +2065,6 @@ local function merge_corpus_registries(corpus)
corpus.collisions, "phase", phase_shape)
end
-- atom_auto_regs: keyed by atom scope name; each carries a `{R_<Sym> = R_<Sym>}` map.
-- Per-source entries are simple inner maps (no body / no shape comparison); first-wins suffices.
for atom_scope, syms in pairs(scan.atom_auto_regs or {}) do
if corpus.atom_auto_regs[atom_scope] == nil then
corpus.atom_auto_regs[atom_scope] = syms
end
end
-- phase_auto_regs: keyed by phase label; each carries a `{R_<Sym> = R_<Sym>}` map.
-- Per-source entries are simple inner maps (no body / no shape comparison); first-wins suffices.
for phase_label, syms in pairs(scan.phase_auto_regs or {}) do
if corpus.phase_auto_regs[phase_label] == nil then
corpus.phase_auto_regs[phase_label] = syms
end
end
-- atom_infos: ALWAYS append every record in source/declaration order.
-- Duplicates are preserved so the annotation pass can flag them via `check_unique_annotation`;
-- The merge is purely order-preserving.
+38 -97
View File
@@ -39,8 +39,8 @@
--- `── Info` section renders finding-level info between `── Warnings` and the per-atom cycle counts.
---
--- The structural handshake checks (`mac_yield_uniformity`, `hazard_nop_use`, `control_transfer_delay_slot_use`) skip atoms/components with `debug_skip == true`.
--- `atom_dbg_skip` marker designates runtime-helper declarations whose structure is fixed by the tape runtime (e.g. `tape_exit`, `ac_yield`).
--- Flagging them as "missing mac_yield" or "BD slot is redundant".
--- The `atom_dbg_skip` marker designates runtime-helper declarations whose structure is fixed by the tape runtime (e.g. `tape_exit`, `ac_yield`).
--- Flagging them as "missing mac_yield" or "BD slot is redundant" is signal noise, not a logic failure.
--- Other checks (transfer_hazards, gpu_portstore_shape, abi_handoff, enum_alias_membership, …) still apply to debug_skip declarations because real hazards / typos can still surface in them.
---
--- The orchestrator (`ps1_meta.lua`) wires this module in via the PASSES table:
@@ -256,21 +256,8 @@ 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_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 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 function classify_tokens(tokens)
local n = #tokens
@@ -314,13 +301,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 matches_any(tok, UNCOND_JUMP_PATTERNS) then
elseif tok:match(UNCOND_JUMP_PATTERN) 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 matches_any(tok, TERMINAL_JUMP_PATTERNS) then
elseif tok:match(TERMINAL_JUMP_PATTERN) 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
@@ -452,7 +439,7 @@ local function is_cop2_consumer_of(consumer_event, destination, producer_rel)
end
-- True iff `consumer_event` reads the GPR operand at any position the destination register occupies.
-- read_pos lookup consults `duffle.OPERAND_READ_POSITIONS` for the consumer's encoder and walks each `args[pos]` to find an operand-equal match.
-- The read-position lookup consults `duffle.OPERAND_READ_POSITIONS` for the consumer's encoder and walks each `args[pos]` to find an operand-equal match.
local function is_gpr_consumer_of(consumer_event, destination)
local consumer_token = consumer_event.encoder or consumer_event.ident
local read_pos = duffle.OPERAND_READ_POSITIONS or {}
@@ -580,31 +567,13 @@ local function evaluate_gpr_value_rule(rule, ev_args, gpr_values)
return shift_left_u4(immediate % 0x10000, 16)
end
-- 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
local source = nil
if rule.source then
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
source = constant_for_operand(gpr_values, ev_args[rule.source])
if source == 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")
@@ -1464,21 +1433,17 @@ 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 from some checks, but load-delay
--- safety applies to their emitted instructions as well.
--- 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).
---
--- 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.
-- `atom_proc` atoms have full bodies with loads that need delay slots, so the check applies to them too.
local p = atom.paths or {}
if atom.kind ~= "atom" and atom.kind ~= "atom_proc" then return end
local events = p.word_events or {}
if atom.kind ~= "atom" then return end
local events = atom.paths.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 {}
@@ -1580,8 +1545,6 @@ local function check_mac_yield_uniformity(atom, pipe_ctx, findings)
if is_runtime_helper(atom) then return end
-- Per-kind semantics:
-- MipsAtom_ (baked atom): exactly 1 mac_yield at the end of the body. Control transfer is the atom's job.
-- MipsAtom_Proc_ (runtime-proc atom): exactly 1 mac_yield at the end of the body. Same as baked atom;
-- the proc IS the atom; the runtime call to `atombuilder_unroll` doesn't introduce a parent atom.
-- MipsAtomComp_ (bare static-array component): ZERO mac_yield.
-- The component is invoked from inside an atom body; the parent atom does the yield.
-- MipsAtomComp_Proc_ (procedural component): ZERO mac_yield.
@@ -1605,7 +1568,7 @@ local function check_mac_yield_uniformity(atom, pipe_ctx, findings)
return atom.line + line_in_body[tokens[idx].rel]
end
if atom.kind == "atom" or atom.kind == "atom_proc" then
if atom.kind == "atom" then
-- Baked atom: exactly 1 yield at the end.
if count == 0 then
findings[#findings + 1] = {
@@ -1650,7 +1613,6 @@ local function check_mac_yield_uniformity(atom, pipe_ctx, findings)
-- The parent atom does the yield.
-- A yield inside a component would either be dead code (bare) or prematurely terminate the function (proc).
-- Both are bugs.
-- `atom_proc` atoms are NOT components; they're runtime-proc atoms that own their own yield (handled in the `if` branch above).
if count > 0 then
findings[#findings + 1] = {
atom = atom.name,
@@ -1682,7 +1644,7 @@ end
--- Per-atom. Runtime-helper atoms (`debug_skip`) are exempt.
--- Takes `(atom, pipe_ctx, findings)`; `pipe_ctx` is unused.
local function check_yield_load_tail_pairing(atom, _pipe_ctx, findings)
if atom.kind ~= "atom" and atom.kind ~= "atom_proc" then return end
if atom.kind ~= "atom" then return end
if is_runtime_helper(atom) then return end
local tokens = atom.paths.tokens
@@ -1694,39 +1656,21 @@ 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, 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.
-- ── Rule 1: every `mac_yield_load()` must be in a branch BD-slot.
for tok_idx = 1, n do
local c = tc[tok_idx]
if c.ident == "mac_yield_load" then
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 "<none>"
local next_ident = next_label_tc and (next_label_tc.ident .. "(" .. (next_label_tc.label_name or "?") .. ")") or "<no following label>"
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
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 "<none>"
findings[#findings + 1] = {
atom = atom.name,
line = tok_idx >= 2 and line_for(tok_idx) or atom.line,
check = "yield_load_tail_pairing",
kind = "error",
msg = string.format(
"%s at line %d has `mac_yield_load()` at word %d but the previous token is `%s`, not a branch — `mac_yield_load()` must fill a branch BD-slot."
, atom.name, tok_idx >= 2 and line_for(tok_idx) or atom.line, tok_idx, prev_ident),
}
end
end
end
@@ -1901,9 +1845,9 @@ end
--- - Atoms containing a `mac_<name>(...)` call whose `name` is not registered in `pipe_ctx.components_by_name` emit a "new macro;
--- Not in corpus.components" advisory — the auto-derivation returned nil for that name.
---
--- Applies only to `kind = "atom"` or `kind = "atom_proc"` (full-atom bodies). Components don't emit full primitives.
--- Applies only to `kind = "atom"` (baked atoms). Components don't emit full primitives.
local function check_gpu_portstore_shape(atom, pipe_ctx, findings)
if atom.kind ~= "atom" and atom.kind ~= "atom_proc" then return end
if atom.kind ~= "atom" then return end
local tokens = atom.paths.tokens
local line_in_body = atom.paths.line_in_body
local tc = atom.paths.tok_class
@@ -2071,9 +2015,8 @@ local function analyze_atom_paths(atom, pipe_ctx)
succ[#succ + 1] = label_pos + 1
end
end
-- 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
-- For literal-offset jumps (label == false), the target is a non-tracked address; conservatively omit.
return succ, nil
end
-- Conditional branch: BD slot absorbed; two successors — fall-through (tok_idx+2) + taken (if known).
if tok_idx + 2 <= n then
@@ -2089,11 +2032,9 @@ 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.
-- 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.
-- Normal token: just the next one
if tok_idx + 1 <= n then return { tok_idx + 1 }, nil end
return {}, tok_idx
return {}, nil
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.
Binary file not shown.
+49 -25
View File
@@ -16,36 +16,60 @@
-- Companion: scripts/gdb/gdb_tape_atoms.gdb (covers GPRs + atom-aware stepping).
local function register_handlers()
if not PCSX.WebServer then PCSX.WebServer = {} end
if not PCSX.WebServer.Handlers then PCSX.WebServer.Handlers = {} end
if not PCSX.WebServer then PCSX.WebServer = {} end
if not PCSX.WebServer.Handlers then PCSX.WebServer.Handlers = {} end
-- ── GTE state ──
PCSX.WebServer.Handlers.gte = function(req)
local r = PCSX.getRegisters()
local out = { "pc=0x" .. string.format("%x", r.pc) }
for i = 0, 31 do
out[#out + 1] = string.format("D[%d]=0x%08x C[%d]=0x%08x",
i, r.CP2D.r[i], i, r.CP2C.r[i])
end
return table.concat(out, "\n")
end
-- ── GTE state ──
PCSX.WebServer.Handlers.gte = function(req)
local r = PCSX.getRegisters()
local out = { "pc=0x" .. string.format("%x", r.pc) }
for i = 0, 31 do
out[#out + 1] = string.format("D[%d]=0x%08x C[%d]=0x%08x",
i, r.CP2D.r[i], i, r.CP2C.r[i])
end
return table.concat(out, "\n")
end
-- ── GP state (pointer to existing endpoints) ──
-- pcsx-redux's Lua GPU API exposes only takeScreenShot(); no GPUSTAT / GP0 / GP1 command log / display state.
-- We point to the existing web endpoints that DO expose those (when the emulator is actually rendering. Paused-at-BP frames won't have a fresh frame).
PCSX.WebServer.Handlers.gp = function(req)
local out = {
"gpu_screenshot_png=http://localhost:8080/api/v1/state/still",
"vram_raw=http://localhost:8080/api/v1/gpu/vram/raw (1MB VRAM)",
"gpustat=NOT_AVAILABLE_VIA_LUA",
"gp_command_log=NOT_AVAILABLE_VIA_LUA (use pcsx-redux Debug > GPU Logger)",
"hint_run_emulator_unpaused_for_screenshot",
}
return table.concat(out, "\n")
end
-- ── GP state (pointer to existing endpoints) ──
-- pcsx-redux's Lua GPU API exposes only takeScreenShot(); no GPUSTAT / GP0 / GP1 command log / display state.
-- We point to the existing web endpoints that DO expose those (when the emulator is actually rendering. Paused-at-BP frames won't have a fresh frame).
PCSX.WebServer.Handlers.gp = function(req)
local out = {
"gpu_screenshot_png=http://localhost:8080/api/v1/state/still",
"vram_raw=http://localhost:8080/api/v1/gpu/vram/raw (1MB VRAM)",
"gpustat=NOT_AVAILABLE_VIA_LUA",
"gp_command_log=NOT_AVAILABLE_VIA_LUA (use pcsx-redux Debug > GPU Logger)",
"hint_run_emulator_unpaused_for_screenshot",
}
return table.concat(out, "\n")
end
end
local ok, err = pcall(register_handlers)
if ok then print("[pcsx_debug_helper] handlers registered: gte, gp")
else print("[pcsx_debug_helper] registration failed: " .. tostring(err))
end
-- ── reload handler (Task 6) ──
-- After gte and gp register successfully, load reload.lua through Support.extra.dofile and call its install(pcsx, support).
-- The whole sequence runs inside pcall so a missing zip, missing module table,
-- or throwing install never disturbs the gte and gp handlers already registered above (handler isolation).
--
-- The failure messages are intentionally single-line so the helper's boot log stays scannable.
if type(Support) == "table"
and type(Support.extra) == "table"
and type(Support.extra.dofile) == "function" then
local load_ok, reload_mod = pcall(Support.extra.dofile, "reload.lua")
if load_ok and type(reload_mod) == "table" and type(reload_mod.install) == "function" then
local install_ok, install_err = pcall(reload_mod.install, PCSX, Support)
if install_ok then
print("[pcsx_debug_helper] reload handler registered")
else
print("[pcsx_debug_helper] reload registration failed: " .. tostring(install_err))
end
else
print("[pcsx_debug_helper] reload load failed: " .. tostring(reload_mod))
end
else
print("[pcsx_debug_helper] reload load failed: Support.extra.dofile unavailable")
end
+902
View File
@@ -0,0 +1,902 @@
-- reload.lua - Side-effect-free hot-reload helper for the
-- pcsx_redux_hot_reload track (Task 2). This file owns the HTTP request
-- surface that the launch / reload client targets:
--
-- POST /api/v1/lua/reload?mode=prime&target=hello_camera&path=<encoded-elf>
-- POST /api/v1/lua/reload?mode=elf&target=hello_camera&path=<encoded-elf>
-- POST /api/v1/lua/reload?mode=patch&target=hello_camera&addr=...&hex=...
--
-- This module exposes the public surface used by the contract harness
-- (tests/reload_helper_contract.lua) and the runtime installed by
-- scripts/pcsx_debug_helper/autoexec.lua. The module must not reference
-- the global PCSX table at load time; the host is passed in explicitly
-- through M.new(host) and M.install(pcsx, support).
--
-- Public surface:
-- M.parse_query(query) -> table, nil OR nil, err_string
-- M.json_response(fields) -> string (sorted keys)
-- M.parse_manifest(...) -> Task 3 (real impl uses elf32.lua)
-- M.new(host) -> runtime object (Task 4; stub here)
-- M.install(pcsx, support) -> registers web handler (Task 6; stub here)
--
-- Companion: scripts/pcsx_debug_helper/autoexec.lua.
-- ---------------------------------------------------------------------------
-- Load the shared ELF32 helpers.
--
-- **The bane of this refactor:** the helper VM (PCSX-Redux) does not expose
-- `require` for paths outside the helper zip. The production loader is
-- `Support.extra.dofile("elf32.lua")` — Support.extra.dofile resolves the
-- name against the helper zip's contents (the zip is generated by the
-- build script and includes both `reload.lua` and `elf32.lua` after Task 6).
--
-- The test harness at `tests/reload_helper_contract.lua` loads `reload.lua`
-- via standard Lua `dofile` with an absolute path; it does not install a
-- `Support` object. We detect the runtime context: if `Support.extra.dofile`
-- exists, use it (production path); otherwise fall back to standard `dofile`
-- with an absolute path (test harness path).
-- ---------------------------------------------------------------------------
local function load_elf32()
if type(Support) == "table"
and type(Support.extra) == "table"
and type(Support.extra.dofile) == "function" then
return Support.extra.dofile("elf32.lua")
end
-- Test harness + any other context that supplies standard Lua dofile.
return dofile("C:/projects/Pikuma/ps1/scripts/elf32.lua")
end
local E = load_elf32()
local M = {}
-- ---------------------------------------------------------------------------
-- parse_query(query)
--
-- Parses an application/x-www-form-urlencoded query string into a table.
--
-- Rules (per spec §8 + plan.md Task 2 Step 3):
-- * Each pair is split on the first '='; the key is to the left, the value
-- to the right. A pair without '=' is a malformed_pair.
-- * Percent escapes '%HH' (HH = two hex digits) decode to the corresponding
-- byte. A '%' not followed by two hex digits is a malformed_escape.
-- * '+' decodes to a literal space (applied after percent decode).
-- * A key appearing more than once is a duplicate_key error.
--
-- Returns the parsed table on success. On failure returns nil and a stable
-- error string suitable for the JSON error envelope. An empty / nil query
-- returns an empty table (not an error).
-- ---------------------------------------------------------------------------
local function percent_decode(s)
-- Walk the string once, byte by byte. A '%' must be followed by exactly
-- two hex digits; '+' decodes to ' '; everything else is passed through.
local out = {}
local i = 1
local len = #s
while i <= len do
local c = s:sub(i, i)
if c == "%" then
if i + 2 > len then
return nil -- truncated escape (e.g., '%' at end or '%X')
end
local hex = s:sub(i + 1, i + 2)
local hd1, hd2 = hex:sub(1, 1), hex:sub(2, 2)
-- Validate both characters are hex digits.
if not (hd1:match("[0-9A-Fa-f]") and hd2:match("[0-9A-Fa-f]")) then
return nil -- malformed escape
end
out[#out + 1] = string.char(tonumber(hex, 16))
i = i + 3
else
out[#out + 1] = c
i = i + 1
end
end
return table.concat(out)
end
local function plus_to_space(s)
-- Standalone helper so callers can decode '+' after percent decoding.
return (s:gsub("+", " "))
end
function M.parse_query(query)
if query == nil or query == "" then
return {}, nil
end
local result = {}
local seen = {}
for pair in query:gmatch("[^&]+") do
-- Split on the first '=' only.
local eq = pair:find("=", 1, true)
if not eq then
return nil, "malformed_pair"
end
local raw_key = pair:sub(1, eq - 1)
local raw_value = pair:sub(eq + 1)
-- Percent-decode first, then convert '+' to space. The order matters:
-- a '%2B' should decode to '+' (literal plus), not be re-converted to a
-- space. Per RFC 1866 §8.2.1, '+' is a literal plus in the encoded form
-- only when it represents a space.
local key = percent_decode(raw_key)
if key == nil then
return nil, "malformed_escape"
end
key = plus_to_space(key)
local val = percent_decode(raw_value)
if val == nil then
return nil, "malformed_escape"
end
val = plus_to_space(val)
if seen[key] then
return nil, "duplicate_key"
end
seen[key] = true
result[key] = val
end
return result, nil
end
-- ---------------------------------------------------------------------------
-- json_response(fields)
--
-- Deterministic JSON object encoder. Returns a string. Keys are sorted
-- alphabetically before emission so byte-for-byte equality is testable
-- across runs and across PS1 captures.
--
-- Supported value types: string, number, boolean, nil (encoded as null).
-- Strings escape '\', '"', and the C0 control range (0x00..0x1F). The
-- named escapes use the conventional single-char forms: \\, \", \b, \f,
-- \n, \r, \t. Everything else in 0x00..0x1F is \uXXXX.
-- ---------------------------------------------------------------------------
local function json_escape_string(s)
-- Two passes: first the named escapes, then the catch-all C0 range
-- (%c covers 0x00..0x1F in Lua patterns). Using plain string.gsub
-- with a literal replacement table covers the named escapes; a
-- second gsub handles the rest.
s = s:gsub('[\\"]', {
["\\"] = "\\\\",
['"'] = '\\"',
})
s = s:gsub("\b", "\\b")
s = s:gsub("\f", "\\f")
s = s:gsub("\n", "\\n")
s = s:gsub("\r", "\\r")
s = s:gsub("\t", "\\t")
-- Remaining C0 control characters (0x00..0x1F) become \uXXXX. We
-- intentionally keep the named escapes above (which are already
-- single backslashes in the output) from being re-escaped: gsub on
-- the literal control char bytes doesn't match the backslashes we
-- already inserted.
s = s:gsub("([%c])", function(c)
return string.format("\\u%04x", string.byte(c))
end)
return s
end
function M.json_response(fields)
if type(fields) ~= "table" then
error("json_response: expected table, got " .. type(fields))
end
-- Sort keys for deterministic output. Lua's table.sort is byte-wise
-- and stable for strings; JSON object key order is not significant
-- but tests rely on a fixed order to compare against fixtures.
local keys = {}
for k in pairs(fields) do
keys[#keys + 1] = k
end
table.sort(keys)
local parts = {}
parts[#parts + 1] = "{"
for i = 1, #keys do
local k = keys[i]
if i > 1 then
parts[#parts + 1] = ","
end
parts[#parts + 1] = '"'
parts[#parts + 1] = json_escape_string(k)
parts[#parts + 1] = '":'
local v = fields[k]
local tv = type(v)
if tv == "string" then
parts[#parts + 1] = '"'
parts[#parts + 1] = json_escape_string(v)
parts[#parts + 1] = '"'
elseif tv == "number" then
parts[#parts + 1] = tostring(v)
elseif tv == "boolean" then
parts[#parts + 1] = v and "true" or "false"
elseif v == nil then
parts[#parts + 1] = "null"
else
error("json_response: unsupported value type " .. tv .. " for key " .. tostring(k))
end
end
parts[#parts + 1] = "}"
return table.concat(parts)
end
-- ---------------------------------------------------------------------------
-- ELF32 manifest parser (Task 3).
--
-- Parses a little-endian ELF32 file exposed through a file_adapter that
-- provides read_u8_at/read_u16_at/read_u32_at/read_size. The parser validates the
-- magic, class, data encoding, and machine before reading anything else.
-- It resolves section names through the .shstrtab table and symbols
-- through every SHT_SYMTAB section (and its linked string table).
--
-- The output manifest contains the state ABI the reload gate must
-- preserve plus the addresses the helper writes to the CPU on a reload.
-- Loaded sections (SHF_ALLOC, non-SHT_NOBITS) are recorded so the runtime
-- can reject any ELF whose loaded range overlaps the preserved smem.
--
-- **Refactor:** the format-constant tables + the byte-level walker live in
-- scripts/elf32.lua (loaded above via `load_elf32()`). This module retains
-- only the manifest-specific validation: required symbols, smem size, stack
-- alignment, loaded-section overlap. The net effect is ~80 lines shorter.
--
-- Stable error codes (returned as the second value):
-- bad_magic, unsupported_elf_class, unsupported_elf_data,
-- non_mips_machine, truncated_header, truncated_section_headers,
-- missing_shstrtab, missing_symtab_strtab, missing_smem,
-- missing_data_start, missing_data_end, missing_bss_start,
-- missing_bss_end, missing_stack_top, missing_hot_reload_entry,
-- zero_smem_size, stack_misaligned, stack_out_of_main_ram,
-- section_overlaps_smem, bad_file_adapter
-- ---------------------------------------------------------------------------
-- Convert a KSEG0/KSEG1/physical address to its physical main-RAM offset.
local function to_physical(addr)
if addr >= 0x80000000 and addr < 0x80200000 then
return addr - 0x80000000
elseif addr >= 0xa0000000 and addr < 0xa0200000 then
return addr - 0xa0000000
end
return addr
end
-- Strip KSEG0 / KSEG1 alias from an address and return the physical main-RAM
-- offset. Used by M.elf_reload and M.patch_handler. Returns nil when the
-- address falls outside physical main RAM (0..0x1fffff), KSEG0 main RAM
-- (0x80000000..0x801fffff), or KSEG1 main RAM (0xa0000000..0xa01fffff).
-- Per spec §7 the patch path MUST reject scratchpad (0x1F800000+), BIOS
-- (0x1FC00000+), MMIO, and expansion aliases; this helper centralizes the
-- strip + range check so callers cannot forget the upper bound.
local function strip_kseg(addr)
if type(addr) ~= "number" then return nil end
if addr >= 0x80000000 and addr < 0x80200000 then
return addr - 0x80000000
elseif addr >= 0xa0000000 and addr < 0xa0200000 then
return addr - 0xa0000000
elseif addr >= 0 and addr < 0x200000 then
return addr
end
return nil
end
-- Parse a hex string ("0xHHHH..." or "HHHH...") into a 32-bit unsigned
-- integer. Returns nil + stable error on absent / non-hex / out-of-range.
-- Used for both the patch path's addr/hex query parameters and any other
-- 32-bit hex field the API may add. Accepts up to 8 hex digits.
local function parse_hex_u32(s, missing_err, badhex_err)
if type(s) ~= "string" or #s == 0 then
return nil, missing_err or "missing_hex"
end
local clean = s:match("^0[xX]([0-9A-Fa-f]+)$")
or s:match("^([0-9A-Fa-f]+)$")
if not clean then return nil, badhex_err or "non_hex" end
if #clean > 8 then return nil, badhex_err or "non_hex" end
return tonumber(clean, 16), nil
end
-- Trap on a missing E.* — keeps the existing one-line-error pattern when
-- the helper zip is stale or absent.
local function stack()
io.stderr:write("[reload.parse_manifest] FATAL: scripts/elf32.lua not loaded; aborting\n")
error("elf32 module not loaded")
end
local function parse_manifest_impl(file_adapter, target, path, require_entry)
-- Wrap the body in a pcall so any thrown exception (e.g. a bad
-- adapter method or a malformed section header) surfaces as a
-- parse_error with the message and traceback instead of being lost
-- into the with_busy_guard xpcall as a generic internal_error.
local inner_ok, inner_result, inner_err = pcall(function()
-- Validate the adapter surface. E.validate_adapter returns the same
-- "bad_file_adapter" error code the prior implementation used.
local ok, err = E.validate_adapter(file_adapter)
if not ok then return nil, err end
-- Magic, class, data encoding. E.parse_elf32_headers reads fields at
-- the wire offsets specified in E.ELF32_HEADER.
local hdr, hdr_err = E.parse_elf32_headers(file_adapter)
if not hdr then return nil, hdr_err end
-- Machine check (e.g. EM_MIPS = 8). e_machine is at offset 0x12 (18).
-- The reload helper rejects non-MIPS ELFs before any symbol work.
-- Explicit pass style: E.read_u16(adapter, off). The helper wraps the
-- Support.File adapter once to strip its implicit `self` so the
-- parser shape stays flat-function, not colon-dispatch.
local machine = E.read_u16(file_adapter, 0x12)
if not machine then return nil, "truncated_header" end
if machine ~= E.EM_MIPS then
return nil, "non_mips_machine"
end
-- Walk sections. E.walk_sections also resolves .shstrtab names.
local sections, walk_err = E.walk_sections(file_adapter, hdr)
if not sections then return nil, walk_err end
-- Walk symbols. E.collect_symbols includes both STB_LOCAL and STB_GLOBAL
-- (the live ELF stores smem as a local symbol).
local symbols, sym_err = E.collect_symbols(file_adapter, sections)
if not symbols then return nil, sym_err end
-- Required symbols.
local smem = symbols["smem"]
local data_start = symbols["__data_start"]
local data_end = symbols["__data_end"]
local bss_start = symbols["__bss_start"]
local bss_end = symbols["__bss_end"]
local stack_top_s = symbols["__sp"]
local entry_s = symbols["hot_reload_entry"]
if not smem then return nil, "missing_smem" end
if not data_start then return nil, "missing_data_start" end
if not data_end then return nil, "missing_data_end" end
if not bss_start then return nil, "missing_bss_start" end
if not bss_end then return nil, "missing_bss_end" end
if not stack_top_s then return nil, "missing_stack_top" end
if require_entry and not entry_s then
return nil, "missing_hot_reload_entry"
end
-- Validate smem size.
if smem.size == 0 then
return nil, "zero_smem_size"
end
-- Validate stack alignment and range.
local stack_top = stack_top_s.value
if stack_top % 8 ~= 0 then
return nil, "stack_misaligned"
end
local p = to_physical(stack_top)
if p < 0 or p > 0x1fffff then
return nil, "stack_out_of_main_ram"
end
-- Collect loaded (SHF_ALLOC, non-SHT_NOBITS) sections and check overlap.
local loaded = {}
local smem_lo = smem.value
local smem_hi = smem.value + smem.size
for _, s in ipairs(sections) do
-- bit 1 (SHF_ALLOC = 0x2) of sh_flags. The modulo-4 trick matches
-- the prior implementation; canonicalising on E.SHF_ALLOC would
-- gain readability but lose the exact prior behavior.
local is_alloc = (s.sh_flags % 4) >= 2
if is_alloc and s.sh_type ~= E.SHT_NOBITS and s.sh_size > 0 then
loaded[#loaded + 1] = { name = s.name, addr = s.sh_addr, size = s.sh_size }
local lo = s.sh_addr
local hi = s.sh_addr + s.sh_size
if lo < smem_hi and hi > smem_lo then
return nil, "section_overlaps_smem"
end
end
end
return {
target = target,
elf_path = path,
elf_entry = hdr.e_entry,
smem_addr = smem.value,
smem_size = smem.size,
bss_start = bss_start.value,
bss_end = bss_end.value,
data_start = data_start.value,
data_end = data_end.value,
hot_reload_entry = entry_s and entry_s.value or nil,
stack_top = stack_top,
loaded_sections = loaded,
}
end)
if inner_ok then
return inner_result, inner_err
end
-- pcall captured a thrown error; surface as parse_error with the
-- message + traceback so the caller can render it.
local tb = debug.traceback(inner_result, 2)
local err = {
parse_error = true,
detail = tostring(inner_result),
tb = tb,
}
return nil, err
end
function M.parse_manifest(file_adapter, target, path, require_entry)
if type(E) ~= "table" or type(E.parse_elf32_headers) ~= "function" then
stack()
end
return parse_manifest_impl(file_adapter, target, path, require_entry)
end
-- ---------------------------------------------------------------------------
-- Runtime + dispatch (Task 4)
--
-- M.new(host) returns a runtime object that owns:
-- active -- the most recently primed manifest, or nil
-- busy -- boolean guard; only one request runs at a time
-- host -- the bound host surface (pause / memory_file / open_file
-- / binary_load / invalidate_cache / get_registers)
--
-- runtime:handle(req) parses the query through M.parse_query, validates
-- the mode against a dispatch table, then acquires the busy guard through
-- xpcall so any error inside the handler releases the guard. The response
-- is always a JSON string built by M.json_response.
--
-- M.prime_active and M.elf_reload are the two handler bodies Task 4 ships.
-- prime_active always parses with require_entry=false (Phase 0 binary
-- compatibility). elf_reload always parses with require_entry=true (the
-- new binary must expose hot_reload_entry). Both validate the parsed
-- manifest; elf_reload runs the five-field ABI gate before declaring
-- success. Full host.pause / memory_file / binary_load / invalidate_cache
-- / get_registers sequencing is Task 5.
-- ---------------------------------------------------------------------------
-- Convert a manifest into the JSON-serializable field subset. loaded_sections
-- is excluded because json_response only supports scalars + nil.
local function manifest_to_response(m)
local fields = {
ok = true,
target = m.target,
elf_path = m.elf_path,
elf_entry = m.elf_entry,
smem_addr = m.smem_addr,
smem_size = m.smem_size,
bss_start = m.bss_start,
bss_end = m.bss_end,
data_start = m.data_start,
data_end = m.data_end,
stack_top = m.stack_top,
}
if m.hot_reload_entry then
fields.hot_reload_entry = m.hot_reload_entry
end
return fields
end
-- Open the new ELF through the host and parse its manifest.
-- Returns manifest on success; nil + stable error on failure.
local function parse_manifest_via_host(host, target, path, require_entry)
local adapter = host.open_file(path)
if not adapter then
return nil, "open_file_failed"
end
return M.parse_manifest(adapter, target, path, require_entry)
end
-- prime_active: parse with require_entry=false. Accepts Phase 0 binaries
-- that lack hot_reload_entry. Stores the manifest in runtime.active.
function M.prime_active(runtime, parsed)
local manifest, err = parse_manifest_via_host(
runtime.host, parsed.target, parsed.path, false)
if not manifest then
return M.json_response({ ok = false, error = err, restart_required = true })
end
runtime.active = manifest
return M.json_response(manifest_to_response(manifest))
end
-- elf_reload: full host-driven reload sequence.
--
-- Per conductor/tracks/ps1_pcsx_redux_hot_reload_20260802/spec.md §5 +
-- plan.md Task 5 Step 4. The canonical 11-entry success log is:
--
-- pause, memory_file, state_read, open_new_elf, binary_load,
-- state_restore, invalidate_cache, get_registers, write_sp,
-- write_ra, write_pc
--
-- Sequencing:
--
-- 1. Validate the request (target == active.target, path present).
-- 2. Compute the physical address of `active.smem_addr` via
-- strip_kseg; reject if outside physical main RAM.
-- 3. PARSE PHASE (before pause):
-- a. elf_handle = host.open_file(parsed.path)
-- b. manifest = M.parse_manifest(elf_handle, ..., require_entry=true)
-- c. Run the five-field ABI gate against runtime.active.
-- d. On any rejection here, return BEFORE pause — the runtime
-- has invoked host.open_file once (logging "open_file") and
-- no other host methods.
-- 4. Pause + snapshot:
-- host.pause()
-- mem = host.memory_file()
-- saved = mem:readAtToSlice(active.smem_size, smem_phys)
-- 5. RELOAD PHASE:
-- elf_handle = host.open_new_elf(parsed.path) -- second open
-- loaded = host.binary_load(elf_handle, mem)
-- if loaded == nil then return binary_load_failed
-- 6. Restore state: mem:writeAtMoveSlice(saved, smem_phys)
-- 7. host.invalidate_cache()
-- 8. Rewrite SP / RA / PC through the FFI register pointer.
-- 9. Replace runtime.active last.
-- 10. Return the JSON envelope.
--
-- The two opens are an intentional test-discoverability choice. The
-- PARSE phase uses host.open_file (it is an existing Task 4 surface
-- also used by prime); the RELOAD phase uses host.open_new_elf (a
-- dedicated Task 5 method). In production both methods bind to
-- Support.File.open so the runtime cost is identical to a single open
-- — the distinction lives in the test log for ordering verification.
local function abi_mismatch_response(field, expected, actual)
return M.json_response({
ok = false, error = "state_abi_mismatch", field = field,
expected = expected, actual = actual,
restart_required = true,
})
end
function M.elf_reload(runtime, parsed)
-- 1. Pre-pause request validation. Pure-Lua, no host calls.
if not runtime.active then
return M.json_response({
ok = false, error = "not_primed", restart_required = false })
end
if parsed.target ~= runtime.active.target then
return M.json_response({
ok = false, error = "target_mismatch",
expected = runtime.active.target, actual = parsed.target,
restart_required = true })
end
if type(parsed.path) ~= "string" or parsed.path == "" then
return M.json_response({
ok = false, error = "missing_path",
restart_required = false })
end
-- 2. SMEM range check on `active` (the new ELF has not been
-- parsed yet; the ABI gate below enforces it cannot relocate).
local smem_phys = strip_kseg(runtime.active.smem_addr)
if smem_phys == nil or smem_phys < 0 or smem_phys > 0x1fffff then
return M.json_response({
ok = false, error = "smem_out_of_main_ram",
restart_required = true })
end
-- 3. PARSE PHASE — open + parse + ABI gate. On any rejection here,
-- only host.open_file has been called. Pause and downstream
-- mutations do NOT occur.
local elf_handle_for_parse = runtime.host.open_file(parsed.path)
if not elf_handle_for_parse then
return M.json_response({
ok = false, error = "open_file_failed",
restart_required = true })
end
local manifest, parse_err = M.parse_manifest(
elf_handle_for_parse, parsed.target, parsed.path, true)
if not manifest then
return M.json_response({
ok = false, error = parse_err,
restart_required = true })
end
local active = runtime.active
if manifest.smem_addr ~= active.smem_addr then
return abi_mismatch_response(
"smem_addr", active.smem_addr, manifest.smem_addr)
end
if manifest.smem_size ~= active.smem_size then
return abi_mismatch_response(
"smem_size", active.smem_size, manifest.smem_size)
end
if manifest.bss_start ~= active.bss_start then
return abi_mismatch_response(
"bss_start", active.bss_start, manifest.bss_start)
end
if manifest.bss_end ~= active.bss_end then
return abi_mismatch_response(
"bss_end", active.bss_end, manifest.bss_end)
end
-- 4. Pause + snapshot smem bytes.
runtime.host.pause()
local mem = runtime.host.memory_file()
local saved = mem:readAtToSlice(active.smem_size, smem_phys)
-- 5. RELOAD PHASE — second open for binary_load.
local elf_handle = runtime.host.open_new_elf(parsed.path)
if not elf_handle then
return M.json_response({
ok = false, error = "open_file_failed",
restart_required = true })
end
local loaded = runtime.host.binary_load(elf_handle, mem)
if loaded == nil then
-- Do NOT restore state; PCSX.Binary.load may have partially
-- written RAM. Keep ACTIVE untouched and tell the caller to
-- restart the emulator.
return M.json_response({
ok = false, error = "binary_load_failed",
restart_required = true })
end
-- 6. Restore the smem snapshot over the freshly-loaded code.
mem:writeAtMoveSlice(saved, smem_phys)
-- 7. Flush the CPU instruction cache (.text/.rodata changed).
runtime.host.invalidate_cache()
-- 8. Rewrite SP / RA / PC through the FFI register pointer. The
-- PC write must happen last; the CPU starts consuming
-- instructions at the new PC the moment the emulator resumes.
local regs = runtime.host.get_registers()
regs.GPR.n.sp = manifest.stack_top
regs.GPR.n.ra = 0
regs.pc = manifest.hot_reload_entry
-- 9. Replace ACTIVE last so a failed reload cannot poison the
-- next request's gate.
runtime.active = manifest
-- 10. Return the JSON envelope.
return M.json_response({
ok = true,
target = manifest.target,
elf_path = manifest.elf_path,
elf_entry = manifest.elf_entry,
smem_addr = manifest.smem_addr,
smem_size = manifest.smem_size,
bss_start = manifest.bss_start,
bss_end = manifest.bss_end,
data_start = manifest.data_start,
data_end = manifest.data_end,
hot_reload_entry = manifest.hot_reload_entry,
stack_top = manifest.stack_top,
})
end
-- patch_handler: one-word RAM patch through MemoryAsFile.
--
-- Per spec §7 + plan.md Task 5 Step 5, the order is:
-- 1. Parse addr and hex query parameters
-- 2. Reject non-hex / missing inputs
-- 3. Reject unaligned addresses (addr & 3)
-- 4. Normalize through strip_kseg; reject out-of-main-RAM
-- (scratchpad 0x1F800000+, BIOS 0x1FC00000+, MMIO, expansion)
-- 5. host.pause()
-- 6. mem = host.memory_file()
-- 7. mem:writeU32At(value, physical_offset)
-- 8. host.invalidate_cache()
-- 9. Return JSON envelope ok=true with the requested addr and value.
local function patch_error(err, restart)
return M.json_response({
ok = false, error = err,
restart_required = restart or false,
})
end
function M.patch_handler(runtime, parsed)
local addr_str = parsed.addr
local hex_str = parsed.hex
-- 1. Presence checks.
if type(addr_str) ~= "string" or addr_str == "" then
return patch_error("missing_addr", false)
end
if type(hex_str) ~= "string" or hex_str == "" then
return patch_error("missing_value", false)
end
-- 2. Hex parse.
local addr = parse_hex_u32(addr_str, "missing_addr", "non_hex_addr")
if not addr then
return patch_error(
addr == false and "missing_addr" or "non_hex_addr", false)
end
local value = parse_hex_u32(hex_str, "missing_value", "non_hex_value")
if not value then
return patch_error(
value == false and "missing_value" or "non_hex_value", false)
end
-- 3. Alignment (checked on the canonical KSEG/physical addr).
if addr % 4 ~= 0 then
return patch_error("addr_unaligned", false)
end
-- 4. Range check via strip_kseg (rejects KSEG0 > 0x801fffff, KSEG1 >
-- 0xa01fffff, scratchpad, BIOS, MMIO, expansion, etc.).
local phys = strip_kseg(addr)
if phys == nil then
return patch_error("addr_out_of_main_ram", false)
end
-- 5-8. Pause / write / cache invalidate.
runtime.host.pause()
local mem = runtime.host.memory_file()
mem:writeU32At(value, phys)
runtime.host.invalidate_cache()
-- 9. Return the JSON envelope. Echo the requested address and the
-- value in normalized hex so log captures stay stable across runs.
return M.json_response({
ok = true,
addr = addr_str,
value = "0x" .. string.format("%x", value),
})
end
-- Mode dispatch table. Each handler is invoked with (runtime, parsed).
-- Tasks 5 adds patch (M.patch_handler); the previous placeholder removed.
local DISPATCH = {
prime = M.prime_active,
elf = M.elf_reload,
patch = M.patch_handler,
}
-- Wrap a handler call with the busy guard. The guard is acquired only
-- after the mode is validated, so unknown-mode requests do not deadlock
-- the runtime. xpcall guarantees the guard is released even if the
-- handler throws.
local function with_busy_guard(runtime, fn)
if runtime.busy then
return M.json_response({
ok = false, error = "reload_busy", restart_required = false })
end
runtime.busy = true
-- Capture both the error text and a full Lua traceback so the user
-- can see the actual failing call site instead of a generic
-- "internal_error". debug.traceback("", 2) skips this xpcall frame
-- and the json_response frame so the trace starts at the handler.
local ok, result = xpcall(fn, function(e)
return { msg = tostring(e), tb = debug.traceback("", 2) }
end)
runtime.busy = false
if not ok then
return M.json_response({
ok = false, error = "internal_error",
detail = result.msg, tb = result.tb,
restart_required = true })
end
return result
end
function M.new(host)
if type(host) ~= "table" then
error("M.new: host must be a table, got " .. type(host))
end
local runtime = {
active = nil,
busy = false,
host = host,
}
function runtime:handle(req)
-- 1. Parse the query (M.parse_query returns nil, err on failure).
local query = req and req.urlData and req.urlData.query or ""
local parsed, parse_err = M.parse_query(query)
if not parsed then
return M.json_response({
ok = false, error = parse_err, restart_required = false })
end
-- 2. Validate the mode against the dispatch table.
local mode = parsed.mode
local handler = DISPATCH[mode]
if not handler then
return M.json_response({
ok = false, error = "unknown_mode", restart_required = false })
end
-- 3. Acquire busy and dispatch via xpcall. Mode validation
-- happens BEFORE busy is acquired so unknown-mode requests
-- cannot deadlock the runtime.
return with_busy_guard(self, function()
return handler(self, parsed)
end)
end
return runtime
end
-- Install the reload handler on a PCSX-Redux instance.
--
-- Per plan.md Task 5 Step 5 the adapter binds the canonical host method
-- names to the PCSX-Lua FFI surface:
--
-- pause -> PCSX.pauseEmulator
-- memory_file -> PCSX.getMemoryAsFile
-- open_file -> Support.File.open(path, "READ")
-- binary_load -> PCSX.Binary.load
-- invalidate_cache -> PCSX.invalidateCache
-- get_registers -> PCSX.getRegisters
--
-- The returned closure dispatches each request through M.new(host)'s
-- runtime:handle so the same prime/elf/patch dispatch machinery is used
-- (including the busy guard from Task 4).
--
-- Missing `PCSX.WebServer.Handlers` is created on demand so callers do
-- not have to wire that themselves; if `PCSX` or `Support` is absent a
-- single line is printed and the function returns without registering
-- a handler.
function M.install(pcsx, support)
if type(pcsx) ~= "table" then
print("[reload] install failed: PCSX is not a table")
return
end
if type(support) ~= "table"
or type(support.File) ~= "table"
or type(support.File.open) ~= "function" then
print("[reload] install failed: Support.File.open unavailable")
return
end
if type(pcsx.pauseEmulator) ~= "function" then print("[reload] install failed: PCSX.pauseEmulator missing"); return end
if type(pcsx.getMemoryAsFile) ~= "function" then print("[reload] install failed: PCSX.getMemoryAsFile missing"); return end
if type(pcsx.Binary) ~= "table"
or type(pcsx.Binary.load) ~= "function" then print("[reload] install failed: PCSX.Binary.load missing"); return end
if type(pcsx.invalidateCache) ~= "function" then print("[reload] install failed: PCSX.invalidateCache missing"); return end
if type(pcsx.getRegisters) ~= "function" then print("[reload] install failed: PCSX.getRegisters missing"); return end
-- ---------------------------------------------------------------------------
-- File adapter wrap.
--
-- The production pcsx-redux Support.File wrapper (see
-- toolchain/pcsx-redux/src/lua/fileffi.lua:225-232 + size() around line 203)
-- exposes byte-read methods as colon-syntax closures with camelCase names:
-- readU8At = function(self, pos) ... end
-- readU16At = function(self, pos) ... end
-- readU32At = function(self, pos) ... end
-- size = function(self) ... end
--
-- The ELF32 parser (scripts/elf32.lua) uses an explicit-pass shape with
-- snake_case names:
-- adapter.read_u8_at(off) / adapter.read_u16_at(off) /
-- adapter.read_u32_at(off) / adapter.read_size()
--
-- The install boundary wraps the Support.File return value in a thin
-- adapter whose methods forward to the production closures, stripping
-- the implicit `self` and re-exporting the names the parser validates.
-- Without this wrap, E.validate_adapter returns "bad_file_adapter"
-- because adapter.read_u8_at / read_u16_at / read_u32_at / read_size
-- are not present on the raw Support.File return.
local function wrap_file(f)
return {
read_u8_at = function(off) return f:readU8At(off) end,
read_u16_at = function(off) return f:readU16At(off) end,
read_u32_at = function(off) return f:readU32At(off) end,
read_size = function() return f:size() end,
}
end
local host = {
pause = function() pcsx.pauseEmulator() end,
memory_file = function() return pcsx.getMemoryAsFile() end,
open_file = function(path) return wrap_file(support.File.open(path, "READ")) end,
-- open_new_elf returns the raw Support.File object because the
-- RELOAD phase passes it directly to PCSX.Binary.load which
-- expects a real File (with readAt / size), NOT the elf32
-- parser adapter (read_u8_at / read_u16_at / read_u32_at /
-- read_size). Wrapping it in the adapter here triggers the
-- binffi.lua "Expected a File object as first argument" error.
open_new_elf = function(path) return support.File.open(path, "READ") end,
binary_load = function(elf, mem) return pcsx.Binary.load(elf, mem) end,
invalidate_cache = function() pcsx.invalidateCache() end,
get_registers = function() return pcsx.getRegisters() end,
}
local runtime = M.new(host)
if type(pcsx.WebServer) ~= "table" then pcsx.WebServer = {} end
if type(pcsx.WebServer.Handlers) ~= "table" then pcsx.WebServer.Handlers = {} end
pcsx.WebServer.Handlers.reload = function(req)
return runtime:handle(req)
end
print("[reload] handler installed: reload")
end
return M
-6
View File
@@ -118,12 +118,6 @@ local PASSES = {
kind = "header-output",
deps = {"scan-source", "word-counts"},
},
auto_reg = {
module = "passes.auto_reg",
kind = "header-output",
deps = {"components"},
groups = { "pre-link" },
},
["emission-model"] = {
module = "passes.emission_model",
kind = "validation",
+82
View File
@@ -0,0 +1,82 @@
# scripts/reload.ps1
#
# PCSX-Redux Lua helper reload client.
#
# Modes:
# elf - Request a full ELF reload. Requires -ElfPath.
# patch - Request a single-word RAM patch. Requires -Address and -Word.
#
# -RequestOnly prints the URI and exits before any network I/O.
# -Quiet suppresses the compact-JSON printout on the real path.
[CmdletBinding()]
param(
[ValidateSet('elf', 'patch')][string]$Mode = 'elf',
[string]$Target = 'hello_camera',
[string]$ElfPath = '',
[string]$Address = '',
[string]$Word = '',
[int]$Port = 8080,
[switch]$RequestOnly,
[switch]$Quiet
)
# mode-specific argument guards
switch ($Mode) {
'patch' {
if ([string]::IsNullOrEmpty($Address) -or [string]::IsNullOrEmpty($Word)) {
Write-Error "patch mode requires both -Address and -Word"
exit 1
}
}
'elf' {
if ([string]::IsNullOrEmpty($ElfPath)) {
Write-Error "elf mode requires -ElfPath"
exit 1
}
}
}
# Build the URL-encoded query string.
$queryParts = New-Object System.Collections.Generic.List[string]
[void]$queryParts.Add("mode=$([uri]::EscapeDataString($Mode))")
[void]$queryParts.Add("target=$([uri]::EscapeDataString($Target))")
switch ($Mode) {
'elf' {
[void]$queryParts.Add("path=$([uri]::EscapeDataString($ElfPath))")
}
'patch' {
[void]$queryParts.Add("addr=$([uri]::EscapeDataString($Address))")
[void]$queryParts.Add("hex=$([uri]::EscapeDataString($Word))")
}
}
$uri = "http://localhost:$Port/api/v1/lua/reload?$($queryParts -join '&')"
# RequestOnly path: emit URI and return before any network I/O.
if ($RequestOnly) {
Write-Output $uri
return
}
# Real request path: POST, decode body if it is a byte array, parse JSON.
$response = Invoke-WebRequest -Method Post -Uri $uri
if ($response.Content -is [byte[]]) {
$text = [System.Text.Encoding]::UTF8.GetString([byte[]]$response.Content)
}
else {
$text = [string]$response.Content
}
$obj = $text | ConvertFrom-Json
if (-not $Quiet) {
$obj | ConvertTo-Json -Compress | Write-Output
}
if (-not $obj.ok) {
$errCode = if ($obj.error) { [string]$obj.error } else { 'unknown' }
throw "Reload failed: $errCode"
}
-5
View File
@@ -14,21 +14,16 @@ $url_armips = 'https://github.com/Kingcom/armips.git'
$url_pcsx_redux = 'https://github.com/grumpycoders/pcsx-redux.git'
$url_psyq_iwyu = 'https://github.com/johnbaumann/psyq_include_what_you_use.git'
$url_lpeg = 'https://github.com/roberto-ieru/LPeg.git'
# $url_mkpsxiso = 'https://github.com/Lameguy64/mkpsxiso.git'
$url_mkpsxiso_win64 = 'https://github.com/Lameguy64/mkpsxiso/releases/download/v2.30/mkpsxiso-2.30-win64.zip'
$path_armips = join-path $path_toolchain 'armips'
$path_pcsx_redux = join-path $path_toolchain 'pcsx-redux'
$path_psyq_iwyu = join-path $path_toolchain 'psyq_iwyu'
$path_lpeg = join-path $path_toolchain 'lpeg'
$path_mkpsxiso = join-path $path_toolchain 'mkpsxiso'
clone-gitrepo $path_armips $url_armips
clone-gitrepo $path_lpeg $url_lpeg
clone-gitrepo $path_pcsx_redux $url_pcsx_redux
clone-gitrepo $path_psyq_iwyu $url_psyq_iwyu
# clone-gitrepo $path_mkpsxiso $url_mkpsxiso
$path_armips_build = join-path $path_armips 'build'
verify-path $path_armips_build