Author SHA1 Message Date
ed 4fbf550d3c hot-reload attempt (unreviewed, not working) 2026-08-06 10:44:34 -04:00
ed 01f7ceba7c buzzing brain. 2026-08-05 02:48:05 -04:00
ed 6f2eff920d some more review before bed. 2026-08-05 02:00:41 -04:00
ed f25765a7b7 Preparing for camera transformation chapter. 2026-08-05 01:21:25 -04:00
ed 2757aa4330 Fix bug with pad input processing (needed mac_yield load fallthrough case) 2026-08-05 01:08:01 -04:00
ed 748b58c5c5 Codebase overhaul. Metaprogram proofread (part 2). Starting to get serious.
Need to rewrite the ps1 lua metaprogram sometime soonish. Getting too bloated... need to consolidate code paths.

In this push codebase structure is starting to get a bit more realized. Decided todo now to match Pikuma's linking module files vods beginning to reorganize its codebase as well.
Atoms & atom components are not in their on *.atom.c files. (Not calling it tape.c as I don't really bake tapes like that outside of the unity c file so far...)

The lua metaprogram has had additional features added to it yet again to avoid hardcoding module handling and supporting multiple atom files per-module.
Either after the camera or cd-rom section I'll be most likely pausing to fully refactor the metaprogram. Possibly as a full re-write to get the loc minimal.
2026-08-04 23:34:00 -04:00
ed 6441dbc23e Proof-reading lua metaprogram (part 1) 2026-08-04 19:32:43 -04:00
ed 57fdb9e037 improvmenets to delay slot modeling (lua metaprogram) 2026-08-04 18:27:00 -04:00
ed b5953a723b add ac_yield_load and ac_yield_tail for delay slot optimization opportunities. 2026-08-04 17:25:02 -04:00
ed 888ffce859 Finished: Pikuma Linking multiple files (not applying to codebase only watched) 2026-08-04 16:49:02 -04:00
ed 7289e7c89c Added jump_rel (can't use abs jump with asm dsl). Fixes + improvements to ps1 asm meta passes. 2026-08-04 16:01:01 -04:00
56 changed files with 5323 additions and 1976 deletions
+1
View File
@@ -20,3 +20,4 @@ toolchain/lpeg
scratch scratch
toolchain/libpsn00b toolchain/libpsn00b
scripts/pcsx_debug_helper.zip
+68
View File
@@ -142,6 +142,74 @@
"tbreak main", "tbreak main",
"continue" "continue"
] ]
},
{
"name": "Debug: Hello Camera!",
"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": [
"monitor reset shellhalt",
"load build/hello_camera.dwarf-injected.elf",
"source scripts/gdb/gdb_tape_atoms.gdb",
"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"
]
} }
] ]
} }
@@ -1,5 +1,5 @@
/* /*
* atom_dsl.h * dsl.atom.h
* ============================================================================ * ============================================================================
* *
* ATOM DSL: Annotation layer for tape atoms (lottes_tape.h). * ATOM DSL: Annotation layer for tape atoms (lottes_tape.h).
@@ -57,7 +57,6 @@
#ifdef INTELLISENSE_DIRECTIVES #ifdef INTELLISENSE_DIRECTIVES
#pragma once #pragma once
// #include <stdint.h>
#endif #endif
/* ============================================================================ /* ============================================================================
@@ -148,12 +147,12 @@
* ... body ... * ... body ...
* atom_label(bounds_chk) another anchor * atom_label(bounds_chk) another anchor
* *
* atom_offset(culling, bounds_chk) resolved by gen/.offsets.h * atom_offset(culling, bounds_chk) resolved by gen/offsets.h
* *
* The metaprogram generates gen/atom_offsets.h with one #define with the offset value per atom_offset(F, T) call. * The metaprogram generates gen/offsets.h with one #define with the offset value per atom_offset(F, T) call.
* The preprocessor then expands the call to the right immediate value. * The preprocessor then expands the call to the right immediate value.
* *
* If gen/atom_offsets.h is stale (or atom_label(name) is undefined), `atom_offset_F_T` becomes an undefined macro and the C build fails. * If gen/offsets.h is stale (or atom_label(name) is undefined), `atom_offset_F_T` becomes an undefined macro and the C build fails.
* ============================================================================*/ * ============================================================================*/
#define atom_offset(F, T) atom_offset_ ## F ## _ ## T #define atom_offset(F, T) atom_offset_ ## F ## _ ## T
// atom_label is a pure annotation for the metaprogram's offset calculations. // atom_label is a pure annotation for the metaprogram's offset calculations.
+5 -2
View File
@@ -43,7 +43,9 @@
#define R_ restrict #define R_ restrict
#define V_ volatile #define V_ volatile
// Fictional, used for intiution.
#pragma region Fictional //, used for intiution
#define EUB_ restrict // Execute Unit Bound: Data is siloed in the ALU Register File. The Load/Store Unit is bypassed. (Route to Execution Unit. Keep in registers) #define EUB_ restrict // Execute Unit Bound: Data is siloed in the ALU Register File. The Load/Store Unit is bypassed. (Route to Execution Unit. Keep in registers)
#define ISO_ restrict // Isolated Provenance: Alternative to Exu_. Guarantees electrical memory isolation, #define ISO_ restrict // Isolated Provenance: Alternative to Exu_. Guarantees electrical memory isolation,
// unlocking the compilers ability to safely pack data across multiple parallel SIMD lanes (vectorization). // unlocking the compilers ability to safely pack data across multiple parallel SIMD lanes (vectorization).
@@ -67,7 +69,8 @@
#define latch_load_anchor(ptr) //__atomic_load_n(ptr, ooo_anchor_) #define latch_load_anchor(ptr) //__atomic_load_n(ptr, ooo_anchor_)
#define latch_store_drain(ptr, val) //__atomic_store_n(ptr, val, ooo_drain_) #define latch_store_drain(ptr, val) //__atomic_store_n(ptr, val, ooo_drain_)
#define pulse_xchg_weld(ptr, val) //__atomic_exchange_n(ptr, val, ooo_weld_) #define pulse_xchg_weld(ptr, val) //__atomic_exchange_n(ptr, val, ooo_weld_)
//end of: Fictional.
#pragma endreigon Fictional
// R_ (restrict) establishes an "Eigen" or "Proprius" mapping. // R_ (restrict) establishes an "Eigen" or "Proprius" mapping.
-134
View File
@@ -1,134 +0,0 @@
#ifdef INTELLISENSE_DIRECTIVES
#pragma once
#endif
// Auto-generated by ps1_meta.lua — DO NOT EDIT
// Source: C:\projects\Pikuma\ps1\code\duffle\lottes_tape.h
// Component atoms (MipsAtomComp_(ac_*)) -> macro variants (mac_*)
#ifndef WORD_COUNT
#define WORD_COUNT(name, count) enum { words_##name = (count) };
#endif
/* atom_dbg_skip */
/* ---------------------------------------------------------------------------
* MACRO ATOM Components (Reusable Assembly Components)
* These do NOT yield. They are expanded inline inside Tape Atoms.
* ---------------------------------------------------------------------------*/
// The 'Yield' sequence for Tape Atoms (mac_yield).
#define mac_yield(...) \
load_word(R_AtomJmp, R_TapePtr, 0) \
, add_ui_self( R_TapePtr, S_(MipsCode)) \
, jump_reg( R_AtomJmp) \
, nop
WORD_COUNT(mac_yield, 4)
/* atom_dbg_skip */
/* Words: 3; Loads 3 S2 indices from the face array */
#define mac_load_tri_indices(...) \
load_half_u(R_T0, R_FaceCursor, 0 * S_(S2)) \
, load_half_u(R_T1, R_FaceCursor, 1 * S_(S2)) \
, load_half_u(R_T2, R_FaceCursor, 2 * S_(S2))
WORD_COUNT(mac_load_tri_indices, 3)
/* atom_dbg_skip */
/* Words: 18; Translates indices to vertex addresses and pushes them to GTE */
#define mac_gte_load_tri_verts(...) \
shift_lleft(R_AT, R_T0, v3s2_byteoff) \
, add_u_self(R_AT, R_VertBase) \
, 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_T1, v3s2_byteoff) \
, add_u_self(R_AT, R_VertBase) \
, 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_T2, v3s2_byteoff) \
, add_u_self(R_AT, R_VertBase) \
, 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)
WORD_COUNT(mac_gte_load_tri_verts, 18)
/* 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. */
#define mac_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_OtBase) /* T1 = & OrderingTable[OTZ] */ \
, load_word( R_AT, R_T1, O_(PolyTag,code)) /* AT = old_ot_head */ \
, 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_PrimCursor, O_(PolyTag,code)) /* prim->tag = packed(prim_length, old_addr) */ \
, shift_lleft( R_AT, R_PrimCursor, 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_f3, 11)
/* 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. */
#define mac_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_OtBase) /* 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_PrimCursor, O_(PolyTag,code)) /* prim->tag = packed(prim_length, old_addr) */ \
, shift_lleft( R_AT, R_PrimCursor, 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)
/* atom_dbg_skip */
#define mac_pack_color_word(off, cmd, r, g, b) \
load_upper_i(R_AT, (cmd) << 8 | (b)) \
, or_i_self( R_AT, ((g) << 8) | (r)) \
, store_word( R_AT, R_PrimCursor, (off))
WORD_COUNT(mac_pack_color_word, 3)
/* atom_dbg_skip */
#define mac_format_f3_color(r, g, b) \
mac_pack_color_word(O_(Poly_F3,color), gp0_cmd_poly_f3, r, g, b)
WORD_COUNT(mac_format_f3_color, 3)
/* atom_dbg_skip */
/* 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). */
#define mac_gte_store_f3(...) \
gte_sw(C2_SXY0, R_PrimCursor, O_(Poly_F3,p0)) \
, gte_sw(C2_SXY1, R_PrimCursor, O_(Poly_F3,p1)) \
, gte_sw(C2_SXY2, R_PrimCursor, O_(Poly_F3,p2))
WORD_COUNT(mac_gte_store_f3, 3)
#define mac_format_g4_color(r0, g0, b0, r1, g1, b1, r2, g2, b2, r3, g3, b3) \
mac_pack_color_word(O_(Poly_G4,c0), gp0_cmd_poly_g4, r0,g0,b0) \
, mac_pack_color_word(O_(Poly_G4,c1), 0, r1,g1,b1) \
, mac_pack_color_word(O_(Poly_G4,c2), 0, r2,g2,b2) \
, mac_pack_color_word(O_(Poly_G4,c3), 0, r3,g3,b3)
WORD_COUNT(mac_format_g4_color, 12)
/* atom_dbg_skip */
/* 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).
* 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). */
#define mac_gte_store_g4_p012(...) \
gte_sw(C2_SXY0, R_PrimCursor, O_(Poly_G4,p0)) \
, gte_sw(C2_SXY1, R_PrimCursor, O_(Poly_G4,p1)) \
, gte_sw(C2_SXY2, R_PrimCursor, O_(Poly_G4,p2))
WORD_COUNT(mac_gte_store_g4_p012, 3)
/* atom_dbg_skip */
/* Words: 1; Stores the V3 screen coord to the G4's p3 slot.
* 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.
*/
#define mac_gte_store_g4_p3(...) \
gte_sw(C2_SXY2, R_PrimCursor, O_(Poly_G4,p3))
WORD_COUNT(mac_gte_store_g4_p3, 1)
-9
View File
@@ -1,9 +0,0 @@
// Auto-generated by ps1_meta.lua (passes/offsets.lua) — DO NOT EDIT
// Source: C:\projects\Pikuma\ps1\code\duffle\lottes_tape.h
#pragma once
#pragma region lottes_tape
#pragma endregion lottes_tape
+184
View File
@@ -0,0 +1,184 @@
#ifdef INTELLISENSE_DIRECTIVES
#pragma once
#endif
// Auto-generated by ps1_meta.lua — DO NOT EDIT
// Directory: C:\projects\Pikuma\ps1\code\duffle/
// source: C:\projects\Pikuma\ps1\code\duffle\word_count.metadata.h
// source: C:\projects\Pikuma\ps1\code\duffle\dsl.h
// source: C:\projects\Pikuma\ps1\code\duffle\memory.h
// source: C:\projects\Pikuma\ps1\code\duffle\math.h
// source: C:\projects\Pikuma\ps1\code\duffle\gcc_asm.h
// source: C:\projects\Pikuma\ps1\code\duffle\mips.h
// source: C:\projects\Pikuma\ps1\code\duffle\gp.h
// source: C:\projects\Pikuma\ps1\code\duffle\gte.h
// 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\psyq.h
// 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
// source: C:\projects\Pikuma\ps1\code\duffle\gp.atom.c
// source: C:\projects\Pikuma\ps1\code\duffle\pad.atom.c
// source: C:\projects\Pikuma\ps1\code\duffle\psyq.atom.c
// Component atoms (MipsAtomComp_(ac_*)) -> macro variants (mac_*)
#ifndef WORD_COUNT
#define WORD_COUNT(name, count) enum { words_##name = (count) };
#endif
/* atom_dbg_skip */
/* ---------------------------------------------------------------------------
* MACRO ATOM Components (Reusable Assembly Components)
* These do NOT yield. They are expanded inline inside Tape Atoms.
* ---------------------------------------------------------------------------*/
// The 'Yield' sequence for Tape Atoms (mac_yield).
// - mac_yield() is the safe default for atom-endings: 4 words, BD-slot of jr is mandatory nop.
// - mac_yield_load() + mac_yield_tail():
// - unconditional branch: mac_yield_load fills the branch's BD-slot (replaces a nop);
// - mac_yield_tail runs at the branch target (does NOT re-load R_AtomJmp).
#define mac_yield(...) \
load_word(R_AtomJmp, R_TapePtr, 0) \
, add_ui_self( R_TapePtr, S_(MipsCode)) \
, jump_reg( R_AtomJmp) \
, nop
WORD_COUNT(mac_yield, 4)
/* atom_dbg_skip */
#define mac_yield_load(...) \
load_word(R_AtomJmp, R_TapePtr, 0)
WORD_COUNT(mac_yield_load, 1)
/* atom_dbg_skip */
#define mac_yield_tail(...) \
add_ui_self(R_TapePtr, S_(MipsCode)) \
, jump_reg( R_AtomJmp) \
, nop
WORD_COUNT(mac_yield_tail, 3)
/* atom_dbg_skip */
#define mac_load_v2s2(rs_x, rs_y, r_base, offset) \
load_half( rs_x, r_base, O_(V3_S2,x)) \
, load_half( rs_y, r_base, O_(V3_S2,y))
WORD_COUNT(mac_load_v2s2, 2)
/* atom_dbg_skip */
#define mac_store_v2s2(rt_x, rt_y, base, offset) \
store_half(rt_x, base, offset + O_(V2_S2,x)) \
, store_half(rt_y, base, offset + O_(V2_S2,y))
WORD_COUNT(mac_store_v2s2, 2)
/* 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)) \
, store_half(rt_y, base, offset + O_(Rect_S2,y)) \
, store_half(rt_width, base, offset + O_(Rect_S2,width)) \
, store_half(rt_height, base, offset + O_(Rect_S2,height))
WORD_COUNT(mac_store_rects2, 4)
/* atom_dbg_skip */
#define mac_load_tri_indices(r_face_cusor, r_i0, r_i1, r_i2) \
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))
WORD_COUNT(mac_load_tri_indices, 3)
/* atom_dbg_skip */
#define mac_gte_store_f3(r_primitive_cursor) \
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))
WORD_COUNT(mac_gte_store_f3, 3)
/* atom_dbg_skip */
#define mac_gte_load_tri_verts(r_vert_base, r_v0, r_v1, r_v2) \
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)
WORD_COUNT(mac_gte_load_tri_verts, 18)
/* atom_dbg_skip */
#define mac_gte_store_g4_p012(r_primitive_cursor) \
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))
WORD_COUNT(mac_gte_store_g4_p012, 3)
/* atom_dbg_skip */
#define mac_gte_store_g4_p3(r_primitive_cursor) \
gte_sw(C2_SXY2, r_primitive_cursor, O_(Poly_G4,p3))
WORD_COUNT(mac_gte_store_g4_p3, 1)
#define mac_gcmd_push(cmd, reg_transfer, reg_base, port) \
load_upper_i(reg_transfer, cmd >> 16) \
, or_i_self( reg_transfer, cmd & 0xFFFF) \
, store_word( reg_transfer, reg_base, port)
WORD_COUNT(mac_gcmd_push, 3)
/* atom_dbg_skip */
#define mac_store_rgb8(rr, rg, rb, base, offset) \
store_byte(rr, base, offset + O_(RGB8,r)) \
, store_byte(rg, base, offset + O_(RGB8,g)) \
, store_byte(rb, base, offset + O_(RGB8,b))
WORD_COUNT(mac_store_rgb8, 3)
/* atom_dbg_skip */
#define mac_pack_color_word(r_base, off, cmd, r, g, b) \
load_upper_i(R_AT, (cmd) << 8 | (b)) \
, or_i_self( R_AT, ((g) << 8) | (r)) \
, store_word( R_AT, r_base, (off))
WORD_COUNT(mac_pack_color_word, 3)
/* atom_dbg_skip */
#define mac_format_f3_color(r_base, r, g, b) \
mac_pack_color_word(r_base, O_(Poly_F3,color), gp0_cmd_poly_f3, r, g, b)
WORD_COUNT(mac_format_f3_color, 3)
#define mac_format_g4_color(r_prim_cursor, r0, g0, b0, r1, g1, b1, r2, g2, b2, r3, g3, b3) \
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)
WORD_COUNT(mac_format_g4_color, 12)
#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, (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_f3, 11)
#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)
+53
View File
@@ -0,0 +1,53 @@
// Auto-generated by ps1_meta.lua (passes/offsets.lua) — DO NOT EDIT
// Directory: C:\projects\Pikuma\ps1\code\duffle\
// source: C:\projects\Pikuma\ps1\code\duffle\word_count.metadata.h
// source: C:\projects\Pikuma\ps1\code\duffle\dsl.h
// source: C:\projects\Pikuma\ps1\code\duffle\memory.h
// source: C:\projects\Pikuma\ps1\code\duffle\math.h
// source: C:\projects\Pikuma\ps1\code\duffle\gcc_asm.h
// source: C:\projects\Pikuma\ps1\code\duffle\mips.h
// source: C:\projects\Pikuma\ps1\code\duffle\gp.h
// source: C:\projects\Pikuma\ps1\code\duffle\gte.h
// 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\psyq.h
// 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
// source: C:\projects\Pikuma\ps1\code\duffle\gp.atom.c
// source: C:\projects\Pikuma\ps1\code\duffle\pad.atom.c
// source: C:\projects\Pikuma\ps1\code\duffle\psyq.atom.c
#pragma once
#pragma region duffle
// --- atom: pad_bios_snapshot (78 words) ---
#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 {
atom_offset_snap_root_skip_disconnected = _atom_offset_snap_root_skip_disconnected,
atom_offset_disconnected_snap_end = _atom_offset_disconnected_snap_end,
atom_offset_case_2_id_dispatch = _atom_offset_case_2_id_dispatch,
atom_offset_pending_snap_end = _atom_offset_pending_snap_end,
atom_offset_id_dispatch_try_analog_stick = _atom_offset_id_dispatch_try_analog_stick,
atom_offset_id_dispatch_snap_end = _atom_offset_id_dispatch_snap_end,
atom_offset_try_analog_stick_try_analog_pad = _atom_offset_try_analog_stick_try_analog_pad,
atom_offset_analog_stick_snap_end = _atom_offset_analog_stick_snap_end,
atom_offset_try_analog_pad_try_unsupported = _atom_offset_try_analog_pad_try_unsupported,
atom_offset_analog_pad_snap_end = _atom_offset_analog_pad_snap_end,
};
#pragma endregion duffle
+82
View File
@@ -0,0 +1,82 @@
#ifdef INTELLISENSE_DIRECTIVES
# include "dsl.h"
# include "gp.h"
# include "lottes_tape.h"
#endif
ATOM_FILE_DEBUGGER_LINE_MARKER(gp_atom_c);
#pragma region MACs (Mips Atom Components)
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(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)),
})
/* 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)),
})
/* Words: 3; Emits the F3 command+color word (cmd byte | BLUE | GREEN | RED)
* Args: _r, _g, _b are 8-bit RGB byte values (not raw 16-bit fields). */
FI_ Slice_MipsCode ac_format_f3_color(U4 r_base, U1 r, U1 g, U1 b)
atom_dbg_skip MipsAtomComp_Proc_(ac_format_f3_color, { mac_pack_color_word(r_base, O_(Poly_F3,color), gp0_cmd_poly_f3, r, g, b) })
/* Words: 12; Emits the four (code|color) words of a Poly_G4.
* Args: rN,gN,bN are 8-bit RGB byte values for each of the 4 vertices. */
FI_ Slice_MipsCode ac_format_g4_color(U4 r_prim_cursor,
U1 r0, U1 g0, U1 b0,
U1 r1, U1 g1, U1 b1,
U1 r2, U1 g2, U1 b2,
U1 r3, U1 g3, U1 b3)
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.
* 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, (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
})
/* 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)
View File
+76
View File
@@ -0,0 +1,76 @@
#ifdef INTELLISENSE_DIRECTIVES
# include "gen/macs.h"
# include "gen/offsets.h"
# include "gte.h"
# include "gp.h"
# include "lottes_tape.h"
#endif
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(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)),
})
/* 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(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(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),
})
/* 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).
* 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(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)),
})
/* Words: 1; Stores the V3 screen coord to the G4's p3 slot.
* 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(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 Bsked Atoms
typedef Struct_(Binds_SetGteWorld) {
M3_S2* transform;
};
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_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),
load_word(R_T0, R_T3, 8), load_word(R_T1, R_T3, 12), load_word(R_T2, R_T3, 16),
gte_mv_to_ctrl_r(R_T0, gte_cr_RT13), gte_mv_to_ctrl_r(R_T1, gte_cr_RT21), gte_mv_to_ctrl_r(R_T2, gte_cr_RT22),
load_word(R_T0, R_T3, 20), load_word(R_T1, R_T3, 24), load_word(R_T2, R_T3, 28),
gte_mv_to_ctrl_r(R_T0, gte_cr_TRX), gte_mv_to_ctrl_r(R_T1, gte_cr_TRY), gte_mv_to_ctrl_r(R_T2, gte_cr_TRZ),
mac_yield()
};
#pragma endregion Baked Atoms
View File
+141 -194
View File
@@ -1,36 +1,81 @@
#ifdef INTELLISENSE_DIRECTIVES #ifdef INTELLISENSE_DIRECTIVES
# pragma once # pragma once
# include "gen/macs.h"
# include "gen/offsets.h"
# include "dsl.h" # include "dsl.h"
# include "gcc_asm.h" # include "gcc_asm.h"
# include "mips.h" # include "mips.h"
# include "gte.h" # include "gte.h"
# include "memory.h" # include "memory.h"
# include "atom_dsl.h" # include "dsl.atom.h"
# include "gen/duffle.macs.h"
# include "gen/duffle.offsets.h"
#endif #endif
typedef U4 const MipsCode; // Underlying type to mips asm words. #pragma region Tape Drive
typedef Slice_(MipsCode); /* -----------------------------------------------------------------------------
* TAPE DRIVE ABI
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) = * 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
// Used for components with no args (e.g., ac_load_tri_indices) or identifier-args (hardcoded register names). * or finalized conceptually.
// MipsAtomComp_(ac_X) { body } * -----------------------------------------------------------------------------
// expands to: * This ABI and its associated legos were directly inspired by researching
// MipsCode ac_X[] align_(4) = { body }; * the work of Timothy Lottes and Onat Türkçüoğlu; along with many others.
#define MipsAtomComp_(sym) MipsCode sym [] align_(4) = * 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.
// Used for components with value-args (e.g., ac_format_f3_color). * These eventually lead to a terminal atom for the tape which is defined
// FI_ Slice_MipsCode ac_X(args) MipsAtomComp_Proc_(ac_X, { body }) * below as "tape_exit".
// expands to: *
// FI_ Slice_MipsCode ac_X(args) { MipsCode ac_X[] align_(4) = { body }; return slice_from_array(MipsCode, ac_X); } * This behaves as one of the simplest runtime harnesses ontop of a
#define MipsAtomComp_Proc_(sym, ...) { MipsCode sym [] align_(4) = __VA_ARGS__; return slice_from_array(MipsCode, sym); } * host-enviornment's execution engine to author and compose programs with.
* From here various conventions can be further applied.
// Auto-generated component macros (<module>/gen/<dir>/<dir>.macs.h) are included manually by the unity build. * 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
/* Register aliases */ * 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 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 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 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 { enum {
R_AtomJmp = R_T8 atom_reg, /* debug-visible; tape yield handshake scratch */ R_AtomJmp = R_T8 atom_reg, /* debug-visible; tape yield handshake scratch */
R_TapePtr = R_T9 atom_reg, /* The Instruction Stream Pointer */ R_TapePtr = R_T9 atom_reg, /* The Instruction Stream Pointer */
@@ -59,28 +104,53 @@ enum {
R_TScratch6 = R_T6, R_TScratch6 = R_T6,
R_TScratch7 = R_T7, R_TScratch7 = R_T7,
R_TScratch8 = R_T8, R_TScratch8 = R_T8,
R_TScratch10 = R_V0, R_TScratch10 = R_V0, // Tend to be used with gte DMAs
R_TScratch11 = R_V1, R_TScratch11 = R_V1, // Tend to be used with gte DMAs
// Note(Ed): We can technically clobber these, but don't unless we hit a bottleneck. // Note(Ed): We can technically clobber these, but don't unless we hit a bottleneck.
// R_TScratch12 = R_A0, // A 0-2
// R_TScratch13 = R_A1, // S 0-7
// R_TScratch14 = R_A3,
// TODO(Ed): Review S0-S7, they are technically avaialble, we just have to snapshot them at the ABI boundary.
// TODO(Ed): This is technically a waste of cycles for most work? so maybe only do this for expensive atoms on-demand or atom phases.
// TODO(Ed): Sort out the other available registers... (Not sure how much is left avail)
}; };
#pragma region Tape Drive typedef U4 const MipsCode; // Underlying type to mips asm words.
/* --------------------------------------------------------------------------- typedef Slice_(MipsCode);
* TAPE DRIVE ABI & REGISTER ALIASES (the enum moved earlier; see below)
* ---------------------------------------------------------------------------*/ typedef U4 const MipsAtom; // Underlying type to an array of mips asm words that must terminate with an ac_yield.
typedef Slice_(MipsAtom); typedef Slice_MipsAtom Tape; #define MipsAtom_(sym) MipsCode sym [] align_(4) =
// 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 (e.g., ac_format_f3_color).
// FI_ Slice_MipsCode ac_X(args) MipsAtomComp_Proc_(ac_X, { body })
// expands to:
// FI_ Slice_MipsCode ac_X(args) { MipsCode ac_X[] align_(4) = { body }; return slice_from_array(MipsCode, ac_X); }
#define MipsAtomComp_Proc_(sym, ...) { 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.
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.
The two-level concat + `__LINE__` suffix makes the identifier unique per call site
(the identifier embeds the source line, so duplicates across `#include`d files don't collide). */
#define ATOM_FILE_DEBUGGER_LINE_MARKER(file_name) internal U4 const tmpl(atom_file_debugger_line_marker,file_name) = 0
typedef Slice_(MipsAtom); typedef Slice_MipsAtom Tape;
/* The 'Exit' Atom */ /* The 'Exit' Atom */
atom_dbg_skip MipsAtom_(tape_exit) { jump_reg(rret_addr), nop }; atom_dbg_skip MipsAtom_(tape_exit) { jump_reg(rret_addr), nop };
//TODO(Ed): Do we backup R_S0-7 here? Have it in a heavier tape run as a opt-in? Same with V0-1 and A0-3? // TODO(Ed): When we have a substantial workload/throughput, profile each of these to see impact at ABI boundaries.
/* Generalized Tape Engine Runner */
/* Tape Runner (Default) */
FI_ void tape_run(Tape tape) { register U4* tape_ptr rgcc(R_TapePtr) = u4_r(tape.ptr); asm volatile( FI_ void tape_run(Tape tape) { register U4* tape_ptr rgcc(R_TapePtr) = u4_r(tape.ptr); asm volatile(
asm_words( asm_words(
load_word( R_AtomJmp, R_TapePtr, 0) /* Bootstrap the first jump */ load_word( R_AtomJmp, R_TapePtr, 0) /* Bootstrap the first jump */
@@ -91,12 +161,32 @@ FI_ void tape_run(Tape tape) { register U4* tape_ptr rgcc(R_TapePtr) = u4_r(tape
asm_rpins, r_use(tape_ptr) asm_rpins, r_use(tape_ptr)
asm_clobber: asm_clobber:
rlit(R_AT), rlit(R_AT),
rlit(R_V0), rlit(R_V1), rlit(R_V0), rlit(R_V1), // We clobber these for GTE ACs (that don't expose register selection, might expose them in the future...)
rlit(R_T0), rlit(R_T1), rlit(R_T2), rlit(R_T3), rlit(R_T4), 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_T5), rlit(R_T6), rlit(R_T7), rlit(R_T8),
clb_mem_drain clb_mem_drain
); } ); }
/* Tape Runner (Static and Arg Clobbers) */
FI_ void tape_run_a02_s07(Tape tape) { register U4* tape_ptr rgcc(R_TapePtr) = u4_r(tape.ptr); asm volatile(
asm_words(
load_word( R_AtomJmp, R_TapePtr, 0) /* Bootstrap the first jump */
, add_ui_self(R_TapePtr, S_(MipsAtom)) /* Advance tape */
, call_reg( R_AtomJmp) /* jalr $t9 */
, nop /* Branch delay slot */
)
asm_rpins, r_use(tape_ptr)
asm_clobber:
rlit(R_AT),
rlit(R_V0), rlit(R_V1), rlit(R_A0), rlit(R_A1), rlit(R_A2),
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_S0), rlit(R_S1), rlit(R_S2), rlit(R_S3), rlit(R_S4),
rlit(R_S5), rlit(R_S6), rlit(R_S7),
clb_mem_drain
); }
// Procedural authoring of tapes:
typedef Relative_(FArena) Struct_(TapeBuilder) { U4 ptr; U4 capacity; U4 used; }; 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_ 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_old( FArena* arena) { return (TapeBuilder){ arena->start, 0 }; }
@@ -113,7 +203,6 @@ FI_ Tape tb_slice(TapeBuilder tb) { return (Tape){ C_(U4
FI_ void tb_scope_run_end(TapeBuilder* tb) { tb_emit(tb,tape_exit); tape_run(tb_slice(tb[0])); } FI_ void tb_scope_run_end(TapeBuilder* tb) { tb_emit(tb,tape_exit); tape_run(tb_slice(tb[0])); }
#define tb_scope_run(tb) for(U4 tbs_once=0;tbs_once==0;++tbs_once,tb_scope_run_end(tb)) #define tb_scope_run(tb) for(U4 tbs_once=0;tbs_once==0;++tbs_once,tb_scope_run_end(tb))
#pragma endregion Tape Drive #pragma endregion Tape Drive
#pragma region Macro Mips Atom Components #pragma region Macro Mips Atom Components
@@ -123,124 +212,30 @@ FI_ void tb_scope_run_end(TapeBuilder* tb) { tb_emit(tb,tape_exit); tape_run(tb_
* ---------------------------------------------------------------------------*/ * ---------------------------------------------------------------------------*/
// The 'Yield' sequence for Tape Atoms (mac_yield). // The 'Yield' sequence for Tape Atoms (mac_yield).
// - mac_yield() is the safe default for atom-endings: 4 words, BD-slot of jr is mandatory nop.
// - mac_yield_load() + mac_yield_tail():
// - unconditional branch: mac_yield_load fills the branch's BD-slot (replaces a nop);
// - mac_yield_tail runs at the branch target (does NOT re-load R_AtomJmp).
atom_dbg_skip MipsAtomComp_(ac_yield) { atom_dbg_skip MipsAtomComp_(ac_yield) {
load_word(R_AtomJmp, R_TapePtr, 0), load_word(R_AtomJmp, R_TapePtr, 0),
add_ui_self( R_TapePtr, S_(MipsCode)), add_ui_self( R_TapePtr, S_(MipsCode)),
jump_reg( R_AtomJmp), nop, jump_reg( R_AtomJmp), nop,
}; };
enum { atom_dbg_skip MipsAtomComp_(ac_yield_load) {
R_PrimCursor = R_T7 atom_reg atom_type(U4*), /* VRAM output cursor (primitive buffer) */ load_word(R_AtomJmp, R_TapePtr, 0),
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 */
#define R_PrimCursor_Code R_T7_Code
#define R_FaceCursor_Code R_T4_Code
#define R_VertBase_Code R_T5_Code
#define R_OtBase_Code R_T6_Code
}; };
/* Words: 3; Loads 3 S2 indices from the face array */ atom_dbg_skip MipsAtomComp_(ac_yield_tail) {
atom_dbg_skip MipsAtomComp_(ac_load_tri_indices) { add_ui_self(R_TapePtr, S_(MipsCode)),
load_half_u(R_T0, R_FaceCursor, 0 * S_(S2)), jump_reg( R_AtomJmp), nop,
load_half_u(R_T1, R_FaceCursor, 1 * S_(S2)),
load_half_u(R_T2, R_FaceCursor, 2 * S_(S2)),
}; };
/* Words: 18; Translates indices to vertex addresses and pushes them to GTE */
atom_dbg_skip MipsAtomComp_(ac_gte_load_tri_verts) {
shift_lleft(R_AT, R_T0, v3s2_byteoff), add_u_self(R_AT, R_VertBase), 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_T1, v3s2_byteoff), add_u_self(R_AT, R_VertBase), 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_T2, v3s2_byteoff), add_u_self(R_AT, R_VertBase), 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),
};
/* 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. */
MipsAtomComp_(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_OtBase), // T1 = & OrderingTable[OTZ]
load_word( R_AT, R_T1, O_(PolyTag,code)), // AT = old_ot_head
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_PrimCursor, O_(PolyTag,code)), // prim->tag = packed(prim_length, old_addr)
shift_lleft( R_AT, R_PrimCursor, 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
};
/* 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. */
MipsAtomComp_(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_OtBase), // 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_PrimCursor, O_(PolyTag,code)), // prim->tag = packed(prim_length, old_addr)
shift_lleft( R_AT, R_PrimCursor, 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
};
/* 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 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_PrimCursor, (off)),
})
/* Words: 3; Emits the F3 command+color word (cmd byte | BLUE | GREEN | RED)
* Args: _r, _g, _b are 8-bit RGB byte values (not raw 16-bit fields). */
FI_ Slice_MipsCode ac_format_f3_color(U1 r, U1 g, U1 b)
atom_dbg_skip MipsAtomComp_Proc_(ac_format_f3_color, { mac_pack_color_word(O_(Poly_F3,color), gp0_cmd_poly_f3, r, g, b) })
/* 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). */
atom_dbg_skip MipsAtomComp_(ac_gte_store_f3) {
gte_sw(C2_SXY0, R_PrimCursor, O_(Poly_F3,p0)),
gte_sw(C2_SXY1, R_PrimCursor, O_(Poly_F3,p1)),
gte_sw(C2_SXY2, R_PrimCursor, O_(Poly_F3,p2)),
};
/* 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(
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, {
mac_pack_color_word(O_(Poly_G4,c0), gp0_cmd_poly_g4, r0,g0,b0),
mac_pack_color_word(O_(Poly_G4,c1), 0, r1,g1,b1),
mac_pack_color_word(O_(Poly_G4,c2), 0, r2,g2,b2),
mac_pack_color_word(O_(Poly_G4,c3), 0, r3,g3,b3),
})
/* 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).
* 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). */
atom_dbg_skip MipsAtomComp_(ac_gte_store_g4_p012) {
gte_sw(C2_SXY0, R_PrimCursor, O_(Poly_G4,p0)),
gte_sw(C2_SXY1, R_PrimCursor, O_(Poly_G4,p1)),
gte_sw(C2_SXY2, R_PrimCursor, O_(Poly_G4,p2)),
};
/* Words: 1; Stores the V3 screen coord to the G4's p3 slot.
* 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.
*/
atom_dbg_skip MipsAtomComp_(ac_gte_store_g4_p3) { gte_sw(C2_SXY2, R_PrimCursor, O_(Poly_G4,p3)) };
#pragma endregion Macro Atom Components #pragma endregion Macro Atom Components
#pragma region Mips Atom Builder #pragma region Mips Atom Builder
// This allows for runtime procedural authoring of mips atoms. // This helps with runtime procedural authoring of mips atoms.
typedef Struct_(FMipsAtom512) { U4 data[512]; U4 used; }; typedef Struct_(FMipsAtom512) { U4 data[512]; U4 used; };
@@ -263,57 +258,9 @@ FI_ void atombuilder_end(MipsAtomBuilder_R ab) {
} }
#define mipsatom_from_builder(ab) (Slice_MipsCode){ab.start, ab.used} #define mipsatom_from_builder(ab) (Slice_MipsCode){ab.start, ab.used}
#pragma endregion Mips Atom Builder #pragma endregion Mips Atom Builder
#pragma region Baked Mips Atoms #pragma region Baked Mips Atoms
// These atoms are resolved at compile time and are (usually) statically linked readonly data. // These atoms are resolved at compile time and are (usually) statically linked readonly data.
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
* 2. $a0 = bios_flushcache (arg0)
* 3. $t0 = bios_table_addr ; t0 = &BIOS A-function table
* 4. jalr $t0, $ra ; call BIOS(flushcache)
* nop ; branch delay slot
* 5. lw $ra, 4($sp); jr $ra ; restore & return
* 6. sp += 8
*/
internal MipsAtom_(mips_flush_icache) {
add_ui(rstack_ptr, rstack_ptr, -MipsStackAlignment), // sp -= 8
store_word(rret_addr, rstack_ptr, S_(U4)), // sw $ra, 4($sp)
add_ui(rret_0, rdiscard, bios_flushcache), // addiu $a0, $0, 0x44
add_ui(rtmp_0, rdiscard, bios_table_addr), // addiu $t0, $0, 0xA0
jump_link(rtmp_0, rret_addr), nop, // jalr $t0, $ra, BD slot
load_word(rret_addr, rstack_ptr, S_(U4)), // lw $ra, 4($sp)
jump_reg(rret_addr), // jr $ra
add_ui(rstack_ptr, rstack_ptr, MipsStackAlignment), // sp += 8 (BD)
mac_yield(),
};
typedef Struct_(Binds_SetGteWorld) {
M3_S2* transform;
};
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_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),
load_word(R_T0, R_T3, 8), load_word(R_T1, R_T3, 12), load_word(R_T2, R_T3, 16),
gte_mv_to_ctrl_r(R_T0, gte_cr_RT13), gte_mv_to_ctrl_r(R_T1, gte_cr_RT21), gte_mv_to_ctrl_r(R_T2, gte_cr_RT22),
load_word(R_T0, R_T3, 20), load_word(R_T1, R_T3, 24), load_word(R_T2, R_T3, 28),
gte_mv_to_ctrl_r(R_T0, gte_cr_TRX), gte_mv_to_ctrl_r(R_T1, gte_cr_TRY), gte_mv_to_ctrl_r(R_T2, gte_cr_TRZ),
mac_yield()
};
#pragma endregion Baked Mips Atoms #pragma endregion Baked Mips Atoms
+29
View File
@@ -0,0 +1,29 @@
#ifdef INTELLISENSE_DIRECTIVES
# include "gen/macs.h"
# include "gen/offsets.h"
# include "math.h"
# include "lottes_tape.h"
#endif
ATOM_FILE_DEBUGGER_LINE_MARKER(math_atom_c);
#pragma region MACs (Mips Atom Component)
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(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_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)),
store_half(rt_height, base, offset + O_(Rect_S2,height)),
})
#pragma endregion MACs (Mips Atom Component)
+5 -10
View File
@@ -34,10 +34,10 @@ typedef Struct_(V4_S4) { S4 x; S4 y; S4 z; S4 w; };
typedef Struct_(R2_S2) { V2_S2 p0; V2_S2 p1; }; typedef Struct_(R2_S2) { V2_S2 p0; V2_S2 p1; };
typedef Struct_(R2_S4) { V2_S4 p0; V2_S4 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_S2) { S2 x; S2 y; S2 width; S2 height; };
typedef Struct_(Rect_S4) { S4 x; S4 y; S4 width; S4 height; }; typedef Struct_(Rect_S4) { S4 x; S4 y; S4 width; S4 height; };
typedef Struct_(M3_S2) { A3x3_S2 m; A3_S4 t; }; typedef Struct_(M3_S2) { A3x3_S2 m; A3_S4 t; };
typedef Array_(V2_S2, 2); typedef Array_(V2_S2, 2);
typedef Array_(V2_S2, 3); typedef Array_(V2_S2, 3);
@@ -61,10 +61,5 @@ FI_ void add_a3s4_fp(A3_S4_R out_a, A3_S4 b) {
(out_a[0])[2] += b[2] >> 1; (out_a[0])[2] += b[2] >> 1;
} }
FI_ void add_v3s4(V3_S4_R out_a, V3_S4 b) { FI_ void add_v3s4 (V3_S4_R out_a, V3_S4 b) { add_a3s4 (pcast(A3_S4_R, out_a), pcast(A3_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 add_v3s4_fp(V3_S4_R out_a, V3_S4 b) {
add_a3s4_fp(pcast(A3_S4_R, out_a), pcast(A3_S4, b));
}
+38
View File
@@ -0,0 +1,38 @@
#ifdef INTELLISENSE_DIRECTIVES
# include "gen/macs.h"
# include "gen/offsets.h"
# include "lottes_tape.h"
#endif
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
* 2. $a0 = bios_flushcache (arg0)
* 3. $t0 = bios_table_addr ; t0 = &BIOS A-function table
* 4. jalr $t0, $ra ; call BIOS(flushcache)
* nop ; branch delay slot
* 5. lw $ra, 4($sp); jr $ra ; restore & return
* 6. sp += 8
*/
internal MipsAtom_(mips_flush_icache) {
add_ui(rstack_ptr, rstack_ptr, -MipsStackAlignment), // sp -= 8
store_word(rret_addr, rstack_ptr, S_(U4)), // sw $ra, 4($sp)
add_ui(rret_0, rdiscard, bios_flushcache), // addiu $a0, $0, 0x44
add_ui(rtmp_0, rdiscard, bios_table_addr), // addiu $t0, $0, 0xA0
jump_link(rtmp_0, rret_addr), nop, // jalr $t0, $ra, BD slot
load_word(rret_addr, rstack_ptr, S_(U4)), // lw $ra, 4($sp)
jump_reg(rret_addr), // jr $ra
add_ui(rstack_ptr, rstack_ptr, MipsStackAlignment), // sp += 8 (BD)
mac_yield(),
};
#pragma endregion Baked Atoms
+20 -2
View File
@@ -362,10 +362,28 @@ enum { _BitOffsets = 0
/* call_reg rs — jump-and-link to register-held address; link in $ra. */ /* call_reg rs — jump-and-link to register-held address; link in $ra. */
#define call_reg(rs) jump_link((rs), R_RA) #define call_reg(rs) jump_link((rs), R_RA)
/* j target — absolute jump within the current 256MB region. */ /* j target — absolute jump within the current 256MB region.
* 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.
*/
#define jump(off) enc_i(op_j, R_0, R_0, (off)) #define jump(off) enc_i(op_j, R_0, R_0, (off))
/* call_addr off — jump-and-link to immediate address. */ /* 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`.
*/
#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.
*/
#define call_addr(off) enc_i(op_jal, R_0, R_0, (off)) #define call_addr(off) enc_i(op_jal, R_0, R_0, (off))
/* --- Store family (mirrors the load family) --- */ /* --- Store family (mirrors the load family) --- */
+184
View File
@@ -0,0 +1,184 @@
#ifdef INTELLISENSE_DIRECTIVES
# include "gen/macs.h"
# include "gen/offsets.h"
# include "mips.h"
# include "dsl.atom.h"
# include "lottes_tape.h"
# include "pad.h"
#endif
ATOM_FILE_DEBUGGER_LINE_MARKER(pad_atom_c);
#pragma region Baked Atoms
/* ----- pad_bios_snapshot -----
* Per-frame snapshot of one BIOS pad buffer into PadState.
* Decoder (branch ladder on raw[0] status + raw[1] id):
* 1. raw[0] == 0xFF -> Disconnected (buttons=0, axes=0x80)
* 2. raw[0]==0 && raw[1]==0 -> Pending (buttons=0, axes=0x80)
* 3. raw[1] == 0x41 -> Digital (buttons normalized; axes=0x80)
* 4. raw[1] == 0x53 -> AnalogStick (buttons normalized; axes from raw[4..7])
* 5. raw[1] in 0x7x -> AnalogPad (buttons normalized; axes from raw[4..7])
* 6. else -> Unsupported (buttons=0, axes=0x80)
*
* Buttons normalization: byte_swap16((~raw_buttons) & 0xFFFF).
* raw_buttons = load_half_u(raw, 2) = raw[2] | (raw[3] << 8).
* byte_swap16(x) = (x >> 8) | (x << 8); nor(x, R_0) = ~x. store_half truncates to 16 bits so the upper-16 mask is implicit in the store.
*
* Register use (atom-local; no wave-context touched):
* R_T0 = raw base (kept throughout; axes loads read raw[4..7] from R_T0)
* R_T1 = state base (kept throughout; all stores go through R_T1)
* R_T2 = raw[0] status (alive across the disc/pending/id dispatch, then dead)
* R_T3 = raw[1] id (alive across the id dispatch, then dead)
* R_T4 = scratch (shifts, compares, immediate loads, store values)
* R_T5 = scratch (parallel lui+ori for the 0x80808080 axes constant + byte-swap target)
*/
enum {
R_PadRaw = R_T0 atom_reg atom_type(U1),
R_PadState = R_T1 atom_reg,
R_RawStatus = R_T2 atom_reg,
R_RawId = R_T3 atom_reg,
};
typedef Struct_(Binds_PadBiosSnapshot) {
PadBiosRaw* raw;
PadState* state;
};
internal MipsAtom_(pad_bios_snapshot) atom_info(atom_bind(Binds_PadBiosSnapshot)
, 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)),
load_word(R_PadState, R_TapePtr, O_(Binds_PadBiosSnapshot,state)),
add_ui_self( R_TapePtr, S_(Binds_PadBiosSnapshot)),
/* === Read raw[0] (status) + raw[1] (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, 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. */
/* 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).
* 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. */
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. */
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, 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_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, 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
* 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, 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). */
/* 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(),
atom_label(try_unsupported) /* === Case 7: Unsupported — fall through from the AnalogPad range-check miss. */
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)),
/* 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)
mac_yield_load(),
atom_label(snap_end)
/* NOT mac_yield() — R_AtomJmp was already loaded in the BD-slot of the case-exit branch. */
mac_yield_tail(),
};
#pragma endregion Baked Atoms
View File
+7
View File
@@ -0,0 +1,7 @@
#ifdef INTELLISENSE_DIRECTIVES
# include "gen/macs.h"
# include "gen/offsets.h"
# include "psyq.h"
#endif
ATOM_FILE_DEBUGGER_LINE_MARKER(pysq_atom_c);
@@ -1,8 +1,8 @@
#ifdef INTELLISENSE_DIRECTIVES #ifdef INTELLISENSE_DIRECTIVES
# pragma once # pragma once
# include "duffle/dsl.h" # include "dsl.h"
# include "duffle/math.h" # include "math.h"
# include "duffle/gp.h" # include "gp.h"
#endif #endif
typedef Struct_(DrawEnv_Packed) { U4 tag; U4 code[15]; }; typedef Struct_(DrawEnv_Packed) { U4 tag; U4 code[15]; };
+1 -1
View File
@@ -6,7 +6,7 @@
// One line per macro that appears in your atom sources. // One line per macro that appears in your atom sources.
// //
// This file is encoding-macros-only. // This file is encoding-macros-only.
// The auto-generated component macros (mac_X) live in duffle/gen/<dir>.macs.h (included separately by the unity build). // The auto-generated component macros (mac_X) live in the source directory's own gen/macs.h (per-directory aggregation; included separately by the unity build).
// The unity build should include THIS file and the .macs.h file in the same TU, with both wrapped // The unity build should include THIS file and the .macs.h file in the same TU, with both wrapped
// (or the include guard order handled) to avoid WORD_COUNT redeclaration. // (or the include guard order handled) to avoid WORD_COUNT redeclaration.
// //
@@ -2,53 +2,23 @@
#pragma once #pragma once
#endif #endif
// Auto-generated by ps1_meta.lua — DO NOT EDIT // Auto-generated by ps1_meta.lua — DO NOT EDIT
// Source: C:\projects\Pikuma\ps1\code\hello_joypad\hello_joypad.tape.c // 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
// Component atoms (MipsAtomComp_(ac_*)) -> macro variants (mac_*) // Component atoms (MipsAtomComp_(ac_*)) -> macro variants (mac_*)
#ifndef WORD_COUNT #ifndef WORD_COUNT
#define WORD_COUNT(name, count) enum { words_##name = (count) }; #define WORD_COUNT(name, count) enum { words_##name = (count) };
#endif #endif
/* atom_dbg_skip */
#define mac_load_v2s2(rs_x, rs_y, r_base, offset) \
load_half( rs_x, r_base, O_(V3_S2,x)) \
, load_half( rs_y, r_base, O_(V3_S2,y))
WORD_COUNT(mac_load_v2s2, 2)
/* atom_dbg_skip */
#define mac_store_v2s2(rt_x, rt_y, base, offset) \
store_half(rt_x, base, offset + O_(V2_S2,x)) \
, store_half(rt_y, base, offset + O_(V2_S2,y))
WORD_COUNT(mac_store_v2s2, 2)
/* 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)) \
, store_half(rt_y, base, offset + O_(Rect_S2,y)) \
, store_half(rt_width, base, offset + O_(Rect_S2,width)) \
, store_half(rt_height, base, offset + O_(Rect_S2,height))
WORD_COUNT(mac_store_rects2, 4)
/* atom_dbg_skip */
#define mac_store_rgb8(rr, rg, rb, base, offset) \
store_byte(rr, base, offset + O_(DrawEnv,initial_bg_color.r)) \
, store_byte(rg, base, offset + O_(DrawEnv,initial_bg_color.g)) \
, store_byte(rb, base, offset + O_(DrawEnv,initial_bg_color.b))
WORD_COUNT(mac_store_rgb8, 3)
#define mac_gcmd_push(cmd, reg_transfer, reg_base, port) \
load_upper_i(reg_transfer, cmd >> 16) \
, or_i_self( reg_transfer, cmd & 0xFFFF) \
, store_word( reg_transfer, reg_base, port)
WORD_COUNT(mac_gcmd_push, 3)
#define mac_put_disp_env(reg_transfer, reg_base, port) \ #define mac_put_disp_env(reg_transfer, reg_base, port) \
mac_gcmd_push(gp0_word_draw_area_top_left_origin, reg_transfer, reg_base, port) \ mac_gcmd_push(gp0_word_draw_area_top_left_origin, reg_transfer, reg_base, port) \
, mac_gcmd_push(gp0_word_draw_area_bottom_right_320x240, reg_transfer, reg_base, port) \ , mac_gcmd_push(gp0_word_draw_area_bottom_right_320x240, reg_transfer, reg_base, port) \
, mac_gcmd_push(gp0_word_set_mask_bit(), reg_transfer, reg_base, port) \ , mac_gcmd_push(gp0_word_set_mask_bit(), reg_transfer, reg_base, port) \
, mac_gcmd_push(gp0_word_draw_area_top_left_origin, reg_transfer, reg_base, port) \ , mac_gcmd_push(gp0_word_draw_area_top_left_origin, reg_transfer, reg_base, port) \
, mac_gcmd_push(gp0_word_draw_area_bottom_right_320x240, reg_transfer, reg_base, port) , mac_gcmd_push(gp0_word_draw_area_bottom_right_320x240, reg_transfer, reg_base, port)
WORD_COUNT(mac_put_disp_env, 15) WORD_COUNT(mac_put_disp_env, 5)
#define mac_put_draw_env(reg_transfer, reg_base, port) \ #define mac_put_draw_env(reg_transfer, reg_base, port) \
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_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. */ \
@@ -67,5 +37,5 @@ WORD_COUNT(mac_put_disp_env, 15)
, mac_gcmd_push(gp0_word_set_texture_window(), reg_transfer, reg_base, port) /* code[13..14] Padding (NOP) — completes the 16-word packet. */ \ , mac_gcmd_push(gp0_word_set_texture_window(), reg_transfer, reg_base, port) /* code[13..14] Padding (NOP) — completes the 16-word packet. */ \
, mac_gcmd_push(gp0_word_nop(), reg_transfer, reg_base, port) \ , mac_gcmd_push(gp0_word_nop(), reg_transfer, reg_base, port) \
, mac_gcmd_push(gp0_word_nop(), reg_transfer, reg_base, port) , mac_gcmd_push(gp0_word_nop(), reg_transfer, reg_base, port)
WORD_COUNT(mac_put_draw_env, 48) WORD_COUNT(mac_put_draw_env, 16)
+50
View File
@@ -0,0 +1,50 @@
// Auto-generated by ps1_meta.lua (passes/offsets.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
#pragma once
#pragma region hello_camera
// --- atom: pad_apply_input (60 words) ---
#define _atom_offset_dpad_left_exit_dpad_left 6
#define _atom_offset_dpad_right_exit_dpad_right 6
#define _atom_offset_dead_zone_low_check_dead_low_active 8
#define _atom_offset_dead_zone_high_check_dead_high_active 15
#define _atom_offset_dead_zone_skip_exit_stick 24
#define _atom_offset_end_low_exit_stick 12
enum {
atom_offset_dpad_left_exit_dpad_left = _atom_offset_dpad_left_exit_dpad_left,
atom_offset_dpad_right_exit_dpad_right = _atom_offset_dpad_right_exit_dpad_right,
atom_offset_dead_zone_low_check_dead_low_active = _atom_offset_dead_zone_low_check_dead_low_active,
atom_offset_dead_zone_high_check_dead_high_active = _atom_offset_dead_zone_high_check_dead_high_active,
atom_offset_dead_zone_skip_exit_stick = _atom_offset_dead_zone_skip_exit_stick,
atom_offset_end_low_exit_stick = _atom_offset_end_low_exit_stick,
};
// --- atom: cube_g4_face (76 words) ---
#define _atom_offset_cull_cube_g4_face_exit 41
#define _atom_offset_bounds_chk_cube_g4_face_exit 24
enum {
atom_offset_cull_cube_g4_face_exit = _atom_offset_cull_cube_g4_face_exit,
atom_offset_bounds_chk_cube_g4_face_exit = _atom_offset_bounds_chk_cube_g4_face_exit,
};
// --- atom: floor_f3_face (58 words) ---
#define _atom_offset_culling_floor_f3_face_exit 25
#define _atom_offset_bounds_chk_floor_f3_face_exit 16
enum {
atom_offset_culling_floor_f3_face_exit = _atom_offset_culling_floor_f3_face_exit,
atom_offset_bounds_chk_floor_f3_face_exit = _atom_offset_bounds_chk_floor_f3_face_exit,
};
#pragma endregion hello_camera
+468
View File
@@ -0,0 +1,468 @@
#ifdef INTELLISENSE_DIRECTIVES
# pragma once
# include "duffle/gen/macs.h"
# include "duffle/gen/offsets.h"
# include "duffle/dsl.atom.h"
# include "duffle/lottes_tape.h"
# include "duffle/mips.h"
# include "duffle/gte.h"
# include "duffle/gp.h"
# include "duffle/pad.h"
# include "duffle/word_count.metadata.h"
# include "duffle/psyq.h"
# include "duffle/math.atom.c"
# include "duffle/mips.atom.c"
# include "duffle/gte.atom.c"
# include "duffle/gp.atom.c"
# include "duffle/psyq.atom.c"
# include "gen/offsets.h"
# include "gen/macs.h"
# include "hello_camera.h"
#endif
ATOM_FILE_DEBUGGER_LINE_MARKER(hello_joypad_atom_c);
#pragma region MACs (Mips Atom components)
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),
mac_gcmd_push(gp0_word_draw_area_bottom_right_320x240, reg_transfer, reg_base, port),
mac_gcmd_push(gp0_word_set_mask_bit(), reg_transfer, reg_base, port),
mac_gcmd_push(gp0_word_draw_area_top_left_origin, reg_transfer, reg_base, port),
mac_gcmd_push(gp0_word_draw_area_bottom_right_320x240, reg_transfer, reg_base, port),
})
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:
* - libpsyx source: `toolchain/psyq-4_7/lib/libgpu.a` (binary, function `PutDrawEnv`)
* - PSX-SPX doc: https://problemkaputt.de/psx-spx.htm#gputdrawingcommands
* - PSYQ SDK: `setdrawenv` / `makelongdr_env` source
* - NOCASH PSX spec: §"GP0(E1h) Draw Mode setting" through §"DR_ENV"
*
* The 16-word format is documented in the PSYQ SDK manual and on NOCASH's PSX-spec.txt. The libpsyx reference is at:
* ./toolchain/psyq-4_7/lib/libgpu.a
* (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
*/
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) */
mac_gcmd_push(gp0_word_set_texture_window(), reg_transfer, reg_base, port), /* code[1] TextureWindow (tw=(0,0)) */
mac_gcmd_push(enc_gp0_draw_area_tl_word(0, ScreenRes_Y), reg_transfer, reg_base, port), /* code[2] DrawArea top-left (clip.x=0, clip.y=ScreenRes_Y=240) */
mac_gcmd_push(gp0_word_draw_area_bottom_right_320x240, reg_transfer, reg_base, port), /* code[3] DrawArea bottom-right (clip.x+w=320, clip.y+h=480) */
mac_gcmd_push(gp0_word_set_draw_offset(), reg_transfer, reg_base, port), /* code[4] DrawOffset (ofs=(0,0)) — bare-cmd word; the GPU uses the current state machine. */
mac_gcmd_push(gp0_word_dr_env_mask(), reg_transfer, reg_base, port), /* code[5] Mask (dtd=0, dfe=1, isbg=1) — 0xE6 cmd + isbg bit. */
mac_gcmd_push(gp0_word_dr_env_bg_color_cmd(1, 7, 7, 7), reg_transfer, reg_base, port), /* code[6] Initial-bg-color + auto-clear (isbg=1, r=7, g=7, b=7). */
mac_gcmd_push(gp0_word_dr_env_draw_mode(1), reg_transfer, reg_base, port), /* code[7] Re-assert DrawMode with isbg=1 (isbg-flag set; the 0xE1 cmd byte plus isbg only). */
/* code[8..10] Padding (NOP — GPU discards; the DR_ENV requires 16 words total). */
mac_gcmd_push(gp0_word_nop(), reg_transfer, reg_base, port),
mac_gcmd_push(gp0_word_nop(), reg_transfer, reg_base, port),
mac_gcmd_push(gp0_word_nop(), reg_transfer, reg_base, port),
/* code[11..12] TextureWindow bottom-right (tw.x+tw.w=0, tw.y+tw.h=0) — libpsyx emits twice. */
mac_gcmd_push(gp0_word_set_texture_window(), reg_transfer, reg_base, port),
mac_gcmd_push(gp0_word_set_texture_window(), reg_transfer, reg_base, port),
/* code[13..14] Padding (NOP) — completes the 16-word packet. */
mac_gcmd_push(gp0_word_nop(), reg_transfer, reg_base, port),
mac_gcmd_push(gp0_word_nop(), reg_transfer, reg_base, port),
})
#pragma endregion MACs
#pragma region Baked Atoms
enum {
R_ScreenX = R_T5 atom_reg atom_type(U2),
R_ScreenY = R_T6 atom_reg atom_type(U2),
R_ScreenBuf = R_T7 atom_reg, /* Caller-pinned: & smem.screen_buf */
#define R_ScreenBuf_Code R_T7_Code
};
//screen_env_init. Mirrors the libpsyx's SetDefDispEnv + SetDefDrawEnv + the manual enable_auto_clear / initial_bg_color writes.
internal MipsAtom_(screen_env_init) atom_info(atom_phase(screen_init)
, atom_reads(R_T0, R_ScreenX, R_ScreenY, R_ScreenBuf)
, atom_writes(R_T0, R_ScreenX, R_ScreenY)
) {
/* display[0] = (0, 0, 320, 240); rest of struct zeroed. */
add_ui(R_ScreenX, R_0, ScreenRes_X), add_ui(R_ScreenY, R_0, ScreenRes_Y),
mac_store_v2s2(R_ScreenX, R_ScreenY, R_ScreenBuf, O_(DisplayEnv,display_area.width) + OA_(DoubleBuffer,display,0)),
store_word(R_0, R_ScreenBuf, O_(DisplayEnv,display_area) + OA_(DoubleBuffer,display,0)),
store_word(R_0, R_ScreenBuf, O_(DisplayEnv,screen) + OA_(DoubleBuffer,display,0)),
store_word(R_0, R_ScreenBuf, O_(DisplayEnv,vinterlace) + OA_(DoubleBuffer,display,0)),
/* display[1] = (0, 240, 320, 240); rest of struct zeroed. */
mac_store_rects2(R_0, R_ScreenY, R_ScreenX, R_ScreenY, R_ScreenBuf, O_(DisplayEnv,display_area) + OA_(DoubleBuffer,display,1)),
store_word(R_0, R_ScreenBuf, O_(DisplayEnv,screen) + OA_(DoubleBuffer,display,1)),
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_ScreenX, R_ScreenY, R_ScreenBuf, O_(DrawEnv,clip_area.width) + OA_(DoubleBuffer,draw,1)),
/* draw[0].texture_window = (0, 0, 0, 0); two word-zeroes cover the full 8-byte tw field. */
store_word(R_0, R_ScreenBuf, O_(DrawEnv,texture_window.x) + OA_(DoubleBuffer,draw,0)),
store_word(R_0, R_ScreenBuf, O_(DrawEnv,texture_window.width) + OA_(DoubleBuffer,draw,0)),
store_word(R_0, R_ScreenBuf, O_(DrawEnv,drawing_offset[0].x) + OA_(DoubleBuffer,draw,1)),
store_word(R_0, R_ScreenBuf, O_(DrawEnv,texture_window.x) + OA_(DoubleBuffer,draw,1)),
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),
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)),
/* draw[0] control bytes: flag_dither=1, flag_draw_on_display=1 (the dfe bit per psx-spx; libpsyx sets it via `SetDefDrawEnv`'s conditional at C11_only.elf:0x80012728), enable_auto_clear=1. Each byte is named;
* the previous `store_word(R_0, ..., +20)` overwrote all four with zero. */
add_ui(R_T0, R_0, 1),
store_byte(R_T0, R_ScreenBuf, O_(DrawEnv,flag_dither) + OA_(DoubleBuffer,draw,0)),
store_byte(R_T0, R_ScreenBuf, O_(DrawEnv,flag_draw_on_display) + OA_(DoubleBuffer,draw,0)),
store_byte(R_T0, R_ScreenBuf, O_(DrawEnv,enable_auto_clear) + OA_(DoubleBuffer,draw,0)),
store_byte(R_T0, R_ScreenBuf, O_(DrawEnv,flag_dither) + OA_(DoubleBuffer,draw,1)),
store_byte(R_T0, R_ScreenBuf, O_(DrawEnv,flag_draw_on_display) + OA_(DoubleBuffer,draw,1)),
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),
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(),
};
enum {
R_IO_BaseAddr = R_T4 atom_reg, /* Caller-pinned: IO_BASE_ADDR = 0x1F800000 */
#define R_IO_BaseAddr_Code R_T4_Code
};
internal MipsAtom_(gp_screen_init) atom_info(atom_phase(screen_init), atom_reads(R_IO_BaseAddr)) {
store_word(R_0, R_IO_BaseAddr, GPIO_PORT1_OFFSET), /* GP1(00h) Reset */
mac_gcmd_push(gp1_word_ResetCmdBuffer(), R_T5, R_IO_BaseAddr, GPIO_PORT1_OFFSET), /* GP1(01h) ClearFIFO */
mac_gcmd_push(gp1_word_AcknowledgeIRQ(), R_T5, R_IO_BaseAddr, GPIO_PORT1_OFFSET), /* GP1(02h) AckIRQ */
mac_gcmd_push(gp1_word_DisplayOn(), R_T5, R_IO_BaseAddr, GPIO_PORT1_OFFSET), /* GP1(03h) Display ON */
mac_gcmd_push(gp1_word_dma_to_gpu(), R_T5, R_IO_BaseAddr, GPIO_PORT1_OFFSET), /* GP1(04h) DMADirection=2 (CPU→GPU). libpsyx's per-frame PutDrawEnv/DrawOTag use DMA2; without this the DMA queue never drains. */
mac_gcmd_push(gp1_word_StartDisplayArea(), R_T5, R_IO_BaseAddr, GPIO_PORT1_OFFSET), /* GP1(05h) StartDisplayArea (X=0, Y=0) */
/* 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_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_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_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;
};
enum {
R_PadStateT5 = R_T5 atom_reg,
R_CubeRot = R_T1 atom_reg,
R_FloorRot = R_T2 atom_reg,
};
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)
) {
/* Pop Binds from tape (state, cube_rot, floor_rot) */
load_word(R_PadStateT5, R_TapePtr, O_(Binds_PadApplyInput,state)),
load_word(R_CubeRot, R_TapePtr, O_(Binds_PadApplyInput,cube_rot)),
load_word(R_FloorRot, R_TapePtr, O_(Binds_PadApplyInput,floor_rot)),
add_ui_self( R_TapePtr, S_(Binds_PadApplyInput)),
/* Load pad[0].buttons into R_T0. */
load_word(R_T0, R_PadStateT5, O_(PadState,buttons)), nop,
// Note(Ed): Potential op with delay slot?
/* D-pad Left: cube_rot.y += 30, floor_rot.y += 5. */
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),
add_si( R_T3, R_T3, 5),
store_half(R_T4, R_CubeRot, O_(V3_S2,y)),
store_half(R_T3, R_FloorRot, O_(V3_S2,y)),
atom_label(exit_dpad_left)
/* D-pad Right: cube_rot.y -= 30, floor_rot.y -= 5. */
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),
add_si( R_T3, R_T3, -5),
store_half(R_T4, R_CubeRot, O_(V3_S2,y)),
store_half(R_T3, R_FloorRot, O_(V3_S2,y)),
atom_label(exit_dpad_right)
/* 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)),
/* 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, 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, 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, 0x80), /* BD-slot: pre-load 0x80 for dead_high_active */
jump_rel(atom_offset(dead_zone_skip, exit_stick)),
mac_yield_load(),
atom_label(dead_low_active)
/* R_T3 = left_x (from line 632 lbu; not clobbered between dead_zone_low_check branch + its BD-slot `add_ui R_T4, 0x80`).
* The earlier `load_byte_u(R_T3, ...)` reload was redundant and introduced a load-use hazard on the next `sub_u`.
* R_T4 = 0x80 from the BD-slot of `dead_zone_low_check`'s branch_ne. */
sub_u( R_T3, R_T4, R_T3), /* R_T3 = 0x80 - left_x */
/* delta = 0x80 - left_x (positive). */
/* R_T4 = cube_delta */
shift_aright(R_T4, R_T3, 2),
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;
* doesn't read R_T0; R_T4 settles by the subsequent add_u). */
load_half( R_T0, R_FloorRot, O_(V3_S2,y)),
shift_aright(R_T4, R_T3, 5),
add_u( R_T0, R_T0, R_T4),
store_half( R_T0, R_FloorRot, O_(V3_S2,y)),
jump_rel(atom_offset(end_low, exit_stick)),
mac_yield_load(),
atom_label(dead_high_active)
/* R_T3 = left_x (from line 641 lbu in dead_check_upper; not clobbered between dead_zone_high_check branch + its BD-slot `add_ui R_T4, 0x80`).
* The earlier `load_byte_u(R_T3, ...)` reload was redundant and introduced a load-use hazard on the next `sub_u`.
* R_T4 = 0x80 from the BD-slot of `dead_zone_high_check`'s branch_ne. */
sub_u( R_T3, R_T4, R_T3),
/* 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,
add_u( R_T0, R_T0, R_T4),
store_half( R_T0, R_CubeRot, O_(V3_S2,y)),
/* R_T4 = floor_delta (signed) — moved into the load-delay slot of the floor load below. */
load_half( R_T0, R_FloorRot, O_(V3_S2,y)),
shift_aright(R_T4, R_T3, 5),
add_u( R_T0, R_T0, R_T4),
store_half( R_T0, R_FloorRot, O_(V3_S2,y)),
atom_label(no_jump_fallthrough)
mac_yield_load(),
atom_label(exit_stick)
/* NOT mac_yield() — R_AtomJmp was already loaded in the BD-slot of the dead-zone/exit branch. */
mac_yield_tail(),
};
enum {
R_PrimCursor = R_T7 atom_reg atom_type(U4*), /* VRAM output cursor (primitive buffer) */
R_FaceCursor = R_T4 atom_reg atom_type(V4_S2*), /* Cube face-index cursor (V4_S2*); floor context switches to V3_S2* via atom_phase */
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 */
#define R_PrimCursor_Code R_T7_Code
#define R_FaceCursor_Code R_T4_Code
#define R_VertBase_Code R_T5_Code
#define R_OtBase_Code R_T6_Code
};
typedef Struct_(Binds_CubeTri) {
U4 PrimCursor;
V4_S2* FaceCursor;
V3_S2* VertBase;
U4* OtBase;
};
internal MipsAtom_(rbind_cube_g4_face) atom_info(atom_bind(Binds_CubeTri), atom_phase(cube_g4)
, atom_reads(R_TapePtr)
, atom_writes(R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase, R_TapePtr)
){
/* Pop 4 arguments from the tape directly into the workspace registers */
load_word(R_PrimCursor, R_TapePtr, O_(Binds_CubeTri,PrimCursor)),
load_word(R_FaceCursor, R_TapePtr, O_(Binds_CubeTri,FaceCursor)),
load_word(R_VertBase, R_TapePtr, O_(Binds_CubeTri,VertBase)),
load_word(R_OtBase, R_TapePtr, O_(Binds_CubeTri,OtBase)),
add_ui_self( R_TapePtr, S_(Binds_CubeTri)),
mac_yield()
};
// 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),
atom_writes(R_PrimCursor, R_FaceCursor)
){
load_half_u(R_T0, R_FaceCursor, 0 * S_(S2)),
load_half_u(R_T1, R_FaceCursor, 1 * S_(S2)),
load_half_u(R_T2, R_FaceCursor, 2 * S_(S2)),
load_half_u(R_T3, R_FaceCursor, 3 * S_(S2)),
mac_gte_load_tri_verts(R_VertBase, R_T0, R_T1, R_T2),
nop2, gte_cmdw_rotate_translate_perspective_triple, // required cpu -> gte delay slot
gte_cmdw_nclip,
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, 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)),
gte_mv_to_data_r(R_V0, C2_VXY0), gte_mv_to_data_r(R_V1, C2_VZ0),
mac_gte_store_g4_p012(R_PrimCursor),
gte_cmdw_rotate_translate_perspective_single,
mac_gte_store_g4_p3(R_PrimCursor),
gte_cmdw_avg_sort_z4,
gte_mv_from_data_r(R_T1, C2_OTZ),
add_ui( R_AT, R_0, OrderingTbl_Len),
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_g4(R_OtBase, R_PrimCursor),
mac_format_g4_color(R_PrimCursor,
/* c0 magenta */ 0xFF, 0x00, 0xFF,
/* c1 yellow */ 0xFF, 0xFF, 0x00,
/* c2 cyan */ 0x00, 0xFF, 0xFF,
/* c3 green */ 0x00, 0xFF, 0x00),
// end: branch(bounds_chk)
// end: branch(cull)
atom_label(cube_g4_face_exit)
add_ui_self(R_PrimCursor, S_(Poly_G4)), /* 9 words = Poly_G4 */
add_ui_self(R_FaceCursor, S_(S2) * 4), /* 4 × S2 = 8 bytes */
mac_yield()
};
typedef Struct_(Binds_FloorTri) {
U4 PrimCursor;
V3_S2* FaceCursor;
V3_S2* VertBase;
U4* OtBase;
};
internal
MipsAtom_(rbind_floor_f3_face) atom_info(atom_bind(Binds_FloorTri), atom_phase(floor_f3)
, atom_reads(R_TapePtr)
, atom_writes(R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase, R_TapePtr)
){
/* Pop 4 arguments from the tape directly into the workspace registers */
load_word(R_PrimCursor, R_TapePtr, O_(Binds_FloorTri,PrimCursor)),
load_word(R_FaceCursor, R_TapePtr, O_(Binds_FloorTri,FaceCursor)),
load_word(R_VertBase, R_TapePtr, O_(Binds_FloorTri,VertBase)),
load_word(R_OtBase, R_TapePtr, O_(Binds_FloorTri,OtBase)),
add_ui_self( R_TapePtr, S_(Binds_FloorTri)),
mac_yield()
};
// atom_dbg_skip
internal
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_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.
/* Format Primitive */
mac_gte_store_f3(R_PrimCursor),
/* Calculate Depth */
gte_avg_sort_z3,
gte_mv_from_data_r(R_T1, C2_OTZ),
/* Bounds Check OTZ < 2048 (Branch forward to skip insertion) */
add_ui( R_AT, R_0, OrderingTbl_Len),
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_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)
// end: branch(culling)
/* Advance Input Cursor & Yield (Both branch targets land here) */
atom_label(floor_f3_face_exit)
add_ui_self(R_FaceCursor, S_(S2) * 4), /* Advance Face Cursor (4 * S2 = 8 bytes) */
mac_yield()
};
typedef Struct_(Binds_SyncPrimitiveArena) { U4 used; U4 cursor; };
internal MipsAtom_(sync_primitive_arena) atom_info(atom_bind(Binds_SyncPrimitiveArena)
, atom_reads( R_TapePtr, R_PrimCursor)
, atom_writes(R_TapePtr)
){
load_word(R_AT, R_TapePtr, O_(Binds_SyncPrimitiveArena,used)),
load_word(R_T0, R_TapePtr, O_(Binds_SyncPrimitiveArena,cursor)),
add_ui_self( R_TapePtr, S_(Binds_SyncPrimitiveArena)),
/* Calculate byte offset and store directly back to RAM */
sub_u( R_T0, R_PrimCursor, R_T0), // R_T0 = R_PrimCursor - binds.cursor
store_word(R_T0, R_AT, 0), // R_AT[0] = R_T0
mac_yield()
};
#pragma endregion Baked Atoms
+342
View File
@@ -0,0 +1,342 @@
#pragma region Vendors
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
// #include "libgpu.h"
// #include "libetc.h"
// #include "libgte.h"
#pragma endregion Vendors
#pragma region Duffle Headers
# include "duffle/gen/macs.h"
# include "duffle/gen/offsets.h"
#include "duffle/word_count.metadata.h"
#include "duffle/dsl.h"
#include "duffle/memory.h"
#include "duffle/math.h"
#include "duffle/gcc_asm.h"
#include "duffle/mips.h"
#include "duffle/gp.h"
#include "duffle/gte.h"
#include "duffle/pad.h"
#include "duffle/dsl.atom.h"
#include "duffle/lottes_tape.h"
#include "duffle/psyq.h"
#pragma endregion Duffle Headers
#pragma region Duffle TUs
#include "duffle/math.atom.c"
#include "duffle/mips.atom.c"
#include "duffle/gte.atom.c"
#include "duffle/gp.atom.c"
#include "duffle/pad.atom.c"
#include "duffle/psyq.atom.c"
#pragma endregion Duffle TUs
#pragma region Hello Camera Headers
# include "gen/macs.h"
# include "gen/offsets.h"
#include "hello_camera.h"
#pragma endregion Hello Camera Headers
#pragma region Hello Joypad TUs
#include "hello_camera.atom.c"
#pragma endregion Hello Joypad TUs
enum {
Scratchpad_Len = 1024,
MemTape_Len = 512,
};
typedef Struct_(SMemory) {
PrimitiveArena primitives;
A2_OrderingTable_Buffer ordering_tbl;
DoubleBuffer screen_buf;
S4 active_buf_id;
U4 MemTape[MemTape_Len];
M3_S2 tform_world;
Ent_Cube cube;
Ent_Floor floor;
PadBiosRaw pad_raw[2];
PadState pad[2];
U4_V scratchpad; // d-cache
};
global SMemory smem;
extern SMemory smem;
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];
assert(pa->used + type_width < PrimitiveBuff_Len);
B1* next = buf + pa->used;
pa->used += type_width;
return next;
}
#define prim_alloc(type) (type*)prim__alloc(S_(type), slit( stringify(type)))
/* 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;
// 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, 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
);
/* 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, 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
);
}
GCC_OPTIMIZATION_DISABLE
void update(PrimitiveArena* pa, U4* ordering_buf)
{
TapeBuilder tb = tb_make(slice_ut_arr(smem.MemTape));
if (1) // Pad Input
{
tb.used = 0; tb_scope_run(& tb) {
/* BIOS-owned polling: per-frame snapshot of both ports. */
tb_emit_(pad_bios_snapshot);
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]);
/* 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);
}
}
orderingtbl_clear_reverse(ordering_buf, OrderingTbl_Len);
// Update the position based on acceleration and velocity
gknown V3_S4_R pos = & smem.cube.pos;
gknown V3_S4_R vel = & smem.cube.vel;
gknown V3_S4_R acc = & smem.cube.accel;
add_v3s4(vel, acc[0]);
add_v3s4_fp(pos, vel[0]);
// vel->x += acc->x;
// vel->y += acc->y;
// vel->z += acc->z;
// pos->x += vel->x;
// pos->y += vel->y;
// pos->z += vel->z;
if (pos->y + 150 > smem.floor.pos.y) vel->y *= -1;
// Prep
S4 nclip = 0;
S4 orderingtbl_z = 0;
A2_S2 p; //???
S4 flag; //????
// Draw cube
if (1)
{
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;
tb.used = 0; tb_scope(& tb) {
tb_emit(& tb, rbind_cube_g4_face);
tb_data(& tb, prim_cursor);
tb_data(& tb, u4_(smem.cube.faces));
tb_data(& tb, u4_(smem.cube.verts));
tb_data(& tb, u4_(ordering_buf));
for (U4 i = 0; i < Cube_num_faces; i++) {
// Two triangles per quad face: (x,y,z) and (x,z,w)
tb_emit(& tb, cube_g4_face);
}
tb_emit(& tb, sync_primitive_arena);
tb_data(& tb, u4_(& pa->used));
tb_data(& tb, prim_base);
}
tape_run(tb_slice(tb));
// smem.cube.rot.y += 30;
}
// Draw floor
if (1)
{
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;
// TODO(Ed): We should do a bounds check beforehand to confirm pa can hold all tris?
// The tape atoms in-flight should not need to care.
// Prepare the tape. (Push protocol to tape)
tb.used = 0; tb_scope(& tb) {
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
tb_data(& tb, prim_cursor);
tb_data(& tb, u4_(smem.floor.faces));
tb_data(& tb, u4_(smem.floor.verts));
tb_data(& tb, u4_(ordering_buf));
for (U4 i = 0; i < Floor_num_faces; i++) {
tb_emit(& tb, floor_f3_face);
}
// After floor_f3_face iterations complete, the primitive arena's used counter needs updating.
tb_emit(& tb, sync_primitive_arena);
tb_data(& tb, u4_(& pa->used));
tb_data(& tb, prim_base);
}
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;
}
}
GCC_OPTIMIZATION_ENABLE
void render(void) {
}
void gp_display_frame(DoubleBuffer* screen_buf, S4* active_buf_id, U4* ordering_buf, PrimitiveArena* pa) {
draw_sync(0);
vsync(0);
displayenv_put(& r_(screen_buf->display)[active_buf_id[0] ]);
drawenv_put (& r_(screen_buf->draw) [active_buf_id[0] ]);
{
draw_orderingtbl(ordering_buf + OrderingTbl_Len - 1);
pa->used = 0;
}
active_buf_id[0] = ! active_buf_id[0]; // Swap current buffer
}
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};
smem.scratchpad = C_(U4_V, 0x1F800000);
// smem.primitives.used = 0;
// smem.active_buf_id = 0;
/*Persistent Entity Setup*/{
ent_cube128_init(& smem.cube.verts, & smem.cube.faces); {
Ent_Cube* cube = & smem.cube;
cube->rot = v3s2(0, 0, 0);
cube->scale = v3s4_fp_one();
cube->accel = v3s4(0, 1, 0);
cube->pos = v3s4(0, -400, 1800);
}
ent_floor_init(& smem.floor.verts, & smem.floor.faces); {
Ent_Floor* floor = & smem.floor;
floor->rot = v3s2(0, 0, 0);
floor->pos = v3s4(0, 450, 1800);
floor->scale = v3s4_fp_one();
}
}
TapeBuilder tb = tb_make(slice_ut_arr(smem.MemTape)); {
reset_graph(0);
/* Direct BIOS: poll both ports during VBlank. */
pad_bios_init_start(& smem.pad_raw[0], & smem.pad_raw[1]);
/* 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;
tb.used = 0; tb_scope_run(& tb) {
tb_emit(& tb, screen_env_init);
tb_emit(& tb, gp_screen_init);
}
}
hot_reload_entry();
return 0;
}
GCC_OPTIMIZATION_ENABLE
+102
View File
@@ -0,0 +1,102 @@
#ifdef INTELLISENSE_DIRECTIVES
# pragma once
# include "duffle/dsl.h"
# include "duffle/math.h"
# include "duffle/gp.h"
# include "duffle/pad.h"
#endif
enum {
// PrimitiveBuff_Len = 4096,
// OrderingTbl_Len = 2048,
PrimitiveBuff_Len = 131072,
OrderingTbl_Len = 8192,
};
enum {
ScreenRes_X = 320,
ScreenRes_Y = 240,
ScreenZ = 320,
ScreenRes_CenterX = (ScreenRes_X >> 1),
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);
typedef B1 PrimitiveBuffer[PrimitiveBuff_Len];
typedef Array_(PrimitiveBuffer, 2);
typedef Struct_(PrimitiveArena) {
A2_PrimitiveBuffer buf;
U4 used;
};
#define Cube_num_verts 8
typedef Array_(V3_S2, Cube_num_verts);
#define Cube_num_faces 6
typedef Array_(V4_S2, Cube_num_faces);
I_ void ent_cube128_init(A8_V3_S2* verts, A6_V4_S2* faces) {
LP_ A8_V3_S2 baked_verts = (A8_V3_S2) {
{ -128, -128, -128 },
{ 128, -128, -128 },
{ 128, -128, 128 },
{ -128, -128, 128 },
{ -128, 128, -128 },
{ 128, 128, -128 },
{ 128, 128, 128 },
{ -128, 128, 128 }
};
LP_ A6_V4_S2 baked_faces = (A6_V4_S2) {
{ 3, 2, 0, 1 },
{ 0, 1, 4, 5 },
{ 4, 5, 7, 6 },
{ 1, 2, 5, 6 },
{ 2, 3, 6, 7 },
{ 3, 0, 7, 4 },
};
mem_copy(u4_(verts), u4_(& baked_verts), S_(A8_V3_S2) );
mem_copy(u4_(faces), u4_(& baked_faces), S_(A6_V4_S2) );
return;
}
typedef Struct_(Ent_Cube) {
V3_S4 accel;
V3_S4 vel;
V3_S4 pos;
V3_S4 scale;
V3_S2 rot;
A8_V3_S2 verts;
A6_V4_S2 faces;
};
#define Floor_num_verts 4
typedef Array_(V3_S2, Floor_num_verts);
#define Floor_num_faces 2
typedef Array_(V3_S2, Floor_num_faces);
I_ void ent_floor_init(A4_V3_S2* verts, A2_V3_S2* faces) {
LP_ A4_V3_S2 baked_verts = (A4_V3_S2) {
{ -900, 0, -900 },
{ -900, 0, 900 },
{ 900, 0, -900 },
{ 900, 0, 900 },
};
LP_ A2_V3_S2 baked_faces = (A2_V3_S2) {
{ 0, 1, 2 },
{ 1, 3, 2 },
};
mem_copy(u4_(verts), u4_(& baked_verts), S_(A4_V3_S2));
mem_copy(u4_(faces), u4_(& baked_faces), S_(A2_V3_S2));
};
typedef Struct_(Ent_Floor) {
V3_S4 accel;
V3_S4 pos;
V3_S4 scale;
V3_S2 rot;
A4_V3_S2 verts;
A2_V3_S2 faces;
};
+3 -3
View File
@@ -14,13 +14,13 @@
#include "duffle/gp.h" #include "duffle/gp.h"
#include "duffle/gte.h" #include "duffle/gte.h"
# include "duffle/gen/duffle.macs.h" # include "duffle/gen/macs.h"
# include "duffle/gen/duffle.offsets.h" # include "duffle/gen/offsets.h"
#include "duffle/atom_dsl.h" #include "duffle/atom_dsl.h"
#include "duffle/lottes_tape.h" #include "duffle/lottes_tape.h"
#include "duffle/word_count.metadata.h" #include "duffle/word_count.metadata.h"
# include "gen/hello_gte.offsets.h" # include "gen/offsets.h"
#include "hello_gte.h" #include "hello_gte.h"
#include "hello_gte.tape.c" #include "hello_gte.tape.c"
+6 -6
View File
@@ -1,10 +1,10 @@
#ifdef INTELLISENSE_DIRECTIVES #ifdef INTELLISENSE_DIRECTIVES
# include "duffle/gen/duffle.macs.h" # include "duffle/gen/macs.h"
# include "duffle/gen/duffle.offsets.h" # include "duffle/gen/offsets.h"
# include "duffle/atom_dsl.h" # include "duffle/atom_dsl.h"
# include "duffle/lottes_tape.h" # include "duffle/lottes_tape.h"
# include "duffle/word_count.metadata.h" # include "duffle/word_count.metadata.h"
# include "gen/hello_gte.offsets.h" # include "gen/offsets.h"
# include "hello_gte.h" # include "hello_gte.h"
#endif #endif
@@ -125,9 +125,9 @@ MipsAtom_(cube_g4_face) atom_info(atom_phase(cube_g4),
gte_cmdw_avg_sort_z4, gte_cmdw_avg_sort_z4,
gte_mv_from_data_r(R_T1, C2_OTZ), gte_mv_from_data_r(R_T1, C2_OTZ),
add_ui( R_AT, R_0, OrderingTbl_Len),
set_lt_u( R_AT, R_T1, R_AT),
add_ui( R_AT, R_0, OrderingTbl_Len),
set_lt_u( R_AT, R_T1, R_AT),
branch_equal(R_AT, R_0, atom_offset(bounds_chk, cube_g4_face_exit)), nop, branch_equal(R_AT, R_0, atom_offset(bounds_chk, cube_g4_face_exit)), nop,
mac_insert_ot_tag_g4(), mac_insert_ot_tag_g4(),
mac_format_g4_color( mac_format_g4_color(
@@ -184,7 +184,7 @@ MipsAtom_(floor_f3_face) atom_info(atom_phase(floor_f3)
/* Calculate Depth */ /* Calculate Depth */
gte_avg_sort_z3, gte_avg_sort_z3,
gte_mv_from_data_r(R_T1, C2_OTZ), gte_mv_from_data_r(R_T1, C2_OTZ),
/* Bounds Check OTZ < 2048 (Branch forward to skip insertion) */ /* Bounds Check OTZ < OrderingTbl_Len (Branch forward to skip insertion) */
add_ui( R_AT, R_0, OrderingTbl_Len), add_ui( R_AT, R_0, OrderingTbl_Len),
set_lt_u( R_AT, R_T1, R_AT), set_lt_u( R_AT, R_T1, R_AT),
branch_equal(R_AT, R_0, atom_offset(bounds_chk, floor_f3_face_exit)), nop, branch_equal(R_AT, R_0, atom_offset(bounds_chk, floor_f3_face_exit)), nop,
+41
View File
@@ -0,0 +1,41 @@
#ifdef INTELLISENSE_DIRECTIVES
#pragma once
#endif
// Auto-generated by ps1_meta.lua — DO NOT EDIT
// Directory: C:\projects\Pikuma\ps1\code\hello_joypad/
// source: C:\projects\Pikuma\ps1\code\hello_joypad\hello_joypad.c
// source: C:\projects\Pikuma\ps1\code\hello_joypad\hello_joypad.h
// source: C:\projects\Pikuma\ps1\code\hello_joypad\hello_joypad.atom.c
// Component atoms (MipsAtomComp_(ac_*)) -> macro variants (mac_*)
#ifndef WORD_COUNT
#define WORD_COUNT(name, count) enum { words_##name = (count) };
#endif
#define mac_put_disp_env(reg_transfer, reg_base, port) \
mac_gcmd_push(gp0_word_draw_area_top_left_origin, reg_transfer, reg_base, port) \
, mac_gcmd_push(gp0_word_draw_area_bottom_right_320x240, reg_transfer, reg_base, port) \
, mac_gcmd_push(gp0_word_set_mask_bit(), reg_transfer, reg_base, port) \
, mac_gcmd_push(gp0_word_draw_area_top_left_origin, reg_transfer, reg_base, port) \
, mac_gcmd_push(gp0_word_draw_area_bottom_right_320x240, reg_transfer, reg_base, port)
WORD_COUNT(mac_put_disp_env, 5)
#define mac_put_draw_env(reg_transfer, reg_base, port) \
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) */ \
, mac_gcmd_push(gp0_word_set_texture_window(), reg_transfer, reg_base, port) /* code[1] TextureWindow (tw=(0,0)) */ \
, mac_gcmd_push(enc_gp0_draw_area_tl_word(0, ScreenRes_Y), reg_transfer, reg_base, port) /* code[2] DrawArea top-left (clip.x=0, clip.y=ScreenRes_Y=240) */ \
, mac_gcmd_push(gp0_word_draw_area_bottom_right_320x240, reg_transfer, reg_base, port) /* code[3] DrawArea bottom-right (clip.x+w=320, clip.y+h=480) */ \
, mac_gcmd_push(gp0_word_set_draw_offset(), reg_transfer, reg_base, port) /* code[4] DrawOffset (ofs=(0,0)) — bare-cmd word; the GPU uses the current state machine. */ \
, mac_gcmd_push(gp0_word_dr_env_mask(), reg_transfer, reg_base, port) /* code[5] Mask (dtd=0, dfe=1, isbg=1) — 0xE6 cmd + isbg bit. */ \
, mac_gcmd_push(gp0_word_dr_env_bg_color_cmd(1, 7, 7, 7), reg_transfer, reg_base, port) /* code[6] Initial-bg-color + auto-clear (isbg=1, r=7, g=7, b=7). */ \
, mac_gcmd_push(gp0_word_dr_env_draw_mode(1), reg_transfer, reg_base, port) /* code[7] Re-assert DrawMode with isbg=1 (isbg-flag set; the 0xE1 cmd byte plus isbg only). */ /* code[8..10] Padding (NOP — GPU discards; the DR_ENV requires 16 words total). */ \
, mac_gcmd_push(gp0_word_nop(), reg_transfer, reg_base, port) \
, mac_gcmd_push(gp0_word_nop(), reg_transfer, reg_base, port) \
, mac_gcmd_push(gp0_word_nop(), reg_transfer, reg_base, port) /* code[11..12] TextureWindow bottom-right (tw.x+tw.w=0, tw.y+tw.h=0) — libpsyx emits twice. */ \
, mac_gcmd_push(gp0_word_set_texture_window(), reg_transfer, reg_base, port) \
, mac_gcmd_push(gp0_word_set_texture_window(), reg_transfer, reg_base, port) /* code[13..14] Padding (NOP) — completes the 16-word packet. */ \
, mac_gcmd_push(gp0_word_nop(), reg_transfer, reg_base, port) \
, mac_gcmd_push(gp0_word_nop(), reg_transfer, reg_base, port)
WORD_COUNT(mac_put_draw_env, 16)
@@ -1,13 +1,16 @@
// Auto-generated by ps1_meta.lua (passes/offsets.lua) — DO NOT EDIT // Auto-generated by ps1_meta.lua (passes/offsets.lua) — DO NOT EDIT
// Source: C:\projects\Pikuma\ps1\code\hello_joypad\hello_joypad.tape.c // Directory: C:\projects\Pikuma\ps1\code\hello_joypad\
// source: C:\projects\Pikuma\ps1\code\hello_joypad\hello_joypad.c
// source: C:\projects\Pikuma\ps1\code\hello_joypad\hello_joypad.h
// source: C:\projects\Pikuma\ps1\code\hello_joypad\hello_joypad.atom.c
#pragma once #pragma once
#pragma region hello_joypad.tape #pragma region hello_joypad
// --- atom: cube_g4_face (77 words) --- // --- atom: cube_g4_face (76 words) ---
#define _atom_offset_cull_cube_g4_face_exit 42 #define _atom_offset_cull_cube_g4_face_exit 41
#define _atom_offset_bounds_chk_cube_g4_face_exit 24 #define _atom_offset_bounds_chk_cube_g4_face_exit 24
enum { enum {
@@ -28,15 +31,15 @@ enum {
// --- atom: pad_bios_snapshot (78 words) --- // --- atom: pad_bios_snapshot (78 words) ---
#define _atom_offset_snap_root_skip_disconnected 8 #define _atom_offset_snap_root_skip_disconnected 8
#define _atom_offset_disconnected_snap_end 60 #define _atom_offset_disconnected_snap_end 61
#define _atom_offset_case_2_id_dispatch 8 #define _atom_offset_case_2_id_dispatch 8
#define _atom_offset_pending_snap_end 50 #define _atom_offset_pending_snap_end 51
#define _atom_offset_id_dispatch_try_analog_stick 11 #define _atom_offset_id_dispatch_try_analog_stick 11
#define _atom_offset_id_dispatch_snap_end 37 #define _atom_offset_id_dispatch_snap_end 38
#define _atom_offset_try_analog_stick_try_analog_pad 12 #define _atom_offset_try_analog_stick_try_analog_pad 12
#define _atom_offset_analog_stick_snap_end 23 #define _atom_offset_analog_stick_snap_end 24
#define _atom_offset_try_analog_pad_try_unsupported 11 #define _atom_offset_try_analog_pad_try_unsupported 11
#define _atom_offset_analog_pad_snap_end 9 #define _atom_offset_analog_pad_snap_end 10
enum { enum {
atom_offset_snap_root_skip_disconnected = _atom_offset_snap_root_skip_disconnected, atom_offset_snap_root_skip_disconnected = _atom_offset_snap_root_skip_disconnected,
@@ -57,8 +60,8 @@ enum {
#define _atom_offset_dpad_right_exit_dpad_right 6 #define _atom_offset_dpad_right_exit_dpad_right 6
#define _atom_offset_dead_zone_low_check_dead_low_active 8 #define _atom_offset_dead_zone_low_check_dead_low_active 8
#define _atom_offset_dead_zone_high_check_dead_high_active 15 #define _atom_offset_dead_zone_high_check_dead_high_active 15
#define _atom_offset_dead_zone_skip_exit_stick 23 #define _atom_offset_dead_zone_skip_exit_stick 24
#define _atom_offset_end_low_exit_stick 11 #define _atom_offset_end_low_exit_stick 12
enum { enum {
atom_offset_dpad_left_exit_dpad_left = _atom_offset_dpad_left_exit_dpad_left, atom_offset_dpad_left_exit_dpad_left = _atom_offset_dpad_left_exit_dpad_left,
@@ -69,5 +72,5 @@ enum {
atom_offset_end_low_exit_stick = _atom_offset_end_low_exit_stick, atom_offset_end_low_exit_stick = _atom_offset_end_low_exit_stick,
}; };
#pragma endregion hello_joypad.tape #pragma endregion hello_joypad
@@ -1,51 +1,29 @@
#ifdef INTELLISENSE_DIRECTIVES #ifdef INTELLISENSE_DIRECTIVES
# include "duffle/gen/duffle.macs.h" # pragma once
# include "duffle/gen/duffle.offsets.h" # include "duffle/gen/macs.h"
# include "duffle/atom_dsl.h" # include "duffle/gen/offsets.h"
# include "duffle/dsl.atom.h"
# include "duffle/lottes_tape.h" # include "duffle/lottes_tape.h"
# include "duffle/mips.h" # include "duffle/mips.h"
# include "duffle/gte.h" # include "duffle/gte.h"
# include "duffle/gp.h" # include "duffle/gp.h"
# include "duffle/pad.h" # include "duffle/pad.h"
# include "duffle/word_count.metadata.h" # include "duffle/word_count.metadata.h"
# include "psyq.h" # include "duffle/psyq.h"
# include "gen/hello_joypad.offsets.h" # include "duffle/math.atom.c"
# include "gen/hello_joypad.macs.h" # include "duffle/mips.atom.c"
# include "duffle/gte.atom.c"
# include "duffle/gp.atom.c"
# include "duffle/psyq.atom.c"
# include "gen/offsets.h"
# include "gen/macs.h"
# include "hello_joypad.h" # include "hello_joypad.h"
#endif #endif
ATOM_FILE_DEBUGGER_LINE_MARKER(hello_joypad_atom_c);
#pragma region MACs (Mips Atom components) #pragma region MACs (Mips Atom components)
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(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_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)),
store_half(rt_height, base, offset + O_(Rect_S2,height)),
})
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_(DrawEnv,initial_bg_color.r)),
store_byte(rg, base, offset + O_(DrawEnv,initial_bg_color.g)),
store_byte(rb, base, offset + O_(DrawEnv,initial_bg_color.b)),
})
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_put_disp_env(U4 reg_transfer, U4 reg_base, U2 port) FI_ Slice_MipsCode ac_put_disp_env(U4 reg_transfer, U4 reg_base, U2 port)
MipsAtomComp_Proc_(ac_put_disp_env, { MipsAtomComp_Proc_(ac_put_disp_env, {
// Emits 5 GP0 commands for buffer 0 (display_area = (0,0,320,240)). // Emits 5 GP0 commands for buffer 0 (display_area = (0,0,320,240)).
@@ -114,68 +92,6 @@ MipsAtomComp_Proc_(ac_put_draw_env, {
#pragma region Baked Atoms #pragma region Baked Atoms
/* DIAGNOSTIC 1: Pure tape loop test */
internal MipsAtom_(diag_yield) { mac_yield() };
/* DIAGNOSTIC 2: Pure memory test (No GTE). Draws a fixed cyan triangle. */
internal MipsAtom_(diag_color) {
store_word( R_0, R_T7, 0),
load_upper_i(R_AT, gp0_cmd_poly_f3 << 8 | 0xFF), /* High: MipsCode Poly_F3(0x20) + Color B:FF */
or_i_self( R_AT, 0xFF00), /* Low: Color G:FF, R:00 (Cyan) */
store_word( R_AT, R_T7, 4),
/* Fake coordinates - Swapped winding order to prevent GPU culling! */
load_upper_i(R_AT, 0x0010), or_i_self(R_AT, 0x0010), store_word(R_AT, R_T7, 8), /* (16, 16) */
load_upper_i(R_AT, 0x0050), or_i_self(R_AT, 0x0010), store_word(R_AT, R_T7, 12), /* (80, 16) */
load_upper_i(R_AT, 0x0010), or_i_self(R_AT, 0x0050), store_word(R_AT, R_T7, 16), /* (16, 80) */
add_ui( R_T1, R_0, 10),
shift_lleft_self(R_T1, S_(U4)/2),
add_u_self( R_T1, R_T6),
load_word( R_AT, R_T1, 0),
load_upper_i(R_V0, (S_(Poly_F3)/S_(U4) - S_(PolyTag)/S_(U4)) << PolyTag_len_bits),
store_word( R_AT, R_T7, 0),
shift_lleft(R_AT, R_T7, S_(PolyTag_len_bits)), shift_lright(R_AT, R_AT, S_(PolyTag_len_bits)),
or_u_self( R_AT, R_V0),
store_word( R_AT, R_T1, 0),
add_ui(R_T7, R_T7, 20),
mac_yield()
};
/* DIAGNOSTIC 3: Pure GTE test (No Memory Writes) */
internal MipsAtom_(diag_gte) {
/* Load 3 indices */
load_half_u(R_T0, R_T4, 0),
load_half_u(R_T1, R_T4, 2),
load_half_u(R_T2, R_T4, 4),
/* Load Vertices into GTE */
shift_lleft( R_AT, R_T0, 3), add_u( R_AT, R_AT, R_T5),
load_word(R_V0, R_AT, 0), load_word(R_V1, R_AT, 4),
gte_mv_to_data_r(R_V0, C2_VXY0), gte_mv_to_data_r(R_V1, C2_VZ0),
shift_lleft( R_AT, R_T1, 3), add_u(R_AT, R_AT, R_T5),
load_word(R_V0, R_AT, 0), load_word(R_V1, R_AT, 4),
gte_mv_to_data_r(R_V0, C2_VXY1), gte_mv_to_data_r(R_V1, C2_VZ1),
shift_lleft(R_AT, R_T2, 3), add_u(R_AT, R_AT, R_T5),
load_word(R_V0, R_AT, 0), load_word(R_V1, R_AT, 4),
gte_mv_to_data_r(R_V0, C2_VXY2), gte_mv_to_data_r(R_V1, C2_VZ2),
/* Run Math */
nop2, gte_cmdw_rtpt,
nop2, gte_cmdw_nclip,
nop2,
/* Advance Face Cursor and Yield */
add_ui(R_T4, R_T4, 8),
mac_yield()
};
enum { enum {
R_ScreenX = R_T5 atom_reg atom_type(U2), R_ScreenX = R_T5 atom_reg atom_type(U2),
R_ScreenY = R_T6 atom_reg atom_type(U2), R_ScreenY = R_T6 atom_reg atom_type(U2),
@@ -264,6 +180,17 @@ internal MipsAtom_(gp_screen_init) atom_info(atom_phase(screen_init), atom_reads
mac_yield(), mac_yield(),
}; };
enum {
R_PrimCursor = R_T7 atom_reg atom_type(U4*), /* VRAM output cursor (primitive buffer) */
R_FaceCursor = R_T4 atom_reg atom_type(V4_S2*), /* Cube face-index cursor (V4_S2*); floor context switches to V3_S2* via atom_phase */
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 */
#define R_PrimCursor_Code R_T7_Code
#define R_FaceCursor_Code R_T4_Code
#define R_VertBase_Code R_T5_Code
#define R_OtBase_Code R_T6_Code
};
typedef Struct_(Binds_CubeTri) { typedef Struct_(Binds_CubeTri) {
U4 PrimCursor; U4 PrimCursor;
V4_S2* FaceCursor; V4_S2* FaceCursor;
@@ -294,20 +221,23 @@ MipsAtom_(cube_g4_face) atom_info(atom_phase(cube_g4),
load_half_u(R_T2, R_FaceCursor, 2 * S_(S2)), load_half_u(R_T2, R_FaceCursor, 2 * S_(S2)),
load_half_u(R_T3, R_FaceCursor, 3 * S_(S2)), load_half_u(R_T3, R_FaceCursor, 3 * S_(S2)),
mac_gte_load_tri_verts(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, // required cpu -> gte delay slot nop2, gte_cmdw_rotate_translate_perspective_triple, // required cpu -> gte delay slot
gte_cmdw_nclip, gte_cmdw_nclip,
gte_mv_from_data_r(R_T0, C2_MAC0), nop, gte_mv_from_data_r(R_T0, C2_MAC0), nop,
branch_le_zero(R_T0, atom_offset(cull, cube_g4_face_exit)), 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, only on the body path. */
store_word(R_0, R_PrimCursor, O_(Poly_G4, tag)), 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), 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)), 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), gte_mv_to_data_r(R_V0, C2_VXY0), gte_mv_to_data_r(R_V1, C2_VZ0),
mac_gte_store_g4_p012(), mac_gte_store_g4_p012(R_PrimCursor),
gte_cmdw_rotate_translate_perspective_single, gte_cmdw_rotate_translate_perspective_single,
mac_gte_store_g4_p3(), mac_gte_store_g4_p3(R_PrimCursor),
gte_cmdw_avg_sort_z4, gte_cmdw_avg_sort_z4,
gte_mv_from_data_r(R_T1, C2_OTZ), gte_mv_from_data_r(R_T1, C2_OTZ),
@@ -315,8 +245,8 @@ MipsAtom_(cube_g4_face) atom_info(atom_phase(cube_g4),
set_lt_u( R_AT, R_T1, R_AT), set_lt_u( R_AT, R_T1, R_AT),
branch_equal(R_AT, R_0, atom_offset(bounds_chk, cube_g4_face_exit)), nop, branch_equal(R_AT, R_0, atom_offset(bounds_chk, cube_g4_face_exit)), nop,
mac_insert_ot_tag_g4(), mac_insert_ot_tag_g4(R_OtBase, R_PrimCursor),
mac_format_g4_color( mac_format_g4_color(R_PrimCursor,
/* c0 magenta */ 0xFF, 0x00, 0xFF, /* c0 magenta */ 0xFF, 0x00, 0xFF,
/* c1 yellow */ 0xFF, 0xFF, 0x00, /* c1 yellow */ 0xFF, 0xFF, 0x00,
/* c2 cyan */ 0x00, 0xFF, 0xFF, /* c2 cyan */ 0x00, 0xFF, 0xFF,
@@ -356,8 +286,8 @@ MipsAtom_(floor_f3_face) atom_info(atom_phase(floor_f3)
, atom_reads( R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase) , atom_reads( R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase)
, atom_writes(R_PrimCursor, R_FaceCursor) , atom_writes(R_PrimCursor, R_FaceCursor)
) { ) {
mac_load_tri_indices( R_T0, R_T1, R_T2), mac_load_tri_indices( R_FaceCursor, R_T0, R_T1, R_T2),
mac_gte_load_tri_verts(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 nop2, gte_cmdw_rotate_translate_perspective_triple, // 2 nops retire the final cpu -> gte writes before RTPT
gte_cmdw_nclip, gte_cmdw_nclip,
@@ -365,7 +295,7 @@ MipsAtom_(floor_f3_face) atom_info(atom_phase(floor_f3)
gte_mv_from_data_r(R_T0, C2_MAC0), 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 */ /* Format Primitive */
mac_gte_store_f3(), mac_gte_store_f3(R_PrimCursor),
/* Calculate Depth */ /* Calculate Depth */
gte_avg_sort_z3, gte_avg_sort_z3,
@@ -374,8 +304,8 @@ MipsAtom_(floor_f3_face) atom_info(atom_phase(floor_f3)
add_ui( R_AT, R_0, OrderingTbl_Len), add_ui( R_AT, R_0, OrderingTbl_Len),
set_lt_u( R_AT, R_T1, R_AT), set_lt_u( R_AT, R_T1, R_AT),
branch_equal(R_AT, R_0, atom_offset(bounds_chk, floor_f3_face_exit)), nop, branch_equal(R_AT, R_0, atom_offset(bounds_chk, floor_f3_face_exit)), nop,
mac_format_f3_color(0xFF, 0xFF, 0xFF), // RGB-form (R=FF, G=FF, B=FF = white) mac_format_f3_color(R_PrimCursor, 0xFF, 0xFF, 0xFF), // RGB-form (R=FF, G=FF, B=FF = white)
mac_insert_ot_tag_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) */ add_ui_self(R_PrimCursor, S_(Poly_F3)), /* Advance Prim Cursor (5 words) */
// Note(Ed): No bounds checking, should be checked before atom runs. // Note(Ed): No bounds checking, should be checked before atom runs.
// end: branch(bounds_chk) // end: branch(bounds_chk)
@@ -459,9 +389,11 @@ atom_label(disconnected) /* === Disconnected body. */
load_upper_i(R_T4, 0x8080), or_i_self(R_T4, 0x8080), load_upper_i(R_T4, 0x8080), or_i_self(R_T4, 0x8080),
store_word( R_T4, R_PadState, O_(PadState,left_x)), store_word( R_T4, R_PadState, O_(PadState,left_x)),
store_byte( R_RawId, R_PadState, O_(PadState,id)), store_byte( R_RawId, R_PadState, O_(PadState,id)),
branch_equal(R_0, R_0, atom_offset(disconnected, snap_end)), nop, jump_rel(atom_offset(disconnected, snap_end)),
// TODO(Ed): Lua metaprogram: Support jump instruction here.. /* BD-slot: load next atom's entry point (replaces the nop).
// jump(atom_offset(disconnected, snap_end)), nop, * The unconditional branch always jumps to snap_end, where mac_yield_tail()
* transfers control to R_AtomJmp without re-loading it. */
mac_yield_load(),
atom_label(skip_disconnected) atom_label(skip_disconnected)
/* === Case 2: Pending (status == 0 && id == 0) /* === Case 2: Pending (status == 0 && id == 0)
@@ -479,9 +411,8 @@ atom_label(pending) /* === Pending body */
load_upper_i(R_T4, 0x8080), or_i_self(R_T4, 0x8080), load_upper_i(R_T4, 0x8080), or_i_self(R_T4, 0x8080),
store_word( R_T4, R_PadState, O_(PadState,left_x)), store_word( R_T4, R_PadState, O_(PadState,left_x)),
store_byte( R_RawId, R_PadState, O_(PadState,id)), store_byte( R_RawId, R_PadState, O_(PadState,id)),
branch_equal(R_0, R_0, atom_offset(pending, snap_end)), nop, jump_rel(atom_offset(pending, snap_end)),
// TODO(Ed): Lua metaprogram: Support jump instruction here.. mac_yield_load(),
// jump(atom_offset(pending, snap_end)), nop,
atom_label(id_dispatch) /* === Case 3-6: ID dispatch */ atom_label(id_dispatch) /* === Case 3-6: ID dispatch */
add_ui(R_T4, R_0, 0x41), branch_ne(R_RawId, R_T4, atom_offset(id_dispatch, try_analog_stick)), add_ui(R_T4, R_0, 0x41), branch_ne(R_RawId, R_T4, atom_offset(id_dispatch, try_analog_stick)),
@@ -503,9 +434,8 @@ atom_label(id_dispatch) /* === Case 3-6: ID dispatch */
add_ui( R_T4, R_0, 0x41), add_ui( R_T4, R_0, 0x41),
store_byte( R_T4, R_PadState, O_(PadState,id)), store_byte( R_T4, R_PadState, O_(PadState,id)),
branch_equal(R_0, R_0, atom_offset(id_dispatch, snap_end)), nop, jump_rel(atom_offset(id_dispatch, snap_end)),
// TODO(Ed): Lua metaprogram: Support jump instruction here.. mac_yield_load(),
// jump(atom_offset(id_dispatch, snap_end)), nop,
atom_label(try_analog_stick) /* === Case 4: AnalogStick (id == 0x53)*/ atom_label(try_analog_stick) /* === Case 4: AnalogStick (id == 0x53)*/
add_ui(R_T4, R_0, 0x53), branch_ne(R_RawId, R_T4, atom_offset(try_analog_stick, try_analog_pad)), add_ui(R_T4, R_0, 0x53), branch_ne(R_RawId, R_T4, atom_offset(try_analog_stick, try_analog_pad)),
@@ -526,9 +456,8 @@ atom_label(analog_stick) /* === AnalogStick body
store_half( R_T4, R_PadState, O_(PadState,right_x)), 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) */ 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)), store_byte( R_T5, R_PadState, O_(PadState,id)),
branch_equal(R_0, R_0, atom_offset(analog_stick, snap_end)), nop, jump_rel(atom_offset(analog_stick, snap_end)),
// TODO(Ed): Lua metaprogram: Support jump instruction here.. mac_yield_load(),
// jump(atom_offset(analog_stick, snap_end)), nop,
atom_label(try_analog_pad) /* === Case 5-6: AnalogPad (id & 0xF0 == 0x70) */ atom_label(try_analog_pad) /* === Case 5-6: AnalogPad (id & 0xF0 == 0x70) */
and_i( R_T4, R_RawId, 0xF0), and_i( R_T4, R_RawId, 0xF0),
@@ -550,9 +479,8 @@ atom_label(analog_pad) /* === AnalogPad body
store_half( R_T4, R_PadState, O_(PadState,right_x)), store_half( R_T4, R_PadState, O_(PadState,right_x)),
store_byte( R_RawId, R_PadState, O_(PadState,id)), store_byte( R_RawId, R_PadState, O_(PadState,id)),
branch_equal(R_0, R_0, atom_offset(analog_pad, snap_end)), nop, jump_rel(atom_offset(analog_pad, snap_end)),
// TODO(Ed): Lua metaprogram: Support jump instruction here.. mac_yield_load(),
// jump(atom_offset(analog_pad, snap_end)), nop,
atom_label(try_unsupported) /* === Case 7: Unsupported — fall through from the AnalogPad range-check miss. */ atom_label(try_unsupported) /* === Case 7: Unsupported — fall through from the AnalogPad range-check miss. */
add_ui( R_T4, R_0, PadStatus_Unsupported), add_ui( R_T4, R_0, PadStatus_Unsupported),
@@ -565,8 +493,12 @@ atom_label(try_unsupported) /* === Case 7: Unsupported — fall through from the
store_byte( R_T4, R_PadState, O_(PadState,id)), store_byte( R_T4, R_PadState, O_(PadState,id)),
/* Fall through to snap_end. */ /* Fall through to snap_end. */
atom_label(no_jump_fallthrough)
mac_yield_load(),
atom_label(snap_end) atom_label(snap_end)
mac_yield(), /* NOT mac_yield() — R_AtomJmp was already loaded in the BD-slot of the case-exit branch. */
mac_yield_tail(),
}; };
/* ----- pad_apply_input ----- /* ----- pad_apply_input -----
@@ -650,10 +582,8 @@ atom_label(dead_check_upper)
/* R_T4 = (0x90 < left_x) ? 1 : 0 → (left_x > 0x90) ? 1 : 0 */ /* 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)), set_lt_u(R_T4, R_T4, R_T3), branch_ne(R_T4, R_0, atom_offset(dead_zone_high_check, dead_high_active)),
add_ui( R_T4, R_0, 0x80), /* BD-slot: pre-load 0x80 for dead_high_active */ add_ui( R_T4, R_0, 0x80), /* BD-slot: pre-load 0x80 for dead_high_active */
branch_equal(R_0, R_0, atom_offset(dead_zone_skip, exit_stick)), nop, jump_rel(atom_offset(dead_zone_skip, exit_stick)),
/* Fall-through = left_x in [0x70, 0x90] (dead zone); skip analog entirely. */ mac_yield_load(),
// TODO(Ed): Lua metaprogram: Support jump instruction here..
// jump(atom_offset(dead_zone_skip, exit_stick)), nop,
atom_label(dead_low_active) atom_label(dead_low_active)
/* R_T3 = left_x (from line 632 lbu; not clobbered between dead_zone_low_check branch + its BD-slot `add_ui R_T4, 0x80`). /* R_T3 = left_x (from line 632 lbu; not clobbered between dead_zone_low_check branch + its BD-slot `add_ui R_T4, 0x80`).
@@ -675,9 +605,8 @@ atom_label(dead_low_active)
add_u( R_T0, R_T0, R_T4), add_u( R_T0, R_T0, R_T4),
store_half( R_T0, R_FloorRot, O_(V3_S2,y)), store_half( R_T0, R_FloorRot, O_(V3_S2,y)),
branch_equal(R_0, R_0, atom_offset(end_low, exit_stick)), nop, jump_rel(atom_offset(end_low, exit_stick)),
// TODO(Ed): Lua metaprogram: Support jump instruction here.. mac_yield_load(),
// jump(atom_offset(end_low, exit_stick)), nop,
atom_label(dead_high_active) atom_label(dead_high_active)
/* R_T3 = left_x (from line 641 lbu in dead_check_upper; not clobbered between dead_zone_high_check branch + its BD-slot `add_ui R_T4, 0x80`). /* R_T3 = left_x (from line 641 lbu in dead_check_upper; not clobbered between dead_zone_high_check branch + its BD-slot `add_ui R_T4, 0x80`).
@@ -698,8 +627,12 @@ atom_label(dead_high_active)
add_u( R_T0, R_T0, R_T4), add_u( R_T0, R_T0, R_T4),
store_half( R_T0, R_FloorRot, O_(V3_S2,y)), store_half( R_T0, R_FloorRot, O_(V3_S2,y)),
atom_label(no_jump_fallthrough)
mac_yield_load(),
atom_label(exit_stick) atom_label(exit_stick)
mac_yield(), /* NOT mac_yield() — R_AtomJmp was already loaded in the BD-slot of the dead-zone/exit branch. */
mac_yield_tail(),
}; };
#pragma endregion Baked Atoms #pragma endregion Baked Atoms
+45 -149
View File
@@ -1,9 +1,17 @@
#pragma region Vendors
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <assert.h> #include <assert.h>
// #include "libgpu.h" // #include "libgpu.h"
// #include "libetc.h" // #include "libetc.h"
// #include "libgte.h" // #include "libgte.h"
#pragma endregion Vendors
#pragma region Duffle Headers
# include "duffle/gen/macs.h"
# include "duffle/gen/offsets.h"
#include "duffle/word_count.metadata.h"
#include "duffle/dsl.h" #include "duffle/dsl.h"
#include "duffle/memory.h" #include "duffle/memory.h"
@@ -15,109 +23,43 @@
#include "duffle/gte.h" #include "duffle/gte.h"
#include "duffle/pad.h" #include "duffle/pad.h"
# include "duffle/gen/duffle.macs.h" #include "duffle/dsl.atom.h"
# include "duffle/gen/duffle.offsets.h"
#include "duffle/atom_dsl.h"
#include "duffle/lottes_tape.h" #include "duffle/lottes_tape.h"
#include "duffle/word_count.metadata.h"
#include "psyq.h" #include "duffle/psyq.h"
#pragma endregion Duffle Headers
#pragma region Duffle TUs
#include "duffle/math.atom.c"
#include "duffle/mips.atom.c"
#include "duffle/gte.atom.c"
#include "duffle/gp.atom.c"
#include "duffle/psyq.atom.c"
#pragma endregion Duffle TUs
#pragma region Joypade Headers
# include "gen/macs.h"
# include "gen/offsets.h"
# include "gen/hello_joypad.macs.h"
# include "gen/hello_joypad.offsets.h"
#include "hello_joypad.h" #include "hello_joypad.h"
#pragma region Joypad Headers
#include "psyq.c" #pragma region Hello Joypad TUs
#include "hello_joypad.tape.c" #include "hello_joypad.atom.c"
#pragma endregion Hello Joypad TUs
typedef U4 OrderingTable_Buffer[OrderingTbl_Len];
typedef Array_(OrderingTable_Buffer, 2);
typedef B1 PrimitiveBuffer[PrimitiveBuff_Len];
typedef Array_(PrimitiveBuffer, 2);
typedef Struct_(PrimitiveArena) {
A2_PrimitiveBuffer buf;
U4 used;
};
#define Cube_num_verts 8
typedef Array_(V3_S2, Cube_num_verts);
#define Cube_num_faces 6
typedef Array_(V4_S2, Cube_num_faces);
I_ void ent_cube128_init(A8_V3_S2* verts, A6_V4_S2* faces) {
LP_ A8_V3_S2 baked_verts = (A8_V3_S2) {
{ -128, -128, -128 },
{ 128, -128, -128 },
{ 128, -128, 128 },
{ -128, -128, 128 },
{ -128, 128, -128 },
{ 128, 128, -128 },
{ 128, 128, 128 },
{ -128, 128, 128 }
};
LP_ A6_V4_S2 baked_faces = (A6_V4_S2) {
{ 3, 2, 0, 1 },
{ 0, 1, 4, 5 },
{ 4, 5, 7, 6 },
{ 1, 2, 5, 6 },
{ 2, 3, 6, 7 },
{ 3, 0, 7, 4 },
};
mem_copy(u4_(verts), u4_(& baked_verts), S_(A8_V3_S2) );
mem_copy(u4_(faces), u4_(& baked_faces), S_(A6_V4_S2) );
return;
}
typedef Struct_(Ent_Cube) {
V3_S4 accel;
V3_S4 vel;
V3_S4 pos;
V3_S4 scale;
V3_S2 rot;
A8_V3_S2 verts;
A6_V4_S2 faces;
};
#define Floor_num_verts 4
typedef Array_(V3_S2, Floor_num_verts);
#define Floor_num_faces 2
typedef Array_(V3_S2, Floor_num_faces);
I_ void ent_floor_init(A4_V3_S2* verts, A2_V3_S2* faces) {
LP_ A4_V3_S2 baked_verts = (A4_V3_S2) {
{ -900, 0, -900 },
{ -900, 0, 900 },
{ 900, 0, -900 },
{ 900, 0, 900 },
};
LP_ A2_V3_S2 baked_faces = (A2_V3_S2) {
{ 0, 1, 2 },
{ 1, 3, 2 },
};
mem_copy(u4_(verts), u4_(& baked_verts), S_(A4_V3_S2));
mem_copy(u4_(faces), u4_(& baked_faces), S_(A2_V3_S2));
};
typedef Struct_(Ent_Floor) {
V3_S4 accel;
V3_S4 pos;
V3_S4 scale;
V3_S2 rot;
A4_V3_S2 verts;
A2_V3_S2 faces;
};
enum { enum {
Scratchpad_Len = 1024, Scratchpad_Len = 1024,
MemTape_Len = 512, MemTape_Len = 512,
}; };
typedef Struct_(SMemory) { typedef Struct_(SMemory) {
U4 MemTape[MemTape_Len];
DoubleBuffer screen_buf;
A2_OrderingTable_Buffer ordering_tbl;
PrimitiveArena primitives; PrimitiveArena primitives;
A2_OrderingTable_Buffer ordering_tbl;
DoubleBuffer screen_buf;
S4 active_buf_id; S4 active_buf_id;
U4 MemTape[MemTape_Len];
M3_S2 tform_world; M3_S2 tform_world;
Ent_Cube cube; Ent_Cube cube;
@@ -212,50 +154,6 @@ NI_ void pad_bios_init_start(PadBiosRaw* raw0, PadBiosRaw* raw1)
); );
} }
void gp_screen_init_c11(DoubleBuffer* screen_buf, S4* active_buf_id)
{
reset_graph(0);
// Set the current initial buffer
active_buf_id[0] = 0;
// Just setting env data, not interacting with console hw.
// First buffer area
displayenv_init(& r_(screen_buf->display)[0], 0, 0, ScreenRes_X, ScreenRes_Y);
drawenv_init (& r_(screen_buf->draw )[0], 0, ScreenRes_Y, ScreenRes_X, ScreenRes_Y);
// Second buffer area
displayenv_init(& r_(screen_buf->display)[1], 0, ScreenRes_Y, ScreenRes_X, ScreenRes_Y);
drawenv_init (& r_(screen_buf->draw )[1], 0, 0, ScreenRes_X, ScreenRes_Y);
// Set the back/drawing buffer
screen_buf->draw[0].enable_auto_clear = true;
screen_buf->draw[1].enable_auto_clear = true;
// Set the background clear color
screen_buf->draw[0].initial_bg_color = rgb8( .r = 7, .g = 7, .b = 7 );
screen_buf->draw[1].initial_bg_color = rgb8( .r = 7, .g = 7, .b = 7 );
// screen_buf->draw[1].initial_bg_color = rgb8( .r = 47, .g = 13, .b = 0 );
displayenv_put(& r_(screen_buf->display)[ active_buf_id[0] ]);
drawenv_put (& r_(screen_buf->draw )[ active_buf_id[0] ]);
// Initialize and setup the GTE geometry offsets
geom_init();
geom_set_offset(ScreenRes_CenterX, ScreenRes_CenterY);
geom_set_screen(ScreenZ);
set_display_enabled(1); // gp_DisplayEnabled
}
void gp_display_frame(DoubleBuffer* screen_buf, S4* active_buf_id, U4* ordering_buf, PrimitiveArena* pa) {
draw_sync(0);
vsync(0);
displayenv_put(& r_(screen_buf->display)[active_buf_id[0] ]);
drawenv_put (& r_(screen_buf->draw) [active_buf_id[0] ]);
{
draw_orderingtbl(ordering_buf + OrderingTbl_Len - 1);
pa->used = 0;
}
active_buf_id[0] = ! active_buf_id[0]; // Swap current buffer
}
GCC_OPTIMIZATION_DISABLE GCC_OPTIMIZATION_DISABLE
void update(PrimitiveArena* pa, U4* ordering_buf) void update(PrimitiveArena* pa, U4* ordering_buf)
{ {
@@ -478,28 +376,25 @@ void update(PrimitiveArena* pa, U4* ordering_buf)
// C-side state (pa->used) has already been updated by the tape! // C-side state (pa->used) has already been updated by the tape!
// smem.floor.rot.y += 5; // smem.floor.rot.y += 5;
} }
// --- TAPE DIAGNOSTICS ---
if (0)
{
LP_ U4 mem_temp_tape[512]; FArena tape_arena; farena_init(& tape_arena, slice_ut_arr(mem_temp_tape));
TapeBuilder tb = tb_make_old(& tape_arena); tb_scope(& tb) {
// Skip set_gte_world atom for diagnostics to isolate the triangle loop
for (U4 i = 0; i < Floor_num_faces; i++) {
// tb_emit(& tb, code_diag_yield);
// tb_emit(& tb, code_diag_color);
// tb_emit(& tb, code_diag_gte);
}
}
B1* prim_cursor = (B1*)r_(pa->buf)[smem.active_buf_id] + pa->used;
tape_run(tb_slice(tb));
pa->used = (U4)prim_cursor - (U4)r_(pa->buf)[smem.active_buf_id];
}
} }
GCC_OPTIMIZATION_ENABLE GCC_OPTIMIZATION_ENABLE
void render(void) { void render(void) {
} }
void gp_display_frame(DoubleBuffer* screen_buf, S4* active_buf_id, U4* ordering_buf, PrimitiveArena* pa) {
draw_sync(0);
vsync(0);
displayenv_put(& r_(screen_buf->display)[active_buf_id[0] ]);
drawenv_put (& r_(screen_buf->draw) [active_buf_id[0] ]);
{
draw_orderingtbl(ordering_buf + OrderingTbl_Len - 1);
pa->used = 0;
}
active_buf_id[0] = ! active_buf_id[0]; // Swap current buffer
}
GCC_OPTIMIZATION_DISABLE
int main(void) int main(void)
{ {
smem = (SMemory){0}; smem = (SMemory){0};
@@ -543,3 +438,4 @@ int main(void)
}; };
return 0; return 0;
} }
GCC_OPTIMIZATION_ENABLE
+78 -2
View File
@@ -7,8 +7,10 @@
#endif #endif
enum { enum {
PrimitiveBuff_Len = 4096, // PrimitiveBuff_Len = 4096,
OrderingTbl_Len = 2048 // OrderingTbl_Len = 2048,
PrimitiveBuff_Len = 131072,
OrderingTbl_Len = 8192,
}; };
enum { enum {
@@ -24,3 +26,77 @@ enum {
}; };
#define v3s4_fp_one() v3s4(fp_one, fp_one, fp_one) #define v3s4_fp_one() v3s4(fp_one, fp_one, fp_one)
typedef U4 OrderingTable_Buffer[OrderingTbl_Len];
typedef Array_(OrderingTable_Buffer, 2);
typedef B1 PrimitiveBuffer[PrimitiveBuff_Len];
typedef Array_(PrimitiveBuffer, 2);
typedef Struct_(PrimitiveArena) {
A2_PrimitiveBuffer buf;
U4 used;
};
#define Cube_num_verts 8
typedef Array_(V3_S2, Cube_num_verts);
#define Cube_num_faces 6
typedef Array_(V4_S2, Cube_num_faces);
I_ void ent_cube128_init(A8_V3_S2* verts, A6_V4_S2* faces) {
LP_ A8_V3_S2 baked_verts = (A8_V3_S2) {
{ -128, -128, -128 },
{ 128, -128, -128 },
{ 128, -128, 128 },
{ -128, -128, 128 },
{ -128, 128, -128 },
{ 128, 128, -128 },
{ 128, 128, 128 },
{ -128, 128, 128 }
};
LP_ A6_V4_S2 baked_faces = (A6_V4_S2) {
{ 3, 2, 0, 1 },
{ 0, 1, 4, 5 },
{ 4, 5, 7, 6 },
{ 1, 2, 5, 6 },
{ 2, 3, 6, 7 },
{ 3, 0, 7, 4 },
};
mem_copy(u4_(verts), u4_(& baked_verts), S_(A8_V3_S2) );
mem_copy(u4_(faces), u4_(& baked_faces), S_(A6_V4_S2) );
return;
}
typedef Struct_(Ent_Cube) {
V3_S4 accel;
V3_S4 vel;
V3_S4 pos;
V3_S4 scale;
V3_S2 rot;
A8_V3_S2 verts;
A6_V4_S2 faces;
};
#define Floor_num_verts 4
typedef Array_(V3_S2, Floor_num_verts);
#define Floor_num_faces 2
typedef Array_(V3_S2, Floor_num_faces);
I_ void ent_floor_init(A4_V3_S2* verts, A2_V3_S2* faces) {
LP_ A4_V3_S2 baked_verts = (A4_V3_S2) {
{ -900, 0, -900 },
{ -900, 0, 900 },
{ 900, 0, -900 },
{ 900, 0, 900 },
};
LP_ A2_V3_S2 baked_faces = (A2_V3_S2) {
{ 0, 1, 2 },
{ 1, 3, 2 },
};
mem_copy(u4_(verts), u4_(& baked_verts), S_(A4_V3_S2));
mem_copy(u4_(faces), u4_(& baked_faces), S_(A2_V3_S2));
};
typedef Struct_(Ent_Floor) {
V3_S4 accel;
V3_S4 pos;
V3_S4 scale;
V3_S2 rot;
A4_V3_S2 verts;
A2_V3_S2 faces;
};
-3
View File
@@ -1,3 +0,0 @@
#ifdef INTELLISENSE_DIRECTIVES
# include "psyq.h"
#endif
+371 -147
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_root = split-path -Path $PSScriptRoot -Parent
$path_build = join-path $path_root 'build' $path_build = join-path $path_root 'build'
$path_code = join-path $path_root 'code' $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 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 --- # --- Toolchain Definition ---
# Assumes 'mipsel-none-elf' toolchain is in your system's PATH. # Assumes 'mipsel-none-elf' toolchain is in your system's PATH.
$Prefix = "mipsel-none-elf" $Prefix = "mipsel-none-elf"
@@ -220,11 +322,11 @@ function make-binary { param([string]$elf, [string]$exe)
} }
function ps1-meta { param( function ps1-meta { param(
[string]$unity_root, [string] $unity_root,
[string[]]$sources, [string[]]$sources,
[Parameter(Mandatory=$true)][string]$metadata, [Parameter(Mandatory=$true)][string]$metadata,
[string]$out_root = (join-path $path_build 'gen'), [string] $out_root = (join-path $path_build 'gen'),
[string[]]$passes = @('--pre-link'), [string[]]$passes = @('--pre-link'),
[string[]]$extra_args = @() [string[]]$extra_args = @()
) )
# `--unity-root` and `--source` are # `--unity-root` and `--source` are
@@ -242,6 +344,40 @@ function ps1-meta { param(
exit 2 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' $script = join-path $path_scripts 'ps1_meta.lua'
$input_summary = if ($null -ne $unity_root -and $unity_root -ne '') { $input_summary = if ($null -ne $unity_root -and $unity_root -ne '') {
"unity=$unity_root" "unity=$unity_root"
@@ -265,6 +401,73 @@ function ps1-meta { param(
} }
} }
function inject-dwarf { param(
[string]$elf,
[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_inject_elf = join-path $path_build "$base_name.dwarf-injected.elf"
if (-not (Test-Path $path_dwarf_line_bin)) { return }
if (-not (Test-Path $path_dwarf_aranges_bin)) { return }
if (-not (Test-Path $path_dwarf_rnglists_bin)) { return }
Write-Host "[build] DWARF-injecting $elf -> $path_inject_elf"
Copy-Item -LiteralPath $elf -Destination $path_inject_elf -Force
# Objcopy call 1: 3x --update-section for the PC-mapping tables (line, aranges, rnglists).
$objcopy_args_dwarf_pc = @(
"--update-section=.debug_line=$path_dwarf_line_bin",
"--update-section=.debug_aranges=$path_dwarf_aranges_bin",
"--update-section=.debug_rnglists=$path_dwarf_rnglists_bin"
)
& $Objcopy @objcopy_args_dwarf_pc $path_inject_elf 2>&1 | Out-Null
if ($LASTEXITCODE -ne 0) {
Write-Warning "[build] objcopy dwarf-pc splice failed (exit $LASTEXITCODE); removing $path_inject_elf"
Remove-Item -LiteralPath $path_inject_elf -ErrorAction SilentlyContinue
return
}
# Objcopy call 2: 3x --update-section + 2x --add-section for the debug-data tables (info, abbrev, str, loc, loclists).
$objcopy_args_dwarf_info = @(
"--update-section=.debug_info=$path_dwarf_info_bin",
"--update-section=.debug_abbrev=$path_dwarf_abbrev_bin",
"--update-section=.debug_str=$path_dwarf_str_bin",
"--add-section=.debug_loc=$path_dwarf_loc_bin",
"--add-section=.debug_loclists=$path_dwarf_loclists_bin"
)
& $Objcopy @objcopy_args_dwarf_info $path_inject_elf 2>&1 | Out-Null
if ($LASTEXITCODE -ne 0) {
Write-Warning "[build] objcopy dwarf-info splice failed (exit $LASTEXITCODE); removing $path_inject_elf"
Remove-Item -LiteralPath $path_inject_elf -ErrorAction SilentlyContinue
return
}
# Baked atoms execute from RAM but are emitted as C data arrays, so their ELF sections lack SHF_EXECINSTR.
# GDB discards line rows for non-code sections. Mark only the debug-copy sections executable.
# The original ELF and PS-EXE remain byte/flag unchanged.
& $Objcopy `
--set-section-flags ".rodata=alloc,load,readonly,code,contents" `
--set-section-flags ".data=alloc,load,data,code,contents" `
$path_inject_elf 2>&1 | Out-Null
if ($LASTEXITCODE -ne 0) {
Write-Warning "[build] atom-section flag update failed (exit $LASTEXITCODE); removing $path_inject_elf"
Remove-Item -LiteralPath $path_inject_elf -ErrorAction SilentlyContinue
}
else {
Write-Host "[build] DWARF-injected ELF: $path_inject_elf"
}
}
# inject-dwarf
function build-hello_psyqo { function build-hello_psyqo {
$includes += @() $includes += @()
@@ -391,61 +594,7 @@ function build-hello_gte {
# Post-link: gdb-runtime + dwarf-injection in a single Lua invocation (one luajit cold start). # 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) ps1-meta -unity_root $src_c -metadata $path_atom_metadata -out_root $path_build_gen -passes @('--post-link') ` -extra_args @('--elf', $elf)
$dwarfLineBin = join-path $path_build_gen 'hello_gte.dwarf_line.bin' inject-dwarf $elf $path_build_gen
$dwarfArangesBin = join-path $path_build_gen 'hello_gte.dwarf_aranges.bin'
$dwarfRnglistsBin = join-path $path_build_gen 'hello_gte.dwarf_rnglists.bin'
$injectElf = join-path $path_build 'hello_gte.dwarf-injected.elf'
if ((Test-Path $dwarfLineBin) -and (Test-Path $dwarfArangesBin) -and (Test-Path $dwarfRnglistsBin))
{
Write-Host "[build] DWARF-injecting $elf -> $injectElf"
Copy-Item -LiteralPath $elf -Destination $injectElf -Force
# Objcopy call: 3x --update-section for (line, aranges, rnglists).
$f_args = @(
"--update-section=.debug_line=$dwarfLineBin",
"--update-section=.debug_aranges=$dwarfArangesBin",
"--update-section=.debug_rnglists=$dwarfRnglistsBin"
)
& $Objcopy @f_args $injectElf 2>&1 | Out-Null
if ($LASTEXITCODE -ne 0) {
Write-Warning "[build] objcopy F' splice failed (exit $LASTEXITCODE); removing $injectElf"
Remove-Item -LiteralPath $injectElf -ErrorAction SilentlyContinue
return;
}
$dwarfInfoBin = join-path $path_build_gen 'hello_gte.dwarf_info.bin'
$dwarfAbbrevBin = join-path $path_build_gen 'hello_gte.dwarf_abbrev.bin'
$dwarfStrBin = join-path $path_build_gen 'hello_gte.dwarf_str.bin'
$dwarfLocBin = join-path $path_build_gen 'hello_gte.dwarf_loc.bin'
$dwarfLoclistsBin = join-path $path_build_gen 'hello_gte.dwarf_loclists.bin'
$g_args = @(
"--update-section=.debug_info=$dwarfInfoBin",
"--update-section=.debug_abbrev=$dwarfAbbrevBin",
"--update-section=.debug_str=$dwarfStrBin",
"--add-section=.debug_loc=$dwarfLocBin",
"--add-section=.debug_loclists=$dwarfLoclistsBin"
)
& $Objcopy @g_args $injectElf 2>&1 | Out-Null
if ($LASTEXITCODE -ne 0) {
Write-Warning "[build] objcopy G' splice failed (exit $LASTEXITCODE); removing $injectElf"
Remove-Item -LiteralPath $injectElf -ErrorAction SilentlyContinue
return;
}
# Baked atoms execute from RAM but are emitted as C data arrays, so their ELF sections lack SHF_EXECINSTR.
# GDB discards line rows for non-code sections. Mark only the debug-copy sections executable.
# The original ELF and PS-EXE remain byte/flag unchanged.
& $Objcopy `
--set-section-flags ".rodata=alloc,load,readonly,code,contents" `
--set-section-flags ".data=alloc,load,data,code,contents" `
$injectElf 2>&1 | Out-Null
if ($LASTEXITCODE -ne 0) {
Write-Warning "[build] atom-section flag update failed (exit $LASTEXITCODE); removing $injectElf"
Remove-Item -LiteralPath $injectElf -ErrorAction SilentlyContinue
}
else {
Write-Host "[build] DWARF-injected ELF: $injectElf"
}
}
} }
# build-hello_gte # build-hello_gte
@@ -496,102 +645,177 @@ function build-hello_joypad {
# Post-link: gdb-runtime + dwarf-injection in a single Lua invocation (one luajit cold start). # 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) ps1-meta -unity_root $src_c -metadata $path_atom_metadata -out_root $path_build_gen -passes @('--post-link') ` -extra_args @('--elf', $elf)
$dwarfLineBin = join-path $path_build_gen 'hello_joypad.dwarf_line.bin' inject-dwarf $elf $path_build_gen
$dwarfArangesBin = join-path $path_build_gen 'hello_joypad.dwarf_aranges.bin' }
$dwarfRnglistsBin = join-path $path_build_gen 'hello_joypad.dwarf_rnglists.bin' # build-hello_joypad
$injectElf = join-path $path_build 'hello_joypad.dwarf-injected.elf'
if ((Test-Path $dwarfLineBin) -and (Test-Path $dwarfArangesBin) -and (Test-Path $dwarfRnglistsBin))
{
Write-Host "[build] DWARF-injecting $elf -> $injectElf"
Copy-Item -LiteralPath $elf -Destination $injectElf -Force
# Objcopy call: 3x --update-section for (line, aranges, rnglists).
$f_args = @(
"--update-section=.debug_line=$dwarfLineBin",
"--update-section=.debug_aranges=$dwarfArangesBin",
"--update-section=.debug_rnglists=$dwarfRnglistsBin"
)
& $Objcopy @f_args $injectElf 2>&1 | Out-Null
if ($LASTEXITCODE -ne 0) {
Write-Warning "[build] objcopy F' splice failed (exit $LASTEXITCODE); removing $injectElf"
Remove-Item -LiteralPath $injectElf -ErrorAction SilentlyContinue
return;
}
$dwarfInfoBin = join-path $path_build_gen 'hello_joypad.dwarf_info.bin' function build-hello_camera {
$dwarfAbbrevBin = join-path $path_build_gen 'hello_joypad.dwarf_abbrev.bin' $includes += @()
$dwarfStrBin = join-path $path_build_gen 'hello_joypad.dwarf_str.bin'
$dwarfLocBin = join-path $path_build_gen 'hello_joypad.dwarf_loc.bin'
$dwarfLoclistsBin = join-path $path_build_gen 'hello_joypad.dwarf_loclists.bin'
$g_args = @(
"--update-section=.debug_info=$dwarfInfoBin",
"--update-section=.debug_abbrev=$dwarfAbbrevBin",
"--update-section=.debug_str=$dwarfStrBin",
"--add-section=.debug_loc=$dwarfLocBin",
"--add-section=.debug_loclists=$dwarfLoclistsBin"
)
& $Objcopy @g_args $injectElf 2>&1 | Out-Null
if ($LASTEXITCODE -ne 0) {
Write-Warning "[build] objcopy G' splice failed (exit $LASTEXITCODE); removing $injectElf"
Remove-Item -LiteralPath $injectElf -ErrorAction SilentlyContinue
return;
}
# Baked atoms execute from RAM but are emitted as C data arrays, so their ELF sections lack SHF_EXECINSTR. $path_module = join-path $path_code 'hello_camera'
# GDB discards line rows for non-code sections. Mark only the debug-copy sections executable. $path_duffle = join-path $path_code 'duffle'
# The original ELF and PS-EXE remain byte/flag unchanged. $path_atom_metadata = join-path $path_duffle 'word_count.metadata.h'
& $Objcopy ` $path_build_gen = join-path $path_build 'gen'
--set-section-flags ".rodata=alloc,load,readonly,code,contents" `
--set-section-flags ".data=alloc,load,data,code,contents" ` $src_c = join-path $path_module 'hello_camera.c'
$injectElf 2>&1 | Out-Null ps1-meta -unity_root $src_c -metadata $path_atom_metadata -out_root $path_build_gen
if ($LASTEXITCODE -ne 0) {
Write-Warning "[build] atom-section flag update failed (exit $LASTEXITCODE); removing $injectElf" $assemble_args = @()
Remove-Item -LiteralPath $injectElf -ErrorAction SilentlyContinue $assemble_args += $f_debug
} $assemble_args += $f_optimize_none
else { $assemble_args += ($f_include + $path_code)
Write-Host "[build] DWARF-injected ELF: $injectElf"
$src_asm_crt = join-path $path_nugget_common 'crt0/crt0.s'
$module_asm_crt = join-path $path_build 'crt0.o'
assemble-unit $src_asm_crt $module_asm_crt $includes $assemble_args
$module_c = join-path $path_build 'hello_camera_c.o'
$compile_args = @()
$compile_args += $f_debug
$compile_args += $f_optimize_none
# $compile_args += $f_optimize_intrinsics
# $compile_args += $f_optimize_size
# $compile_args += $f_optimize_debug
$compile_args += ($f_include + $path_code)
compile-unit $src_c $module_c $includes $compile_args
$elf = join-path $path_build 'hello_camera.elf'
$exe = join-path $path_build 'hello_camera.ps-exe'
$link_args = @()
$link_args += $f_debug
# $link_args += $f_optimize_size
$link_modules = @(
$module_asm_crt,
$module_c
)
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
# ── 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 = '')
$dest = if ([string]::IsNullOrEmpty($OutputPath)) {
join-path $path_scripts 'pcsx_debug_helper.zip'
}
else {
$OutputPath
}
$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
}
# --- 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) {
build-hello_joypad $sz = (Get-Item -LiteralPath $dest).Length
Write-Host "[build] helper zip up to date: $dest ($sz bytes); skipping"
return
}
# NO idea if this works yet... write-host "[build] regenerating helper zip -> $dest"
function Send-ToEmulator { param( [string]$exePath ) Copy-Item -LiteralPath $elf32Src -Destination $elf32Dest -Force
$uri = "http://localhost:8080/api/v1/load-exec" try {
# Force the inode release so CreateFromDirectory can write fresh.
# Absolute path is safest for the emulator web server # ZipFile.CreateFromDirectory throws if the destination exists.
$absolutePath = [System.IO.Path]::GetFullPath($exePath) # If PCSX-Redux holds the file open, Remove-Item raises — fall
# back to writing pcsx_debug_helper.zip.new alongside. The next
# Create JSON payload pointing to your compiled .ps-exe # PCSX-Redux restart will read the canonical path; the .new file
$body = @{ filename = $absolutePath } | ConvertTo-Json # is a hint for the optional launch-script patch in fix 3.
if (test-path -LiteralPath $dest) {
Write-Host "Pushing hot-reload to PCSX-Redux..." -ForegroundColor Magenta try {
try { # -ErrorAction Stop is required so the catch below fires.
$response = Invoke-RestMethod -Uri $uri -Method Post -Body $body -ContentType "application/json" # Remove-Item raises a non-terminating error by default
Write-Host "Hot-reload successful!" -ForegroundColor Green # (ErrorActionPreference=Continue), which bypasses catch.
} catch { Remove-Item -LiteralPath $dest -Force -ErrorAction Stop
Write-Warning "Could not connect to PCSX-Redux web server. Ensure the emulator is running and Web Server is enabled." }
} 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 # Invokes reload.ps1 as a child pwsh instead of POSTing to the nonexistent /api/v1/load-exec endpoint.
# Send-ToEmulator (join-path $path_build 'hello_gte.ps-exe') # 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 --- $reloadScript = join-path $path_scripts 'reload.ps1'
# $exe_path = join-path $path_build 'hello_gte.ps-exe' if (-not (test-path -LiteralPath $reloadScript)) {
# $absolute_path = [System.IO.Path]::GetFullPath($exe_path) write-error "[build] reload.ps1 not found at $reloadScript"
exit 1
}
# PCSX-Redux expects the file location in the URL query string? write-host "[build] hot-reloading $ElfPath via reload.ps1" -ForegroundColor Magenta
# We URL-encode the path to ensure backslashes and spaces don't break the HTTP request? & pwsh -NoProfile -File $reloadScript -Mode elf -Target hello_camera -ElfPath $ElfPath
# $encoded_path = [uri]::EscapeDataString($absolute_path) if ($LASTEXITCODE -ne 0) {
# $uri = "http://localhost:8080/api/v1/load-exec?path=$encoded_path" write-error "[build] reload.ps1 failed (exit $LASTEXITCODE)"
exit $LASTEXITCODE
}
}
# Write-Host "Pushing hot-reload to PCSX-Redux..." -ForegroundColor Magenta # Post-build: Regenerate the helper zip (canonical output) and, if -Reload was passed, kick a hot-reload against the just-built ELF.
# try { # 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.
# # Send the request with the query string included Make-HelperZip
# Invoke-RestMethod -Uri $uri -Method Post if ($Reload) {
# Write-Host "Hot-reload successful!" -ForegroundColor Green Send-ToEmulator
# } 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
# }
+242 -211
View File
@@ -1,18 +1,12 @@
--- duffle.lua — shared primitives + domain tables for the tape-atom metaprograms. --- duffle.lua — shared primitives + domain tables for the tape-atom metaprograms.
--- --- * Character classification: `is_space`, `is_alpha`, `is_alnum`, `is_digit`, plus the byte-fast `_byte` variants.
--- One ownership statement, then the rest is signal: --- * String / path primitives: `trim`, `dirname`, `basename_no_ext`, `normalize_path`, `canonical_path_key`, `find_byte`.
--- * **Character classification** (`is_space`, `is_alpha`, `is_alnum`, `is_digit`, plus the byte-fast `_byte` variants). --- * I/O primitives: `read_file`, `write_file`, `ensure_dir`.
--- * **String / path primitives** (`trim`, `dirname`, `basename_no_ext`, `normalize_path`, `canonical_path_key`, `find_byte`). --- * Corpus resolution: `parse_direct_quoted_includes`, `resolve_source_corpus`.
--- * **I/O primitives** (`read_file`, `write_file`, `ensure_dir`). --- * C-language scanner: `skip_ws_and_cmt`, `skip_str_or_cmt`, `read_ident`, `read_parens`, `read_braces`, `read_brackets`, `read_balanced`, `scan_to_char`, `split_top_level_commas`.
--- * **Corpus resolution** (`parse_direct_quoted_includes`, `resolve_source_corpus`). --- * Word-count loader: `load_word_counts` for `WORD_COUNT(...)` metadata files.
--- * **C-language scanner** (`skip_ws_and_cmt`, `skip_str_or_cmt`, `read_ident`, `read_parens`, `read_braces`, `read_brackets`, --- * Line lookup: `LineIndex` returns an O(log N) `line_of(pos)` closure for source-mapping.
--- `read_balanced`, `scan_to_char`, `split_top_level_commas`). --- * Domain tables: `TAPE_ATOM_MACROS`, `GTE_PIPELINE_LATENCY`, `GP0_CMD_SIZE`, `GP0_CMD_BY_SHAPE`, `INSTRUCTION_LATENCY`.
--- * **Word-count loader** (`load_word_counts` for `WORD_COUNT(...)` metadata files).
--- * **Line lookup** (`LineIndex` returns an O(log N) `line_of(pos)` closure for source-mapping).
--- * **Domain tables** (`TAPE_ATOM_MACROS`, `GTE_PIPELINE_LATENCY`, `GP0_CMD_SIZE`, `GP0_CMD_BY_SHAPE`,
--- `GP0_MACRO_CONTRIB`, `INSTRUCTION_LATENCY`).
---
--- **Conventions**: tabs (1/level), EmmyLua annotations, no regex.
local M = {} local M = {}
@@ -24,7 +18,7 @@ local lfs = require("lfs")
-- Cross-file type aliases -- Cross-file type aliases
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
--- @alias Path string -- absolute or CWD-relative file path --- @alias Path string -- Absolute or CWD-relative file path
--- @alias LineNum integer -- 1-indexed source line number --- @alias LineNum integer -- 1-indexed source line number
--- @alias ByteOff integer -- 0-indexed byte offset within a source string --- @alias ByteOff integer -- 0-indexed byte offset within a source string
--- @alias MacroName string -- lower_snake_case macro identifier (e.g. "mac_yield") --- @alias MacroName string -- lower_snake_case macro identifier (e.g. "mac_yield")
@@ -32,10 +26,10 @@ local lfs = require("lfs")
--- @alias Severity string -- "error" | "warning" | "info" --- @alias Severity string -- "error" | "warning" | "info"
--- @class SourceFile --- @class SourceFile
--- @field path Path -- absolute path to the source file --- @field path Path -- Absolute path to the source file
--- @field text string -- the full source text --- @field text string -- Full source text
--- @field dir string -- the directory containing the source --- @field dir string -- Directory containing the source
--- @field basename string -- filename without extension --- @field basename string -- Filename without extension
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- ASCII byte constants -- ASCII byte constants
@@ -73,20 +67,12 @@ local BYTE_DIGIT_9 = 0x39 -- '9'
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Section -1: Bootstrap (path-setup at module load) -- Section -1: Bootstrap (path-setup at module load)
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
--
-- Path setup runs through `scripts/duffle_paths.lua`, which derives the repo root from `debug.getinfo(1, "S").source`
-- (no subprocess, ~0ms) and then calls `require("duffle")`.
-- Entry and pass scripts load `duffle_paths.lua` first; a `find_repo_root` / `setup_package_path` defined here was dead code in practice.
-- `git rev-parse` costs ~100-180ms per subprocess spawn on Windows; `debug.getinfo` is <1ms, so we keep only the fast path.
--
-- To load `duffle.lua` outside `duffle_paths.lua`, set `package.path` manually before `require`.
-- See `docs/guide_metaprogram_ssdl.md` §"I/O primitives" for the pattern.
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Section 0: LPeg patterns (compiled once at module load) -- Section 0: LPeg patterns (compiled once at module load)
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- --
-- LPeg is a required dependency (PEG library, no regex). It's loaded via `package.cpath` — `duffle_paths.lua` wires the path to `toolchain/lpeg/lpeg.dll`. -- LPeg is a required dependency (PEG library). It's loaded via `package.cpath` — `duffle_paths.lua` wires the path to `toolchain/lpeg/lpeg.dll`.
-- LPeg handles the high-level scanner; the byte-by-byte helpers in Section 1 handle classification primitives that LPeg's CPython-level cost would dominate. -- LPeg handles the high-level scanner; the byte-by-byte helpers in Section 1 handle classification primitives that LPeg's CPython-level cost would dominate.
-- --
-- If the require fails, fail loud with an actionable message. The build script (`update_deps.ps1`) builds lpeg.dll into `toolchain/lpeg/`; run it when the dll is missing. -- If the require fails, fail loud with an actionable message. The build script (`update_deps.ps1`) builds lpeg.dll into `toolchain/lpeg/`; run it when the dll is missing.
@@ -100,32 +86,26 @@ end
local P, S, R = lpeg.P, lpeg.S, lpeg.R local P, S, R = lpeg.P, lpeg.S, lpeg.R
-- Character class patterns -- Character class patterns
local alpha_pat = R("AZ", "az") + P("_") local alpha_pat = R("AZ", "az") + P("_")
local digit_pat = R("09") local digit_pat = R("09")
local lpeg_alnum_pat = alpha_pat + digit_pat local lpeg_alnum_pat = alpha_pat + digit_pat
-- Identifier: alpha followed by zero+ alnum. Capture as a string. -- Identifier: alpha followed by zero+ alnum. Capture as a string.
local lpeg_alpha_pat = alpha_pat local lpeg_alpha_pat = alpha_pat
local lpeg_ident_pat = lpeg.C(alpha_pat * lpeg_alnum_pat^0) local lpeg_ident_pat = lpeg.C(alpha_pat * lpeg_alnum_pat^0)
-- String literal: "..." with backslash escapes. local lpeg_str_pat = P('"') * (P(1) - S('"\\') + P('\\') * P(1))^0 * P('"') -- String literal: "..." with backslash escapes.
local lpeg_str_pat = P('"') * (P(1) - S('"\\') + P('\\') * P(1))^0 * P('"') local lpeg_chr_pat = P("'") * (P(1) - S("'\\") + P('\\') * P(1))^0 * P("'") -- Char literal: '...' with backslash escapes.
-- Char literal: '...' with backslash escapes. local lpeg_line_cmt_pat = P("//") * (P(1) - S("\n"))^0 -- Line comment: // ... to end-of-line.
local lpeg_chr_pat = P("'") * (P(1) - S("'\\") + P('\\') * P(1))^0 * P("'") local lpeg_block_cmt_pat = P("/*") * (P(1) - P("*/"))^0 * P("*/") -- Block comment: /* ... */ (no nesting per C standard).
-- Line comment: // ... to end-of-line. local lpeg_str_or_cmt_pat = lpeg_str_pat + lpeg_chr_pat + lpeg_line_cmt_pat + lpeg_block_cmt_pat -- String or comment (any of the four forms).
local lpeg_line_cmt_pat = P("//") * (P(1) - S("\n"))^0
-- Block comment: /* ... */ (no nesting per C standard).
local lpeg_block_cmt_pat = P("/*") * (P(1) - P("*/"))^0 * P("*/")
-- String or comment (any of the four forms).
local lpeg_str_or_cmt_pat = lpeg_str_pat + lpeg_chr_pat + lpeg_line_cmt_pat + lpeg_block_cmt_pat
-- Whitespace + comment skipper: zero+ (whitespace run | string | comment). -- Whitespace + comment skipper: zero+ (whitespace run | string | comment).
local ws_pat = S(" \t\n\r\v\f") local ws_pat = S(" \t\n\r\v\f")
local lpeg_ws_and_cmt_pat = (ws_pat + lpeg_str_or_cmt_pat)^0 local lpeg_ws_and_cmt_pat = (ws_pat + lpeg_str_or_cmt_pat)^0
-- Generic "skip until target, but step over balanced groups" matcher. -- Generic "skip until target, but step over balanced groups" matcher.
-- Used by scan_to_char for non-ident / non-bracket chars. -- Used by scan_to_char for non-ident / non-bracket chars. We accept any single char except the target.
-- We accept any single char except the target.
-- The balanced-group stepping is handled by the caller (via read_balanced). -- The balanced-group stepping is handled by the caller (via read_balanced).
local lpeg_scan_to_target_pat = function(target) return (P(1) - P(target))^0 end local lpeg_scan_to_target_pat = function(target) return (P(1) - P(target))^0 end
@@ -148,12 +128,10 @@ end
-- Single digit. -- Single digit.
function M.is_digit_byte(b) return b and b >= BYTE_DIGIT_0 and b <= BYTE_DIGIT_9 end function M.is_digit_byte(b) return b and b >= BYTE_DIGIT_0 and b <= BYTE_DIGIT_9 end
-- Letter OR digit OR underscore. -- Letter OR digit OR underscore.
function M.is_alnum_byte(b) return M.is_alpha_byte(b) or M.is_digit_byte(b) end function M.is_alnum_byte(b) return M.is_alpha_byte(b) or M.is_digit_byte(b) end
-- String-based wrappers (kept for callers that already have a single-char string; -- String-based wrappers (kept for callers that already have a single-char string; the byte versions are what the hot loops should call).
-- the byte versions are what the hot loops should call).
function M.is_space(c) function M.is_space(c)
if type(c) == "number" then return M.is_space_byte(c) end if type(c) == "number" then return M.is_space_byte(c) end
return c == " " or c == "\t" or c == "\n" or c == "\r" or c == "\v" or c == "\f" return c == " " or c == "\t" or c == "\n" or c == "\r" or c == "\v" or c == "\f"
@@ -184,8 +162,8 @@ end
--- Linear-search for a single-byte target in a string. --- Linear-search for a single-byte target in a string.
--- @param haystack string --- @param haystack string
--- @param target integer -- byte value --- @param target integer -- byte value
--- @param start integer -- optional 1-indexed start (default 1) --- @param start integer -- optional 1-indexed start (default 1)
--- @return integer|nil --- @return integer|nil
function M.find_byte(haystack, target, start) function M.find_byte(haystack, target, start)
for pos = start or 1, #haystack do for pos = start or 1, #haystack do
@@ -309,7 +287,7 @@ local function absolute_normalized_path(path)
end end
--- Return the normalized absolute, Windows-case-folded comparison key for a path. --- Return the normalized absolute, Windows-case-folded comparison key for a path.
--- -Ordinary relative paths resolve against the process cwd. Drive-relative paths are rejected because LuaFileSystem does not expose Windows per-drive current directories. --- Ordinary relative paths resolve against the process cwd. Drive-relative paths are rejected because LuaFileSystem does not expose Windows per-drive current directories.
--- @param path Path --- @param path Path
--- @return string --- @return string
function M.canonical_path_key(path) function M.canonical_path_key(path)
@@ -603,7 +581,7 @@ function M.parse_direct_quoted_includes(source_text)
pos = after pos = after
end end
elseif byte == BYTE_DQUOTE or byte == BYTE_SQUOTE then elseif byte == BYTE_DQUOTE or byte == BYTE_SQUOTE then
-- enter+leave the string literal in one skip; literal bodies cannot contain a directive regardless of what they look like. -- enter + leave the string literal in one skip; literal bodies cannot contain a directive regardless of what they look like.
line_leading = false line_leading = false
local after = M.skip_str_or_cmt(logical_text, pos) local after = M.skip_str_or_cmt(logical_text, pos)
pos = (after > pos) and after or (pos + 1) pos = (after > pos) and after or (pos + 1)
@@ -799,18 +777,17 @@ function M.resolve_source_corpus(options)
end end
return { return {
unity_root = root.path, unity_root = root.path,
project_root = project_root, project_root = project_root,
code_root = code_root, code_root = code_root,
source_order = source_order, source_order = source_order,
sources_by_path = sources_by_path, sources_by_path = sources_by_path,
sources_by_dir = M.group_sources_by_dir(source_order), sources_by_dir = M.group_sources_by_dir(source_order),
resolver = resolver, resolver = resolver,
} }
end end
-- Split a brace-body into top-level comma-separated tokens. Honors nested parens/braces/brackets and skips strings/comments. -- Split a brace-body into top-level comma-separated tokens. Honors nested parens/braces/brackets and skips strings/comments.
--
-- Splits at top-level NEWLINES and SEMICOLONS too, AND emits a token break after a top-level comment/string. -- Splits at top-level NEWLINES and SEMICOLONS too, AND emits a token break after a top-level comment/string.
-- Pure-comment / pure-string chunks contribute 0 words. -- Pure-comment / pure-string chunks contribute 0 words.
function M.split_top_level_commas(body) function M.split_top_level_commas(body)
@@ -846,7 +823,7 @@ function M.split_top_level_commas(body)
if has_real_content(chunk) then if has_real_content(chunk) then
tokens[#tokens + 1] = chunk tokens[#tokens + 1] = chunk
elseif #tokens > 0 then elseif #tokens > 0 then
-- Pure comment/string chunk at top level. -- comment/string chunk at top level.
-- Append it to the LAST token so emit-context callers (components.lua build_component_lines) can convert -- Append it to the LAST token so emit-context callers (components.lua build_component_lines) can convert
-- `// trailing comment` to `/* */` and emit it with the macro body. -- `// trailing comment` to `/* */` and emit it with the macro body.
-- count_token_words only inspects the leading ident, so a trailing comment does not affect the count. -- count_token_words only inspects the leading ident, so a trailing comment does not affect the count.
@@ -920,8 +897,7 @@ function M.tokenize_body(body)
while scan <= len do while scan <= len do
local c = body:byte(scan) local c = body:byte(scan)
-- Terminator bytes (delimit a token at the top level): ',' = 0x2C, '\n' = 0x0A, ';' = 0x3B. -- Terminator bytes (delimit a token at the top level): ',' = 0x2C, '\n' = 0x0A, ';' = 0x3B.
-- These also appear as separators between argument lists inside the parens/braces/brackets, -- These also appear as separators between argument lists inside the parens/braces/brackets, so we stop the scan when we hit any of them.
-- so we stop the scan when we hit any of them.
if c == BYTE_COMMA then break end if c == BYTE_COMMA then break end
if c == BYTE_NEWLINE then break end if c == BYTE_NEWLINE then break end
if c == BYTE_SEMI then break end if c == BYTE_SEMI then break end
@@ -966,7 +942,7 @@ function M.build_body_line_index(body)
local index = {} local index = {}
local len = #body local len = #body
local newline_count = 0 local newline_count = 0
for pos = 1, len do for pos = 1, len do
if pos > 1 then if pos > 1 then
index[pos] = newline_count + 1 index[pos] = newline_count + 1
end end
@@ -1088,18 +1064,18 @@ M.GTE_COMMAND_ALIASES = {
-- * "Store delays are counted in numbers of clock cycles (not in numbers of opcodes). -- * "Store delays are counted in numbers of clock cycles (not in numbers of opcodes).
-- For 3 cycle delay, one must usually insert 3 cached opcodes (or one uncached opcode)." -- For 3 cycle delay, one must usually insert 3 cached opcodes (or one uncached opcode)."
-- --
-- Per PSX-SPX `docs/psx-spx/docs/gtepipelinetimings.md` (the per-instruction input-latch measurement, which is the same -- Per PSX-SPX `docs/psx-spx/docs/gtepipelinetimings.md` (the per-instruction input-latch measurement, which is the same phenomenon modeled from the command side),
-- phenomenon modeled from the command side), the values are: -- the values are:
-- rtps: every data register, every control register (RT/TR/OFX/OFY/H/DQA/DQB) -- rtps: every data register, every control register (RT / TR / OFX / OFY / H / DQA / DQB)
-- rtpt: same superset (rtpt reads V0..V2, the RT matrix, the TR vector, OFX/OFY, H, DQA, DQB) -- rtpt: same superset (rtpt reads V0..V2, the RT matrix, the TR vector, OFX / OFY, H, DQA, DQB)
-- nclip: SXY0, SXY1, SXY2 (no RT/TR/OFX inputs) -- nclip: SXY0, SXY1, SXY2 (no RT / TR / OFX inputs)
-- mvmva: variable (depends on the chosen mx / v / cv selector); treated conservatively as the union of all RT + TR + BK + IR columns -- mvmva: variable (depends on the chosen mx / v / cv selector); treated conservatively as the union of all RT + TR + BK + IR columns
-- (the data inputs the command can read). -- (the data inputs the command can read).
-- op: IR1, IR2, IR3 (cross-product output, atomic; consumers treat as fan-out only) -- op: IR1, IR2, IR3 (cross-product output, atomic; consumers treat as fan-out only)
-- avsz3/avsz4: SZ0..SZ3 + ZSF3/ZSF4 -- avsz3 / avsz4: SZ0..SZ3 + ZSF3/ZSF4
-- --
-- We model the data-register + control-register superset. Every relevant input is in this set per PSX-SPX `gtepipelinetimings.md`; -- We model the data-register + control-register superset. Every relevant input is in this set per PSX-SPX `gtepipelinetimings.md`;
-- the per-input latching values there describe the same number's command-side view -- The per-input latching values there describe the same number's command-side view
-- (a recent mtc2/ctc2 to that register must retire the same number of cycles before the command issues). -- (a recent mtc2/ctc2 to that register must retire the same number of cycles before the command issues).
-- Anything outside this set is safe to clobber immediately after a prior command. -- Anything outside this set is safe to clobber immediately after a prior command.
M.GTE_COMMAND_INPUTS = { M.GTE_COMMAND_INPUTS = {
@@ -1133,7 +1109,7 @@ M.GTE_COMMAND_INPUTS = {
"gte_cr_OFX", "gte_cr_OFY", "gte_cr_H", "gte_cr_OFX", "gte_cr_OFY", "gte_cr_H",
"gte_cr_DQA", "gte_cr_DQB", "gte_cr_DQA", "gte_cr_DQB",
}, },
-- NCLIP: reads SXY0/SXY1/SXY2 only (per PSX-SPX gtepipelinetimings.md §12.6). -- NCLIP: reads SXY0 / SXY1 / SXY2 only (per PSX-SPX gtepipelinetimings.md §12.6).
["gte_cmdw_nclip"] = { ["gte_cmdw_nclip"] = {
"C2_SXY0", "C2_SXY1", "C2_SXY2", "C2_SXY0", "C2_SXY1", "C2_SXY2",
}, },
@@ -1163,7 +1139,6 @@ M.GTE_COMMAND_INPUTS = {
} }
-- GTE command output-set + semantic role table. -- GTE command output-set + semantic role table.
--
-- For each command, the set of C2 data registers the command writes as outputs, paired with the SEMANTIC ROLE of each output. -- For each command, the set of C2 data registers the command writes as outputs, paired with the SEMANTIC ROLE of each output.
-- The semantic role is the basis for the `_post_<cmd>` contract validation. -- The semantic role is the basis for the `_post_<cmd>` contract validation.
-- The contract says "after <cmd>, the latest screen-XY is C2_SXY2" (C2_SXY0 is wrong; the FIFO side effects leave SXY0 as an older FIFO entry, never the newest). -- The contract says "after <cmd>, the latest screen-XY is C2_SXY2" (C2_SXY0 is wrong; the FIFO side effects leave SXY0 as an older FIFO entry, never the newest).
@@ -1220,14 +1195,14 @@ M.GTE_COMMAND_OUTPUTS = {
["gte_cmdw_avsz4"] = { ["gte_cmdw_avsz4"] = {
{ register = "C2_OTZ", role = "otz" }, { register = "C2_OTZ", role = "otz" },
}, },
-- OP (outer product): writes IR1/IR2/IR3 (color-conversion fan-out). -- OP (outer product): writes IR1 / IR2 / IR3 (color-conversion fan-out).
["gte_cmdw_op"] = { ["gte_cmdw_op"] = {
{ register = "C2_IR1", role = "latest_color" }, { register = "C2_IR1", role = "latest_color" },
{ register = "C2_IR2", role = "latest_color" }, { register = "C2_IR2", role = "latest_color" },
{ register = "C2_IR3", role = "latest_color" }, { register = "C2_IR3", role = "latest_color" },
}, },
-- MVMVA: same shape as OP from the role perspective; the single -- MVMVA: same shape as OP from the role perspective; the single
-- MAC result is written to C2_IR1/IR2/IR3. -- MAC result is written to C2_IR1 / IR2 / IR3.
["gte_cmdw_mvmva"] = { ["gte_cmdw_mvmva"] = {
{ register = "C2_IR1", role = "latest_color" }, { register = "C2_IR1", role = "latest_color" },
{ register = "C2_IR2", role = "latest_color" }, { register = "C2_IR2", role = "latest_color" },
@@ -1241,17 +1216,16 @@ M.GTE_COMMAND_OUTPUTS = {
-- A subsequent MTC2/CTC2 overwrite of one of those outputs before the latch window expires is a hazard: -- A subsequent MTC2/CTC2 overwrite of one of those outputs before the latch window expires is a hazard:
-- the latched value in the pipeline gets overwritten by the CPU before the pipeline consumes it. -- the latched value in the pipeline gets overwritten by the CPU before the pipeline consumes it.
-- --
-- This relation is the command -> register direction (the command is the producer; MTC2/CTC2 is the consumer). -- This relation is the command -> register direction (the command is the producer; MTC2 / CTC2 is the consumer).
-- It is the inverse of the MTC2 -> command input propagation (register -> command direction), which is staged by the -- It is the inverse of the MTC2 -> command input propagation (register -> command direction), which is staged by the producer step of `analyze_hardware_relations`.
-- producer step of `analyze_hardware_relations`.
-- --
-- The schema mirrors the producer-side relations (`direction`, `evidence`, `violation_kind`); `required` counts the -- The schema mirrors the producer-side relations (`direction`, `evidence`, `violation_kind`);
-- emitted words strictly between the command's last output word and the overwrite. -- `required` counts the emitted words strictly between the command's last output word and the overwrite.
-- `required = 0` permits the immediately following overwrite; `required = 4` requires four intervening words. -- `required = 0` permits the immediately following overwrite; `required = 4` requires four intervening words.
-- --
-- Per PSX-SPX `gtepipelinetimings.md` the per-command input latching measurements are the same numbers inverted. -- Per PSX-SPX `gtepipelinetimings.md` the per-command input latching measurements are the same numbers inverted.
-- They describe when a recent MTC2/CTC2 must retire before the command issues; this table describes when a recent -- They describe when a recent MTC2 / CTC2 must retire before the command issues.
-- command's outputs latch into the pipeline before a later MTC2/CTC2 overwrites them. -- This table describes when a recent command's outputs latch into the pipeline before a later MTC2/CTC2 overwrites them.
-- --
-- Consumers: -- Consumers:
-- * passes/static_analysis.lua::analyze_hardware_relations (stages post-command latch relations in `pending` after a GTE command). -- * passes/static_analysis.lua::analyze_hardware_relations (stages post-command latch relations in `pending` after a GTE command).
@@ -1266,9 +1240,7 @@ M.GTE_COMMAND_LATCH_WINDOWS = {
{ register = "C2_OTZ", required = 4 }, { register = "C2_OTZ", required = 4 },
{ register = "C2_IR0", required = 4 }, { register = "C2_IR0", required = 4 },
}, },
-- RTPT: same latching as RTPS (the LAST projection in SXY2 is the -- RTPT: same latching as RTPS (the LAST projection in SXY2 is the newest one; the earlier SXY0 / SXY1 entries are part of the batched triple).
-- newest one; the earlier SXY0 / SXY1 entries are part of the
-- batched triple).
["gte_cmdw_rtpt"] = { ["gte_cmdw_rtpt"] = {
{ register = "C2_SXY0", required = 4 }, { register = "C2_SXY0", required = 4 },
{ register = "C2_SXY1", required = 4 }, { register = "C2_SXY1", required = 4 },
@@ -1287,7 +1259,7 @@ M.GTE_COMMAND_LATCH_WINDOWS = {
["gte_cmdw_avsz4"] = { ["gte_cmdw_avsz4"] = {
{ register = "C2_OTZ", required = 4 }, { register = "C2_OTZ", required = 4 },
}, },
-- OP / MVMVA: IR1/IR2/IR3 latch for 4 emitted words. -- OP / MVMVA: IR1 / IR2 / IR3 latch for 4 emitted words.
["gte_cmdw_op"] = { ["gte_cmdw_op"] = {
{ register = "C2_IR1", required = 4 }, { register = "C2_IR1", required = 4 },
{ register = "C2_IR2", required = 4 }, { register = "C2_IR2", required = 4 },
@@ -1300,18 +1272,14 @@ M.GTE_COMMAND_LATCH_WINDOWS = {
}, },
} }
-- GTE component result contracts were removed: the `_post_<cmd>` naming convention was a soft convention
-- (the user did not want it formalized via static-analysis enforcement). A proper `atom_info` directive for ordering semantics is a future TODO.
-- Operand-class table for the COP2->GPR load-delay check. -- Operand-class table for the COP2->GPR load-delay check.
--
-- Maps each emitting-token ident to the set of GPR operand positions it reads. -- Maps each emitting-token ident to the set of GPR operand positions it reads.
-- Covers the current encoder vocabulary (`code/duffle/mips.h` + `code/duffle/gte.h`); add rows here as new encoders land. -- Covers the current encoder vocabulary (`code/duffle/mips.h` + `code/duffle/gte.h`); add rows here as new encoders land.
-- --
-- Semantics: -- Semantics:
-- * A "GPR operand position" is the textual slot in the macro's argument list, 1-based; e.g. `load_word(rt, base, off)` has -- * A "GPR operand position" is the textual slot in the macro's argument list, 1-based; e.g. `load_word(rt, base, off)` has
-- positional operands 1 (rt), 2 (base), 3 (off). The table reads operands 1 + 2 + 3 to find what GPRs the macro touches. -- positional operands 1 (rt), 2 (base), 3 (off). The table reads operands 1 + 2 + 3 to find what GPRs the macro touches.
-- * The check tracks one entry per destination GPR per MFC2/CFC2 event. -- * The check tracks one entry per destination GPR per MFC2 / CFC2 event.
-- A subsequent event counts as a "use" iff any of its read operand positions reference that destination GPR's ident (e.g. `R_T0`). -- A subsequent event counts as a "use" iff any of its read operand positions reference that destination GPR's ident (e.g. `R_T0`).
-- * Branch delay slots are out of scope (MIPS control-flow; tracked separately). -- * Branch delay slots are out of scope (MIPS control-flow; tracked separately).
M.OPERAND_READ_POSITIONS = { M.OPERAND_READ_POSITIONS = {
@@ -1324,13 +1292,13 @@ M.OPERAND_READ_POSITIONS = {
["sub_s"] = {1, 2, 3}, ["sub_s"] = {1, 2, 3},
["sub_u"] = {1, 2, 3}, ["sub_u"] = {1, 2, 3},
["and_i"] = {1, 2}, ["and_i"] = {1, 2},
["and"] = {1, 2, 3}, ["and"] = {1, 2, 3},
["or_i"] = {1, 2}, ["or_i"] = {1, 2},
["or_i_self"] = {1}, ["or_i_self"] = {1},
["or"] = {1, 2, 3}, ["or"] = {1, 2, 3},
["or_self"] = {1, 2}, ["or_self"] = {1, 2},
["xor_i"] = {1, 2}, ["xor_i"] = {1, 2},
["xor"] = {1, 2, 3}, ["xor"] = {1, 2, 3},
["slt_s"] = {1, 2, 3}, ["slt_s"] = {1, 2, 3},
["slt_u"] = {1, 2, 3}, ["slt_u"] = {1, 2, 3},
["slt_si"] = {1, 2}, ["slt_si"] = {1, 2},
@@ -1430,34 +1398,7 @@ M.GP0_CMD_BY_SHAPE = {
["g4"] = 0x38, ["gt4"] = 0x3C, ["g4"] = 0x38, ["gt4"] = 0x3C,
} }
-- TODO(Ed): REMOVE THIS HARDCODE, THIS SHOULD BE RESOLVED AUTOMATICALLY -- Per-instruction cycle cost (best-case, no stalls). Used by the static-analysis pass to emit per-atom cycle budgets.
-- Per-macro prim-buffer contribution: how many 32-bit words each macro writes to the primitive being built in main RAM.
-- (This counts RAM-side prim-buffer words, not .text instruction words.)
-- The sum across `mac_format_X_color` + `mac_gte_store_X_post_*` + `mac_insert_ot_tag_X` calls in an atom body must equal
-- `GP0_CMD_SIZE[GP0_CMD_BY_SHAPE[shape]]`.
M.GP0_MACRO_CONTRIB = {
["mac_format_f3_color"] = 1,
["mac_format_g3_color"] = 3,
["mac_format_g4_color"] = 4,
["mac_gte_store_f3"] = 3,
["mac_gte_store_g3"] = 3,
["mac_gte_store_g4_p012"] = 3,
["mac_gte_store_g4_p3"] = 1,
["mac_insert_ot_tag_f3"] = 1,
["mac_insert_ot_tag_g4"] = 1,
}
-- Per-macro cycle cost (best-case, no stalls). Used by the static-analysis pass to emit per-atom cycle budgets.
-- The counts cover the expanded instruction sequence the macro emits (not just the surface token in source).
-- Worked example — `mac_pack_color_word(off, cmd, r, g, b)` expands to:
-- load_upper_i(R_AT, (cmd << 8) | b) -- 1 cycle
-- or_i_self(R_AT, (g << 8) | r) -- 1 cycle
-- store_word(R_AT, R_PrimCursor, off) -- 1 cycle
-- = 3 cycles total
--
-- `mac_yield` emits a control-transfer sequence (load_word, add_ui_self, jump_reg, nop). The atom body's cycle budget excludes
-- the yield's cost (we model it as 0); the runtime cost lands in the next atom's prologue.
--
-- GTE command values are the GTE instruction's intrinsic cycles — the latency after any pre-cmd `nop2` has retired. -- GTE command values are the GTE instruction's intrinsic cycles — the latency after any pre-cmd `nop2` has retired.
-- When the source emits `nop2, gte_cmdw_X`, the nops' cycles are added separately (1+1) plus the gte_cmdw_X value here: -- When the source emits `nop2, gte_cmdw_X`, the nops' cycles are added separately (1+1) plus the gte_cmdw_X value here:
-- rtpt = 23 + 2 nops = 25 total cycles (PSX-SPX says 23 cycles for the cmd itself; the nops are pre-fill) -- rtpt = 23 + 2 nops = 25 total cycles (PSX-SPX says 23 cycles for the cmd itself; the nops are pre-fill)
@@ -1471,8 +1412,15 @@ M.GP0_MACRO_CONTRIB = {
-- PSX-SPX reports the GTE intrinsic cycles as the total execution time of the command itself (rtpt=23, rtps=15, nclip=8, etc.). -- PSX-SPX reports the GTE intrinsic cycles as the total execution time of the command itself (rtpt=23, rtps=15, nclip=8, etc.).
-- The pre-fill nops are a codebase convention for retiring preceding C2 writes. -- The pre-fill nops are a codebase convention for retiring preceding C2 writes.
-- See `docs/psx-spx/docs/geometrytransformationenginegte.md` for per-command cycle counts and -- See `docs/psx-spx/docs/geometrytransformationenginegte.md` for per-command cycle counts and
-- `docs/psx-spx/docs/gtepipelinetimings.md` for the hardware-verified input-latch boundaries (most inputs become -- `docs/psx-spx/docs/gtepipelinetimings.md` for the hardware-verified input-latch boundaries
-- safe to clobber after 0-4 cycles). -- (most inputs become safe to clobber after 0-4 cycles).
--
-- Per-macro cycle costs (`mac_yield`, `mac_pack_color_word`, ...) and per-macro prim-buffer contributions
-- (`mac_format_*_color`, `mac_gte_store_*`, `mac_insert_ot_tag_*`) are NOT hardcoded here.
-- `passes/components.lua::compute_components_metadata` derives both from each `MipsAtomComp_(ac_X)` body in
-- `code/duffle/lottes_tape.h`, stores the values on `corpus.components[name].cycle_cost` and
-- `corpus.components[name].gp0_contrib`, and `passes/static_analysis.lua` reads those fields directly.
-- The `mac_yield` cost is 0 by convention (the runtime cost lands in the next atom's prologue).
M.INSTRUCTION_LATENCY = { M.INSTRUCTION_LATENCY = {
-- CPU ALU (single-cycle R3000A ops) -- CPU ALU (single-cycle R3000A ops)
["nop"] = 1, ["nop"] = 1,
@@ -1507,27 +1455,29 @@ M.INSTRUCTION_LATENCY = {
["load_byte_u"] = 1, ["load_byte"] = 1, ["load_byte_u"] = 1, ["load_byte"] = 1,
["load_upper_i"] = 1, ["load_upper_i"] = 1,
-- 2-word loads (lui + ori) used for >16-bit immediates -- 2-word loads (lui + ori) used for >16-bit immediates
["load_imm"] = 2, ["load_imm"] = 2,
["load_imm_1w"] = 1, ["load_imm_1w"] = 1,
["load_imm_1w_s0"] = 1, ["load_imm_1w_s0"] = 1,
["load_imm_2w"] = 2, ["load_imm_2w"] = 2,
["load_imm_2w_addi_forced"] = 2, ["load_imm_2w_addi_forced"] = 2,
["load_imm_2w_ori_forced"] = 2, ["load_imm_2w_ori_forced"] = 2,
-- Stores (1 cycle each) -- Stores (1 cycle each)
["store_word"] = 1, ["store_word"] = 1,
["store_half"] = 1, ["store_half"] = 1,
["store_byte"] = 1, ["store_byte"] = 1,
-- Branches (branch + BD slot nop = 2 cycles; the BD slot's nop is -- Branches (branch + BD slot nop = 2 cycles; the BD slot's nop is counted as part of the branch's cost)
-- counted as part of the branch's cost)
["branch_equal"] = 2, ["branch_ne"] = 2, ["branch_equal"] = 2, ["branch_ne"] = 2,
["branch_le_zero"] = 2, ["branch_lt_zero"] = 2, ["branch_le_zero"] = 2, ["branch_lt_zero"] = 2,
["branch_ge_zero"] = 2, ["branch_gt_zero"] = 2, ["branch_ge_zero"] = 2, ["branch_gt_zero"] = 2,
-- `jump_rel(off)` is the within-atom-safe unconditional-jump alias for `branch_equal(R_0, R_0, off)` (see `code/duffle/mips.h`).
-- Same cost as the underlying branch (1 instruction + 1 mandatory BD-slot nop = 2 cycles).
["jump_rel"] = 2,
-- Jumps (jump + BD slot nop = 2 cycles) -- Jumps (jump + BD slot nop = 2 cycles)
["jump"] = 2, ["jump_reg"] = 2, ["jump"] = 2, ["jump_reg"] = 2,
["jump_link"] = 2, ["call_reg"] = 2, ["jump_link"] = 2, ["call_reg"] = 2,
["call_addr"] = 2, ["call_addr"] = 2,
-- COP2 transfers (mtc2/mfc2/ctc2/cfc2 = 1 cycle + COP2 latency; the -- COP2 transfers (mtc2/mfc2/ctc2/cfc2 = 1 cycle + COP2 latency;
-- COP2 latency is usually absorbed by subsequent nops or by the next -- The COP2 latency is usually absorbed by subsequent nops or by the next
-- GTE command's pre-fill nops, so we count 1) -- GTE command's pre-fill nops, so we count 1)
["gte_mv_to_data_r"] = 1, ["gte_mv_to_data_r"] = 1,
["gte_mv_from_data_r"] = 1, ["gte_mv_from_data_r"] = 1,
@@ -1568,22 +1518,6 @@ M.INSTRUCTION_LATENCY = {
["gte_load_v2"] = 2, ["gte_load_v2"] = 2,
["gte_load_v0v1v2"] = 6, ["gte_load_v0v1v2"] = 6,
-- TODO(Ed): REMOVE THIS HARDCODE, THIS SHOULD BE RESOLVED AUTOMATICALLY
-- mac_* helpers (cycle cost = sum of the expanded instructions)
-- mac_yield transfers control; cycle budget is 0 (the next atom absorbs the cost).
["mac_yield"] = 0,
["mac_pack_color_word"] = 3, -- lui + ori + sw
["mac_format_f3_color"] = 3, -- = mac_pack_color_word
["mac_format_g4_color"] = 12, -- 4 x mac_pack_color_word
["mac_load_tri_indices"] = 3, -- 3 x lhu
["mac_gte_load_tri_verts"] = 18, -- 3 x {sll, addu, lw, lw, mtc2, mtc2}
["mac_gte_store_f3"] = 3,
["mac_gte_store_g3"] = 3,
["mac_gte_store_g4_p012"] = 3,
["mac_gte_store_g4_p3"] = 1,
["mac_insert_ot_tag_f3"] = 11, -- 11 .word slots in the macro body
["mac_insert_ot_tag_g4"] = 11,
-- Annotation markers (emit no code; pure metaprogram hints) -- Annotation markers (emit no code; pure metaprogram hints)
["atom_label"] = 0, ["atom_label"] = 0,
["atom_offset"] = 0, ["atom_offset"] = 0,
@@ -1600,8 +1534,7 @@ M.UNKNOWN_INSTRUCTION_CYCLES = 1
-- Hardware-relation policy table. -- Hardware-relation policy table.
-- --
-- The forward walker in `passes/static_analysis.lua::analyze_hardware_relations` reads every emitted word_event, -- The forward walker in `passes/static_analysis.lua::analyze_hardware_relations` reads every emitted word_event, matches its `encoder` against `row.token`, and:
-- matches its `encoder` against `row.token`, and:
-- * stages the event as a producer in `atom.paths.forward_state`; or -- * stages the event as a producer in `atom.paths.forward_state`; or
-- * matches it as a consumer against pending producers and records a hazard on `atom.paths.hazards` when the gap is below `visibility.required`. -- * matches it as a consumer against pending producers and records a hazard on `atom.paths.hazards` when the gap is below `visibility.required`.
-- --
@@ -1621,8 +1554,8 @@ M.UNKNOWN_INSTRUCTION_CYCLES = 1
-- and is reserved for future "self-retires" relations. -- and is reserved for future "self-retires" relations.
-- --
-- Evidence: -- Evidence:
-- * `evidence.confidence` is one of `"exact"`, `"conservative"`, `"unknown"`. The severity comes from `violation_kind`; a hardware -- * `evidence.confidence` is one of `"exact"`, `"conservative"`, `"unknown"`. The severity comes from `violation_kind`;
-- measurement that the vendor caveats may still classify as `"conservative"` even when the underlying timing is numerically known. -- A hardware measurement that the vendor caveats may still classify as `"conservative"` even when the underlying timing is numerically known.
-- * `evidence.source` is the upstream reference (file + line range) the row is sourced from. New rows must carry this citation. -- * `evidence.source` is the upstream reference (file + line range) the row is sourced from. New rows must carry this citation.
-- --
-- Consumers: -- Consumers:
@@ -1732,22 +1665,42 @@ M.HARDWARE_RELATIONS = {
}, },
-- Memory -> COP2 data register (LWC2). -- Memory -> COP2 data register (LWC2).
-- The memory-side timing is not measured by the vendored GTE latch experiment, so this relation has no numeric retirement threshold. -- The memory-side timing is not measured by the vendored GTE latch experiment, so this relation has no numeric retirement threshold.
-- The forward walker emits one info edge at the first command-input consumer and then clears the pending relation. -- The LWC2 destination has TWO retirement regimes (per PSX-SPX):
-- * GTE-command consumer (`gte_cmdw_*`): the GTE pipeline LATCHES the LWC2 result, so a `gte_cmdw_*`
-- in the very next slot uses the latched value. Gap = 0 is allowed. (Per `docs/psx-spx/docs/gtepipelinetimings.md:271-274`.)
-- * Any other consumer: standard MIPS load delay applies. Gap = 1 required. (Per `docs/psx-spx/docs/cpuspecifications.md:407-419`.)
-- Two separate relations so the walker can dispatch by consumer type and emit different severities
-- (the GTE-command path is `info` because the latch is intentional; the non-GTE-consumer path is `error` because the missing nop is a real bug).
{ {
id = "lwc2_unknown_visibility", id = "lwc2_to_gte_command",
semantic = "LWC2", semantic = "LWC2_to_GTE",
token = "gte_lw", token = "gte_lw",
direction = "memory_to_cop2_data", direction = "memory_to_cop2_data",
reads = { domain = "memory", arg = 2 }, reads = { domain = "memory", arg = 2 },
writes = { domain = "cop2.data", arg = 1 }, writes = { domain = "cop2.data", arg = 1 },
visibility = { kind = "unknown_consumer", required = nil }, required = 0, -- GTE-command consumer: gap = 0 OK (latched).
evidence = { evidence = {
confidence = "unknown", confidence = "measured",
source = "gtepipelinetimings.md:271-274", source = "gtepipelinetimings.md:271-274",
}, },
violation_kind = "info", violation_kind = "info",
clear_on_consumer = true, clear_on_consumer = true,
}, },
{
id = "lwc2_to_other_consumer",
semantic = "LWC2_to_other",
token = "gte_lw",
direction = "memory_to_cop2_data",
reads = { domain = "memory", arg = 2 },
writes = { domain = "cop2.data", arg = 1 },
required = 1, -- Non-GTE-consumer: standard MIPS load delay.
evidence = {
confidence = "inferred",
source = "cpuspecifications.md:407-419",
},
violation_kind = "error",
clear_on_consumer = true,
},
-- COP2 data register -> memory (SWC2). A read of C2 state, not a CPU-to-COP2 write. -- COP2 data register -> memory (SWC2). A read of C2 state, not a CPU-to-COP2 write.
-- The policy row stays in for direction/provenance; staging it as a later command-input producer is suppressed. -- The policy row stays in for direction/provenance; staging it as a later command-input producer is suppressed.
{ {
@@ -2079,14 +2032,14 @@ local E_MAC_PREFIX_LEN = 4
--- Expand a body entry into the flat sequence of emitted machine-word events. --- Expand a body entry into the flat sequence of emitted machine-word events.
--- ---
--- Semantics (one event per emitted machine word): --- Semantics (one event per emitted machine word):
--- * **Direct one-word encoders** (`load_word`, `add_ui`, `nop`, `gte_lw`, ...): one event with `ident` = leading ident, `args` = parsed top-level args. --- * Direct one-word encoders `load_word`, `add_ui`, `nop`, `gte_lw`, ...: One event with `ident` = leading ident, `args` = parsed top-level args.
--- * **`nop2`** (2-word pseudo-instruction): two events, both with `ident = "nop"` so the recognized "this slot is a no-op" semantic is visible to downstream analyses. --- * `nop2` (2-word pseudo-instruction): Two events, both with `ident = "nop"` so the recognized "this slot is a no-op" semantic is visible to downstream analyses.
--- * **Any other N-word token** in `word_counts`: N events sharing the same `ident` + `args` so useful CPU words retire slots in the cycle budget. --- * Any other N-word token in `word_counts`: N events sharing the same `ident` + `args` so useful CPU words retire slots in the cycle budget.
--- * **Known `mac_X(...)` calls**: recursively expand the indexed component body, including nested components. Every event from the expansion carries: --- * Known `mac_X(...)` calls: Recursively expand the indexed component body, including nested components. Every event from the expansion carries:
--- - `source` / `line` = the COMPONENT'S source path + the line of the token within the component body (i.e. "definition site"). --- - `source` / `line` = the COMPONENT'S source path + the line of the token within the component body (i.e. "definition site").
--- - `call_source` / `call_line` = the ROOT atom's source path + call-site line, PRESERVED across recursion so nested events still point at the original root. --- - `call_source` / `call_line` = the ROOT atom's source path + call-site line, PRESERVED across recursion so nested events still point at the original root.
--- * **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. --- * 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, not emitted machine words). --- * 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).
--- ---
@@ -2110,12 +2063,12 @@ local E_MAC_PREFIX_LEN = 4
-- word_counts table is authored-metadata + current-component count table. -- word_counts table is authored-metadata + current-component count table.
--- @class EmissionProjection --- @class EmissionProjection
--- @field items table[] -- ordered stream of word|label|offset|invoke_begin|invoke_end --- @field items table[] -- Ordered stream of word|label|offset|invoke_begin|invoke_end
--- @field word_events table[] -- dense view of items where kind == "word" --- @field word_events table[] -- Dense view of items where kind == "word"
--- @field markers table[] -- dense view of items where kind == "label"|"offset" --- @field markers table[] -- Dense view of items where kind == "label"|"offset"
--- @field invocations InvocationRecord[] -- dense view of items where kind == "invoke_begin"|"invoke_end" --- @field invocations InvocationRecord[] -- dense view of items where kind == "invoke_begin"|"invoke_end"
--- @field errors table[] -- token-resolution failures surfaced without fail-loud --- @field errors table[] -- Token-resolution failures surfaced without fail-loud
--- @field warnings table[] -- opaque warnings (e.g. unknown uncounted macro) --- @field warnings table[] -- Opaque warnings (e.g. unknown uncounted macro)
--- @class InvocationRecord --- @class InvocationRecord
--- Lives at `atom.paths.invocations[*]`. Constructed once at the single invocation-construction site --- Lives at `atom.paths.invocations[*]`. Constructed once at the single invocation-construction site
@@ -2123,20 +2076,20 @@ local E_MAC_PREFIX_LEN = 4
--- @field id integer -- 1-based, monotonic per-atom invocation id (0 is reserved for "no open invocation") --- @field id integer -- 1-based, monotonic per-atom invocation id (0 is reserved for "no open invocation")
--- @field parent_id integer -- 0 for the outermost (root) call; otherwise the id of the immediately enclosing invocation --- @field parent_id integer -- 0 for the outermost (root) call; otherwise the id of the immediately enclosing invocation
--- @field kind string -- "comp_bare" | "comp_proc" (component form that triggered the expansion) --- @field kind string -- "comp_bare" | "comp_proc" (component form that triggered the expansion)
--- @field component_name string -- the bare component name without the `mac_` prefix --- @field component_name string -- Bare component name without the `mac_` prefix
--- @field call_text string -- the immediate `mac_X(...)` token text (or root call text for the outermost entry) --- @field call_text string -- Immediate `mac_X(...)` token text (or root call text for the outermost entry)
--- @field root_call_text string -- the IMMUTABLE outermost `mac_X(...)` token text for every word emitted in this call's expansion --- @field root_call_text string -- IMMUTABLE outermost `mac_X(...)` token text for every word emitted in this call's expansion
--- @field call_path string -- source path of the call site (root atom source for direct calls, component source for nested expansions) --- @field call_path string -- Source path of the call site (root atom source for direct calls, component source for nested expansions)
--- @field call_line integer -- source line of the call site --- @field call_line integer -- Source line of the call site
--- @field def_path string -- source path of the component definition --- @field def_path string -- Source path of the component definition
--- @field def_line integer -- source line of the component declaration --- @field def_line integer -- Source line of the component declaration
--- @field start_pos integer -- 0-based emitted-word position of the FIRST word inside this invocation (the value of `word_idx` AT `emit_invoke_begin` time, BEFORE the first word is emitted). Words emitted inside this invocation occupy `start_pos..start_pos+#body_lines-1` (inclusive, 0-based). Downstream DWARF/provenance consumers MUST read this; do NOT reconstruct it from `start_word` (which is the 1-based items index including `invoke_begin`/`invoke_end` markers). --- @field start_pos integer -- 0-based emitted-word position of the FIRST word inside this invocation (the value of `word_idx` AT `emit_invoke_begin` time, BEFORE the first word is emitted). Words emitted inside this invocation occupy `start_pos..start_pos+#body_lines-1` (inclusive, 0-based). Downstream DWARF/provenance consumers MUST read this; do NOT reconstruct it from `start_word` (which is the 1-based items index including `invoke_begin`/`invoke_end` markers).
--- @field end_pos integer -- 0-based position of the LAST word inside this invocation (set by `emit_invoke_end` to `word_idx - 1` AFTER all body words are emitted). --- @field end_pos integer -- 0-based position of the LAST word inside this invocation (set by `emit_invoke_end` to `word_idx - 1` AFTER all body words are emitted).
--- @field start_word integer -- 1-based items index of the `invoke_begin` item --- @field start_word integer -- 1-based items index of the `invoke_begin` item
--- @field end_word integer -- 1-based items index of the `invoke_end` item (set by `emit_invoke_end`) --- @field end_word integer -- 1-based items index of the `invoke_end` item (set by `emit_invoke_end`)
--- @field word_count integer -- number of `word` items emitted between `start_word` and `end_word` (inclusive) --- @field word_count integer -- Number of `word` items emitted between `start_word` and `end_word` (inclusive)
--- @field debug_skip boolean -- `debug_skip` stamp; true iff `corpus.components[name].debug_skip` is true at construction. Always boolean (never `nil`). --- @field debug_skip boolean -- `debug_skip` stamp; true iff `corpus.components[name].debug_skip` is true at construction. Always boolean (never `nil`).
--- @field errors table[] -- per-invocation construction errors (cycle / count_mismatch); does not include pass-level errors --- @field errors table[] -- Per-invocation construction errors (cycle / count_mismatch); does not include pass-level errors
-- Internal recursive walker. The items stream holds every emitted event in order; `word_events`, `markers`, -- Internal recursive walker. The items stream holds every emitted event in order; `word_events`, `markers`,
-- `invocations`, `errors`, `warnings` are dense views / side outputs appended alongside. -- `invocations`, `errors`, `warnings` are dense views / side outputs appended alongside.
@@ -2212,10 +2165,15 @@ local function _project_emission_inner(root_body_entry, ctx_table)
end end
local function emit_marker(kind, name, target, line, local function emit_marker(kind, name, target, line,
immediate_call_text, root_call_text_w) immediate_call_text, root_call_text_w,
consuming_encoder, consuming_arg_pos)
local inv_ids = open_invocation_ids_snapshot() local inv_ids = open_invocation_ids_snapshot()
local outermost = inv_ids[1] or 0 local outermost = inv_ids[1] or 0
-- Markers carry the open invocation stack snapshot. `call_text` / `root_call_text` belong to words, not markers — markers are zero-width and skip per-word call-site attribution. -- Markers carry the open invocation stack snapshot. `call_text` / `root_call_text` belong to words, not markers — markers are zero-width and skip per-word call-site attribution.
-- `consuming_encoder` + `consuming_arg_pos` carry the surrounding control-transfer instruction context
-- (e.g. `branch_le_zero` consuming its 3rd argument, or `jump` / `call_addr` consuming their only argument).
-- `passes/offsets.lua` reads these to dispatch per-consuming-instruction offset encoding.
-- nil for top-level markers (where the marker is the entire token — no surrounding consuming instruction).
local it = { local it = {
kind = kind, kind = kind,
name = name, name = name,
@@ -2225,46 +2183,111 @@ local function _project_emission_inner(root_body_entry, ctx_table)
outermost_invocation_id = outermost, outermost_invocation_id = outermost,
} }
if target ~= nil then it.target = target end if target ~= nil then it.target = target end
if consuming_encoder then it.consuming_encoder = consuming_encoder end
if consuming_arg_pos then it.consuming_arg_pos = consuming_arg_pos end
items[#items + 1] = it items[#items + 1] = it
markers[#markers + 1] = { markers[#markers + 1] = {
kind = kind, kind = kind,
name = name, name = name,
line = line, line = line,
word_index = word_idx, word_index = word_idx,
target = target, target = target,
consuming_encoder = consuming_encoder,
consuming_arg_pos = consuming_arg_pos,
} }
end end
local function emit_embedded_markers(tok, tok_line) -- Count top-level commas in `tok` between position `from_pos` (inclusive) and `to_pos` (exclusive).
-- Tracks paren depth so commas inside nested () don't count. Skips string literals + comments.
-- Used by `emit_embedded_markers` to compute `consuming_arg_pos` for each embedded marker.
local function count_top_level_commas(tok, from_pos, to_pos)
local depth = 0
local count = 0
local i = from_pos
while i < to_pos do
local c = tok:sub(i, i)
if c == "'" or c == '"' then
local next_pos = M.skip_str_or_cmt(tok, i)
i = (next_pos > i) and next_pos or (i + 1)
elseif c == "/" and tok:sub(i + 1, i + 1) == "/" then
-- line comment: skip to end of line
local nl = tok:find("\n", i, true)
i = (nl and nl + 1) or (#tok + 1)
elseif c == "/" and tok:sub(i + 1, i + 1) == "*" then
-- block comment: skip to matching */
local close = tok:find("*/", i + 2, true)
i = (close and close + 2) or (#tok + 1)
elseif c == "(" then
depth = depth + 1
i = i + 1
elseif c == ")" then
depth = depth - 1
i = i + 1
elseif c == "," and depth == 0 then
count = count + 1
i = i + 1
else
i = i + 1
end
end
return count
end
-- Find the position of the consuming instruction's open paren (the `(` that starts the consuming instruction's argument list).
-- Returns nil if the token's leading text isn't an ident followed by `(` (e.g. the ident is at the start of a non-instruction token).
local function find_consuming_paren(tok)
local i = 1
while i <= #tok do
local c = tok:sub(i, i)
if c == "(" then return i end
if not c:match("[%w_]") and c ~= " " then return nil end
i = i + 1
end
return nil
end
local function emit_embedded_markers(tok, tok_line, consuming_encoder)
-- When called with a non-nil `consuming_encoder`, the marker is nested inside that instruction's argument list.
-- We compute each marker's arg position by counting top-level commas between the consuming instruction's `(` and the marker's start.
local consuming_paren = nil
if consuming_encoder then consuming_paren = find_consuming_paren(tok) end
local pos = 1 local pos = 1
while pos <= #tok do while pos <= #tok do
-- trim leading whitespace and comments before each scan. -- Trim leading whitespace and comments before each scan.
pos = M.skip_ws_and_cmt(tok, pos) pos = M.skip_ws_and_cmt(tok, pos)
if pos > #tok then break end if pos > #tok then break end
local ident, after = M.read_ident(tok, pos) local ident, after = M.read_ident(tok, pos)
if not ident then if not ident then
-- not an ident: token is a string or comment; skip or one-step. -- Not an ident: token is a string or comment; skip or one-step.
local next_pos = M.skip_str_or_cmt(tok, pos) local next_pos = M.skip_str_or_cmt(tok, pos)
pos = (next_pos > pos) and next_pos or (pos + 1) pos = (next_pos > pos) and next_pos or (pos + 1)
goto continue_loop goto continue_loop
end end
if ident ~= "atom_label" and ident ~= "atom_offset" then if ident ~= "atom_label" and ident ~= "atom_offset" then
-- ordinary ident; nothing to emit, step past the ident only. -- Ordinary ident; nothing to emit, step past the ident only.
pos = after pos = after
goto continue_loop goto continue_loop
end end
-- marker ident: parse the (...) arguments. -- Marker ident: parse the (...) arguments.
local open = M.skip_ws_and_cmt(tok, after) local open = M.skip_ws_and_cmt(tok, after)
local inner, after_paren = M.read_parens(tok, open) local inner, after_paren = M.read_parens(tok, open)
if not inner then if not inner then
-- (...) unreadable: fall back to non-marker behavior. -- (...) Unreadable: fall back to non-marker behavior.
pos = after pos = after
goto continue_loop goto continue_loop
end end
-- commit: label takes 1 arg, offset takes 2. -- Commit: label takes 1 arg, offset takes 2.
-- For embedded markers, propagate the consuming_encoder + the marker's arg position
-- (1-based) so `passes/offsets.lua` can dispatch per-consuming-instruction offset encoding.
-- Top-level markers (no consuming_encoder) get nil for both — the offsets pass treats
-- them as branch-equivalent for backward compatibility.
local arg_pos = nil
if consuming_encoder and consuming_paren then
arg_pos = count_top_level_commas(tok, consuming_paren + 1, pos) + 1
end
local args = split_top_level_args(inner) local args = split_top_level_args(inner)
if ident == "atom_label" then emit_marker("label", args[1] or "", nil, tok_line) if ident == "atom_label" then emit_marker("label", args[1] or "", nil, tok_line, nil, nil, consuming_encoder, arg_pos)
else emit_marker("offset", args[1] or "", args[2] or "", tok_line) else emit_marker("offset", args[1] or "", args[2] or "", tok_line, nil, nil, consuming_encoder, arg_pos)
end end
pos = after_paren pos = after_paren
::continue_loop:: ::continue_loop::
@@ -2276,14 +2299,13 @@ local function _project_emission_inner(root_body_entry, ctx_table)
next_inv_id = next_inv_id + 1 next_inv_id = next_inv_id + 1
-- Invocation-level debug_skip stamp: Emission pass owns `atom.paths.invocations[*].debug_skip`. -- Invocation-level debug_skip stamp: Emission pass owns `atom.paths.invocations[*].debug_skip`.
-- The stamp is resolved from the `corpus.components[name]` registry (passed in via `ctx_table.components` by `emission_model.run`), -- The stamp is resolved from the `corpus.components[name]` registry (passed in via `ctx_table.components` by `emission_model.run`),
-- NOT from a parallel skip map, source-text re-parse, or second pass over `invocations`.
-- Unmarked components stamp `false` (not `nil`) so consumers can dispatch on the boolean without nil checks. -- Unmarked components stamp `false` (not `nil`) so consumers can dispatch on the boolean without nil checks.
-- --
-- The walker has already found the component body in `ctx_table.component_index[component_name]`, so the matching entry MUST exist in `ctx_table.components[component_name]` -- The walker has already found the component body in `ctx_table.component_index[component_name]`, so the matching entry MUST exist in `ctx_table.components[component_name]`
-- (both registries are populated from the same source by the components pass). -- (both registries are populated from the same source by the components pass).
-- A missing entry is a corpus-plumbing bug; we fail loudly here rather than silently stamp `false` and mask the regression. -- A missing entry is a corpus-plumbing bug; we fail loudly here rather than silently stamp `false` and mask the regression.
local components = ctx_table.components local components = ctx_table.components
local component_def = components and components[component_name] or nil local component_def = components and components[component_name] or nil
if not component_def then if not component_def then
error("duffle.emit_invoke_begin: component " .. string.format("%q", component_name) error("duffle.emit_invoke_begin: component " .. string.format("%q", component_name)
.. " is present in `component_index` (the walker matched a `mac_" .. component_name .. "()` call) but absent from `components` (the canonical corpus.components registry). " .. " is present in `component_index` (the walker matched a `mac_" .. component_name .. "()` call) but absent from `components` (the canonical corpus.components registry). "
@@ -2347,7 +2369,6 @@ local function _project_emission_inner(root_body_entry, ctx_table)
end end
-- Resolve the per-token word count. If unresolved, surface ONE warning -- Resolve the per-token word count. If unresolved, surface ONE warning
-- (NOT an error; the build does not fail-loud on an uncounted opaque word)
-- and fall back to 1 opaque word so the cycle budget still accounts for the slot. -- and fall back to 1 opaque word so the cycle budget still accounts for the slot.
local function resolve_count(ident, tok_line) local function resolve_count(ident, tok_line)
local wc = ctx_table.word_counts local wc = ctx_table.word_counts
@@ -2371,10 +2392,10 @@ local function _project_emission_inner(root_body_entry, ctx_table)
end end
-- Recursive walker: walk one body entry, possibly descending into components. -- Recursive walker: walk one body entry, possibly descending into components.
-- `walk_parent_inv_id` is the invocation ID of the enclosing call (0 for the root call). -- walk_parent_inv_id: Invocation ID of the enclosing call (0 for the root call).
-- `walk_root_call_text` is the outermost `mac_X(...)` token text (preserved across recursion). -- walk_root_call_text: Outermost `mac_X(...)` token text (preserved across recursion).
-- `walk_immediate_call_text` is the IMMEDIATE outer `mac_X(...)` token text for words emitted in this body — nil for the root atom body. -- walk_immediate_call_text: IMMEDIATE outer `mac_X(...)` token text for words emitted in this body — nil for the root atom body.
-- The two trackers are propagated as separate parameters so words deep inside nested expansions correctly identify both their immediate call site and the outermost call site. -- Two trackers are propagated as separate parameters so words deep inside nested expansions correctly identify both their immediate call site and the outermost call site.
local function walk_body_entry(body_entry, walk_parent_inv_id, local function walk_body_entry(body_entry, walk_parent_inv_id,
walk_root_call_text, walk_immediate_call_text) walk_root_call_text, walk_immediate_call_text)
local tokens = body_entry.body_tokens or {} local tokens = body_entry.body_tokens or {}
@@ -2391,9 +2412,19 @@ local function _project_emission_inner(root_body_entry, ctx_table)
local _, args = token_ident_and_args(tok) local _, args = token_ident_and_args(tok)
local tok_line = line_of(body_off + bt.rel) or 0 local tok_line = line_of(body_off + bt.rel) or 0
-- embedded markers live only in non-marker tokens. -- embedded markers live only in non-marker tokens.
if ident ~= "atom_label" and ident ~= "atom_offset" then emit_embedded_markers(tok, tok_line) end -- Pass `ident` as the consuming instruction so `emit_embedded_markers` can compute each marker's arg position + record the consuming_encoder for the offsets pass.
-- Canonicalize `jump_rel` to `branch_equal` (its preprocessor-expanded form) so the `consuming_encoder` metadata in marker records is canonical.
-- `jump_rel`: unconditional jump alias from `code/duffle/mips.h`.
local consuming_encoder_for_markers = (ident == "jump_rel") and "branch_equal" or ident
if ident ~= "atom_label" and ident ~= "atom_offset" then
emit_embedded_markers(tok, tok_line, consuming_encoder_for_markers)
end
-- atom_label / atom_offset: terminal markers, no further descent. -- atom_label / atom_offset: terminal markers, no further descent.
if ident == "atom_label" then emit_marker("label", args[1] or "", nil, tok_line); return -- Top-level markers (the marker IS the entire token) have no consuming instruction;
-- nil for both `consuming_encoder` and `consuming_arg_pos`.
-- The offsets pass treats these as branch-equivalent for backward compatibility.
-- TODO(Ed): Review this don't want legacy cruft here..
if ident == "atom_label" then emit_marker("label", args[1] or "", nil, tok_line); return
elseif ident == "atom_offset" then emit_marker("offset", args[1] or "", args[2] or "", tok_line); return elseif ident == "atom_offset" then emit_marker("offset", args[1] or "", args[2] or "", tok_line); return
end end
if ident:sub(1, 4) == "mac_" then if ident:sub(1, 4) == "mac_" then
@@ -2402,7 +2433,7 @@ local function _project_emission_inner(root_body_entry, ctx_table)
if comp then if comp then
local invocation_root_call_text = walk_root_call_text or tok local invocation_root_call_text = walk_root_call_text or tok
if ctx_table.visiting[bare] then if ctx_table.visiting[bare] then
-- cycle: still allocate inv_id, emit zero-width begin/end, record the cycle error; do NOT recurse. -- Cycle: still allocate inv_id, emit zero-width begin/end, record the cycle error; do NOT recurse.
local inv = emit_invoke_begin(comp.kind or "comp_bare", bare, tok, invocation_root_call_text, def_source, tok_line) local inv = emit_invoke_begin(comp.kind or "comp_bare", bare, tok, invocation_root_call_text, def_source, tok_line)
inv.parent_id = walk_parent_inv_id inv.parent_id = walk_parent_inv_id
inv.call_text = tok inv.call_text = tok
@@ -2417,16 +2448,16 @@ local function _project_emission_inner(root_body_entry, ctx_table)
emit_invoke_end(inv) emit_invoke_end(inv)
return return
end end
-- first visit: descend + count + count_mismatch-check below. -- First visit: descend + count + count_mismatch-check below.
ctx_table.visiting[bare] = true ctx_table.visiting[bare] = true
local inv = emit_invoke_begin(comp.kind or "comp_bare", bare, tok, invocation_root_call_text, def_source, tok_line) local inv = emit_invoke_begin(comp.kind or "comp_bare", bare, tok, invocation_root_call_text, def_source, tok_line)
inv.parent_id = walk_parent_inv_id inv.parent_id = walk_parent_inv_id
inv.call_text = tok inv.call_text = tok
inv.def_path = comp.source inv.def_path = comp.source
inv.def_line = comp.declaration inv.def_line = comp.declaration
-- propagate trackers into the recursive walk: -- Propagate trackers into the recursive walk:
-- immediate_call_text = this call's tok (the IMMEDIATE outer call for words emitted in this body) -- immediate_call_text = this call's tok (the IMMEDIATE outer call for words emitted in this body)
-- root_call_text = the OUTERMOST call (immutable across the recursion) -- root_call_text = the OUTERMOST call (immutable across the recursion)
walk_body_entry({ walk_body_entry({
body_tokens = comp.body_tokens or {}, body_tokens = comp.body_tokens or {},
body_off = comp.body_off or 0, body_off = comp.body_off or 0,
@@ -2439,7 +2470,7 @@ local function _project_emission_inner(root_body_entry, ctx_table)
tok) tok)
ctx_table.visiting[bare] = nil ctx_table.visiting[bare] = nil
emit_invoke_end(inv) emit_invoke_end(inv)
-- count `word` items inside [start_word, end_word]. -- Count `word` items inside [start_word, end_word].
local wc_inside = 0 local wc_inside = 0
for i = inv.start_word, inv.end_word do for i = inv.start_word, inv.end_word do
local it = items[i] local it = items[i]
@@ -2465,9 +2496,9 @@ local function _project_emission_inner(root_body_entry, ctx_table)
end end
-- mac_X NOT in component_index: fall through to opaque emit. -- mac_X NOT in component_index: fall through to opaque emit.
end end
-- direct encoder, or mac_X-without-component: resolve count + emit n words. -- Direct encoder, or mac_X-without-component: resolve count + emit n words.
-- resolve_count may emit a warning if the count is unresolved. -- Resolve_count may emit a warning if the count is unresolved.
local n = resolve_count(ident, tok_line) local n = resolve_count(ident, tok_line)
local out_ident = (ident == "nop2") and "nop" or ident local out_ident = (ident == "nop2") and "nop" or ident
for _ = 1, n do for _ = 1, n do
emit_word(out_ident, args, tok_line, tok, def_source, def_line, walk_immediate_call_text, walk_root_call_text) emit_word(out_ident, args, tok_line, tok, def_source, def_line, walk_immediate_call_text, walk_root_call_text)
@@ -2482,9 +2513,9 @@ local function _project_emission_inner(root_body_entry, ctx_table)
-- Initialize the per-walk mutable context. -- Initialize the per-walk mutable context.
-- `visiting` is the active DFS component stack; `root_call_path` / `root_call_line` are preserved across recursion so nested words always point at the -- `visiting` is the active DFS component stack; `root_call_path` / `root_call_line` are preserved across recursion so nested words always point at the
-- ORIGINAL root atom call site. -- ORIGINAL root atom call site.
ctx_table.visiting = ctx_table.visiting or {} ctx_table.visiting = ctx_table.visiting or {}
ctx_table.root_call_path = ctx_table.root_call_path or "" ctx_table.root_call_path = ctx_table.root_call_path or ""
ctx_table.root_call_line = ctx_table.root_call_line or 0 ctx_table.root_call_line = ctx_table.root_call_line or 0
-- Walk first; the pass caller stamps the root call site for direct words after the projection returns. -- Walk first; the pass caller stamps the root call site for direct words after the projection returns.
-- For nested words the def_path / def_line already point at the component source and MUST be preserved (the stamping helper checks for that). -- For nested words the def_path / def_line already point at the component source and MUST be preserved (the stamping helper checks for that).
+68 -84
View File
@@ -1,13 +1,8 @@
--- elf_dwarf.lua — ELF32 + DWARF + atoms source-map utilities. --- elf_dwarf.lua — ELF32 + DWARF + atoms source-map utilities.
--- All ELF32 + DWARF-specific code lives here.
---
--- **What this module contains:** --- **What this module contains:**
--- - **Format-constant tables** (the byte-offset / opcode / size encyclopedias for ELF32, DWARF4 aranges, DWARF5 rnglists, DWARF line-program, MIPS). --- - **Format-constant tables** (the byte-offset / opcode / size encyclopedias for ELF32, DWARF4 aranges, DWARF5 rnglists, DWARF line-program, MIPS).
--- Every constant carries a spec:` comment naming the spec section that defines it. --- Every constant carries a spec:` comment naming the spec section that defines it.
--- - **I/O helpers**: little-endian byte read/write, ELF32 section walker, nm symbol reader, source-map parser, native directory glob. --- - **I/O helpers**: little-endian byte read/write, ELF32 section walker, nm symbol reader, source-map parser, native directory glob.
---
--- **Conventions:** tabs (1/level), EmmyLua annotations, no regex,
--- Lua 5.3 compatible.
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Native dependencies -- Native dependencies
@@ -60,19 +55,19 @@ M.DW_AT = {
} }
M.DW_FORM = { M.DW_FORM = {
addr = 0x01, addr = 0x01,
data1 = 0x0B, data1 = 0x0B,
data2 = 0x05, data2 = 0x05,
data4 = 0x06, data4 = 0x06,
string = 0x08, string = 0x08,
strp = 0x0E, strp = 0x0E,
exprloc = 0x18, exprloc = 0x18,
ref4 = 0x13, ref4 = 0x13,
udata = 0x0F, udata = 0x0F,
ref_sig8 = 0x20, ref_sig8 = 0x20,
implicit_const = 0x21, implicit_const = 0x21,
flag_present = 0x19, flag_present = 0x19,
sec_offset = 0x17, sec_offset = 0x17,
} }
M.DW_ATE = { M.DW_ATE = {
@@ -104,13 +99,9 @@ M.MIPS_BYTES_PER_WORD = 0x04
-- ---------------------------------------------------------------------------- -- ----------------------------------------------------------------------------
-- ELF32 (System V ABI gABI v1.2) -- ELF32 (System V ABI gABI v1.2)
-- ---------------------------------------------------------------------------- -- ----------------------------------------------------------------------------
--- **Wire-offset contract:** format offsets, fixed-width reader offsets, LEB/parser cursors, --- **Wire-offset contract:** format offsets, fixed-width reader offsets, LEB/parser cursors, and section-relative values are zero-based wire offsets.
--- and section-relative values are zero-based wire offsets. Only Lua string APIs receive --- Only Lua string APIs receive a `+ 1` conversion at their boundary (`byte`, `sub`, and `find`).
--- 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.
---
--- ELF/DWARF field offsets are expressed in hex so they map directly to the
--- zero-based byte positions in the binary file.
--- spec: System V ABI gABI v1.2 §"ELF Header" (Table 1) + §"Section Header Table" --- spec: System V ABI gABI v1.2 §"ELF Header" (Table 1) + §"Section Header Table"
M.ELF32 = { M.ELF32 = {
@@ -192,8 +183,7 @@ M.DWARF_LINE_OPS = {
DW_LNE_end_sequence = 1, -- spec: §6.2.5.3 DW_LNE_end_sequence = 1, -- spec: §6.2.5.3
DW_LNE_set_address = 2, -- spec: §6.2.5.3 DW_LNE_set_address = 2, -- spec: §6.2.5.3
-- Standard opcode header (§6.2.5.1) -- Standard opcode header (§6.2.5.1)
-- opcode_base + line_range are 1-byte header fields; hex so they map -- opcode_base + line_range are 1-byte header fields; hex so they map directly to the line-program header byte sequence.
-- directly to the line-program header byte sequence.
-- line_base stays signed decimal (=-5) since 0xFB obscures the spec semantics. -- line_base stays signed decimal (=-5) since 0xFB obscures the spec semantics.
opcode_base = 0x0D, opcode_base = 0x0D,
line_base = -5, line_base = -5,
@@ -249,12 +239,10 @@ M.DWARF5_DEBUG_LINE = {
--- Read a 4-byte little-endian unsigned integer from `buf` at zero-based wire offset `off`. --- Read a 4-byte little-endian unsigned integer from `buf` at zero-based wire offset `off`.
--- Equivalent to `string.unpack("<I4", buf, off + 1)` but avoids the table-return shape + works under LuaJIT 2.1 --- Equivalent to `string.unpack("<I4", buf, off + 1)` but avoids the table-return shape + works under LuaJIT 2.1
--- (which has partial `string.unpack` coverage). --- (which has partial `string.unpack` coverage).
---
--- **Convention:** `off` is a zero-based wire offset; `+ 1` is applied only at the `string.byte` boundary. --- **Convention:** `off` is a zero-based wire offset; `+ 1` is applied only at the `string.byte` boundary.
--- ---
--- **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 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 0 contributes its value directly; byte 1 is shifted left by 8 (= 0x100); byte 2 by 16 (= 0x10000); byte 3 by 24 (= 0x1000000).
--- (= 0x100); byte 2 by 16 (= 0x10000); byte 3 by 24 (= 0x1000000).
--- @param buf string --- @param buf string
--- @param off integer -- zero-based wire offset --- @param off integer -- zero-based wire offset
--- @return integer --- @return integer
@@ -278,12 +266,11 @@ end
-- Pure-Lua 5.3 LEB128 readers (no `bit` library). `2^shift` arithmetic matches the existing parser. -- Pure-Lua 5.3 LEB128 readers (no `bit` library). `2^shift` arithmetic matches the existing parser.
-- Offsets are 0-based; returns (value, next_pos). -- Offsets are 0-based; returns (value, next_pos).
-- Promoted from `local function` to M.* exports so passes/dwarf_injection.lua -- Promoted from `local function` to M.* exports so passes/dwarf_injection.lua can import them as file-scope locals per the 2nd-caller lift precedent
-- can import them as file-scope locals per the 2nd-caller lift precedent
-- (the uleb128 + sleb128 encoders were promoted the same way). -- (the uleb128 + sleb128 encoders were promoted the same way).
function M.read_uleb128_at(buf, pos) function M.read_uleb128_at(buf, pos)
local value, shift = 0, 0 local value, shift = 0, 0
local len = #buf local len = #buf
while pos < len do while pos < len do
local b = buf:byte(pos + 1) local b = buf:byte(pos + 1)
value = value + (b % 0x80) * (2 ^ shift) value = value + (b % 0x80) * (2 ^ shift)
@@ -427,13 +414,13 @@ local function read_form_value(buf, str_buf, pos, form)
-- The constant is declared in the abbrev; no value bytes in the DIE. -- The constant is declared in the abbrev; no value bytes in the DIE.
return nil, pos return nil, pos
elseif form == M.DW_FORM.ref_sig8 then elseif form == M.DW_FORM.ref_sig8 then
-- DW_FORM_ref_sig8 (DWARF5 §7.4.2): an 8-byte value identifying a type -- DW_FORM_ref_sig8 (DWARF5 §7.4.2): An 8-byte value identifying a type by signature.
-- by signature. The low 4 bytes (LE) are the type signature (content hash); -- The low 4 bytes (LE) are the type signature (content hash);
-- the high 4 bytes (LE) are a CU-relative offset into the matching type unit. -- The high 4 bytes (LE) are a CU-relative offset into the matching type unit.
-- Consumers use the low 4 to look up the type unit (see M.find_type_unit_by_signature) -- Consumers use the low 4 to look up the type unit (see M.find_type_unit_by_signature)
-- then the high 4 to resolve the specific type within it. -- then the high 4 to resolve the specific type within it.
-- Return the low 4 as the primary value to preserve the (value, next_pos) shape; -- Return the low 4 as the primary value to preserve the (value, next_pos) shape;
-- the high 4 is exposed via M.read_ref_sig8 (which returns both halves). -- the high 4 is exposed via M.read_ref_sig8 (which returns both halves).
local _, _, next_pos = M.read_ref_sig8(buf, pos) local _, _, next_pos = M.read_ref_sig8(buf, pos)
return M.read_u32_le(buf, pos), next_pos return M.read_u32_le(buf, pos), next_pos
else else
@@ -470,11 +457,11 @@ end
-- @param target_sig_hi integer -- high 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 -- @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) function M.find_type_unit_by_signature(info, target_sig_lo, target_sig_hi)
local pos = 0 local pos = 0
local section_len = #info local section_len = #info
while pos + 4 < section_len do while pos + 4 < section_len do
local unit_length = M.read_u32_le(info, pos) local unit_length = M.read_u32_le(info, pos)
if unit_length == 0xFFFFFFFF then if unit_length == 0xFFFFFFFF then
return nil, nil -- DWARF64 not supported return nil, nil -- DWARF64 not supported
end end
-- unit_length is the body size, NOT including the 4-byte unit_length field itself. -- unit_length is the body size, NOT including the 4-byte unit_length field itself.
@@ -503,9 +490,9 @@ function M.find_type_unit_by_signature(info, target_sig_lo, target_sig_hi)
-- byte 8-15: type_signature (8) -- byte 8-15: type_signature (8)
-- byte 16-19: type_offset (4) -- byte 16-19: type_offset (4)
local unit_type = info:byte(body_start + 2 + 1) -- 0-based +2 = unit_type in 1-indexed local unit_type = info:byte(body_start + 2 + 1) -- 0-based +2 = unit_type in 1-indexed
if unit_type == 0x02 then -- DW_UT_type if unit_type == 0x02 then -- DW_UT_type
local sig_lo, sig_hi, _ = M.read_ref_sig8(info, body_start + 8) -- 0-based +8 = type_signature in 1-indexed local sig_lo, sig_hi, _ = M.read_ref_sig8(info, body_start + 8) -- 0-based +8 = type_signature in 1-indexed
if sig_lo == target_sig_lo and sig_hi == target_sig_hi then if sig_lo == target_sig_lo and sig_hi == target_sig_hi then
local type_offset = M.read_u32_le(info, body_start + 16) -- 0-based +16 = type_offset in 1-indexed local type_offset = M.read_u32_le(info, body_start + 16) -- 0-based +16 = type_offset in 1-indexed
return pos, type_offset return pos, type_offset
end end
@@ -571,14 +558,14 @@ function M.read_elf_sections(elf_path, section_names)
return result return result
end end
local f = io.open(elf_path, "rb") local f = io.open(elf_path, "rb")
if not f then if not f then
io.stderr:write(string.format("[elf_dwarf.read_elf_sections] io.open failed: %s\n", elf_path)) io.stderr:write(string.format("[elf_dwarf.read_elf_sections] io.open failed: %s\n", elf_path))
return result return result
end end
-- Read the ELF32 header. -- Read the ELF32 header.
local header = f:read(M.ELF32.header_bytes) local header = f:read(M.ELF32.header_bytes)
if not header or #header < M.ELF32.header_bytes then 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") io.stderr:write("[elf_dwarf.read_elf_sections] ELF too small for ELF32 header\n")
f:close() f:close()
@@ -610,7 +597,7 @@ function M.read_elf_sections(elf_path, section_names)
-- Read the section-header string table (.shstrtab) so we can resolve section names from their `sh_name` offsets. -- 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) f:seek("set", e_shoff + e_shstrndx * e_shentsize)
local strtab_hdr = f:read(e_shentsize) local strtab_hdr = f:read(e_shentsize)
if not strtab_hdr or #strtab_hdr < e_shentsize then if not strtab_hdr or #strtab_hdr < e_shentsize then
io.stderr:write("[elf_dwarf.read_elf_sections] could not read .shstrtab header\n") io.stderr:write("[elf_dwarf.read_elf_sections] could not read .shstrtab header\n")
f:close() f:close()
@@ -629,7 +616,7 @@ function M.read_elf_sections(elf_path, section_names)
for sh_idx = 0, e_shnum - 1 do for sh_idx = 0, e_shnum - 1 do
f:seek("set", e_shoff + sh_idx * e_shentsize) f:seek("set", e_shoff + sh_idx * e_shentsize)
local sh = f:read(e_shentsize) local sh = f:read(e_shentsize)
if not sh or #sh < e_shentsize then break end 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_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_offset = M.read_u32_le(sh, M.ELF32.sh_offset_offset)
@@ -679,15 +666,14 @@ function M.read_nm(elf_path)
local SYM_ST_INFO = 0x0C local SYM_ST_INFO = 0x0C
local n_syms = #symtab / SYM_ENTRY_BYTES local n_syms = #symtab / SYM_ENTRY_BYTES
for i = 0, n_syms - 1 do for i = 0, n_syms - 1 do
local entry_off = i * SYM_ENTRY_BYTES local entry_off = i * SYM_ENTRY_BYTES
local st_info = symtab:byte(entry_off + SYM_ST_INFO + 1) local st_info = symtab:byte(entry_off + SYM_ST_INFO + 1)
-- High nibble = binding (STB_LOCAL=0, STB_GLOBAL=1, STB_WEAK=2). -- 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 -- 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).
-- (LuaJIT's `>>` is 5.3+, but math.floor(x/16) works on all versions). local binding = math.floor(st_info / 16)
local binding = math.floor(st_info / 16) if binding == 0 or binding == 1 then -- STB_LOCAL or STB_GLOBAL
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)
local st_size = M.read_u32_le(symtab, entry_off + SYM_ST_SIZE) if st_size > 0 then
if st_size > 0 then
local st_name_off = M.read_u32_le(symtab, entry_off + SYM_ST_NAME) local st_name_off = M.read_u32_le(symtab, entry_off + SYM_ST_NAME)
-- Extract the name from .strtab (null-terminated C string). -- 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_end = strtab:find("\0", st_name_off + 1, true) or (st_name_off + 1)
@@ -779,8 +765,8 @@ function M.sleb128(n)
local b = n % (LEB_DATA_MASK + 1) -- extract low 7 bits local b = n % (LEB_DATA_MASK + 1) -- extract low 7 bits
n = (n - b) / (LEB_DATA_MASK + 1) -- arithmetic shift right by 7 n = (n - b) / (LEB_DATA_MASK + 1) -- arithmetic shift right by 7
-- Termination: remaining value bits fit in the sign bit of the last byte. -- Termination: remaining value bits fit in the sign bit of the last byte.
if n == 0 and b < SLEB_SIGN_BIT then more = false end -- positive terminator if n == 0 and b < SLEB_SIGN_BIT then more = false end -- positive terminator
if n == -1 and b >= SLEB_SIGN_BIT then more = false end -- negative terminator if n == -1 and b >= SLEB_SIGN_BIT then more = false end -- negative terminator
if more then b = b + LEB_CONT_BIT end if more then b = b + LEB_CONT_BIT end
bytes[#bytes + 1] = string.char(b) bytes[#bytes + 1] = string.char(b)
end end
@@ -811,14 +797,14 @@ end
--- @param n integer -- any integer (negative allowed) --- @param n integer -- any integer (negative allowed)
--- @return integer --- @return integer
function M.sleb128_size(n) function M.sleb128_size(n)
local more = true local more = true
local bytes = 0 local bytes = 0
local v = n local v = n
while more do while more do
local b = v % (LEB_DATA_MASK + 1) -- extract low 7 bits local b = v % (LEB_DATA_MASK + 1) -- extract low 7 bits
v = (v - b) / (LEB_DATA_MASK + 1) -- arithmetic shift right by 7 v = (v - b) / (LEB_DATA_MASK + 1) -- arithmetic shift right by 7
if v == 0 and b < SLEB_SIGN_BIT then more = false end -- positive terminator if v == 0 and b < SLEB_SIGN_BIT then more = false end -- positive terminator
if v == -1 and b >= SLEB_SIGN_BIT then more = false end -- negative terminator if v == -1 and b >= SLEB_SIGN_BIT then more = false end -- negative terminator
if more then b = b + LEB_CONT_BIT end if more then b = b + LEB_CONT_BIT end
bytes = bytes + 1 bytes = bytes + 1
end end
@@ -855,47 +841,45 @@ end
--- (which replaced the former hardcoded `ATOM_SOURCE_FILE_INDEX` + `PROVENANCE_BASENAME_TO_FILE_INDEX` table per `conductor/tracks/dwarf_file_index_lookup_20260731/`) --- (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. --- 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; --- @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`)
--- both shapes work since the splice preserves `.debug_line`)
--- @return table|nil, table|nil, table|nil --- @return table|nil, table|nil, table|nil
--- basename_to_index: { [basename] = 1-based-per-unit-file-index, ... } --- basename_to_index: { [basename] = 1-based-per-unit-file-index, ... }
--- basenames: { [1-based-per-unit-file-index] = basename, ... } --- basenames: { [1-based-per-unit-file-index] = basename, ... }
--- paths: { [1-based-per-unit-file-index] = full path (mixed slashes), ... } --- paths: { [1-based-per-unit-file-index] = full path (mixed slashes), ... }
function M.read_line_unit_file_table(elf_path) function M.read_line_unit_file_table(elf_path)
local sections = M.read_elf_sections(elf_path, { ".debug_line", ".debug_line_str" }) local sections = M.read_elf_sections(elf_path, { ".debug_line", ".debug_line_str" })
local line = sections[".debug_line"] local line = sections[".debug_line"]
local lstr = sections[".debug_line_str"] or "" local lstr = sections[".debug_line_str"] or ""
if not line or line == "" then if not line or line == "" then
io.stderr:write("[elf_dwarf.read_line_unit_file_table] no .debug_line section in: " .. tostring(elf_path) .. "\n") io.stderr:write("[elf_dwarf.read_line_unit_file_table] no .debug_line section in: " .. tostring(elf_path) .. "\n")
return nil return nil
end end
local basenames = {} local basenames = {}
local basename_to_index = {} local basename_to_index = {}
local paths = {} local paths = {}
--- Read one form-code's bytes from `buf` at position `p` according to `form`. --- Read one form-code's bytes from `buf` at position `p` according to `form`.
--- Returns (value, after) where `value` is: --- Returns (value, after) where `value` is:
--- * the resolved string (DW_FORM_line_strp / DW_FORM_string) --- * the resolved string (DW_FORM_line_strp / DW_FORM_string)
--- * the ULEB128 number (DW_FORM_udata) --- * the ULEB128 number (DW_FORM_udata)
--- * nil + skip-bytes (DW_FORM_data16; we don't surface the MD5) --- * nil + skip-bytes (DW_FORM_data16; we don't surface the MD5)
local function read_form(buf, lstr_buf, p, form) local function read_form(buf, lstr_buf, p, form)
if form == M.DWARF5_DEBUG_LINE.form_line_strp then if form == M.DWARF5_DEBUG_LINE.form_line_strp then
local strp = M.read_u32_le(buf, p) local strp = M.read_u32_le(buf, p)
local end_pos = lstr_buf:find("\0", strp + 1, true) or (#lstr_buf + 1) local end_pos = lstr_buf:find("\0", strp + 1, true) or (#lstr_buf + 1)
return lstr_buf:sub(strp + 1, end_pos - 1), p + M.DWARF5_DEBUG_LINE.form_strp_bytes return lstr_buf:sub(strp + 1, end_pos - 1), p + M.DWARF5_DEBUG_LINE.form_strp_bytes
elseif form == M.DWARF5_DEBUG_LINE.form_string then elseif form == M.DWARF5_DEBUG_LINE.form_string then
local nul = buf:find("\0", p + 1, true) or (#buf + 1) local nul = buf:find("\0", p + 1, true) or (#buf + 1)
return buf:sub(p + 1, nul - 1), nul return buf:sub(p + 1, nul - 1), nul
elseif form == M.DWARF5_DEBUG_LINE.form_udata then elseif form == M.DWARF5_DEBUG_LINE.form_udata then
local v, after = M.read_uleb128_at(buf, p) local v, after = M.read_uleb128_at(buf, p)
return v, after return v, after
elseif form == M.DWARF5_DEBUG_LINE.form_data16 then elseif form == M.DWARF5_DEBUG_LINE.form_data16 then
return nil, p + M.DWARF5_DEBUG_LINE.form_data16_bytes return nil, p + M.DWARF5_DEBUG_LINE.form_data16_bytes
else else
-- Unsupported form in a directory/file-table entry: best-effort skip. -- Unsupported form in a directory/file-table entry: best-effort skip.
-- We do NOT stderr-write because the crt0.s DWARF5 line unit (gcc-as emitted) uses DW_FORM_addr (0x01) for what is effectively a path entry, -- We do NOT stderr-write because the crt0.s DWARF5 line unit (gcc-as emitted) uses DW_FORM_addr (0x01) for what is effectively a path entry, which is non-standard.
-- which is non-standard.
-- The C-unit's DWARF3 paths are read via the parallel DWARF3 path and never see this error. -- The C-unit's DWARF3 paths are read via the parallel DWARF3 path and never see this error.
-- Callers should consult `basename_to_index` for the paths they care about and ignore this unit if it produced none. -- Callers should consult `basename_to_index` for the paths they care about and ignore this unit if it produced none.
return nil, p return nil, p
@@ -916,9 +900,9 @@ function M.read_line_unit_file_table(elf_path)
local dirs = {} local dirs = {}
while up < body_end do while up < body_end do
local nul = buf:find("\0", up + 1, true) or (body_end + 1) local nul = buf:find("\0", up + 1, true) or (body_end + 1)
if nul > body_end then break end if nul > body_end then break end
local len = nul - up - 1 local len = nul - up - 1
if len == 0 then up = nul break end if len == 0 then up = nul break end
dirs[#dirs + 1] = buf:sub(up + 1, nul - 1) dirs[#dirs + 1] = buf:sub(up + 1, nul - 1)
up = nul up = nul
end end
@@ -926,14 +910,14 @@ function M.read_line_unit_file_table(elf_path)
local unit_paths = {} local unit_paths = {}
while up < body_end do while up < body_end do
local nul = buf:find("\0", up + 1, true) or (body_end + 1) local nul = buf:find("\0", up + 1, true) or (body_end + 1)
if nul > body_end or nul == up + 1 then up = nul break end if nul > body_end or nul == up + 1 then up = nul break end
local path = buf:sub(up + 1, nul - 1) local path = buf:sub(up + 1, nul - 1)
up = nul up = nul
local didx, up_next = M.read_uleb128_at(buf, up); up = up_next local didx, up_next = M.read_uleb128_at(buf, up); up = up_next
local _time, up_next2 = M.read_uleb128_at(buf, up); up = up_next2 local _time, up_next2 = M.read_uleb128_at(buf, up); up = up_next2
local _size, up_next3 = M.read_uleb128_at(buf, up); up = up_next3 local _size, up_next3 = M.read_uleb128_at(buf, up); up = up_next3
local idx = #unit_basenames + 1 local idx = #unit_basenames + 1
local bs = path:match("[^/\\]+$") or path local bs = path:match("[^/\\]+$") or path
unit_paths[idx] = path unit_paths[idx] = path
unit_basenames[idx] = bs unit_basenames[idx] = bs
dirs[1] = dirs[1] or "" -- safety: gcc emits "" sentinel dir at 0 dirs[1] = dirs[1] or "" -- safety: gcc emits "" sentinel dir at 0
@@ -1006,7 +990,7 @@ function M.read_line_unit_file_table(elf_path)
local section_end = #line local section_end = #line
while p + 4 <= section_end do while p + 4 <= section_end do
local unit_length = M.read_u32_le(line, p) local unit_length = M.read_u32_le(line, p)
if unit_length == 0xFFFFFFFF then if unit_length == 0xFFFFFFFF then
io.stderr:write("[elf_dwarf.read_line_unit_file_table] 64-bit DWARF (initial-length 0xFFFFFFFF); not supported\n") io.stderr:write("[elf_dwarf.read_line_unit_file_table] 64-bit DWARF (initial-length 0xFFFFFFFF); not supported\n")
return nil return nil
end end
+95 -15
View File
@@ -1,41 +1,77 @@
# scripts/launch_pcsx_debug.ps1 # scripts/launch_pcsx_debug.ps1
# #
# One-shot launcher for debug sessions: starts pcsx-redux with the .ps-exe # 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 # loaded, the gdb stub enabled, the web server enabled, AND the
# so external CLI tools (gdb's `shell` command, etc.) # pcsx_debug_helper Lua plugin loaded so external CLI tools can drive
# can read GTE state via http://localhost:8080/api/v1/lua/gte # reloads via http://localhost:8080/api/v1/lua/reload.
# (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
# #
# After launch: # After launch:
# - gdb: target remote localhost:3333 # - 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). # Companion: scripts/debug_psyq.ps1 (bare launch — no .ps-exe, no helper).
[CmdletBinding()] [CmdletBinding()]
param( param(
[string]$PcsxPath = (Join-Path $PSScriptRoot '..\toolchain\pcsx-redux\vsprojects\x64\Release\pcsx-redux.exe'), [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'), [string]$HelperZip = (Join-Path $PSScriptRoot 'pcsx_debug_helper.zip'),
[int] $GdbPort = 3333, [int] $GdbPort = 3333,
[int] $WebPort = 8080 [int] $WebPort = 8080,
[ValidateSet('interpreter', 'dynarec')][string]$Cpu = 'interpreter'
) )
$ErrorActionPreference = 'Stop' $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 ── # ── Pre-checks ──
foreach ($p in @($PcsxPath, $ExePath, $HelperZip)) { foreach ($p in @($PcsxPath, $ExePath, $ElfPath, $HelperZip)) {
if (-not (Test-Path $p)) { if (-not (Test-Path -LiteralPath $p)) {
Write-Error "Missing: $p" Write-Error "Missing: $p"
exit 1 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. # Kill any existing pcsx-redux so the archive file isn't locked.
Get-Process pcsx-redux -ErrorAction SilentlyContinue | Stop-Process -Force Get-Process pcsx-redux -ErrorAction SilentlyContinue | Stop-Process -Force
Start-Sleep -Seconds 2 Start-Sleep -Seconds 2
@@ -44,17 +80,23 @@ Start-Sleep -Seconds 2
$absExe = [System.IO.Path]::GetFullPath($ExePath) $absExe = [System.IO.Path]::GetFullPath($ExePath)
$absZip = [System.IO.Path]::GetFullPath($HelperZip) $absZip = [System.IO.Path]::GetFullPath($HelperZip)
$cpuFlag = if ($Cpu -eq 'dynarec') { '-dynarec' } else { '-interpreter' }
$args = @( $args = @(
'-gdb', '-run' '-gdb', '-run'
'-loadexe', "`"$absExe`"" '-loadexe', "`"$absExe`""
'-archive', "`"$absZip`"" '-archive', "`"$absZip`""
'-webserver'
$cpuFlag
) )
Write-Host "Launching pcsx-redux..." -ForegroundColor Cyan Write-Host "Launching pcsx-redux..." -ForegroundColor Cyan
Write-Host " ps-exe : $absExe" Write-Host " ps-exe : $absExe"
Write-Host " elf : $ElfPath"
Write-Host " helper zip: $absZip" Write-Host " helper zip: $absZip"
Write-Host " gdb : localhost:$GdbPort" 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 "" Write-Host ""
Start-Process -FilePath $PcsxPath -ArgumentList $args | Out-Null 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 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 ""
Write-Host "pcsx-redux running. PIDs:" -ForegroundColor Cyan 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
}
}
+33 -44
View File
@@ -22,11 +22,11 @@ local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
--- @class SourceFile --- @class SourceFile
--- @field path string -- absolute path to the source file --- @field path string -- Absolute path to the source file
--- @field text string -- the full source text --- @field text string -- Full source text
--- @field dir string -- the directory containing the source --- @field dir string -- Directory containing the source
--- @field basename string -- filename without extension --- @field basename string -- Filename without extension
--- @field scan table -- pre-scanned SourceScan payload (from duffle.scan_source) --- @field scan table -- Pre-scanned SourceScan payload (from duffle.scan_source)
--- @class PassCtx --- @class PassCtx
--- @field sources SourceFile[] --- @field sources SourceFile[]
@@ -45,28 +45,28 @@ local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
--- @field warnings table[] --- @field warnings table[]
--- @class AtomAnnotation --- @class AtomAnnotation
--- @field line integer -- source line of the atom_info call --- @field line integer -- Source line of the atom_info call
--- @field macro string -- the macro name (always "atom_info" in the new shape) --- @field macro string -- Macro name (always "atom_info" in the new shape)
--- @field name string -- the atom name --- @field name string -- Atom name
--- @field kind string -- always "info" --- @field kind string -- Always "info"
--- @field binds string|nil -- Binds_X name if any --- @field binds string|nil -- Binds_X name if any
--- @field reads string[] -- R_* names (read targets) --- @field reads string[] -- R_* names (read targets)
--- @field writes string[] -- R_* names (write targets) --- @field writes string[] -- R_* names (write targets)
--- @field errors string[]|nil -- parse-time errors from scan_source (atom_info body malformed) --- @field errors string[]|nil -- Parse-time errors from scan_source (atom_info body malformed)
--- @class DebugSkipMarker -- sub-shape of scan_source.lua's @class DebugSkipMarker --- @class DebugSkipMarker -- Sub-shape of scan_source.lua's @class DebugSkipMarker
--- @field marker_kind string -- exact marker ident read from source. Only "atom_dbg_skip" (bare) is positive. --- @field marker_kind string -- Exact marker ident read from source. Only "atom_dbg_skip" (bare) is positive.
--- @field marker_line integer --- @field marker_line integer
--- @field args string|nil -- trimmed text inside the parens (nil when has_parens is false) --- @field args string|nil -- Trimmed text inside the parens (nil when has_parens is false)
--- @field has_parens boolean --- @field has_parens boolean
--- @field is_bare boolean -- true iff marker_kind == "atom_dbg_skip" AND has_parens == false (the only positive form) --- @field is_bare boolean -- true iff marker_kind == "atom_dbg_skip" AND has_parens == false (the only positive form)
--- @field pending boolean -- true while awaiting the following declaration --- @field pending boolean -- true while awaiting the following declaration
--- @field superseded_by_marker_line integer|nil -- set on a marker that was bumped out of the pending slot --- @field superseded_by_marker_line integer|nil -- Set on a marker that was bumped out of the pending slot
--- @field target_kind string|nil -- "atom" | "comp_bare" | "comp_proc" | "unrelated" once observed --- @field target_kind string|nil -- "atom" | "comp_bare" | "comp_proc" | "unrelated" once observed
--- @class Finding --- @class Finding
--- @field line integer -- source line (or 0 for pass-level) --- @field line integer -- Source line (or 0 for pass-level)
--- @field msg string -- finding message --- @field msg string -- Finding message
--- @class Findings --- @class Findings
--- @field errors Finding[] --- @field errors Finding[]
@@ -74,14 +74,14 @@ local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
--- @field info Finding[] --- @field info Finding[]
--- @class PipeCtx --- @class PipeCtx
--- @field atom_index table<string, AtomAnnotation> -- name -> AtomAnnotation (only kind=="atom") --- @field atom_index table<string, AtomAnnotation> -- Name -> AtomAnnotation (only kind=="atom")
--- @field binds_index table<string, BindsStruct> -- name -> BindsStruct --- @field binds_index table<string, BindsStruct> -- Name -> BindsStruct
--- @field annot_counts table<string, integer> -- name -> annotation count (for unique_annotation check) --- @field annot_counts table<string, integer> -- Name -> annotation count (for unique_annotation check)
--- @field types table<string, RegTypeDefault> -- from scan_source --- @field types table<string, RegTypeDefault> -- From scan_source
--- @field atom_views table<string, AtomViewEntry> -- from scan_source --- @field atom_views table<string, AtomViewEntry> -- From scan_source
--- @field seen_defaults table<string, integer> -- duplicate atom_dbg_reg_default detection --- @field seen_defaults table<string, integer> -- Duplicate atom_dbg_reg_default detection
--- @field seen_field table<string, integer> -- Binds_* -> count of fields (set/checked by check_binds_no_duplicate_fields) --- @field seen_field table<string, integer> -- Binds_* -> count of fields (set/checked by check_binds_no_duplicate_fields)
--- @field _scan SourceScan -- full scan payload (typed-view sub-calls live here) --- @field _scan SourceScan -- Full scan payload (typed-view sub-calls live here)
--- @class AnnotatedResult --- @class AnnotatedResult
--- @field atoms AtomEntry[] --- @field atoms AtomEntry[]
@@ -95,11 +95,10 @@ local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Per-check functions (the CHECK_RULES table's payload) -- Per-check functions (the CHECK_RULES table's payload)
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
--
--- The dispatcher in `validate()` routes each result by convention: existence checks write errors[] and shape checks write warnings[]. --- The dispatcher in `validate()` routes each result by convention: existence checks write errors[] and shape checks write warnings[].
--- `macro_word_drift` writes errors[] for missing or mismatched metadata and info[] for a match. --- `macro_word_drift` writes errors[] for missing or mismatched metadata and info[] for a match.
--- Check: every annotated atom must have a matching MipsAtom_(name) declaration. --- Check: Every annotated atom must have a matching MipsAtom_(name) declaration.
--- @param a AtomAnnotation --- @param a AtomAnnotation
--- @param pipe_ctx PipeCtx --- @param pipe_ctx PipeCtx
--- @param findings Findings --- @param findings Findings
@@ -112,8 +111,8 @@ local function check_atom_decl_exists(a, pipe_ctx, findings)
end end
end end
--- Check: every atom may have AT MOST ONE annotation. --- Check: Every atom may have AT MOST ONE annotation.
--- Post-loop: needs full-corpus `annot_counts` from pipe_ctx. --- Post-loop: Needs full-corpus `annot_counts` from pipe_ctx.
--- @param pipe_ctx PipeCtx --- @param pipe_ctx PipeCtx
--- @param findings Findings --- @param findings Findings
local function check_unique_annotation(pipe_ctx, findings) local function check_unique_annotation(pipe_ctx, findings)
@@ -146,7 +145,7 @@ end
--- Check: TAPE_WORDS(mac_X, N) ↔ WORD_COUNT(mac_X, N) drift. --- Check: TAPE_WORDS(mac_X, N) ↔ WORD_COUNT(mac_X, N) drift.
--- Three outcomes: missing (error), mismatch (error), match (info). --- Three outcomes: missing (error), mismatch (error), match (info).
--- @param m MacroEntry --- @param m MacroEntry
--- @param wc table<string, integer> -- the shared word-count table (from ctx.shared.word_counts) --- @param wc table<string, integer> -- Shared word-count table (from ctx.shared.word_counts)
--- @param findings Findings --- @param findings Findings
local function check_macro_word_drift(m, wc, findings) local function check_macro_word_drift(m, wc, findings)
local declared = wc[m.name] local declared = wc[m.name]
@@ -304,7 +303,7 @@ local function check_binds_no_duplicate_fields(_src, pipe_ctx, findings)
end end
end end
-- Check: debug-skip markers must satisfy shape + placement constraints. -- Check: Debug-skip markers must satisfy shape + placement constraints.
--- Walks the priority list once; each marker produces at most one error, so one source defect yields one finding. --- Walks the priority list once; each marker produces at most one error, so one source defect yields one finding.
--- Priority order (first defect wins): --- Priority order (first defect wins):
--- 1. marker_kind ~= "atom_dbg_skip" -> legacy/renamed spelling (use `atom_dbg_skip`) --- 1. marker_kind ~= "atom_dbg_skip" -> legacy/renamed spelling (use `atom_dbg_skip`)
@@ -315,12 +314,11 @@ end
--- 6. unsupported target_kind -> marker precedes an unrelated declaration --- 6. unsupported target_kind -> marker precedes an unrelated declaration
--- Valid markers stamp `debug_skip` on whole-atom, bare-component, and proc-component declaration records in scan_source.lua. --- Valid markers stamp `debug_skip` on whole-atom, bare-component, and proc-component declaration records in scan_source.lua.
--- @param marker DebugSkipMarker --- @param marker DebugSkipMarker
--- @param _pipe_ctx PipeCtx -- unused today; kept for plex-shape consistency with per_annot --- @param _pipe_ctx PipeCtx -- Unused; kept for consistency with per_annot // TODO(Ed): Remove?
--- @param findings Findings --- @param findings Findings
local function check_skip_marker(marker, _pipe_ctx, findings) local function check_skip_marker(marker, _pipe_ctx, findings)
local kind = marker.marker_kind local kind = marker.marker_kind
local line = marker.marker_line local line = marker.marker_line
-- Left `scan.debug_skip_markers` with production records for `atom_dbg_skip` only; other identifiers take the walker's unrelated branch. -- Left `scan.debug_skip_markers` with production records for `atom_dbg_skip` only; other identifiers take the walker's unrelated branch.
if marker.has_parens then if marker.has_parens then
@@ -371,8 +369,6 @@ local function check_skip_marker(marker, _pipe_ctx, findings)
end end
--- Warn when a source references an unregistered alias. --- Warn when a source references an unregistered alias.
---
--- R_TapePtr, R_AtomJmp, R_PrimCursor, R_FaceCursor, R_VertBase, and R_OtBase opt in through `#define atom_reg` in lottes_tape.h.
--- When a source uses an unregistered R_X, this check emits one pass-level info entry for that source and directs C-ABI register names to explicit alias registration. --- When a source uses an unregistered R_X, this check emits one pass-level info entry for that source and directs C-ABI register names to explicit alias registration.
--- @param _src SourceFile --- @param _src SourceFile
--- @param pipe_ctx PipeCtx --- @param pipe_ctx PipeCtx
@@ -426,8 +422,7 @@ local CHECK_RULES = {
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Validation -- Validation
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- -- Pure check: Read from src.scan, run validations, emit findings. The scan was done once upstream.
-- Pure check: read from src.scan, run validations, emit findings. The scan was done once upstream.
--- Builds one pass-wide pipe_ctx from the merged `corpus.*` registries and source-ordered `corpus.atom_infos`; per-source declarations and bodies remain in `src.scan`. --- Builds one pass-wide pipe_ctx from the merged `corpus.*` registries and source-ordered `corpus.atom_infos`; per-source declarations and bodies remain in `src.scan`.
--- The module ownership contract above requires callers to construct `ctx.shared.corpus` through `build_ctx`; the error message below enforces that gate. --- The module ownership contract above requires callers to construct `ctx.shared.corpus` through `build_ctx`; the error message below enforces that gate.
@@ -473,7 +468,7 @@ end
--- Validate one source against its pre-scanned SourceScan payload + the corpus-wide pipe_ctx. --- Validate one source against its pre-scanned SourceScan payload + the corpus-wide pipe_ctx.
--- @param ctx PassCtx --- @param ctx PassCtx
--- @param src SourceFile --- @param src SourceFile
--- @param corpus_pipe_ctx PipeCtx|nil -- built once per pass from corpus registries; nil builds the same projection here. --- @param corpus_pipe_ctx PipeCtx|nil -- Built once per pass from corpus registries; nil builds the same projection here.
--- @return AnnotatedResult --- @return AnnotatedResult
local function validate(ctx, src, corpus_pipe_ctx) local function validate(ctx, src, corpus_pipe_ctx)
corpus_pipe_ctx = corpus_pipe_ctx or build_corpus_pipe_ctx(ctx) corpus_pipe_ctx = corpus_pipe_ctx or build_corpus_pipe_ctx(ctx)
@@ -503,14 +498,8 @@ local function validate(ctx, src, corpus_pipe_ctx)
end end
-- Build a per-source pipe_ctx: shared lookups come from `corpus_pipe_ctx`, while declarations, bodies, types, views, defaults, and occurrences come from `src.scan`. -- Build a per-source pipe_ctx: shared lookups come from `corpus_pipe_ctx`, while declarations, bodies, types, views, defaults, and occurrences come from `src.scan`.
local seen_defaults = {} local seen_defaults = {}; for reg, _ in pairs (scan.types or {}) do seen_defaults[reg] = (seen_defaults[reg] or 0) + 1 end
for reg, _ in pairs(scan.types or {}) do local atom_infos_list = {}; for _, ai in ipairs(scan.atom_infos or {}) do atom_infos_list[#atom_infos_list + 1] = ai end
seen_defaults[reg] = (seen_defaults[reg] or 0) + 1
end
local atom_infos_list = {}
for _, ai in ipairs(scan.atom_infos or {}) do
atom_infos_list[#atom_infos_list + 1] = ai
end
local pipe_ctx = { local pipe_ctx = {
atom_index = {}, atom_index = {},
+32 -36
View File
@@ -1,8 +1,8 @@
--- passes/atoms_source_map.lua — Per-.word source-line map emitter for tape atoms. --- passes/atoms_source_map.lua — Per-.word source-line map emitter for tape atoms.
--- ---
--- Writer: this pass, given `atom.paths` (the per-atom mutable surface owned by `emission_model`). Readers: --- Writer: this pass, given `atom.paths` (the per-atom mutable surface owned by `emission_model`). Readers:
--- `passes/dwarf_injection.lua` (synthesizes DW_TAG_inlined_subroutine + per-word line program rows) and the gdb-runtime --- `passes/dwarf_injection.lua` (synthesizes DW_TAG_inlined_subroutine + per-word line program rows) and
--- wrapper at `scripts/gdb/gdb_tape_atoms.gdb` (loads the source map via `source <path>`). --- the gdb-runtime wrapper at `scripts/gdb/gdb_tape_atoms.gdb` (loads the source map via `source <path>`).
--- ---
--- Inputs from `atom.paths`: the ordered `items` stream, dense `word_events`, `invocations` views. Outputs: --- Inputs from `atom.paths`: the ordered `items` stream, dense `word_events`, `invocations` views. Outputs:
--- one `WORD N LINE L TEXT T` line per emitted `.word`, plus the per-word provenance form that DWARF synthesis consumes. --- one `WORD N LINE L TEXT T` line per emitted `.word`, plus the per-word provenance form that DWARF synthesis consumes.
@@ -28,7 +28,6 @@
--- ... --- ...
--- ENDATOM --- ENDATOM
--- ``` --- ```
---
--- Marker records are zero-width in `atom.paths.items`, so they emit no WORD rows in the dense word view. --- Marker records are zero-width in `atom.paths.items`, so they emit no WORD rows in the dense word view.
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
@@ -39,8 +38,8 @@
-- (works both standalone + when require'd). `duffle_paths.lua` sets package.path then returns `require("duffle")` -- (works both standalone + when require'd). `duffle_paths.lua` sets package.path then returns `require("duffle")`
-- at the bottom, so the dofile value IS the duffle module. -- at the bottom, so the dofile value IS the duffle module.
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./"
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
local elf_dwarf = require("elf_dwarf") local elf_dwarf = require("elf_dwarf")
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Constants -- Constants
@@ -69,8 +68,8 @@ local FORMAT_VERSION = 1
--- @param atom table --- @param atom table
--- @return table[], integer --- @return table[], integer
local function canonical_word_entries(atom) local function canonical_word_entries(atom)
local paths = atom.paths or {} local paths = atom.paths or {}
local events = paths.word_events or {} local events = paths.word_events or {}
local word_items = {} local word_items = {}
for _, item in ipairs(paths.items or {}) do for _, item in ipairs(paths.items or {}) do
if item.kind == "word" then word_items[#word_items + 1] = item end if item.kind == "word" then word_items[#word_items + 1] = item end
@@ -95,41 +94,38 @@ end
--- Render one atom's provenance stanza. Format 1 line shapes: --- Render one atom's provenance stanza. Format 1 line shapes:
--- `WORD N CALL <src-path>:<src-line> MACRO <name> "<def-path>:<def-line>" BODY <line>` (component invocation) --- `WORD N CALL <src-path>:<src-line> MACRO <name> "<def-path>:<def-line>" BODY <line>` (component invocation)
--- `WORD N CALL <src-path>:<src-line> RAW` (raw `.word` outside any mac_* component) --- `WORD N CALL <src-path>:<src-line> RAW` (raw `.word` outside any mac_* component)
--- Component identity comes from the outermost invocation record; the count-table lookup confirms the component was --- Component identity comes from the outermost invocation record; the count-table lookup confirms the component was declared in `corpus.word_counts`
--- declared in `corpus.word_counts` (populated by word_count_eval + components passes). --- (populated by word_count_eval + components passes).
--- @param src table --- @param src table
--- @param atom table --- @param atom table
--- @param wc table -- identity alias of corpus.word_counts --- @param wc table -- identity alias of corpus.word_counts
--- @return string[], integer --- @return string[], integer
local function emit_provenance_stanza(src, atom, wc) local function emit_provenance_stanza(src, atom, wc)
local lines = {} local lines = {}
local rel_path = src.path:gsub("\\\\", "/") local rel_path = src.path:gsub("\\\\", "/")
local entries, total = canonical_word_entries(atom) local entries, total = canonical_word_entries(atom)
lines[#lines + 1] = string.format('ATOM %s "%s" 0', atom.raw_name or atom.name, rel_path) lines[#lines + 1] = string.format('ATOM %s "%s" 0', atom.raw_name or atom.name, rel_path)
for _, entry in ipairs(entries) do for _, entry in ipairs(entries) do
local inv = entry.invocation local inv = entry.invocation
local macro_count = inv and wc["mac_" .. inv.component_name] local macro_count = inv and wc["mac_" .. inv.component_name]
if inv and macro_count ~= nil then if inv and macro_count ~= nil then
lines[#lines + 1] = string.format( lines[#lines + 1] = string.format('WORD %d CALL %s:%d MACRO %s "%s:%d" BODY %d'
'WORD %d CALL %s:%d MACRO %s "%s:%d" BODY %d', , entry.pos, rel_path, entry.line, inv.component_name
entry.pos, rel_path, entry.line, inv.component_name, , inv.def_path or "", inv.def_line or 0, entry.body_line)
inv.def_path or "", inv.def_line or 0, entry.body_line)
else else
lines[#lines + 1] = string.format( lines[#lines + 1] = string.format("WORD %d CALL %s:%d RAW", entry.pos, rel_path, entry.line)
"WORD %d CALL %s:%d RAW", entry.pos, rel_path, entry.line)
end end
end end
lines[1] = lines[1]:gsub(" 0$", " " .. tostring(total)) lines[1] = lines[1]:gsub(" 0$", " " .. tostring(total))
lines[#lines + 1] = "ENDATOM" lines[#lines + 1] = "ENDATOM"
return lines, total return lines, total
end end
--- Render the full provenance file content for one source. --- Render the full provenance file content for one source.
--- @param src table --- @param src table
--- @param wc table --- @param wc table
--- @return string --- @return string
local function render_provenance(src, wc) local function render_provenance(src, wc)
local lines = {} local lines = {}
@@ -162,8 +158,8 @@ end
--- @param wc table --- @param wc table
--- @return string[], integer --- @return string[], integer
local function emit_atom_stanza(src, atom) local function emit_atom_stanza(src, atom)
local lines = {} local lines = {}
local rel_path = src.path:gsub("\\\\", "/") local rel_path = src.path:gsub("\\\\", "/")
local entries, total = canonical_word_entries(atom) local entries, total = canonical_word_entries(atom)
lines[#lines + 1] = string.format('ATOM %s "%s" 0', atom.raw_name or atom.name, rel_path) lines[#lines + 1] = string.format('ATOM %s "%s" 0', atom.raw_name or atom.name, rel_path)
@@ -179,10 +175,10 @@ end
--- Render the full source map file content for one source (one .atoms.sourcemap.txt per source). Mirrors offsets.lua's --- Render the full source map file content for one source (one .atoms.sourcemap.txt per source). Mirrors offsets.lua's
--- `project_atoms` shape: scan.atoms + scan.raw_atoms, no kind filter. --- `project_atoms` shape: scan.atoms + scan.raw_atoms, no kind filter.
--- @param src table --- @param src table
--- @param wc table --- @param wc table
--- @return string --- @return string
local function render_source_map(src) local function render_source_map(src)
local lines = {} local lines = {}
lines[#lines + 1] = "# FORMAT_VERSION " .. FORMAT_VERSION lines[#lines + 1] = "# FORMAT_VERSION " .. FORMAT_VERSION
lines[#lines + 1] = "# auto-generated by ps1_meta.lua (passes/atoms_source_map.lua) — DO NOT EDIT" lines[#lines + 1] = "# auto-generated by ps1_meta.lua (passes/atoms_source_map.lua) — DO NOT EDIT"
@@ -216,16 +212,16 @@ end
--- @param ctx PassCtx --- @param ctx PassCtx
--- @return table[] -- list of {idx, name, src_path, file_base, addr, size_bytes, words, entries} --- @return table[] -- list of {idx, name, src_path, file_base, addr, size_bytes, words, entries}
local function build_atom_table(ctx) local function build_atom_table(ctx)
local addrs = elf_dwarf.read_nm(ctx.flags.elf_path) local addrs = elf_dwarf.read_nm(ctx.flags.elf_path)
local corpus = ctx.shared and ctx.shared.corpus local corpus = ctx.shared and ctx.shared.corpus
local matched = {} local matched = {}
for _, src in ipairs(corpus.source_order or {}) do for _, src in ipairs(corpus.source_order or {}) do
local file_base = src.path:match("([^/\\\\]+)$") or src.path local file_base = src.path:match("([^/\\\\]+)$") or src.path
local function append(atom) local function append(atom)
if not atom.paths then return end if not atom.paths then return end
local name = atom.raw_name or atom.name local name = atom.raw_name or atom.name
local info = addrs[name] local info = addrs[name]
if not info then return end if not info then return end
local entries, total = canonical_word_entries(atom) local entries, total = canonical_word_entries(atom)
matched[#matched + 1] = { matched[#matched + 1] = {
@@ -248,9 +244,10 @@ local function build_atom_table(ctx)
return matched return matched
end end
--- Append the 9 gdb command definitions to `lines`. Pure gdb scripting — addresses come from `nm`, the convenience --- Append the 9 gdb command definitions to `lines`. Pure gdb scripting — addresses come from `nm`,
--- vars set in `emit_gdb_runtime` provide printf args, and each command is a static sequence of `printf` / `tbreak` / --- the convenience vars set in `emit_gdb_runtime` provide printf args, and
--- `if ... end` blocks. The Lua pass emits N atoms' worth of lines; runtime iteration is gdb's job. --- each command is a static sequence of `printf` / `tbreak` / `if ... end` blocks.
--- The Lua pass emits N atoms' worth of lines; runtime iteration is gdb's job.
--- ---
--- Why hardcoded per-atom: gdb's `$` substitution doesn't concat inside var names — `$__atom_name_$__i` in a `while` --- Why hardcoded per-atom: gdb's `$` substitution doesn't concat inside var names — `$__atom_name_$__i` in a `while`
--- loop resolves to one literal identifier, not `name_i`. Compile-time emission is the only path. --- loop resolves to one literal identifier, not `name_i`. Compile-time emission is the only path.
@@ -395,8 +392,7 @@ local function append_gdb_commands(lines, matched)
end end
--- Emit the gdb-runtime file (post-link). Pure gdb scripting — addresses come from `mipsel-none-elf-nm -S`, get embedded --- Emit the gdb-runtime file (post-link). Pure gdb scripting — addresses come from `mipsel-none-elf-nm -S`, get embedded
--- in `<ctx.out_root>/gdb_tape_atoms_runtime.gdb`, and load via `set $var = ...` + `define ... end` blocks at gdb --- in `<ctx.out_root>/gdb_tape_atoms_runtime.gdb`, and load via `set $var = ...` + `define ... end` blocks at gdb source-time.
--- source-time.
--- @param ctx PassCtx --- @param ctx PassCtx
local function emit_gdb_runtime(ctx) local function emit_gdb_runtime(ctx)
if not (ctx.flags and ctx.flags.gdb_runtime) then return end if not (ctx.flags and ctx.flags.gdb_runtime) then return end
+208 -71
View File
@@ -6,10 +6,9 @@
--- Reads the pre-scanned SourceScan payload from `duffle.scan_source` for `MipsAtomComp_(ac_X)` and `MipsAtomComp_Proc_(ac_X, { body })` declarations, --- 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. --- then resolves the function-args string from the preceding `FI_ Slice_MipsCode ac_X(...)` declaration via a backward walk.
--- ---
--- Emits one `<dir_basename>.macs.h` per source with `#define mac_X(sig) \` macros plus `WORD_COUNT(mac_X, N)` entries for downstream offset computation. --- 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).
--- **Conventions**: tabs (1/level), EmmyLua annotations, no regex, --- The directory itself is the namespace, so the filename does not repeat the module name.
--- Lua 5.3 compatible.
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Module-scope requires + package.path setup -- Module-scope requires + package.path setup
@@ -41,43 +40,44 @@ local MAC_PREFIX_LEN = 4
local BYTE_NEWLINE = 10 local BYTE_NEWLINE = 10
local BYTE_SLASH = 47 local BYTE_SLASH = 47
-- Source dir basename used as the output `.macs.h` filename. -- Output gen subdirectory + filename (per-directory aggregation; the directory name is the namespace).
local GEN_SUBDIR = "gen" local GEN_SUBDIR = "gen"
local MACS_FILENAME = "macs.h"
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Type declarations -- Type declarations
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
--- @class SourceFile --- @class SourceFile
--- @field path string -- absolute path to the source file --- @field path string -- Absolute path to the source file
--- @field text string -- the full source text --- @field text string -- Full source text
--- @field dir string -- the directory containing the source --- @field dir string -- Directory containing the source
--- @field basename string -- filename without extension --- @field basename string -- Filename without extension
--- @field scan table -- pre-scanned SourceScan payload (from duffle.scan_source) --- @field scan table -- Pre-scanned SourceScan payload (from duffle.scan_source)
--- @class PassCtx --- @class PassCtx
--- @field sources SourceFile[] -- all source files in the build --- @field sources SourceFile[] -- All source files in the build
--- @field metadata_path string -- path to word_count.metadata.h --- @field metadata_path string -- Path to word_count.metadata.h
--- @field shared table -- cross-pass shared state --- @field shared table -- Cross-pass shared state
--- @field out_root string -- output root (e.g. "build/gen") --- @field out_root string -- Output root (e.g. "build/gen")
--- @field project_root string -- project root (e.g. "code/") --- @field project_root string -- Project root (e.g. "code/")
--- @field upstream table<string, table> -- per-pass upstream outputs --- @field upstream table<string, table> -- Per-pass upstream outputs
--- @field flags table -- CLI flags --- @field flags table -- CLI flags
--- @field verbose boolean -- log diagnostic info --- @field verbose boolean -- Log diagnostic info
--- @class PassResult --- @class PassResult
--- @field outputs table[] -- {kind=, path=} entries describing emit files --- @field outputs table[] -- {kind=, path=} entries describing emit files
--- @field errors table[] -- {line=, msg=} entries; build-stops --- @field errors table[] -- {line=, msg=} entries; build-stops
--- @field warnings table[] -- {line=, msg=} entries; build-succeeds --- @field warnings table[] -- {line=, msg=} entries; build-succeeds
--- @class Component --- @class Component
--- @field name string -- atom name (without `ac_` prefix) --- @field name string -- Atom name (without `ac_` prefix)
--- @field body string -- brace-delimited body (without the braces) --- @field body string -- Brace-delimited body (without the braces)
--- @field args string|nil -- function-args string (function form only) --- @field args string|nil -- Function-args string (function form only)
--- @field line integer -- source line of the declaration --- @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 comment string|nil -- Scanner-owned `declaration_comment`; the components pass reads it from the scanner record
--- @field kind string -- "comp_bare" | "comp_proc" --- @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 --- @field debug_skip boolean -- Mirror of `a.debug_skip` (scanner-owned); true iff a bare `atom_dbg_skip` marker immediately preceded the declaration
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Local helpers (file I/O + path normalization) -- Local helpers (file I/O + path normalization)
@@ -231,7 +231,6 @@ end
-- --
-- Skips `//` sequences that are inside string or character literals -- Skips `//` sequences that are inside string or character literals
-- (a rough heuristic — sufficient for component bodies which don't have those constructs). -- (a rough heuristic — sufficient for component bodies which don't have those constructs).
--
--- @param s string --- @param s string
--- @return string --- @return string
local function convert_line_comments_to_block(s) local function convert_line_comments_to_block(s)
@@ -300,7 +299,7 @@ local function word_count_rec(name, comp_by_name, wc, cache)
local trimmed = t.tok local trimmed = t.tok
if trimmed ~= "" then if trimmed ~= "" then
local lookup = strip_mac_prefix(duffle.read_ident(trimmed, 1)) local lookup = strip_mac_prefix(duffle.read_ident(trimmed, 1))
if lookup and comp_by_name[lookup] then if lookup and comp_by_name[lookup] then
-- It's a `mac_X(...)` call. Recurse. -- It's a `mac_X(...)` call. Recurse.
n = n + word_count_rec(lookup, comp_by_name, wc, cache) n = n + word_count_rec(lookup, comp_by_name, wc, cache)
elseif lookup and wc and wc[lookup] then elseif lookup and wc and wc[lookup] then
@@ -339,6 +338,116 @@ local function count_all_components(components, wc)
return counts return counts
end end
-- ═══════════════════════════════════════════
-- Per-component metadata derivation (replaces the hardcoded `M.GP0_MACRO_CONTRIB` + `M.INSTRUCTION_LATENCY[mac_*]` tables that previously lived in `duffle.lua`).
--
-- Each `MipsAtomComp_(ac_X) { body }` definition in `code/duffle/lottes_tape.h` is the canonical source.
-- The `mac_X(...)` macros are GENERATED from these definitions by `emit_component_macros_h` for tape-side composition;
-- the metaprogram must NEVER walk the generated variants to derive metadata.
-- Always walk the original `MipsAtomComp_` body via `cc.body_tokens`.
-- ═══════════════════════════════════════════
--- (internal) Recursive cycle-cost derivation. Sum `latency[ident]` per emitted instruction in the component body,
--- recursing through nested `mac_*` calls (so `mac_format_g4_color`'s cost = 4 × `mac_pack_color_word`'s cost).
--- Special rule: `mac_yield`'s cost = 0 (per `lottes_tape.h:125-130` "the runtime cost lands in the next atom's prologue").
--- @param name string -- component bare name (e.g. "yield", "pack_color_word")
--- @param comp_by_name table<string, Component>
--- @param latency table<string, integer>
--- @param cache table<string, integer> -- shared memoization; `-1` sentinel detects cycles
--- @return integer
local function cycle_cost_rec(name, comp_by_name, latency, cache)
if cache[name] ~= nil then return cache[name] end
cache[name] = -1
local cc = comp_by_name[name]
local n
if cc then
if name == "yield" then
-- mac_yield's cost is 0 by convention (the runtime cost lands in the next atom's prologue).
n = 0
else
n = 0
local tokens = cc.body_tokens
for _, t in ipairs(tokens) do
local trimmed = t.tok
if trimmed ~= "" then
local ident = duffle.read_ident(trimmed, 1)
if ident and ident:sub(1, MAC_PREFIX_LEN) == MAC_PREFIX then
-- Nested `mac_X(...)` call: recurse.
local nested = ident:sub(MAC_PREFIX_LEN + 1)
n = n + cycle_cost_rec(nested, comp_by_name, latency, cache)
else
-- Leaf instruction or pseudo-macro. Look up in INSTRUCTION_LATENCY; default 1.
n = n + (latency[ident] or 1)
end
end
end
end
else
n = 1
end
cache[name] = n
return n
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.
--- 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>
--- @param cache table<string, integer>
--- @return integer
local function gp0_contrib_rec(name, comp_by_name, cache)
if cache[name] ~= nil then return cache[name] end
cache[name] = -1
local cc = comp_by_name[name]
local n
if cc then
n = 0
local tokens = cc.body_tokens
for _, t in ipairs(tokens) do
local trimmed = t.tok
if trimmed ~= "" then
local ident = duffle.read_ident(trimmed, 1)
if ident and ident:sub(1, MAC_PREFIX_LEN) == MAC_PREFIX then
-- Nested `mac_X(...)` call: recurse.
local nested = ident:sub(MAC_PREFIX_LEN + 1)
n = n + gp0_contrib_rec(nested, comp_by_name, cache)
elseif ident == "store_word" or ident == "store_half" or ident == "store_byte" then
if trimmed:find("R_PrimCursor", 1, true) then
n = n + 1
end
end
end
end
else
n = 0
end
cache[name] = n
return n
end
--- Compute `cycle_cost` + `gp0_contrib` for every component in `components` in a single pass.
--- Memoization cache is built ONCE (per source) and shared across both helpers so that
--- a nested `mac_Y` reference inside a `mac_X` body computes its values once.
--- @param components Component[]
--- @param latency table<string, integer>
--- @return table<string, {cycle_cost=integer, gp0_contrib=integer}>
local function compute_components_metadata(components, latency)
local comp_by_name = {}
for _, cc in ipairs(components) do comp_by_name[cc.name] = cc end
local cc_cache = {}
local gc_cache = {}
local out = {}
for _, c in ipairs(components) do
out[c.name] = {
cycle_cost = cycle_cost_rec(c.name, comp_by_name, latency, cc_cache),
gp0_contrib = gp0_contrib_rec(c.name, comp_by_name, gc_cache),
}
end
return out
end
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Per-component emit logic -- Per-component emit logic
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
@@ -348,8 +457,8 @@ end
--- @param s string --- @param s string
--- @return string[] --- @return string[]
local function split_comment_lines(s) local function split_comment_lines(s)
local out = {} local out = {}
local pos = 1 local pos = 1
local s_len = #s local s_len = #s
while pos <= s_len do while pos <= s_len do
local nl = s:find("\n", pos, true) local nl = s:find("\n", pos, true)
@@ -424,9 +533,9 @@ local function build_component_lines(c, counts)
local tokens = duffle.split_top_level_commas(c.body) local tokens = duffle.split_top_level_commas(c.body)
for i = 1, #tokens do tokens[i] = duffle.trim(tokens[i]) end for i = 1, #tokens do tokens[i] = duffle.trim(tokens[i]) end
local sig = signature_from_args(c.args) local sig = signature_from_args(c.args)
-- Direct lookup against the per-source precomputed `counts` table (built once by count_all_components). -- Direct lookup against the per-source precomputed `counts` table (built once by count_all_components).
local n = counts[c.name] local n = counts[c.name]
if n > 0 then if n > 0 then
emit_macro_body(lines, c, sig, tokens) emit_macro_body(lines, c, sig, tokens)
@@ -445,9 +554,15 @@ end
--- Build the boilerplate header lines (the `#ifdef INTELLISENSE_DIRECTIVES` block, --- 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). --- the `// Auto-generated` comment, the `// Source:` line, and the self-contained `WORD_COUNT` macro definition).
--- @param src SourceFile --- @param dir string -- the absolute source directory
--- @param sources SourceFile[] -- sources contributing to this directory (for the header comment)
--- @return string[] --- @return string[]
local function header_boilerplate(src) local function header_boilerplate(dir, sources)
local source_lines = { "// Directory: " .. duffle.to_absolute_path(dir) .. "/" }
for _, src in ipairs(sources) do
source_lines[#source_lines + 1] = "// source: " .. duffle.to_absolute_path(src.path)
end
local source_blob = table.concat(source_lines, "\n")
return { return {
-- #pragma once wrapped in #ifdef INTELLISENSE_DIRECTIVES, matching the convention in lottes_tape.h. -- #pragma once wrapped in #ifdef INTELLISENSE_DIRECTIVES, matching the convention in lottes_tape.h.
-- The build does manual unity includes (the user controls include order), so the pragma is only active for IDE/tooling. -- The build does manual unity includes (the user controls include order), so the pragma is only active for IDE/tooling.
@@ -455,7 +570,7 @@ local function header_boilerplate(src)
"#pragma once", "#pragma once",
"#endif", "#endif",
"// Auto-generated by ps1_meta.lua — DO NOT EDIT", "// Auto-generated by ps1_meta.lua — DO NOT EDIT",
"// Source: " .. duffle.to_absolute_path(src.path), source_blob,
"// Component atoms (MipsAtomComp_(ac_*)) -> macro variants (mac_*)", "// Component atoms (MipsAtomComp_(ac_*)) -> macro variants (mac_*)",
"", "",
-- Self-contained: define WORD_COUNT if not already defined. -- Self-contained: define WORD_COUNT if not already defined.
@@ -468,30 +583,30 @@ local function header_boilerplate(src)
} }
end end
--- Compute the output path for one source's `.macs.h` file. --- Compute the per-directory output path for `.macs.h`.
--- The pre-rework convention uses the *directory* basename (not the source file basename) --- e.g. any source in `code/duffle/` produces `code/duffle/gen/macs.h` regardless of source filename.
--- e.g. `code/duffle/lottes_tape.h` produces `code/duffle/gen/duffle.macs.h`. --- The directory name is the namespace; the filename does not repeat it.
--- This matches what the C codebase #includes. --- @param dir string -- the absolute source directory
--- @param src SourceFile
--- @return string -- the output directory --- @return string -- the output directory
--- @return string -- the full output path --- @return string -- the full output path
local function compute_macs_h_path(src) local function compute_macs_h_path(dir)
local out_dir = src.dir .. "/" .. GEN_SUBDIR local out_dir = dir .. "/" .. GEN_SUBDIR
local out_path = out_dir .. "/" .. duffle.basename_no_ext(src.dir) .. ".macs.h" local out_path = out_dir .. "/" .. MACS_FILENAME
return out_dir, out_path return out_dir, out_path
end end
--- Emit a per-source `.macs.h` header with the `mac_X` macros + `WORD_COUNT` entries. --- 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). --- 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 ctx PassCtx
--- @param src SourceFile --- @param dir string -- the absolute source directory
--- @param components Component[] --- @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) --- @param counts table<string, integer> -- precomputed word counts (from count_all_components)
--- @return string|nil -- path to the written file (nil if no components) --- @return string|nil -- path to the written file (nil if no components)
local function emit_component_macros_h(ctx, src, components, counts) local function emit_component_macros_h(ctx, dir, sources, components, counts)
if #components == 0 then return nil end if #components == 0 then return nil end
local out_dir, out_path = compute_macs_h_path(src) local out_dir, out_path = compute_macs_h_path(dir)
local lines = header_boilerplate(src) local lines = header_boilerplate(dir, sources)
for _, c in ipairs(components) do for _, c in ipairs(components) do
for _, l in ipairs(build_component_lines(c, counts)) do for _, l in ipairs(build_component_lines(c, counts)) do
@@ -534,32 +649,36 @@ end
--- (internal) Populate `corpus.components` with this source's components-by-name map. --- (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"). --- First declaration wins; later declarations of the same bare name are dropped and recorded as a collision via `corpus.collisions` (kind = "component").
--- The pass does NOT write to `ctx.shared.components` (ownership follows the canonical contract). --- The pass does NOT write to `ctx.shared.components`.
--- The `debug_skip` field mirrors the scanner-owned declaration record (`c.debug_skip`).
--- No parallel skip map is built here; consumers that need the per-component skip state read `corpus.components[name].debug_skip` directly. --- No parallel skip map is built here; consumers that need the per-component skip state read `corpus.components[name].debug_skip` directly.
--- @param corpus table -- the corpus --- The `cycle_cost` + `gp0_contrib` fields are populated from `metadata[c.name]` (computed by `compute_components_metadata` against the original `MipsAtomComp_` body).
--- @param corpus table -- the corpus
--- @param src SourceFile --- @param src SourceFile
--- @param components Component[] --- @param components Component[]
local function update_canonical_components(corpus, src, components) --- @param metadata table<string, {cycle_cost=integer, gp0_contrib=integer}>
local function update_canonical_components(corpus, src, components, metadata)
local rel_path = src.path:gsub("\\", "/") local rel_path = src.path:gsub("\\", "/")
for _, c in ipairs(components) do for _, c in ipairs(components) do
-- Keyed by bare name (e.g. `yield`, `load_tri_indices`). -- Keyed by bare name (e.g. `yield`, `load_tri_indices`).
-- The atoms_source_map pass looks up components by bare name from the corpus; -- The atoms_source_map pass looks up components by bare name from the corpus;
-- `mac_` prefix lives at the call-site identifier and is stripped before lookup. -- `mac_` prefix lives at the call-site identifier and is stripped before lookup.
local m = metadata and metadata[c.name] or nil
if corpus.components[c.name] == nil then if corpus.components[c.name] == nil then
corpus.components[c.name] = { corpus.components[c.name] = {
name = c.name, name = c.name,
line = c.line, line = c.line,
path = rel_path, path = rel_path,
kind = c.kind or "comp_bare", kind = c.kind or "comp_bare",
debug_skip = c.debug_skip == true, debug_skip = c.debug_skip == true,
cycle_cost = m and m.cycle_cost or nil,
gp0_contrib = m and m.gp0_contrib or nil,
} }
else else
-- A second declaration of the same bare name: record a typed collision so static-analysis + the report can surface it. -- A second declaration of the same bare name: record a typed collision so static-analysis + the report can surface it.
-- Identical-shape declarations (same path + line) reuse the first-wins entry without a collision record. -- Identical-shape declarations (same path + line) reuse the first-wins entry without a collision record.
local existing = corpus.components[c.name] local existing = corpus.components[c.name]
if existing.path ~= rel_path or existing.line ~= c.line then if existing.path ~= rel_path or existing.line ~= c.line then
local kind = c.kind or "comp_bare" local kind = c.kind or "comp_bare"
local first_kind = existing.kind or "comp_bare" local first_kind = existing.kind or "comp_bare"
corpus.collisions[#corpus.collisions + 1] = { corpus.collisions[#corpus.collisions + 1] = {
kind = "component", kind = "component",
@@ -624,21 +743,39 @@ function M.run(ctx)
-- * `corpus.component_body_index[name]` — body / line_of / source index -- * `corpus.component_body_index[name]` — body / line_of / source index
-- The pass writes to the corpus only; consumers read from the corpus directly. -- The pass writes to the corpus only; consumers read from the corpus directly.
for _, src in ipairs(corpus.source_order) do -- Per-directory aggregation: every source in the same directory contributes to one `gen/macs.h`.
-- project_components reads from src.scan + does backward lookups on src.text -- The directory itself is the namespace. `corpus.sources_by_dir` preserves source-order within each bucket (matches `corpus.source_order`).
local components = project_components(src.text, src.scan) local sources_by_dir = corpus.sources_by_dir or duffle.group_sources_by_dir(corpus.source_order)
if #components > 0 then for dir, sources in pairs(sources_by_dir) do
-- Compute all component word counts once per source. -- Aggregate components from every source in this directory.
-- Use `corpus.word_counts` so the recursive lookup sees both authored-metadata entries -- `project_components` returns nil for sources with no `MipsAtomComp_` declarations; we skip those.
-- (loaded by word_count_eval.run) AND same-source component entries (populated earlier in this loop by `update_canonical_word_counts`). local aggregated_components = {}
local counts = count_all_components(components, corpus.word_counts) local metadata_per_source = {}
local macs_path = emit_component_macros_h(ctx, src, components, counts) for _, src in ipairs(sources) do
local per_source = project_components(src.text, src.scan) or {}
for _, c in ipairs(per_source) do
aggregated_components[#aggregated_components + 1] = c
end
if #per_source > 0 then
metadata_per_source[src] = compute_components_metadata(per_source, duffle.INSTRUCTION_LATENCY)
end
end
if #aggregated_components > 0 then
-- Compute word counts across the aggregated set. `corpus.word_counts` carries the
-- same-source + prior-directory entries so the recursive lookup sees both.
local counts = count_all_components(aggregated_components, corpus.word_counts)
local macs_path = emit_component_macros_h(ctx, dir, sources, aggregated_components, counts)
if macs_path then if macs_path then
outputs[#outputs + 1] = { macs_h = macs_path } outputs[#outputs + 1] = { macs_h = macs_path }
-- Populate the projections AFTER disk emission (so the byte-identical `.macs.h` contract is preserved before any current-count mutation). -- Populate the projections AFTER disk emission (byte-identical `.macs.h` contract).
update_canonical_word_counts(corpus, components, counts) update_canonical_word_counts(corpus, aggregated_components, counts)
update_canonical_components(corpus, src, components) for _, src in ipairs(sources) do
update_canonical_component_body_index(corpus, src, components, src.scan) local per_source = project_components(src.text, src.scan) or {}
if #per_source > 0 then
update_canonical_components(corpus, src, per_source, metadata_per_source[src])
update_canonical_component_body_index(corpus, src, per_source, src.scan)
end
end
end end
end end
end end
+91 -100
View File
@@ -73,12 +73,8 @@ local DW_RLE_start_length = DWARF5_RNGLISTS.start_length
-- File-index lookup for the existing main line unit (Unit 2). -- File-index lookup for the existing main line unit (Unit 2).
-- Populated at pass start by `init_file_index_lookup(elf_path)` from the runtime ELF (see `elf_dwarf.read_line_unit_file_table`). -- Populated at pass start by `init_file_index_lookup(elf_path)` from the runtime ELF (see `elf_dwarf.read_line_unit_file_table`).
-- The hardcoded indices and the `PROVENANCE_BASENAME_TO_FILE_INDEX` table that previously lived here were retired in `conductor/tracks/dwarf_file_index_lookup_20260731/` local _file_index_by_basename = nil -- [basename] = 1-based line-table file index
-- (red of the local _file_path_by_index = nil -- [1-based index] = full source path (diagnostics / future consumers)
-- `TODO(Ed): Remove this HARDCODE` from line 156); the runtime lookup reads the
-- actual gcc-emitted `.debug_line` file table instead.
local _file_index_by_basename = nil -- [basename] = 1-based line-table file index
local _file_path_by_index = nil -- [1-based index] = full source path (diagnostics / future consumers)
local _default_atom_source_index = nil -- any valid index used in opaque-row fallbacks local _default_atom_source_index = nil -- any valid index used in opaque-row fallbacks
-- RR_<R_Name> debug-visible variables come from the merged register_alias_registry filtered to aliases whose code is a valid MIPS GPR 0..31 -- RR_<R_Name> debug-visible variables come from the merged register_alias_registry filtered to aliases whose code is a valid MIPS GPR 0..31
@@ -111,8 +107,8 @@ local ABBREV_TYPED_VIEW_POINTER = 0x6E -- 110: DW_TAG_pointer_type no children
-- DWARF5 §7.7.3 loclist opcodes. -- DWARF5 §7.7.3 loclist opcodes.
local DW_LLE_end_of_list = 0x00 local DW_LLE_end_of_list = 0x00
local DW_LLE_start_length = 0x08 local DW_LLE_start_length = 0x08
local DW_OP_reg0 = 0x50 -- base reg op; regN = 0x50 + N local DW_OP_reg0 = 0x50 -- base reg op; regN = 0x50 + N
local DW_OP_breg0 = 0x70 -- base breg op; bregN = 0x70 + N (SLEB offset) local DW_OP_breg0 = 0x70 -- base breg op; bregN = 0x70 + N (SLEB offset)
local DW_OP_piece = 0x93 local DW_OP_piece = 0x93
local MIPS_LOAD_DELAY_BYTES = 0x08 -- 1 load word + 1 BD-slot word local MIPS_LOAD_DELAY_BYTES = 0x08 -- 1 load word + 1 BD-slot word
@@ -188,15 +184,17 @@ end
--- Resolve an absolute provenance path to the line-unit file index used by the emitting line program. --- Resolve an absolute provenance path to the line-unit file index used by the emitting line program.
--- Normalizes mixed `/` and `\` separators to a basename and looks it up against the runtime-computed file table populated by `init_file_index_lookup`. --- Normalizes mixed `/` and `\` separators to a basename and looks it up against the runtime-computed file table populated by `init_file_index_lookup`.
--- ---
--- Fails loudly on an unknown provenance basename: adding a new component source file will produce a clear error message naming the missing basename and listing the .debug_line file table contents, --- Returns 0 (the DWARF `set_file(0)` "no file change" sentinel) when the basename is not in the file table.
--- so the user can either confirm the gcc include order, the unity-root, or the `.debug_line` file table contents. --- This is a normal occurrence: the compiler only adds a file to the `.debug_line` file table when the file has line-numbered content (i.e., code).
--- Silent fallback would mask the new-file case by misattributing component rows to an arbitrary source file. --- Files containing only static-array data (e.g. `MipsAtomComp_` declarations in `gp.atom.c`, `psyq.atom.c`, `pad.atom.c` — the OT-tag inserts, etc.) produce no line numbers,
--- so gcc omits them from the file table.
--- The DWARF emitter then keeps the previous line-program file state instead of pointing at a file that has no entries to walk.
--- A stderr warning is emitted per-miss so the user can audit which files the compiler dropped.
--- @param path string -- absolute provenance path (mixed slashes accepted) --- @param path string -- absolute provenance path (mixed slashes accepted)
--- @return integer -- 1-based line-unit file index --- @return integer -- 1-based line-unit file index, or 0 on miss (DWARF no-change sentinel)
local function resolve_provenance_file_index(path) local function resolve_provenance_file_index(path)
if _file_index_by_basename == nil then if _file_index_by_basename == nil then
error("[dwarf_injection] resolve_provenance_file_index called before init_file_index_lookup. " error("[dwarf_injection] resolve_provenance_file_index called before init_file_index_lookup. Is M.run being entered correctly (with --elf)?")
.. "Is M.run being entered correctly (with --elf)?")
end end
if path == nil or path == "" then if path == nil or path == "" then
error("[dwarf_injection] resolve_provenance_file_index: empty path") error("[dwarf_injection] resolve_provenance_file_index: empty path")
@@ -205,19 +203,17 @@ local function resolve_provenance_file_index(path)
local normalized = path:gsub("\\", "/") local normalized = path:gsub("\\", "/")
-- Take the last path component (the basename). -- Take the last path component (the basename).
local basename = normalized:match("([^/]+)$") or normalized local basename = normalized:match("([^/]+)$") or normalized
local idx = _file_index_by_basename[basename] local idx = _file_index_by_basename[basename]
if idx ~= nil then return idx end if idx ~= nil then return idx end
-- Last-resort exact-path match (handles paths that don't reduce to a known basename). -- Last-resort exact-path match (handles paths that don't reduce to a known basename).
for i, p in pairs(_file_path_by_index) do for i, p in pairs(_file_path_by_index) do
if p and p:gsub("\\", "/") == normalized then return i end if p and p:gsub("\\", "/") == normalized then return i end
end end
-- Build an error message listing the known basenames for fast diagnostics. -- File is in the corpus but gcc omitted it from the .debug_line file table (data-only content).
local known = {} -- Return 0 = DWARF `set_file(0)` no-change sentinel so the line program keeps its prior file state.
for k in pairs(_file_index_by_basename) do known[#known + 1] = k end io.stderr:write(string.format("[dwarf_injection] line-table miss: '%s' (basename '%s') not in .debug_line file table; "
table.sort(known) .. "falling back to set_file(0)\n", path, basename))
error(string.format("[dwarf_injection] resolve_provenance_file_index: unknown provenance basename '%s' (from '%s'). " return 0
.. "Known basenames in the .debug_line file table (%d): %s"
, basename, path, #known, table.concat(known, ", ")))
end end
local DW_FORM_addr = 0x01 local DW_FORM_addr = 0x01
@@ -232,7 +228,6 @@ local DW_FORM_sec_offset = 0x17 -- 4-byte section-relative offset (into .d
-- DW_OP_reg0 + DW_OP_piece are declared above (lines 114-116) alongside the other DWARF5 §7.7.3 loclist opcodes. -- DW_OP_reg0 + DW_OP_piece are declared above (lines 114-116) alongside the other DWARF5 §7.7.3 loclist opcodes.
local DW_ATE_unsigned = 0x07 -- DWARF5 §7.8.1: DW_ATE_unsigned (used for U4 base type) local DW_ATE_unsigned = 0x07 -- DWARF5 §7.8.1: DW_ATE_unsigned (used for U4 base type)
-- (DW_LANG_Mips_Assembler = 0x8001 was used in the, but we want this CU to look like a C TU so VSCode's Variables pane treats it as code.) -- (DW_LANG_Mips_Assembler = 0x8001 was used in the, but we want this CU to look like a C TU so VSCode's Variables pane treats it as code.)
@@ -452,9 +447,9 @@ end
--- Statement-state rules: --- Statement-state rules:
--- * A marked whole atom emits one opaque is_stmt=false range row and no nested component rows; its subprogram symbol/range remains available. --- * A marked whole atom emits one opaque is_stmt=false range row and no nested component rows; its subprogram symbol/range remains available.
--- * Per-row policy at every other PC: --- * Per-row policy at every other PC:
--- - Call-site row of any invocation's first word: is_stmt = true (unconditional; `want_call = true`). --- - Call-site row of any invocation's first word: is_stmt = true (unconditional; `want_call = true`).
--- - Body row of any invocation (first or subsequent): is_stmt = not inv.debug_skip (`want_body = not inv.debug_skip`). --- - Body row of any invocation (first or subsequent): is_stmt = not inv.debug_skip (`want_body = not inv.debug_skip`).
--- - RAW word (no containing invocation): is_stmt = true (unconditional). --- - RAW word (no containing invocation): is_stmt = true (unconditional).
--- * The previous per-word `marked_idx` ancestor walk and the GDB 12 zero-instruction-prologue duplicate row at atom entry are DELETED; the new --- * The previous per-word `marked_idx` ancestor walk and the GDB 12 zero-instruction-prologue duplicate row at atom entry are DELETED; the new
--- first-word emission IS the entry statement. --- first-word emission IS the entry statement.
--- * Whole-atom suppression wins over component markers; no nested inversion. --- * Whole-atom suppression wins over component markers; no nested inversion.
@@ -616,10 +611,8 @@ local function build_atom_sequence(atom)
-- NOT anc.body_lines[1] (= the line of the first WORD, which is wrong when the outer's body starts with a nested call). -- NOT anc.body_lines[1] (= the line of the first WORD, which is wrong when the outer's body starts with a nested call).
for ai, anc in ipairs(entry_1_ancestry) do for ai, anc in ipairs(entry_1_ancestry) do
assert(anc.body_lines, "missing body_lines: emitter did not run emission-model") assert(anc.body_lines, "missing body_lines: emitter did not run emission-model")
assert(anc.body_lines[1] ~= nil assert(anc.body_lines[1] ~= nil, "dwarf_injection: body_lines[1] missing on first-word entry for inv=" .. tostring(anc.component_name))
, "dwarf_injection: body_lines[1] missing on first-word entry for inv=" .. tostring(anc.component_name)) assert(anc.call_path and anc.call_path ~= "", "dwarf_injection: inv.call_path is missing on invocation " .. tostring(anc.component_name) .. "; emitter did not run emission-model.")
assert(anc.call_path and anc.call_path ~= ""
, "dwarf_injection: inv.call_path is missing on invocation " .. tostring(anc.component_name) .. "; emitter did not run emission-model.")
emit_row(resolve_provenance_file_index(anc.call_path), anc.call_line, true) emit_row(resolve_provenance_file_index(anc.call_path), anc.call_line, true)
local is_outermost = (ai == 1) local is_outermost = (ai == 1)
if not (is_outermost and anc.debug_skip) then if not (is_outermost and anc.debug_skip) then
@@ -644,8 +637,7 @@ local function build_atom_sequence(atom)
-- all OTHER ancestors emit body_lines[1] with is_stmt = not debug_skip. -- all OTHER ancestors emit body_lines[1] with is_stmt = not debug_skip.
-- --
-- This re-emits the outer ancestor's call-site + body rows at the inner's first word PC -- This re-emits the outer ancestor's call-site + body rows at the inner's first word PC
-- for debugger context: source-level stepping now shows the outer body line -- for debugger context: source-level stepping now shows the outer body line (not the inner body line) when stepping into the inner. PROBLEM B fix.
-- (not the inner body line) when stepping into the inner. PROBLEM B fix.
-- The body_lines[1] row references body_first_line_of[anc.id] (= the body's first content line in the parent's source), -- The body_lines[1] row references body_first_line_of[anc.id] (= the body's first content line in the parent's source),
-- NOT anc.body_lines[1] (= the line of the first WORD, which is wrong when the outer's body starts with a nested call: -- NOT anc.body_lines[1] (= the line of the first WORD, which is wrong when the outer's body starts with a nested call:
-- gdb 12.1 picks the displayed line as the LAST row at the same PC in byte-stream order, -- gdb 12.1 picks the displayed line as the LAST row at the same PC in byte-stream order,
@@ -653,12 +645,9 @@ local function build_atom_sequence(atom)
local ancestry = ancestry_idx[idx] local ancestry = ancestry_idx[idx]
for ai, anc in ipairs(ancestry) do for ai, anc in ipairs(ancestry) do
assert(anc.body_lines, "missing body_lines: emitter did not run emission-model") assert(anc.body_lines, "missing body_lines: emitter did not run emission-model")
assert(anc.body_lines[1] ~= nil assert(anc.body_lines[1] ~= nil, string.format("missing body_lines[1] for inv=%s start_pos=%d len=%d", anc.component_name, anc.start_pos, #(anc.body_lines or {})))
, string.format("missing body_lines[1] for inv=%s start_pos=%d len=%d", assert(anc.call_path and anc.call_path ~= "", "dwarf_injection: inv.call_path is missing on invocation " .. tostring(anc.component_name) .. "; emitter did not run emission-model.")
anc.component_name, anc.start_pos, #(anc.body_lines or {}))) emit_row(resolve_provenance_file_index(anc.call_path), anc.call_line, true)
assert(anc.call_path and anc.call_path ~= ""
, "dwarf_injection: inv.call_path is missing on invocation " .. tostring(anc.component_name) .. "; emitter did not run emission-model.")
emit_row(resolve_provenance_file_index(anc.call_path), anc.call_line, true)
local is_outermost = (ai == 1) local is_outermost = (ai == 1)
if not (is_outermost and anc.debug_skip) then if not (is_outermost and anc.debug_skip) then
emit_row(resolve_provenance_file_index(anc.def_path), body_first_line_of[anc.id] or anc.body_lines[1], not anc.debug_skip) emit_row(resolve_provenance_file_index(anc.def_path), body_first_line_of[anc.id] or anc.body_lines[1], not anc.debug_skip)
@@ -673,10 +662,8 @@ local function build_atom_sequence(atom)
-- Marked invocations emit non-statement body rows at every body word; unmarked invocations emit statement body rows. -- Marked invocations emit non-statement body rows at every body word; unmarked invocations emit statement body rows.
assert(inv.body_lines, "missing body_lines: emitter did not run emission-model") assert(inv.body_lines, "missing body_lines: emitter did not run emission-model")
local words_into = idx - inv.start_pos local words_into = idx - inv.start_pos
assert(inv.body_lines[words_into] ~= nil assert(inv.body_lines[words_into] ~= nil, string.format("missing body_lines[%d] for inv=%s start_pos=%d len=%d idx=%d", words_into, inv.component_name, inv.start_pos, #(inv.body_lines or {}), idx))
, string.format("missing body_lines[%d] for inv=%s start_pos=%d len=%d idx=%d", emit_row(resolve_provenance_file_index(inv.def_path), inv.body_lines[words_into], not inv.debug_skip)
words_into, inv.component_name, inv.start_pos, #(inv.body_lines or {}), idx))
emit_row(resolve_provenance_file_index(inv.def_path), inv.body_lines[words_into], not inv.debug_skip)
else else
-- RAW word: single call-site row, always a statement target (the word itself is unmarked). -- RAW word: single call-site row, always a statement target (the word itself is unmarked).
emit_row(call_file_idx, entry.line, true) emit_row(call_file_idx, entry.line, true)
@@ -716,8 +703,8 @@ end
--- `{comp_name, call_file, call_line, comp_file, comp_line, start_pos, end_pos, body_lines, debug_skip}`. `body_lines[k]` --- `{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. --- is the k-th word's source line within the component body.
--- ---
--- @param corpus table -- the corpus 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` --- @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) 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. -- Cross-ref: keep only atoms present in BOTH the nm symbol table AND `corpus.atoms_by_name`. Output is sorted by ascending addr.
@@ -734,8 +721,8 @@ local function build_atom_table(corpus, addrs)
local word_events = paths.word_events or {} local word_events = paths.word_events or {}
local invocations_proj = paths.invocations or {} local invocations_proj = paths.invocations or {}
-- Build the dense entries list from `word_events`. -- Build the dense entries list from `word_events`.
-- `word_events[i].i` = the 0-based `.word` position -- `word_events[i].i` = the 0-based `.word` position
-- `call_line` = the root atom's physical source line for that word (stamped by emission_model) -- `call_line` = the root atom's physical source line for that word (stamped by emission_model)
local entries = {} local entries = {}
for idx, ev in ipairs(word_events) do for idx, ev in ipairs(word_events) do
entries[#entries + 1] = { entries[#entries + 1] = {
@@ -777,9 +764,8 @@ local function build_atom_table(corpus, addrs)
local out = {} local out = {}
-- Walk every source's atom list (which preserves source order + per-source src_path). -- Walk every source's atom list (which preserves source order + per-source src_path).
-- Cross-ref with the nm symbol table; atoms absent from `addrs` are skipped (an atom -- Cross-ref with the nm symbol table; atoms absent from `addrs` are skipped
-- declared in source but not emitted as a symbol is a metaprogram or atom-info bug, not -- (an atom declared in source but not emitted as a symbol is a metaprogram or atom-info bug, not a source-correlation bug — emit_no_emit would catch it upstream).
-- a source-correlation bug — emit_no_emit would catch it upstream).
for _, src in ipairs((corpus and corpus.source_order) or {}) do for _, src in ipairs((corpus and corpus.source_order) or {}) do
local src_path = src.path or "" local src_path = src.path or ""
for _, atom_rec in ipairs(((src.scan or {}).atoms) or {}) do for _, atom_rec in ipairs(((src.scan or {}).atoms) or {}) do
@@ -840,12 +826,14 @@ end
--- load_word(R_FaceCursor, R_TapePtr, O_(Binds_CubeTri,FaceCursor)), --- load_word(R_FaceCursor, R_TapePtr, O_(Binds_CubeTri,FaceCursor)),
--- ... --- ...
--- ---
--- Also matches `load_half` / `load_half_u` / `load_byte` / `load_byte_u` (any MIPS load instruction with `(R_<reg>, R_<base>, O_(<Binds_X>, FieldName))` shape).
--- Every field's `byte_size` + `offset` determine which load to emit; this function only records the (reg, field) pair.
---
--- The GPR for each `R_<reg>` is looked up in the merged register_alias_registry; aliases absent from the registry --- The GPR for each `R_<reg>` is looked up in the merged register_alias_registry; aliases absent from the registry
--- (no `atom_reg` opt-in) are silently skipped — the resulting rbind record will be incomplete and the atom will fail to bind a usable piece chain. --- (no `atom_reg` opt-in) are silently skipped — the resulting rbind record will be incomplete and the atom will fail to bind a usable piece chain.
--- This is intentional: silently falling back to a hardcoded GPR would mask the missing opt-in. --- 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 --- 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).
--- statements (each entry is a single `load_word(...)` call or other statement).
--- @param body_tokens table[] -- the atom's pre-tokenized body statements (from atom.body_tokens) --- @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 binds_name string -- expected Binds_X name (skip pairs with mismatching binds)
--- @param registries table -- merged registries from collect_per_source_registries --- @param registries table -- merged registries from collect_per_source_registries
@@ -853,14 +841,18 @@ end
local function parse_body_load_pairs(body_tokens, binds_name, registries) local function parse_body_load_pairs(body_tokens, binds_name, registries)
local pairs = {} local pairs = {}
local reg_index_by_name = (registries and registries.register_alias_registry) or {} local reg_index_by_name = (registries and registries.register_alias_registry) or {}
-- One regex that matches any of: load_word, load_half, load_half_u, load_byte, load_byte_u, gte_lw, gte_lwc2.
-- The captured ident is `kind`; `inner` holds the parens body for arg parsing.
local load_pattern = "^(load_word|load_half|load_half_u|load_byte|load_byte_u|gte_lw|gte_lwc2)%s*%((.*)%)$"
for _, t in ipairs(body_tokens or {}) do for _, t in ipairs(body_tokens or {}) do
local tok = duffle.trim(t.tok or "") local tok = duffle.trim(t.tok or "")
-- Match "load_word(...)" — the entire call is one body_tokens entry. local kind, inner = tok:match(load_pattern)
local inner = tok:match("^load_word%s*%((.*)%)$") if kind then
if inner then
local args = duffle.split_top_level_commas(inner) local args = duffle.split_top_level_commas(inner)
-- Expected shape: (R_<reg>, R_TapePtr, O_(Binds_<X>, FieldName)) -- Expected shape for an rbind piece-chain load: (R_<reg>, R_TapePtr, O_(Binds_<X>, FieldName))
if #args >= 3 then -- The second arg MUST be R_TapePtr — loads from other bases (e.g. `load_byte_u(R_RawStatus, R_PadRaw, 0)`)
-- are field-derivative loads that read already-bound tape values; they're NOT a new piece-chain.
if #args >= 3 and duffle.trim(args[2]) == "R_TapePtr" then
local reg_name = duffle.trim(args[1]) local reg_name = duffle.trim(args[1])
local third_arg = duffle.trim(args[3]) local third_arg = duffle.trim(args[3])
-- Match O_(Binds_<X>, FieldName) -- Match O_(Binds_<X>, FieldName)
@@ -879,9 +871,7 @@ local function parse_body_load_pairs(body_tokens, binds_name, registries)
end end
--- Collect every rbind atom + the matching Binds_X struct + (reg, field) pairs. --- Collect every rbind atom + the matching Binds_X struct + (reg, field) pairs.
---
--- Inputs come from the dep-closed `scan-source` pass (the per-source `src.scan` payload is preserved on each `corpus.source_order` entry). --- Inputs come from the dep-closed `scan-source` pass (the per-source `src.scan` payload is preserved on each `corpus.source_order` entry).
---
--- Returns: --- Returns:
--- rbind_atoms = {[atom_name] = {binds, fields, regs, byte_size, info_line}} --- rbind_atoms = {[atom_name] = {binds, fields, regs, byte_size, info_line}}
--- rbind_structs = {[binds_name] = {byte_size, fields, atom_names}} --- rbind_structs = {[binds_name] = {byte_size, fields, atom_names}}
@@ -926,7 +916,7 @@ local function parse_rbind_atoms(corpus, atom_table, registries)
local body_tokens_by_atom = {} local body_tokens_by_atom = {}
for _, src in ipairs((corpus and corpus.source_order) or {}) do for _, src in ipairs((corpus and corpus.source_order) or {}) do
local scan = src.scan local scan = src.scan
if scan then if scan then
for _, atom in ipairs(scan.atoms or {}) do for _, atom in ipairs(scan.atoms or {}) do
body_tokens_by_atom[atom.name] = atom.body_tokens body_tokens_by_atom[atom.name] = atom.body_tokens
end end
@@ -945,8 +935,8 @@ local function parse_rbind_atoms(corpus, atom_table, registries)
for atom_name, ai in pairs(ai_by_atom) do for atom_name, ai in pairs(ai_by_atom) do
if ai.binds then if ai.binds then
local struct = rbind_structs[ai.binds] local struct = rbind_structs[ai.binds]
local body_toks = body_tokens_by_atom[atom_name] local body_toks = body_tokens_by_atom[atom_name]
if struct and body_toks then if struct and body_toks then
local pairs = parse_body_load_pairs(body_toks, ai.binds, registries) local pairs = parse_body_load_pairs(body_toks, ai.binds, registries)
if #pairs > 0 then if #pairs > 0 then
@@ -981,7 +971,8 @@ end
--- (the final unit, referenced by the main CU's DW_AT_stmt_list). --- (the final unit, referenced by the main CU's DW_AT_stmt_list).
--- ---
--- This builder extends the main compilation unit. --- This builder extends the main compilation unit.
--- A detached synthetic line unit has no DW_AT_stmt_list referencing it, so gdb ignored it (a previous experiment); byte 13 is the first special opcode, not the extended-opcode marker. --- A detached synthetic line unit has no DW_AT_stmt_list referencing it, so gdb ignored it (a previous experiment);
--- byte 13 is the first special opcode, not the extended-opcode marker.
--- The existing final unit already contains hello_gte_tape.c as file index 11 and ends with a valid end_sequence. --- The existing final unit already contains hello_gte_tape.c as file index 11 and ends with a valid end_sequence.
--- We preserve its bytes, append independent atom sequences, and increase only that unit's DWARF32 unit_length. --- We preserve its bytes, append independent atom sequences, and increase only that unit's DWARF32 unit_length.
--- @param existing string -- existing section bytes, byte-for-byte --- @param existing string -- existing section bytes, byte-for-byte
@@ -992,9 +983,7 @@ local function build_dwarf_line_section(existing, atom_table)
-- Build the sequences. -- Build the sequences.
local sequences = {} local sequences = {}
for _, atom in ipairs(atom_table) do for _, atom in ipairs(atom_table) do sequences[#sequences + 1] = build_atom_sequence(atom) end
sequences[#sequences + 1] = build_atom_sequence(atom)
end
local appended = table.concat(sequences) local appended = table.concat(sequences)
-- Walk DWARF32 line units and retain the final unit's bounds. -- Walk DWARF32 line units and retain the final unit's bounds.
@@ -1002,7 +991,7 @@ local function build_dwarf_line_section(existing, atom_table)
local unit_pos, last_pos, last_length, last_end = 0, nil, nil, nil local unit_pos, last_pos, last_length, last_end = 0, nil, nil, nil
while unit_pos < #existing do while unit_pos < #existing do
if unit_pos + 4 > #existing then return existing end if unit_pos + 4 > #existing then return existing end
local unit_length = elf_dwarf.read_u32_le(existing, unit_pos) local unit_length = elf_dwarf.read_u32_le(existing, unit_pos)
if unit_length == elf_dwarf.ELF32.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 local unit_end_excl = unit_pos + 4 + unit_length
if unit_end_excl > #existing then return existing end if unit_end_excl > #existing then return existing end
@@ -1528,13 +1517,12 @@ end
--- DW_AT_location = piece-chain (DW_FORM_exprloc) --- DW_AT_location = piece-chain (DW_FORM_exprloc)
--- DW_AT_type = ref4 → structure_type DIE --- DW_AT_type = ref4 → structure_type DIE
--- ---
--- This function does NOT emit the final 0 byte (root terminator). build_debug_info_section splices bytes ahead of the root terminator --- This function does NOT emit the final 0 byte (root terminator).
--- and preserves existing DIE bytes exactly. --- build_debug_info_section splices bytes ahead of the root terminator and preserves existing DIE bytes exactly.
--- ---
--- ref4 basis: DW_FORM_ref4 is CU-relative (offset from the first byte of the CU header). --- ref4 basis: DW_FORM_ref4 is CU-relative (offset from the first byte of the CU header).
--- Our inserted DIEs live in the main CU, so every ref4 = (target section offset) - main_cu_offset. --- Our inserted DIEs live in the main CU, so every ref4 = (target section offset) - main_cu_offset.
--- Per-die section offsets are tracked via the running `next_offset` cursor (= section offset of the NEXT byte to emit). --- Per-die section offsets are tracked via the running `next_offset` cursor (= section offset of the NEXT byte to emit).
---
--- @param main_cu_offset integer -- 0-based section offset of the main CU's unit_length field --- @param main_cu_offset integer -- 0-based section offset of the main CU's unit_length field
--- @param main_cu_end_excl integer -- 0-based section offset of the first byte AFTER the main CU --- @param main_cu_end_excl integer -- 0-based section offset of the first byte AFTER the main CU
--- @param atom_table table[] -- atoms (with atom.rbind set if rbind; atom.invocations set if mac_X(...) calls) --- @param atom_table table[] -- atoms (with atom.rbind set if rbind; atom.invocations set if mac_X(...) calls)
@@ -1654,6 +1642,8 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta
-- --
-- The table is small + explicit — the prototype principle treats the typed-view struct layout as data, not derived state. -- The table is small + explicit — the prototype principle treats the typed-view struct layout as data, not derived state.
local STRUCT_MEMBER_TABLE = { local STRUCT_MEMBER_TABLE = {
-- TODO(Ed): This hardcoding is brittle...
-- TODO(Ed): Better to just have a table for the fundamental types in duffle/dsl.h, we can derive the rest via typedef parsing...
-- 2-element signed short vector (rare; placeholder for future use). -- 2-element signed short vector (rare; placeholder for future use).
V2_S2 = { byte_size = 4, members = { V2_S2 = { byte_size = 4, members = {
{ name = "x", offset = 0, byte_size = 2 }, { name = "x", offset = 0, byte_size = 2 },
@@ -1906,12 +1896,13 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta
end end
local atom_view = (registries.atom_views or {})[atom.name] local atom_view = (registries.atom_views or {})[atom.name]
-- Build the atom-name lookup table once (cheap; O(atom_table)) so step (b) and step (d) can resolve rbind_atom names. -- Build the atom-name lookup table once (cheap; O(atom_table)) so step (b) and step (d) can resolve rbind_atom names.
-- TODO(Ed): Bad assignment?
local atom_by_name = atom_by_name or (function() local m = {}; for _, a in ipairs(atom_table) do if a.name then m[a.name] = a end end; return m end)() local atom_by_name = atom_by_name or (function() local m = {}; for _, a in ipairs(atom_table) do if a.name then m[a.name] = a end end; return m end)()
-- step (b) inputs: this atom's `atom_ctx(<rbind_atom>)` (resolved from the registries' atom_ctxs) -- step (b) inputs: this atom's `atom_ctx(<rbind_atom>)` (resolved from the registries' atom_ctxs)
local this_ctx = registries.atom_ctxs and registries.atom_ctxs[atom.name] local this_ctx = registries.atom_ctxs and registries.atom_ctxs[atom.name]
if this_ctx and this_ctx.rbind_atom then if this_ctx and this_ctx.rbind_atom then
local rbind = atom_by_name_global[this_ctx.rbind_atom] local rbind = atom_by_name_global[this_ctx.rbind_atom]
if rbind and rbind.rbind and rbind.rbind.fields then if rbind and rbind.rbind and rbind.rbind.fields then
atom_view_ctx_fields = {} atom_view_ctx_fields = {}
for _, f in ipairs(rbind.rbind.fields) do atom_view_ctx_fields[f.name] = f end for _, f in ipairs(rbind.rbind.fields) do atom_view_ctx_fields[f.name] = f end
if rbind.rbind.regs then if rbind.rbind.regs then
@@ -1931,11 +1922,11 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta
end end
if my_phase_label then if my_phase_label then
local group = (registries.atom_phases or {})[my_phase_label] local group = (registries.atom_phases or {})[my_phase_label]
if group and group.atoms then if group and group.atoms then
for _, group_atom_name in ipairs(group.atoms) do for _, group_atom_name in ipairs(group.atoms) do
if group_atom_name ~= atom.name then if group_atom_name ~= atom.name then
local cand = atom_by_name_global[group_atom_name] local cand = atom_by_name_global[group_atom_name]
if cand and cand.rbind and cand.rbind.fields then if cand and cand.rbind and cand.rbind.fields then
atom_view_phase_fields = {} atom_view_phase_fields = {}
for _, f in ipairs(cand.rbind.fields) do atom_view_phase_fields[f.name] = f end for _, f in ipairs(cand.rbind.fields) do atom_view_phase_fields[f.name] = f end
if cand.rbind.regs then if cand.rbind.regs then
@@ -1956,7 +1947,7 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta
-- (a) per-atom callsite atom_type(R_X, <T>): most specific; user explicit override for THIS atom only. -- (a) per-atom callsite atom_type(R_X, <T>): most specific; user explicit override for THIS atom only.
function(r_name, alias_code) function(r_name, alias_code)
local override = atom_view and atom_view.reg_type_overrides and atom_view.reg_type_overrides[r_name] local override = atom_view and atom_view.reg_type_overrides and atom_view.reg_type_overrides[r_name]
if override and override.pointer_depth and override.pointer_depth > 0 then if override and override.pointer_depth and override.pointer_depth > 0 then
return type_chain_offsets[override.type_name .. "|" .. override.pointer_depth] return type_chain_offsets[override.type_name .. "|" .. override.pointer_depth]
end end
end, end,
@@ -1964,7 +1955,7 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta
function(r_name, alias_code) function(r_name, alias_code)
local ctx_field_name = reg_to_field_ctx and reg_to_field_ctx[alias_code] local ctx_field_name = reg_to_field_ctx and reg_to_field_ctx[alias_code]
local ctx_f = ctx_field_name and atom_view_ctx_fields and atom_view_ctx_fields[ctx_field_name] local ctx_f = ctx_field_name and atom_view_ctx_fields and atom_view_ctx_fields[ctx_field_name]
if ctx_f and ctx_f.pointer_depth and ctx_f.pointer_depth > 0 then if ctx_f and ctx_f.pointer_depth and ctx_f.pointer_depth > 0 then
return type_chain_offsets[ctx_f.type_name .. "|" .. ctx_f.pointer_depth] return type_chain_offsets[ctx_f.type_name .. "|" .. ctx_f.pointer_depth]
end end
end, end,
@@ -1972,7 +1963,7 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta
function(r_name, alias_code) function(r_name, alias_code)
local field_name = reg_to_field[alias_code] local field_name = reg_to_field[alias_code]
local f = field_name and field_type_by_name[field_name] local f = field_name and field_type_by_name[field_name]
if f and f.pointer_depth and f.pointer_depth > 0 then if f and f.pointer_depth and f.pointer_depth > 0 then
return type_chain_offsets[f.type_name .. "|" .. f.pointer_depth] return type_chain_offsets[f.type_name .. "|" .. f.pointer_depth]
end end
end, end,
@@ -1980,12 +1971,13 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta
function(r_name, alias_code) function(r_name, alias_code)
local phase_field_name = reg_to_field_phase and reg_to_field_phase[alias_code] local phase_field_name = reg_to_field_phase and reg_to_field_phase[alias_code]
local phase_f = phase_field_name and atom_view_phase_fields and atom_view_phase_fields[phase_field_name] local phase_f = phase_field_name and atom_view_phase_fields and atom_view_phase_fields[phase_field_name]
if phase_f and phase_f.pointer_depth and phase_f.pointer_depth > 0 then if phase_f and phase_f.pointer_depth and phase_f.pointer_depth > 0 then
return type_chain_offsets[phase_f.type_name .. "|" .. phase_f.pointer_depth] return type_chain_offsets[phase_f.type_name .. "|" .. phase_f.pointer_depth]
end end
end, end,
-- (e) enum-site atom_type(<T>) default on the registry entry. -- (e) enum-site atom_type(<T>) default on the registry entry.
function(r_name, alias_code) function(r_name, alias_code)
-- TODO(Ed): Bad definition?
if alias and alias.default_type and alias.default_depth and alias.default_depth > 0 then if alias and alias.default_type and alias.default_depth and alias.default_depth > 0 then
return type_chain_offsets[alias.default_type .. "|" .. alias.default_depth] return type_chain_offsets[alias.default_type .. "|" .. alias.default_depth]
end end
@@ -1994,8 +1986,8 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta
-- Iterate `by_alias` in sorted order; Lua's pairs() is non-deterministic, so sorting ensures byte-identical DWARF output across builds. -- Iterate `by_alias` in sorted order; Lua's pairs() is non-deterministic, so sorting ensures byte-identical DWARF output across builds.
for _, r_name in ipairs(by_alias_order) do for _, r_name in ipairs(by_alias_order) do
local alias = by_alias[r_name] local alias = by_alias[r_name]
local rr_name = "RR_" .. strip_r_prefix(r_name) local rr_name = "RR_" .. strip_r_prefix(r_name)
local alias_code = alias.code local alias_code = alias.code
emit(uleb128(ABBREV_VARIABLE)) emit(uleb128(ABBREV_VARIABLE))
emit(rr_name .. "\0") -- DW_FORM_string (DW_AT_name) emit(rr_name .. "\0") -- DW_FORM_string (DW_AT_name)
@@ -2017,7 +2009,7 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta
-- Two PC ranges cover every field: [atom.addr, last_load+8) describes each field as tape memory (DW_OP_bregN + offset) piece, -- Two PC ranges cover every field: [atom.addr, last_load+8) describes each field as tape memory (DW_OP_bregN + offset) piece,
-- and [last_load+8, atom.end) describes each field as a GPR (DW_OP_regN) piece. -- and [last_load+8, atom.end) describes each field as a GPR (DW_OP_regN) piece.
if atom.rbind then if atom.rbind then
local binds_name = atom.rbind.binds local binds_name = atom.rbind.binds
local loclists_offset = loclists_offsets[atom.name] or 0 local loclists_offset = loclists_offsets[atom.name] or 0
emit(uleb128(ABBREV_BIND_VAR_LOCLIST)) emit(uleb128(ABBREV_BIND_VAR_LOCLIST))
emit("bind_args\0") -- DW_FORM_string (DW_AT_name) emit("bind_args\0") -- DW_FORM_string (DW_AT_name)
@@ -2070,7 +2062,7 @@ end
--- ---
--- Fails safely by returning existing sections unchanged if the table walker can't find the table terminator (malformed input). --- Fails safely by returning existing sections unchanged if the table walker can't find the table terminator (malformed input).
--- ---
--- @param existing string -- existing .debug_abbrev bytes, byte-for-byte --- @param existing string -- existing .debug_abbrev bytes, byte-for-byte
--- @param main_abbrev_offset integer -- 0-based offset into `existing` of the main CU's abbrev table --- @param main_abbrev_offset integer -- 0-based offset into `existing` of the main CU's abbrev table
--- @return string, integer -- (new_abbrev_bytes, offset_where_duplicate_table_starts = #existing) --- @return string, integer -- (new_abbrev_bytes, offset_where_duplicate_table_starts = #existing)
local function build_debug_abbrev_section(existing, main_abbrev_offset) local function build_debug_abbrev_section(existing, main_abbrev_offset)
@@ -2088,7 +2080,7 @@ local function build_debug_abbrev_section(existing, main_abbrev_offset)
end end
--- Build the new .debug_str: existing strings + new strings appended. --- Build the new .debug_str: existing strings + new strings appended.
--- @param existing string -- existing .debug_str bytes, byte-for-byte --- @param existing string -- existing .debug_str bytes, byte-for-byte
--- @param atom_table table[] --- @param atom_table table[]
--- @param registries table -- merged registries from collect_per_source_registries --- @param registries table -- merged registries from collect_per_source_registries
--- @return string -- existing bytes plus the deterministic appended strings --- @return string -- existing bytes plus the deterministic appended strings
@@ -2098,7 +2090,6 @@ local function build_debug_str_section(existing, atom_table, registries)
end end
--- Build the new .debug_info: SPLICE inserted DIEs into the MAIN CU as children. --- Build the new .debug_info: SPLICE inserted DIEs into the MAIN CU as children.
---
--- This implementation: --- This implementation:
--- 1. Builds the inserted-children bytes (base_type, struct_types, subprograms with their RR_* + bind_args children) via build_inserted_children. --- 1. Builds the inserted-children bytes (base_type, struct_types, subprograms with their RR_* + bind_args children) via build_inserted_children.
--- 2. Patches the main CU's `unit_length` field to account for the inserted bytes. --- 2. Patches the main CU's `unit_length` field to account for the inserted bytes.
@@ -2108,14 +2099,14 @@ end
--- ---
--- The crt CU (everything before main_cu_start) is preserved. --- The crt CU (everything before main_cu_start) is preserved.
--- @param existing string -- existing .debug_info section bytes --- @param existing string -- existing .debug_info section bytes
--- @param main_cu_start integer -- 0-based offset of the main CU's unit_length field --- @param main_cu_start integer -- 0-based offset of the main CU's unit_length field
--- @param main_cu_end_excl integer -- 0-based offset of the first byte AFTER the main CU --- @param main_cu_end_excl integer -- 0-based offset of the first byte AFTER the main CU
--- @param new_abbrev_offset integer -- 0-based offset into the new .debug_abbrev of the duplicate main table --- @param new_abbrev_offset integer -- 0-based offset into the new .debug_abbrev of the duplicate main table
--- @param atom_table table[] --- @param atom_table table[]
--- @param rbind_structs table -- {[binds_name] = {bytes, fields, atom_names}} --- @param rbind_structs table -- {[binds_name] = {bytes, fields, atom_names}}
--- @param loclists_offsets table -- {[atom_name] = section-relative offset} --- @param loclists_offsets table -- {[atom_name] = section-relative offset}
--- @param registries table -- merged registries from collect_per_source_registries --- @param registries table -- merged registries from collect_per_source_registries
--- @return string -- the rebuilt .debug_info bytes --- @return string -- the rebuilt .debug_info bytes
local function build_debug_info_section(existing, main_cu_start, main_cu_end_excl, new_abbrev_offset, atom_table, rbind_structs, loclists_offsets, registries) local function build_debug_info_section(existing, main_cu_start, main_cu_end_excl, new_abbrev_offset, atom_table, rbind_structs, loclists_offsets, registries)
-- 1) Build the inserted children bytes (just before the main CU's root terminator). -- 1) Build the inserted children bytes (just before the main CU's root terminator).
@@ -2173,13 +2164,13 @@ local SECTION_WRITERS = {
-- Write a list of `{name, data}` section records to disk via SECTION_WRITERS. -- Write a list of `{name, data}` section records to disk via SECTION_WRITERS.
-- @param results table[] -- list of `{name=, data=}` records to write -- @param results table[] -- list of `{name=, data=}` records to write
-- @param ctx PassCtx -- @param ctx PassCtx
-- @param basename string -- output file basename (e.g. "hello_gte") -- @param basename string -- output file basename (e.g. "hello_gte")
-- @return table -- list of {name_bin = path} entries to append to M.run's outputs -- @return table -- list of {name_bin = path} entries to append to M.run's outputs
local function write_sections(results, ctx, basename) local function write_sections(results, ctx, basename)
local outputs = {} local outputs = {}
for _, r in ipairs(results) do for _, r in ipairs(results) do
local path = SECTION_WRITERS[r.name](ctx.out_root, basename) local path = SECTION_WRITERS[r.name](ctx.out_root, basename)
local f = io.open(path, "wb") local f = io.open(path, "wb")
if not f then if not f then
io.stderr:write(string.format("[dwarf_injection] failed to open %s for write\n", path)) io.stderr:write(string.format("[dwarf_injection] failed to open %s for write\n", path))
else else
@@ -2206,7 +2197,7 @@ function M.run(ctx)
end end
-- Guard: --elf is required. -- Guard: --elf is required.
local elf_path = ctx.flags and ctx.flags.elf_path local elf_path = ctx.flags and ctx.flags.elf_path
if not elf_path or elf_path == "" then if not elf_path or elf_path == "" then
io.stderr:write("[dwarf_injection] --elf flag missing\n") io.stderr:write("[dwarf_injection] --elf flag missing\n")
return { outputs = {}, errors = {}, warnings = {} } return { outputs = {}, errors = {}, warnings = {} }
@@ -2217,7 +2208,8 @@ function M.run(ctx)
-- Read the existing DWARF sections directly (no subprocess; io.open + manual ELF32 section-header walk). -- Read the existing DWARF sections directly (no subprocess; io.open + manual ELF32 section-header walk).
-- We need all 8 sections: .debug_line / .debug_aranges / .debug_rnglists get extended (additional rows appended to the existing unit), -- We need all 8 sections: .debug_line / .debug_aranges / .debug_rnglists get extended (additional rows appended to the existing unit),
-- and .debug_info / .debug_abbrev / .debug_str / .debug_loc / .debug_loclists get spliced (the main CU's unit_length is patched; no new compile unit is appended; .debug_loc/.debug_loclists may not exist in the source ELF so we add-section them on splice). -- and .debug_info / .debug_abbrev / .debug_str / .debug_loc / .debug_loclists get spliced
-- (the main CU's unit_length is patched; no new compile unit is appended; .debug_loc/.debug_loclists may not exist in the source ELF so we add-section them on splice).
-- The per-section dispatch is inlined in the writers loop below. -- The per-section dispatch is inlined in the writers loop below.
local existing_sections = elf_dwarf.read_elf_sections(elf_path, { local existing_sections = elf_dwarf.read_elf_sections(elf_path, {
".debug_line", ".debug_aranges", ".debug_rnglists", ".debug_line", ".debug_aranges", ".debug_rnglists",
@@ -2232,12 +2224,11 @@ function M.run(ctx)
init_file_index_lookup(elf_path) init_file_index_lookup(elf_path)
-- Skip state lives in `corpus.atoms_by_name[*].debug_skip` (whole-atom) and `atom.paths.invocations[*].debug_skip` (per-invocation). -- Skip state lives in `corpus.atoms_by_name[*].debug_skip` (whole-atom) and `atom.paths.invocations[*].debug_skip` (per-invocation).
-- `corpus` is the sole canonical source projection. -- `corpus` is the sole canonical source projection.
local corpus = (ctx.shared and ctx.shared.corpus) or {} local corpus = (ctx.shared and ctx.shared.corpus) or {}
local registries = collect_per_source_registries(corpus) local registries = collect_per_source_registries(corpus)
-- Read nm symbols (the ONLY disk-side input to the atom table) and join -- Read nm symbols (the ONLY disk-side input to the atom table) and join them against `corpus.atoms_by_name` + `atom.paths` for word rows + invocation ancestry.
-- them against `corpus.atoms_by_name` + `atom.paths` for word rows + invocation ancestry.
-- Disk source-map/provenance text is not consulted (those are diagnostic artifacts; semantic inputs are in memory). -- Disk source-map/provenance text is not consulted (those are diagnostic artifacts; semantic inputs are in memory).
local addrs = elf_dwarf.read_nm(ctx.flags.elf_path) local addrs = elf_dwarf.read_nm(ctx.flags.elf_path)
local atom_table = build_atom_table(corpus, addrs) local atom_table = build_atom_table(corpus, addrs)
-- Detect rbind atoms + index Binds_* struct fields from the corpus. -- Detect rbind atoms + index Binds_* struct fields from the corpus.
@@ -2261,7 +2252,7 @@ function M.run(ctx)
duffle.ensure_dir(ctx.out_root) duffle.ensure_dir(ctx.out_root)
-- Step 0: layout validation. Bail out safely if the .debug_info layout doesn't match what we expect (crt CU + DWARF5 main CU + final 0 byte). -- Step 0: layout validation. Bail out safely if the .debug_info layout doesn't match what we expect (crt CU + DWARF5 main CU + final 0 byte).
-- A layout mismatch means the gcc emission changed; the safest response is to leave existing sections unchanged and emit no synthetic data, so the build's debug-info step never silently produces broken DWARF. -- A layout mismatch means the gcc emission changed; the safest response is to leave existing sections unchanged and emit no synthetic data, so the build's debug-info step never silently produces broken DWARF.
local existing_info = existing_sections[".debug_info"] or "" local existing_info = existing_sections[".debug_info"] or ""
local existing_abbrev = existing_sections[".debug_abbrev"] or "" local existing_abbrev = existing_sections[".debug_abbrev"] or ""
local main_cu_start, main_cu_end_excl, main_abbrev_offset = find_main_cu_layout(existing_info) local main_cu_start, main_cu_end_excl, main_abbrev_offset = find_main_cu_layout(existing_info)
@@ -2296,7 +2287,7 @@ function M.run(ctx)
local new_info = build_debug_info_section(existing_info, main_cu_start, main_cu_end_excl, new_abbrev_offset, atom_table, rbind_structs, loclists_offsets, registries) local new_info = build_debug_info_section(existing_info, main_cu_start, main_cu_end_excl, new_abbrev_offset, atom_table, rbind_structs, loclists_offsets, registries)
-- Step 2b: rebuild .debug_str now that we know which RR_<R_Name> entries get emitted. -- Step 2b: rebuild .debug_str now that we know which RR_<R_Name> entries get emitted.
-- This aligns with build_debug_info_section's by_alias loop. -- This aligns with build_debug_info_section's by_alias loop.
local new_str = build_debug_str_section(existing_sections[".debug_str"] or "", atom_table, registries) local new_str = build_debug_str_section(existing_sections[".debug_str"] or "", atom_table, registries)
-- Step 3-5: independent sections. -- Step 3-5: independent sections.
+7 -9
View File
@@ -41,7 +41,6 @@ local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
-- Convert the recursive walk's body-relative line numbers into physical source lines once. -- Convert the recursive walk's body-relative line numbers into physical source lines once.
-- The walker builds `line_of` from `body_text` and stamps body-relative line numbers (1..N) into `item.line` and `invocation.call_line`. -- The walker builds `line_of` from `body_text` and stamps body-relative line numbers (1..N) into `item.line` and `invocation.call_line`.
-- This function converts those values to physical source lines at the close site with the forwarded source `line_of` closure. -- This function converts those values to physical source lines at the close site with the forwarded source `line_of` closure.
--
-- `call_line` discipline: -- `call_line` discipline:
-- * ROOT invocations (`inv.parent_id == 0`) receive body-relative `call_line` values directly from `M.LineIndex(body_text)` in the walker. -- * ROOT invocations (`inv.parent_id == 0`) receive body-relative `call_line` values directly from `M.LineIndex(body_text)` in the walker.
-- The source `line_of` closure supplies physical lines at the close site, so this function converts each root value exactly once. -- The source `line_of` closure supplies physical lines at the close site, so this function converts each root value exactly once.
@@ -59,10 +58,9 @@ local function stamp_root_provenance(projection, atom_record, src, corpus)
-- `root_body_line` is the physical source line of the ATOM HEADER byte containing the opening `{`; that byte is one byte BEFORE `atom_record.body_off`. -- `root_body_line` is the physical source line of the ATOM HEADER byte containing the opening `{`; that byte is one byte BEFORE `atom_record.body_off`.
-- The walker assigns line 2 to the body's first content line because line 1 is the trailing `\n` after `{`. Body-text line k therefore maps to `root_body_line + (k - 1)`. -- The walker assigns line 2 to the body's first content line because line 1 is the trailing `\n` after `{`. Body-text line k therefore maps to `root_body_line + (k - 1)`.
-- `body_off - 1` points at the opening `{`, whose line index identifies the header line. `body_off` points after `{` and would shift every word row forward by one line. -- `body_off - 1` points at the opening `{`, whose line index identifies the header line. `body_off` points after `{` and would shift every word row forward by one line.
local root_body_line = root_line_of(atom_record.body_off - 1) local root_body_line = root_line_of(atom_record.body_off - 1) or atom_record.line or 0
or atom_record.line or 0
local component_index = corpus.component_body_index or {} local component_index = corpus.component_body_index or {}
local word_items = {} local word_items = {}
for _, item in ipairs(projection.items) do for _, item in ipairs(projection.items) do
if item.kind == "word" then word_items[#word_items + 1] = item end if item.kind == "word" then word_items[#word_items + 1] = item end
@@ -102,7 +100,7 @@ local function stamp_root_provenance(projection, atom_record, src, corpus)
end end
-- Normalize `inv.call_line` to a physical source line. -- Normalize `inv.call_line` to a physical source line.
-- * ROOT invocations (`parent_id == 0`) carry body-relative `call_line` values from `M.LineIndex(body_text)`; convert them once with `root_body_line`. -- * ROOT invocations (`parent_id == 0`) carry body-relative `call_line` values from `M.LineIndex(body_text)`; convert them once with `root_body_line`.
-- * INNER invocations (`parent_id ~= 0`) carry physical `call_line` values from the component's `line_of`; retain them unchanged. -- * INNER invocations (`parent_id ~= 0`) carry physical `call_line` values from the component's `line_of`; retain them unchanged.
for _, inv in ipairs(projection.invocations) do for _, inv in ipairs(projection.invocations) do
if inv.parent_id == 0 then if inv.parent_id == 0 then
@@ -114,14 +112,14 @@ local function stamp_root_provenance(projection, atom_record, src, corpus)
-- `atoms_source_map` and `dwarf_injection` read `inv.body_lines[k]` directly from the invocation record created here. -- `atoms_source_map` and `dwarf_injection` read `inv.body_lines[k]` directly from the invocation record created here.
-- Component words already carry physical `item.line` values from the walker's COMPONENT line index, so `body_line_for` returns them unchanged. -- Component words already carry physical `item.line` values from the walker's COMPONENT line index, so `body_line_for` returns them unchanged.
for _, inv in ipairs(projection.invocations) do for _, inv in ipairs(projection.invocations) do
local sw = inv.start_word local sw = inv.start_word
local ew = inv.end_word local ew = inv.end_word
local bls = {} local bls = {}
for i = sw, ew do for i = sw, ew do
local it = projection.items and projection.items[i] local it = projection.items and projection.items[i]
if it and it.kind == "word" then if it and it.kind == "word" then
local fake_event = { invocation_ids = { inv.id } } local fake_event = { invocation_ids = { inv.id } }
bls[#bls + 1] = body_line_for(fake_event, it) or 0 bls[#bls + 1] = body_line_for(fake_event, it) or 0
end end
end end
inv.body_lines = bls inv.body_lines = bls
+96 -59
View File
@@ -3,12 +3,11 @@
--- Reads the pre-scanned SourceScan payload (produced once upstream by `duffle.scan_source`) --- Reads the pre-scanned SourceScan payload (produced once upstream by `duffle.scan_source`)
--- for `MipsAtom_(name)` and `MipsCode code_<name>` declarations, computes the word offset --- 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 --- from each `atom_offset(F, T)` marker to its target `atom_label(T)` declaration, and emits
--- `<dir_basename>.offsets.h` with one `#define _atom_offset_F_T = N` per branch. --- `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.
--- ---
--- The offset is `target_word - branch_word - 1` (the standard MIPS branch-immediate encoding: branch_offset = relative_pc_in_words - 1). --- The offset is `target_word - branch_word - 1` (the standard MIPS branch-immediate encoding: branch_offset = relative_pc_in_words - 1).
---
--- **Conventions**: tabs (1/level), EmmyLua annotations, no regex,
--- Lua 5.3 compatible.
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Module-scope requires + package.path setup -- Module-scope requires + package.path setup
@@ -16,12 +15,11 @@
-- Bootstrap: same as entry scripts. See `ps1_meta.lua` for the rationale. -- Bootstrap: same as entry scripts. See `ps1_meta.lua` for the rationale.
-- Bootstrap: load `scripts/duffle_paths.lua` (sets package.path + package.cpath). -- Bootstrap: load `scripts/duffle_paths.lua` (sets package.path + package.cpath).
-- Uses `debug.getinfo` to find this file's own directory, so it works -- Uses `debug.getinfo` to find this file's own directory, so it works both standalone and when require'd from the orchestrator.
-- both standalone and when require'd from the orchestrator.
-- Bootstrap: load `duffle_paths.lua` via `debug.getinfo(1, "S").source` (works both standalone + when require'd). -- Bootstrap: load `duffle_paths.lua` via `debug.getinfo(1, "S").source` (works both standalone + when require'd).
-- duffle_paths.lua sets package.path then returns `require("duffle")` at the bottom, so the dofile value IS the duffle module. -- duffle_paths.lua sets package.path then returns `require("duffle")` at the bottom, so the dofile value IS the duffle module.
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./"
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Constants -- Constants
@@ -39,40 +37,42 @@ local OFFSET_MACRO_COL = 44
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
--- @class SourceFile --- @class SourceFile
--- @field path string -- absolute path to the source file --- @field path string -- Absolute path to the source file
--- @field text string -- the full source text --- @field text string -- Full source text
--- @field dir string -- the directory containing the source --- @field dir string -- Directory containing the source
--- @field basename string -- filename without extension --- @field basename string -- Filename without extension
--- @field scan table -- pre-scanned SourceScan payload (from duffle.scan_source) --- @field scan table -- Pre-scanned SourceScan payload (from duffle.scan_source)
--- @class PassCtx --- @class PassCtx
--- @field shared table -- cross-pass shared state --- @field shared table -- Cross-pass shared state
--- @field shared.corpus table -- canonical corpus projection --- @field shared.corpus table -- Corpus projection
--- @field shared.word_counts table --- @field shared.word_counts table
--- @field out_root string -- output root (e.g. "build/gen") --- @field out_root string -- Output root (e.g. "build/gen")
--- @class PassResult --- @class PassResult
--- @field outputs table[] -- {kind=, path=} entries describing emit files --- @field outputs table[] -- {kind=, path=} entries describing emit files
--- @field errors table[] -- {line=, msg=} entries; build-stops --- @field errors table[] -- {line=, msg=} entries; build-stops
--- @field warnings table[] -- {line=, msg=} entries; build-succeeds --- @field warnings table[] -- {line=, msg=} entries; build-succeeds
--- @class BranchOffset --- @class BranchOffset
--- @field tag string -- the marker tag (e.g. "F" in `atom_offset(F, T)`) --- @field tag string -- Marker tag (e.g. "F" in `atom_offset(F, T)`)
--- @field target string -- the target label name (e.g. "T" in `atom_offset(F, T)`) --- @field target string -- Target label name (e.g. "T" in `atom_offset(F, T)`)
--- @field branch_word integer -- branch word position within the atom body --- @field branch_word integer -- Branch word position within the atom body
--- @field offset integer -- computed `target_word - branch_word - 1` --- @field offset integer -- Computed per consuming instruction (see `compute_offsets`)
--- @field consuming_encoder string|nil -- Instruction consuming the offset (e.g. "branch_le_zero", "jump", "call_addr")
--- @field consuming_arg_pos integer|nil -- 1-based arg position within the consuming instruction's arg list
--- @class AtomData --- @class AtomData
--- @field name string -- atom name --- @field name string -- Atom name
--- @field total_words integer -- total word count of the atom body --- @field total_words integer -- Total word count of the atom body
--- @field offsets BranchOffset[] -- per-branch offset list --- @field offsets BranchOffset[] -- Per-branch offset list
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Canonical marker projection -- Canonical marker projection
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- MARKER_PROJECTORS is the marker-kind data table. -- MARKER_PROJECTORS is the marker-kind data table.
-- The emission-model pass already records marker word positions; -- The emission-model pass already records marker word positions + consuming-instruction context;
-- this pass only projects those records into the label/branch lookup shape needed by offset computation. -- this pass only projects those records into the label/branch lookup shape needed by offset computation.
local MARKER_PROJECTORS = { local MARKER_PROJECTORS = {
label = function(state, marker) label = function(state, marker)
@@ -80,9 +80,11 @@ local MARKER_PROJECTORS = {
end, end,
offset = function(state, marker) offset = function(state, marker)
state.branches[#state.branches + 1] = { state.branches[#state.branches + 1] = {
tag = marker.name, tag = marker.name,
target = marker.target, target = marker.target,
branch_word = marker.word_index, branch_word = marker.word_index,
consuming_encoder = marker.consuming_encoder,
consuming_arg_pos = marker.consuming_arg_pos,
} }
end, end,
} }
@@ -95,7 +97,7 @@ local function project_markers(markers)
local state = { labels = {}, branches = {} } local state = { labels = {}, branches = {} }
for _, marker in ipairs(markers or {}) do for _, marker in ipairs(markers or {}) do
local project = MARKER_PROJECTORS[marker.kind] local project = MARKER_PROJECTORS[marker.kind]
if project then project(state, marker) end if project then project(state, marker) end
end end
return state.labels, state.branches return state.labels, state.branches
end end
@@ -104,8 +106,19 @@ end
-- Offset computation + header generation -- Offset computation + header generation
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
--- Compute branch offsets as `target_word - branch_word - 1` (the standard MIPS branch-immediate encoding). --- Compute branch offsets per consuming instruction.
--- @param labels table<string, integer> --- Disposition table:
--- `branch_*` -> relative offset: `target_word - branch_word - 1` (MIPS branch-immediate encoding).
--- `jump` / `call_addr` -> same value as `branch_*` (a relative word offset).
--- The duffle headers' `enc_i` macro truncates the value to the immediate-field width (16 bits for branches, 26 bits for jumps).
--- For tape-atom bodies within a single module, this works for `j`/`jal` because the linker's symbol resolution produces the correct 26-bit absolute target via standard `j` relocations.
--- For cross-module `j`/`jal` (atom body in one module, target in another), the linker emits a `R_MIPS_26` relocation against the lower 26 bits; the upper 4 bits come from the PC of the delay slot following the `j`.
--- The metaprogram doesn't know either at compile time, so the emitted value is the relative word offset that the duffle `enc_i` macro places in the immediate field; the toolchain handles the rest.
--- `jump_reg` / `call_reg` / `jump_link` -> ERROR. Register-form jumps have no offset field; `atom_offset` is invalid.
---
--- Top-level `atom_offset(F, T)` markers (where the marker is the entire token — `consuming_encoder` == nil) default to `branch_*` behavior (relative offset).
--- This preserves backward compatibility for any top-level marker that may exist outside a control-transfer instruction.
--- @param labels table<string, integer>
--- @param branches table[] --- @param branches table[]
--- @return BranchOffset[] --- @return BranchOffset[]
local function compute_offsets(labels, branches) local function compute_offsets(labels, branches)
@@ -115,11 +128,23 @@ local function compute_offsets(labels, branches)
if not target then if not target then
error("Branch target '" .. br.target .. "' has no atom_label (at word " .. br.branch_word .. ")") error("Branch target '" .. br.target .. "' has no atom_label (at word " .. br.branch_word .. ")")
end end
local consuming = br.consuming_encoder
local offset
if consuming == "jump_reg" or consuming == "call_reg" or consuming == "jump_link" then
-- Register-form jumps have no offset field. `atom_offset` cannot be used here.
error("atom_offset cannot be used with " .. consuming
.. " (register-form jumps have no offset field); at word " .. br.branch_word)
end
-- All other consuming instructions (including `branch_*`, `jump`, `call_addr`, and nil for top-level markers) use the same relative offset value.
-- The MIPS encoding differs per opcode but the duffle `enc_i` macro handles the truncation to the immediate-field width.
offset = target - br.branch_word - 1
results[#results + 1] = { results[#results + 1] = {
target = br.target, target = br.target,
tag = br.tag, tag = br.tag,
branch_word = br.branch_word, branch_word = br.branch_word,
offset = target - br.branch_word - 1, offset = offset,
consuming_encoder = br.consuming_encoder,
consuming_arg_pos = br.consuming_arg_pos,
} }
end end
return results return results
@@ -167,44 +192,48 @@ local function emit_atom_offsets(add, atom)
add("") add("")
end end
--- Generate the per-source .offsets.h header. --- Generate the per-directory .offsets.h header.
--- @param source_path string --- @param dir string -- the absolute source directory
--- @param atoms_data AtomData[] --- @param sources table[] -- sources contributing to this directory (for the header comment)
--- @param atoms_data AtomData[]
--- @return string --- @return string
local function generate_header(source_path, atoms_data) local function generate_header(dir, sources, atoms_data)
local basename = duffle.basename_no_ext(source_path) local dir_basename = duffle.basename_no_ext(dir)
local lines = {} local lines = {}
local function add(s) lines[#lines + 1] = s end local function add(s) lines[#lines + 1] = s end
add("// Auto-generated by ps1_meta.lua (passes/offsets.lua) — DO NOT EDIT") add("// Auto-generated by ps1_meta.lua (passes/offsets.lua) — DO NOT EDIT")
add("// Source: " .. source_path) add("// Directory: " .. dir:gsub("/", "\\") .. "\\")
for _, src in ipairs(sources) do
add("// source: " .. src.path:gsub("/", "\\"))
end
add("#pragma once") add("#pragma once")
add("") add("")
add("#pragma region " .. basename) add("#pragma region " .. dir_basename)
add("") add("")
add("") add("")
for _, atom in ipairs(atoms_data) do for _, atom in ipairs(atoms_data) do
emit_atom_offsets(add, atom) emit_atom_offsets(add, atom)
end end
add("#pragma endregion " .. basename) add("#pragma endregion " .. dir_basename)
add("") add("")
return table.concat(lines, "\n") .. "\n" return table.concat(lines, "\n") .. "\n"
end end
local M = {} local M = {}
--- (internal) Process one source: render offsets from canonical atom paths. --- (internal) Aggregate atoms from every source in one directory, render the per-directory `offsets.h`.
--- Returns the offsets_h path if a header was written, or nil. --- Returns the offsets_h path if a header was written, or nil.
--- @param ctx PassCtx --- @param ctx PassCtx
--- @param src SourceFile --- @param dir string -- the absolute source directory
--- @param sources SourceFile[] -- sources in this directory
--- @return string|nil -- the offsets_h path --- @return string|nil -- the offsets_h path
local function process_source(ctx, src) local function process_directory(ctx, dir, sources)
local atoms_data = {} local atoms_data = {}
local scan = src.scan or {}
local function append_atom(atom) local function append_atom(atom)
local paths = atom and atom.paths local paths = atom and atom.paths
if not paths then return end if not paths then return end
local labels, branches = project_markers(paths.markers) local labels, branches = project_markers(paths.markers)
atoms_data[#atoms_data + 1] = { atoms_data[#atoms_data + 1] = {
@@ -214,19 +243,22 @@ local function process_source(ctx, src)
} }
end end
for _, atom in ipairs(scan.atoms or {}) do append_atom(atom) end for _, src in ipairs(sources) do
for _, atom in ipairs(scan.raw_atoms or {}) do append_atom(atom) end local scan = src.scan or {}
for _, atom in ipairs(scan.atoms or {}) do append_atom(atom) end
for _, atom in ipairs(scan.raw_atoms or {}) do append_atom(atom) end
end
if #atoms_data == 0 then return nil end if #atoms_data == 0 then return nil end
local out_path = src.dir .. "/gen/" .. duffle.basename_no_ext(src.dir) .. ".offsets.h" local out_path = dir .. "/gen/offsets.h"
duffle.ensure_dir(duffle.dirname(out_path)) duffle.ensure_dir(duffle.dirname(out_path))
duffle.write_file(out_path, generate_header(src.path:gsub("/", "\\"), atoms_data)) duffle.write_file(out_path, generate_header(dir, sources, atoms_data))
return out_path return out_path
end end
--- Run the offsets pass. --- Run the offsets pass.
--- For each canonical source, emits a per-module `<dir_basename>.offsets.h` --- For each canonical source-directory, emits a per-directory `gen/offsets.h`
--- containing constants for every marker recorded in atom.paths. --- containing constants for every marker recorded in atom.paths across every source in that directory.
--- @param ctx PassCtx --- @param ctx PassCtx
--- @return PassResult --- @return PassResult
function M.run(ctx) function M.run(ctx)
@@ -235,12 +267,17 @@ function M.run(ctx)
local warnings = {} local warnings = {}
local corpus = ctx.shared and ctx.shared.corpus local corpus = ctx.shared and ctx.shared.corpus
if type(corpus) ~= "table" or type(corpus.source_order) ~= "table" then if type(corpus) ~= "table" then
error("offsets.run requires ctx.shared.corpus.source_order (canonical corpus).", 0) error("offsets.run requires ctx.shared.corpus", 0)
end
if type(corpus.source_order) ~= "table" then
error("offsets.run requires ctx.shared.corpus.source_order.", 0)
end end
for _, src in ipairs(corpus.source_order) do -- Per-directory aggregation: every source in the same directory contributes to one `gen/offsets.h`.
local out_path = process_source(ctx, src) local sources_by_dir = corpus.sources_by_dir or duffle.group_sources_by_dir(corpus.source_order)
for dir, sources in pairs(sources_by_dir) do
local out_path = process_directory(ctx, dir, sources)
if out_path then if out_path then
outputs[#outputs + 1] = { offsets_h = out_path } outputs[#outputs + 1] = { offsets_h = out_path }
end end
+77 -76
View File
@@ -1,22 +1,21 @@
--- passes/report.lua — Per-MODULE annotation report renderer + --- passes/report.lua — Per-MODULE annotation report renderer + project-wide summary writer.
--- project-wide summary writer.
--- ---
--- Two output files per build: --- Two output files per build:
--- - `build/gen/<dir_basename>.annotations.txt` — one per source-directory containing atoms; aggregates across all sources in the directory. --- - `build/gen/<dir_basename>.annotations.txt` — one per source-directory containing atoms; aggregates across all sources in the directory.
--- - `build/gen/annotation_validation.txt` — the project summary. --- - `build/gen/annotation_validation.txt` — the project summary.
--- ---
--- The annotation pass emits `errors.h` files per module and the canonical `corpus.sources_by_dir` projection groups sources by directory. --- The annotation pass emits `errors.h` files per module and the canonical `corpus.sources_by_dir` projection groups sources by directory.
--- This pass iterates the canonical dir projection directly and re-validates each source via `annotation.validate()` to get the detailed per-source results. --- This pass iterates the dir projection directly and re-validates each source via `annotation.validate()` to get the detailed per-source results.
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Module-scope requires + package.path setup -- Module-scope requires + package.path setup
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Resolve `arg[0]` to an absolute-ish script directory so that `require("duffle")` resolves against `scripts/` regardless of CWD. -- Resolve `arg[0]` to an absolute-ish script directory so that `require("duffle")` resolves against `scripts/` regardless of CWD.
-- Bootstrap: see `ps1_meta.lua` for the rationale. -- Bootstrap: See `ps1_meta.lua` for the rationale.
-- Bootstrap: load `scripts/duffle_paths.lua` (sets package.path + package.cpath). -- Bootstrap: Load `scripts/duffle_paths.lua` (sets package.path + package.cpath).
-- Uses `debug.getinfo` to find this file's own directory, so it works both standalone and when require'd from the orchestrator. -- Uses `debug.getinfo` to find this file's own directory, so it works both standalone and when require'd from the orchestrator.
-- Bootstrap: load `duffle_paths.lua` via `debug.getinfo(1, "S").source` (works both standalone + when require'd). -- Bootstrap: Load `duffle_paths.lua` via `debug.getinfo(1, "S").source` (works both standalone + when require'd).
-- duffle_paths.lua sets package.path then returns `require("duffle")` at the bottom, so the dofile value IS the duffle module. -- duffle_paths.lua sets package.path then returns `require("duffle")` at the bottom, so the dofile value IS the duffle module.
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./"
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
@@ -37,7 +36,7 @@ local atoms_source_map = dofile(_bootstrap_dir .. "atoms_source_map.lua")
-- Section separators used in the rendered text reports. -- Section separators used in the rendered text reports.
-- The thin rules are hand-tuned to align with the per-section content width; do not change without also checking the section renderers below. -- The thin rules are hand-tuned to align with the per-section content width; do not change without also checking the section renderers below.
local RULE_THICK = "========================================================" local RULE_THICK = "========================================================"
local SECTION_HEADER_ATOMS = "── Atoms ────────────────────────────────────────────────" local SECTION_HEADER_ATOMS = "── Atoms ────────────────────────────────────────────────"
local SECTION_HEADER_ANNOTS = "── Annotations ──────────────────────────────────────────" local SECTION_HEADER_ANNOTS = "── Annotations ──────────────────────────────────────────"
local SECTION_HEADER_BINDS = "── Binds_* structs ──────────────────────────────────────" local SECTION_HEADER_BINDS = "── Binds_* structs ──────────────────────────────────────"
@@ -59,83 +58,83 @@ local PASS_NAME = "report"
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
--- @class SourceFile --- @class SourceFile
--- @field path string -- absolute path to the source file --- @field path string -- Absolute path to the source file
--- @field text string -- the full source text --- @field text string -- Full source text
--- @field dir string -- the directory containing the source --- @field dir string -- Directory containing the source
--- @field basename string -- filename without extension --- @field basename string -- Filename without extension
--- @class PassCtx --- @class PassCtx
--- @field sources SourceFile[] -- all source files in the build --- @field sources SourceFile[] -- All source files in the build
--- @field metadata_path string -- path to word_count.metadata.h --- @field metadata_path string -- Path to word_count.metadata.h
--- @field shared table -- cross-pass shared state --- @field shared table -- Cross-pass shared state
--- @field out_root string -- output root (e.g. "build/gen") --- @field out_root string -- Output root (e.g. "build/gen")
--- @field project_root string -- project root (e.g. "code/") --- @field project_root string -- Project root (e.g. "code/")
--- @field upstream table<string, table> -- per-pass upstream outputs --- @field upstream table<string, table> -- Per-pass upstream outputs
--- @field flags table -- CLI flags + per-pass stash --- @field flags table -- CLI flags + per-pass stash
--- @field verbose boolean -- if true, log diagnostic info --- @field verbose boolean -- If true, log diagnostic info
--- @class PassResult --- @class PassResult
--- @field outputs table[] -- {kind=, path=} entries describing emit files --- @field outputs table[] -- {kind=, path=} entries describing emit files
--- @field errors table[] -- {line=, msg=} entries; build-stops --- @field errors table[] -- {line=, msg=} entries; build-stops
--- @field warnings table[] -- {line=, msg=} entries; build-succeeds --- @field warnings table[] -- {line=, msg=} entries; build-succeeds
-- Shapes produced by `passes/annotation.lua`'s `M.validate()`. -- Shapes produced by `passes/annotation.lua`'s `M.validate()`.
--- @class AtomEntry --- @class AtomEntry
--- @field name string -- atom name (e.g. "cube_g4_face") --- @field name string -- Atom name (e.g. "cube_g4_face")
--- @field line integer -- source line of the atom declaration --- @field line integer -- Source line of the atom declaration
--- @class AnnotEntry --- @class AnnotEntry
--- @field line integer -- source line --- @field line integer -- Source line
--- @field macro string -- the macro name (e.g. "atom_reads") --- @field macro string -- Macro name (e.g. "atom_reads")
--- @field name string -- the atom name (if a `name(...)` was given) --- @field name string -- Atom name (if a `name(...)` was given)
--- @field kind string -- "atom_info" | "atom_bind" | ... --- @field kind string -- "atom_info" | "atom_bind" | ...
--- @field binds string|nil -- Binds_X name if any --- @field binds string|nil -- Binds_X name if any
--- @field reads string[] -- R_* names (read targets) --- @field reads string[] -- R_* names (read targets)
--- @field writes string[] -- R_* names (write targets) --- @field writes string[] -- R_* names (write targets)
--- @field error string|nil -- error message if annotation was malformed --- @field error string|nil -- Error message if annotation was malformed
--- @class BindsField --- @class BindsField
--- @field name string -- field name --- @field name string -- Field name
--- @field offset integer -- byte offset within the Binds_X struct --- @field offset integer -- Byte offset within the Binds_X struct
--- @class BindsStruct --- @class BindsStruct
--- @field name string -- struct name (e.g. "Binds_Floor") --- @field name string -- Struct name (e.g. "Binds_Floor")
--- @field line integer -- source line of the typedef --- @field line integer -- Source line of the typedef
--- @field bytes integer -- total byte size --- @field bytes integer -- Total byte size
--- @field fields BindsField[] -- the field list --- @field fields BindsField[] -- The field list
--- @class MacroEntry --- @class MacroEntry
--- @field name string -- macro name (e.g. "WORD_COUNT(my_macro, 4)") --- @field name string -- Macro name (e.g. "WORD_COUNT(my_macro, 4)")
--- @field line integer -- source line --- @field line integer -- Source line
--- @field words integer -- declared word count --- @field words integer -- Declared word count
--- @class Finding --- @class Finding
--- @field line integer -- source line --- @field line integer -- Source line
--- @field msg string -- finding message --- @field msg string -- Finding message
--- @class AnnotationResult --- @class AnnotationResult
--- @field source string -- set by this pass; original source path --- @field source string -- Set by this pass; original source path
--- @field atoms AtomEntry[] -- atom declarations in this source --- @field atoms AtomEntry[] -- Atom declarations in this source
--- @field annots AnnotEntry[] -- annotation entries --- @field annots AnnotEntry[] -- Annotation entries
--- @field macros MacroEntry[] -- macro word-count declarations --- @field macros MacroEntry[] -- Macro word-count declarations
--- @field binds BindsStruct[] -- Binds_* struct declarations --- @field binds BindsStruct[] -- Binds_* struct declarations
--- @field errors Finding[] -- errors from validation --- @field errors Finding[] -- Errors from validation
--- @field warnings Finding[] -- warnings from validation --- @field warnings Finding[] -- Warnings from validation
--- @field info table -- info summary (not rendered here) --- @field info table -- Info summary (not rendered here)
--- @class ModuleEntry --- @class ModuleEntry
--- @field dir string -- absolute directory path --- @field dir string -- Absolute directory path
--- @field dir_basename string -- basename (e.g. "duffle", "gte_hello") --- @field dir_basename string -- Basename (e.g. "duffle", "gte_hello")
--- @field atoms_count integer -- pre-counted atoms for filtering --- @field atoms_count integer -- Pre-counted atoms for filtering
--- @class ModuleReport --- @class ModuleReport
--- @field dir string -- module directory --- @field dir string -- Module directory
--- @field sources SourceFile[] -- sources in this module --- @field sources SourceFile[] -- Sources in this module
--- @field results AnnotationResult[] -- per-source validate() results --- @field results AnnotationResult[] -- Per-source validate() results
--- @class ProjectReport --- @class ProjectReport
--- @field results AnnotationResult[] -- all per-source results --- @field results AnnotationResult[] -- All per-source results
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Per-MODULE annotation report (aggregated across all sources in a dir) -- Per-MODULE annotation report (aggregated across all sources in a dir)
@@ -153,9 +152,16 @@ end
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
--- Render the thin project-wide summary (`build/atom_meta_report.summary.md`). --- Render the thin project-wide summary (`build/atom_meta_report.summary.md`).
--- @param all_results { module:string, atoms:integer, annots:integer, binds:integer, --- @param all_results {
--- macros:integer, findings:integer, errors:integer, --- module:string,
--- warnings:integer, info:integer }[] --- atoms:integer,
--- annots:integer,
--- binds:integer,
--- macros:integer,
--- findings:integer,
--- errors:integer,
--- warnings:integer,
--- info:integer }[]
--- @return string --- @return string
local function render_project_summary(all_results) local function render_project_summary(all_results)
local lines = { local lines = {
@@ -165,13 +171,10 @@ local function render_project_summary(all_results)
"| module | atoms | annots | binds | macros | findings | errors | warnings | info |", "| module | atoms | annots | binds | macros | findings | errors | warnings | info |",
"|--------|-------|--------|-------|--------|----------|--------|----------|------|", "|--------|-------|--------|-------|--------|----------|--------|----------|------|",
} }
local totals = { atoms = 0, annots = 0, binds = 0, macros = 0, local totals = { atoms = 0, annots = 0, binds = 0, macros = 0, findings = 0, errors = 0, warnings = 0, info = 0 }
findings = 0, errors = 0, warnings = 0, info = 0 }
for _, e in ipairs(all_results) do for _, e in ipairs(all_results) do
lines[#lines + 1] = string.format( lines[#lines + 1] = string.format("| %s | %d | %d | %d | %d | %d | %d | %d | %d |"
"| %s | %d | %d | %d | %d | %d | %d | %d | %d |", , e.module, e.atoms, e.annots, e.binds, e.macros, e.findings, e.errors, e.warnings, e.info)
e.module, e.atoms, e.annots, e.binds, e.macros,
e.findings, e.errors, e.warnings, e.info)
totals.atoms = totals.atoms + e.atoms totals.atoms = totals.atoms + e.atoms
totals.annots = totals.annots + e.annots totals.annots = totals.annots + e.annots
totals.binds = totals.binds + e.binds totals.binds = totals.binds + e.binds
@@ -181,23 +184,21 @@ local function render_project_summary(all_results)
totals.warnings = totals.warnings + e.warnings totals.warnings = totals.warnings + e.warnings
totals.info = totals.info + e.info totals.info = totals.info + e.info
end end
lines[#lines + 1] = string.format( lines[#lines + 1] = string.format("| **TOTAL** | %d | %d | %d | %d | %d | %d | %d | %d |"
"| **TOTAL** | %d | %d | %d | %d | %d | %d | %d | %d |", , totals.atoms, totals.annots, totals.binds, totals.macros, totals.findings, totals.errors, totals.warnings, totals.info)
totals.atoms, totals.annots, totals.binds, totals.macros,
totals.findings, totals.errors, totals.warnings, totals.info)
return table.concat(lines, "\n") .. "\n" return table.concat(lines, "\n") .. "\n"
end end
--- Render the per-module verbose source-map markdown (`build/<module>.atoms.md`). --- Render the per-module verbose source-map markdown (`build/<module>.atoms.md`).
--- Per-source sub-section, per-atom stanza with sourcemap + provenance rows. --- Per-source sub-section, per-atom stanza with sourcemap + provenance rows.
--- Pulls sourcemap + provenance from `atoms_source_map` (no second source walk). --- Pulls sourcemap + provenance from `atoms_source_map` (no second source walk).
--- @param dir string --- @param dir string
--- @param dir_sources SourceFile[] --- @param dir_sources SourceFile[]
--- @param wc table<string, integer> --- @param wc table<string, integer>
--- @return string --- @return string
local function render_module_atoms_md(dir, dir_sources, wc) local function render_module_atoms_md(dir, dir_sources, wc)
local dir_basename = source_basename(dir) local dir_basename = source_basename(dir)
local lines = { local lines = {
"# " .. dir_basename .. " — atoms (verbose source map)", "# " .. dir_basename .. " — atoms (verbose source map)",
"> Per-word call-site + provenance. Auto-generated.", "> Per-word call-site + provenance. Auto-generated.",
"", "",
@@ -249,14 +250,14 @@ end
--- Aggregates annotation + static-analysis content across all sources in `dir`. --- Aggregates annotation + static-analysis content across all sources in `dir`.
--- Annotations come from re-running `annotation.validate()` per source (the existing pattern); --- Annotations come from re-running `annotation.validate()` per source (the existing pattern);
--- static-analysis comes from `corpus.static_analysis_results[dir_basename]` (populated by `static_analysis.lua` — no second corpus_pipe_ctx build). --- static-analysis comes from `corpus.static_analysis_results[dir_basename]` (populated by `static_analysis.lua` — no second corpus_pipe_ctx build).
--- @param dir string --- @param dir string
--- @param dir_sources SourceFile[] --- @param dir_sources SourceFile[]
--- @param annot_results AnnotationResult[] --- @param annot_results AnnotationResult[]
--- @param sa_results table -- corpus.static_analysis_results[dir_basename] --- @param sa_results table -- corpus.static_analysis_results[dir_basename]
--- @return string --- @return string
local function render_module_meta_report(dir, dir_sources, annot_results, sa_results) local function render_module_meta_report(dir, dir_sources, annot_results, sa_results)
local dir_basename = source_basename(dir) local dir_basename = source_basename(dir)
local lines = { local lines = {
"# " .. dir_basename .. " — atom meta report", "# " .. dir_basename .. " — atom meta report",
"> Auto-generated by ps1_meta.lua (passes/report.lua). Do not edit.", "> Auto-generated by ps1_meta.lua (passes/report.lua). Do not edit.",
"", "",
@@ -326,8 +327,8 @@ local function render_module_meta_report(dir, dir_sources, annot_results, sa_res
local binds = a.binds or "" local binds = a.binds or ""
local reads = (#a.reads > 0 and table.concat(a.reads, ",")) or "" local reads = (#a.reads > 0 and table.concat(a.reads, ",")) or ""
local writes = (#a.writes > 0 and table.concat(a.writes, ",")) or "" local writes = (#a.writes > 0 and table.concat(a.writes, ",")) or ""
add(string.format("| %s | %d | %s | %s | %s | %s |", add(string.format("| %s | %d | %s | %s | %s | %s |"
src_name, a.line, a.name, binds, reads, writes)) , src_name, a.line, a.name, binds, reads, writes))
end end
end end
end end
@@ -418,7 +419,7 @@ local function render_module_meta_report(dir, dir_sources, annot_results, sa_res
for _, a in ipairs(sorted) do for _, a in ipairs(sorted) do
local p = a.paths or {} local p = a.paths or {}
local src_name = a.source_path and source_basename(a.source_path) or "" local src_name = a.source_path and source_basename(a.source_path) or ""
local notes = "" local notes = ""
if p.has_loops then notes = notes .. " [loop!]" end if p.has_loops then notes = notes .. " [loop!]" end
if p.unknown_macros and #p.unknown_macros > 0 then if p.unknown_macros and #p.unknown_macros > 0 then
notes = notes .. " [unknown: " .. table.concat(p.unknown_macros, ", ") .. "]" notes = notes .. " [unknown: " .. table.concat(p.unknown_macros, ", ") .. "]"
@@ -426,7 +427,7 @@ local function render_module_meta_report(dir, dir_sources, annot_results, sa_res
add(string.format("| %s | %s | %d | %d | %d | %d | %s |", add(string.format("| %s | %s | %d | %d | %d | %d | %s |",
a.name, src_name, a.name, src_name,
p.cycles_min or 0, p.cycles_max or 0, p.cycles_min or 0, p.cycles_max or 0,
p.branches or 0, p.paths or 0, notes)) p.branches or 0, p.paths or 0, notes))
end end
add("") add("")
+149 -154
View File
@@ -2,7 +2,6 @@
--- ---
--- Single source-walk pass that produces the fat `SourceScan` payload consumed by all downstream passes. Walks each corpus source record once, --- 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: --- extracting every construct type the metaprograms need:
---
--- MipsAtom_ (kind = "atom", with optional atom_info inner) --- MipsAtom_ (kind = "atom", with optional atom_info inner)
--- MipsAtomComp_ (kind = "comp_bare") --- MipsAtomComp_ (kind = "comp_bare")
--- MipsAtomComp_Proc_ (kind = "comp_proc", body inside last {}) --- MipsAtomComp_Proc_ (kind = "comp_proc", body inside last {})
@@ -14,8 +13,6 @@
--- The result is attached to each `src.scan` so downstream passes can read from `src.scan.atoms` / `src.scan.binds` / etc. without re-walking the source. --- The result is attached to each `src.scan` so downstream passes can read from `src.scan.atoms` / `src.scan.binds` / etc. without re-walking the source.
--- This is the first pass in the dep graph (no deps). --- This is the first pass in the dep graph (no deps).
--- Every other pass that reads source structure depends on this one — see `ps1_meta.lua :: PASSES`. --- Every other pass that reads source structure depends on this one — see `ps1_meta.lua :: PASSES`.
---
--- **Conventions**: tabs (1/level), EmmyLua annotations, no regex, Lua 5.3 compatible
-- Bootstrap: same as entry scripts. See `ps1_meta.lua` for the rationale. -- Bootstrap: same as entry scripts. See `ps1_meta.lua` for the rationale.
-- Bootstrap: load `scripts/duffle_paths.lua` (sets package.path + package.cpath). -- Bootstrap: load `scripts/duffle_paths.lua` (sets package.path + package.cpath).
@@ -23,7 +20,7 @@
-- Bootstrap: load `duffle_paths.lua` via `debug.getinfo(1, "S").source` (works both standalone + when required). -- Bootstrap: load `duffle_paths.lua` via `debug.getinfo(1, "S").source` (works both standalone + when required).
-- duffle_paths.lua sets package.path then returns `require("duffle")` at the bottom, so the dofile value IS the duffle module. -- duffle_paths.lua sets package.path then returns `require("duffle")` at the bottom, so the dofile value IS the duffle module.
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./"
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
-- Forward declarations for helpers used by earlier parsers (parse_enum_body_fields needs parse_enum_int_literal; -- Forward declarations for helpers used by earlier parsers (parse_enum_body_fields needs parse_enum_int_literal;
-- parse_typedef_binds needs duffle.find_byte). -- parse_typedef_binds needs duffle.find_byte).
@@ -50,12 +47,12 @@ local parse_enum_int_literal
--- @field line_of fun(pos: integer): integer -- shared LineIndex closure --- @field line_of fun(pos: integer): integer -- shared LineIndex closure
--- @class DebugSkipMarker --- @class DebugSkipMarker
--- @field marker_kind string -- exact marker ident read from source. Only "atom_dbg_skip" (bare) is positive; any other ident reaches the unrelated fallback and is never associated with a declaration. --- @field marker_kind string -- Exact marker ident read from source. Only "atom_dbg_skip" (bare) is positive; any other ident reaches the unrelated fallback and is never associated with a declaration.
--- @field marker_line integer -- line of the marker ident start --- @field marker_line integer -- Line of the marker ident start
--- @field marker_pos integer -- byte position of the marker ident start (the comment walker anchors here) --- @field marker_pos integer -- Byte position of the marker ident start (the comment walker anchors here)
--- @field is_bare boolean -- true iff marker_kind == "atom_dbg_skip" AND has_parens == false (the only positive form) --- @field is_bare boolean -- true iff marker_kind == "atom_dbg_skip" AND has_parens == false (the only positive form)
--- @field has_parens boolean -- true iff a `(...)` follows the marker ident (diagnostic-only) --- @field has_parens boolean -- true iff a `(...)` follows the marker ident (diagnostic-only)
--- @field args string|nil -- trimmed args inside the `(...)` (nil when has_parens is false) --- @field args string|nil -- Trimmed args inside the `(...)` (nil when has_parens is false)
--- @field pending boolean -- true while awaiting the following declaration --- @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 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" | "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)
@@ -71,15 +68,15 @@ local parse_enum_int_literal
--- @field reg string -- "R_T0" --- @field reg string -- "R_T0"
--- @field type_name string --- @field type_name string
--- @field pointer_depth integer --- @field pointer_depth integer
--- @field source_line integer -- line of the call site (callsite or enum-site) --- @field source_line integer -- Line of the call site (callsite or enum-site)
--- @class AtomCtxEntry --- @class AtomCtxEntry
--- @field rbind_atom string -- the rbind atom ident that this consumer should propagate types from --- @field rbind_atom string -- The rbind atom ident that this consumer should propagate types from
--- @field info_line integer --- @field info_line integer
--- @field source string -- absolute path of the source file --- @field source string -- Absolute path of the source file
--- @class AtomPhaseGroup --- @class AtomPhaseGroup
--- @field atoms string[] -- atom names tagged with this phase label (source-order) --- @field atoms string[] -- Atom names tagged with this phase label (source-order)
--- @class AtomViewEntry --- @class AtomViewEntry
--- @field atom_name string -- e.g. "red_cube_g4_face" --- @field atom_name string -- e.g. "red_cube_g4_face"
@@ -88,11 +85,11 @@ local parse_enum_int_literal
--- @field info_line integer -- line of the atom_info call --- @field info_line integer -- line of the atom_info call
--- @class SourceFile --- @class SourceFile
--- @field path string -- absolute path to the source file --- @field path string -- Absolute path to the source file
--- @field text string -- the full source text --- @field text string -- Full source text
--- @field dir string -- the directory containing the source --- @field dir string -- Directory containing the source
--- @field basename string -- filename without extension --- @field basename string -- Filename without extension
--- @field scan table -- pre-scanned SourceScan payload (set by this pass) --- @field scan table -- Pre-scanned SourceScan payload (set by this pass)
--- @class PassCtx --- @class PassCtx
--- @field sources SourceFile[] --- @field sources SourceFile[]
@@ -111,15 +108,15 @@ local parse_enum_int_literal
--- @class AtomEntry --- @class AtomEntry
--- @field line integer --- @field line integer
--- @field name string -- atom name (for components: without ac_ prefix) --- @field name string -- Atom name (for components: without ac_ prefix)
--- @field body string -- brace-delimited body (without the braces) --- @field body string -- Brace-delimited body (without the braces)
--- @field body_off integer -- char offset of body[1] in source --- @field body_off integer -- Char offset of body[1] in source
--- @field kind string -- "atom" | "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 raw_name string -- Un-stripped name (for components: with ac_ prefix)
--- @field ident_pos integer -- position of the MipsAtom_/MipsAtomComp_ ident start --- @field ident_pos integer -- Position of the MipsAtom_/MipsAtomComp_ ident start
--- @field after_paren integer -- position past the closing paren --- @field after_paren integer -- Position past the closing paren
--- @field debug_skip boolean -- true when an `atom_dbg_skip` bare marker immediately precedes this declaration (sole-owner stamp; see push_debug_skip_marker) --- @field debug_skip boolean -- true when an `atom_dbg_skip` bare marker immediately precedes this declaration (sole-owner stamp; see push_debug_skip_marker)
--- @field declaration_comment string|nil -- populated by the scanner (backward walk past the marker, captures contiguous `/* */` or `//` block) --- @field declaration_comment string|nil -- Populated by the scanner (backward walk past the marker, captures contiguous `/* */` or `//` block)
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Local helpers (shared by per-form parsers) -- Local helpers (shared by per-form parsers)
@@ -139,10 +136,10 @@ local QUALIFIER_KEYWORDS = {
local AC_PREFIX = "ac_" local AC_PREFIX = "ac_"
local AC_PREFIX_LEN = 3 local AC_PREFIX_LEN = 3
-- Strip the "ac_" prefix from a component name. --- Strip the "ac_" prefix from a component name.
-- Returns the input unchanged if it doesn't start with the prefix. --- Returns the input unchanged if it doesn't start with the prefix.
-- @param raw_name string --- @param raw_name string
-- @return string --- @return string
local function strip_ac_prefix(raw_name) local function strip_ac_prefix(raw_name)
if #raw_name > AC_PREFIX_LEN and raw_name:sub(1, AC_PREFIX_LEN) == AC_PREFIX then if #raw_name > AC_PREFIX_LEN and raw_name:sub(1, AC_PREFIX_LEN) == AC_PREFIX then
return raw_name:sub(AC_PREFIX_LEN + 1) return raw_name:sub(AC_PREFIX_LEN + 1)
@@ -156,7 +153,7 @@ end
local function push_debug_skip_marker(out, marker) local function push_debug_skip_marker(out, marker)
local markers = out.debug_skip_markers local markers = out.debug_skip_markers
local prior = markers[#markers] local prior = markers[#markers]
if prior and prior.pending then if prior and prior.pending then
prior.pending = false prior.pending = false
prior.superseded_by_marker_line = marker.marker_line prior.superseded_by_marker_line = marker.marker_line
end end
@@ -178,25 +175,25 @@ end
-- Returns (body, after_brace, body_off) on success, or (nil, fallback_pos) on no brace. -- Returns (body, after_brace, body_off) on success, or (nil, fallback_pos) on no brace.
-- `fallback_pos` defaults to `after_paren + 1` (the common "advance by 1" case). -- `fallback_pos` defaults to `after_paren + 1` (the common "advance by 1" case).
local function find_body_braces(source, after_paren, fallback) local function find_body_braces(source, after_paren, fallback)
local brace = duffle.scan_to_char(source, "{", after_paren) local brace = duffle.scan_to_char(source, "{", after_paren)
if not brace then return nil, fallback or (after_paren + 1) end if not brace then return nil, fallback or (after_paren + 1) end
local body, after_brace = duffle.read_braces(source, brace) local body, after_brace = duffle.read_braces(source, brace)
return body, after_brace, brace + 1 return body, after_brace, brace + 1
end end
-- Walk backward from `start_pos` capturing contiguous `/* */` block(s) and --- Walk backward from `start_pos` capturing contiguous `/* */` block(s) and
-- `//` line(s) that immediately precede it. The caller (preceding_declaration_comment) --- `//` line(s) that immediately precede it. The caller (preceding_declaration_comment)
-- supplies `start_pos` so the walker does not need to detect marker shape or prelude layout. --- supplies `start_pos` so the walker does not need to detect marker shape or prelude layout.
-- The scanner already knows the marker_pos + decl ident_pos and threads that knowledge forward. --- The scanner already knows the marker_pos + decl ident_pos and threads that knowledge forward.
-- ---
-- The walker captures: --- The walker captures:
-- - Block comment close `*/` followed by walking back to `/*`. --- - Block comment close `*/` followed by walking back to `/*`.
-- - `//` line comments (the line containing the current non-ws position starts with `//`). --- - `//` line comments (the line containing the current non-ws position starts with `//`).
-- It stops at the first non-ws char that does not begin a comment block or line. --- It stops at the first non-ws char that does not begin a comment block or line.
-- Empty string if no comment is adjacent. --- Empty string if no comment is adjacent.
-- @param source string --- @param source string
-- @param start_pos integer -- exclusive upper bound for the captured block --- @param start_pos integer -- exclusive upper bound for the captured block
-- @return string --- @return string
local function preceding_comment_walk_backward(source, start_pos) local function preceding_comment_walk_backward(source, start_pos)
local pieces = {} local pieces = {}
local scan_pos = start_pos local scan_pos = start_pos
@@ -249,13 +246,13 @@ local function preceding_comment_walk_backward(source, start_pos)
return table.concat(pieces, "\n") return table.concat(pieces, "\n")
end end
-- Resolve the start position for the declaration-comment walk. --- Resolve the start position for the declaration-comment walk.
-- When a debug-skip marker is pending, the walker must start from the position immediately before the marker ident --- When a debug-skip marker is pending, the walker must start from the position immediately before the marker ident
-- (so it walks backward past the marker text and any `FI_ MipsAtom ac_X(args)` proc-prelude layout — neither of which is visible if we start from the declaration ident_pos). --- (so it walks backward past the marker text and any `FI_ MipsAtom ac_X(args)` proc-prelude layout — neither of which is visible if we start from the declaration ident_pos).
-- When no marker is pending, the walker starts from the declaration ident_pos directly. --- When no marker is pending, the walker starts from the declaration ident_pos directly.
-- @param pending_marker DebugSkipMarker|nil --- @param pending_marker DebugSkipMarker|nil
-- @param ident_pos integer -- declaration ident position --- @param ident_pos integer -- declaration ident position
-- @return integer --- @return integer
local function comment_walk_start(pending_marker, ident_pos) local function comment_walk_start(pending_marker, ident_pos)
if pending_marker then if pending_marker then
return pending_marker.marker_pos - 1 return pending_marker.marker_pos - 1
@@ -263,16 +260,16 @@ local function comment_walk_start(pending_marker, ident_pos)
return ident_pos - 1 return ident_pos - 1
end end
-- Attach the pending marker to the next declaration. --- Attach the pending marker to the next declaration.
-- The declaration form disambiguates whole atoms from components; the resolved `debug_skip` is stamped directly on the declaration record --- The declaration form disambiguates whole atoms from components; the resolved `debug_skip` is stamped directly on the declaration record
-- (sole-owner discipline; see push_debug_skip_marker). --- (sole-owner discipline; see push_debug_skip_marker).
-- ---
-- A marker is POSITIVE (stamps `debug_skip = true` on the declaration) iff: --- A marker is POSITIVE (stamps `debug_skip = true` on the declaration) iff:
-- marker_kind == "atom_dbg_skip" AND is_bare == true --- 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`. --- 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 out SourceScan
-- @param target_kind string|nil -- "atom" | "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 --- @return boolean|nil -- true iff the marker is the positive bare form
local function attach_debug_skip_marker(out, target_kind) local function attach_debug_skip_marker(out, target_kind)
local markers = out.debug_skip_markers local markers = out.debug_skip_markers
local marker = markers[#markers] local marker = markers[#markers]
@@ -390,13 +387,13 @@ local function walk_body_fields(body, build_field)
while body_pos <= body_len do while body_pos <= body_len do
body_pos = duffle.skip_ws_and_cmt(body, body_pos) body_pos = duffle.skip_ws_and_cmt(body, body_pos)
if body_pos > body_len then break end if body_pos > body_len then break end
local first, first_end = duffle.read_ident(body, body_pos) local first, first_end = duffle.read_ident(body, body_pos)
if not first then if not first then
body_pos = body_pos + 1 body_pos = body_pos + 1
else else
local after_first = duffle.skip_ws_and_cmt(body, first_end) local after_first = duffle.skip_ws_and_cmt(body, first_end)
local result, new_pos = build_field(first, first_end, after_first) local result, new_pos = build_field(first, first_end, after_first)
if result then fields[#fields + 1] = result end if result then fields[#fields + 1] = result end
body_pos = new_pos or first_end body_pos = new_pos or first_end
-- Skip a single trailing `,` or `;`. -- Skip a single trailing `,` or `;`.
if body_pos <= body_len and (body:sub(body_pos, body_pos) == "," or body:sub(body_pos, body_pos) == ";") then if body_pos <= body_len and (body:sub(body_pos, body_pos) == "," or body:sub(body_pos, body_pos) == ";") then
@@ -442,7 +439,7 @@ local function parse_enum_body_fields(body)
local value local value
local new_pos local new_pos
if body:sub(after_name, after_name) == "=" then if body:sub(after_name, after_name) == "=" then
local val_pos = duffle.skip_ws_and_cmt(body, after_name + 1) local val_pos = duffle.skip_ws_and_cmt(body, after_name + 1)
local v, end_pos = parse_enum_int_literal(body, val_pos) local v, end_pos = parse_enum_int_literal(body, val_pos)
if v ~= nil then if v ~= nil then
value = v value = v
@@ -461,16 +458,16 @@ end
-- Returns a positive integer byte_size when the chain bottoms out at a builtin, or nil if the chain is broken, exceeds TYPE_CHAIN_MAX_DEPTH, or contains a cycle. -- Returns a positive integer byte_size when the chain bottoms out at a builtin, or nil if the chain is broken, exceeds TYPE_CHAIN_MAX_DEPTH, or contains a cycle.
local function resolve_typedef_byte_size(type_name, type_name_registry, visited, depth) local function resolve_typedef_byte_size(type_name, type_name_registry, visited, depth)
if depth > TYPE_CHAIN_MAX_DEPTH then return nil end if depth > TYPE_CHAIN_MAX_DEPTH then return nil end
if visited[type_name] then return nil end if visited[type_name] then return nil end
visited[type_name] = true visited[type_name] = true
-- Check the builtin primitive map FIRST. -- Check the builtin primitive map FIRST.
-- This handles undeclared builtin idents (e.g. `__UINT32_TYPE__` appears as underlying_type in `typedef __UINT32_TYPE__ TSet_(V4_S2);` -- This handles undeclared builtin idents (e.g. `__UINT32_TYPE__` appears as underlying_type in `typedef __UINT32_TYPE__ TSet_(V4_S2);`
-- even though the fixture never declares `__UINT32_TYPE__` itself). -- even though the fixture never declares `__UINT32_TYPE__` itself).
local builtin = BUILTIN_BYTE_SIZES[type_name] local builtin = BUILTIN_BYTE_SIZES[type_name]
if builtin ~= nil then return builtin end if builtin ~= nil then return builtin end
local entry = type_name_registry[type_name] local entry = type_name_registry[type_name]
if not entry then return nil end if not entry then return nil end
-- Confident: this entry was already resolved by the propagation pass (e.g., a builtin or a struct whose fields are all resolved). -- Confident: this entry was already resolved by the propagation pass (e.g., a builtin or a struct whose fields are all resolved).
@@ -519,9 +516,9 @@ local function propagate_type_sizes(out)
for name, entry in pairs(reg) do for name, entry in pairs(reg) do
if entry.byte_size == nil then if entry.byte_size == nil then
local resolved = resolve_typedef_byte_size(name, reg, {}, 1) local resolved = resolve_typedef_byte_size(name, reg, {}, 1)
if resolved ~= nil then if resolved ~= nil then
entry.byte_size = resolved entry.byte_size = resolved
any_change = true any_change = true
end end
end end
end end
@@ -828,7 +825,7 @@ end
--- Parse a decimal/negative-decimal/hex integer literal starting at byte position `start`. --- 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. --- Returns (value, end_pos) on success, or (nil, start) on failure / no match.
--- Accepts: 12, -1, 0, 0x10, 0X1F, -0x10. --- Accepts: 12, -1, 0, 0x10, 0X1F, -0x10.
--- @param text string --- @param text string
--- @param start integer --- @param start integer
--- @return integer|nil, integer --- @return integer|nil, integer
--- Implementation note: this is a plain assignment (not `local function`) --- Implementation note: this is a plain assignment (not `local function`)
@@ -952,9 +949,9 @@ end
--- Always saves the raw RHS text into `code_macro_bodies` (for cross-source fallback during chain resolution), --- Always saves the raw RHS text into `code_macro_bodies` (for cross-source fallback during chain resolution),
--- then (if resolvable) stores the resolved integer code into `code_macros` keyed by the macro name. --- then (if resolvable) stores the resolved integer code into `code_macros` keyed by the macro name.
--- `directive_start` points at the `#` byte. The function is silent on non-matching directives, the caller skips the line in any case. --- `directive_start` points at the `#` byte. The function is silent on non-matching directives, the caller skips the line in any case.
--- @param source string --- @param source string
--- @param directive_start integer -- byte position of `#` --- @param directive_start integer -- byte position of `#`
--- @param code_macros table -- out._code_macros / ctx.shared._code_macros --- @param code_macros table -- out._code_macros / ctx.shared._code_macros
--- @param code_macro_bodies table -- out._code_macro_bodies / ctx.shared._code_macro_bodies --- @param code_macro_bodies table -- out._code_macro_bodies / ctx.shared._code_macro_bodies
local function try_extract_code_macro(source, directive_start, code_macros, code_macro_bodies) local function try_extract_code_macro(source, directive_start, code_macros, code_macro_bodies)
local rest = duffle.skip_ws_and_cmt(source, directive_start + 1) local rest = duffle.skip_ws_and_cmt(source, directive_start + 1)
@@ -985,8 +982,8 @@ end
--- Populates `code_macros` with resolved integer codes AND `code_macro_bodies` with raw RHS text --- Populates `code_macros` with resolved integer codes AND `code_macro_bodies` with raw RHS text
--- (used by the chain walker as cross-source fallback during pass 1b in `M.run`); ignores everything else. --- (used by the chain walker as cross-source fallback during pass 1b in `M.run`); ignores everything else.
--- Used by `M.run` pass 1a to build the cross-source `_code_macros` + `_code_macro_bodies` registries before pass 1b resolves chains. --- Used by `M.run` pass 1a to build the cross-source `_code_macros` + `_code_macro_bodies` registries before pass 1b resolves chains.
--- @param source string --- @param source string
--- @param code_macros table --- @param code_macros table
--- @param code_macro_bodies table --- @param code_macro_bodies table
local function scan_source_pre_pass(source, code_macros, code_macro_bodies) local function scan_source_pre_pass(source, code_macros, code_macro_bodies)
local pos = 1 local pos = 1
@@ -1043,7 +1040,7 @@ local function parse_enum_atom_type_default(body, pos)
if pos > #body then return nil, 0, pos end if pos > #body then return nil, 0, pos end
-- Bare `atom_type` word with word-bounding on both sides. -- Bare `atom_type` word with word-bounding on both sides.
local ident, ident_end = duffle.read_ident(body, pos) local ident, ident_end = duffle.read_ident(body, pos)
if ident ~= "atom_type" then return nil, 0, pos end if ident ~= "atom_type" then return nil, 0, pos end
if pos > 1 then if pos > 1 then
local prev = body:byte(pos - 1) local prev = body:byte(pos - 1)
if duffle.is_alnum_byte(prev) then return nil, 0, pos end if duffle.is_alnum_byte(prev) then return nil, 0, pos end
@@ -1054,19 +1051,19 @@ local function parse_enum_atom_type_default(body, pos)
end end
-- Expect `( ... )` immediately after. -- Expect `( ... )` immediately after.
local open_pos = duffle.skip_ws_and_cmt(body, ident_end) local open_pos = duffle.skip_ws_and_cmt(body, ident_end)
if open_pos > #body or body:sub(open_pos, open_pos) ~= "(" then return nil, 0, pos end if open_pos > #body or body:sub(open_pos, open_pos) ~= "(" then return nil, 0, pos end
local inner, after_close = duffle.read_parens(body, open_pos) local inner, after_close = duffle.read_parens(body, open_pos)
-- Reject any trailing tokens past the close paren other than comma / close-brace (next enum entry / end of enum). -- Reject any trailing tokens past the close paren other than comma / close-brace (next enum entry / end of enum).
local residue = duffle.skip_ws_and_cmt(body, after_close) local residue = duffle.skip_ws_and_cmt(body, after_close)
if residue <= #body then if residue <= #body then
local rbyte = body:byte(residue) local rbyte = body:byte(residue)
if rbyte ~= BYTE_COMMA and rbyte ~= BYTE_CLOSE_BRACE then return nil, 0, pos end if rbyte ~= BYTE_COMMA and rbyte ~= BYTE_CLOSE_BRACE then return nil, 0, pos end
end end
-- Parse the type chain inside the parens (e.g. `V4_S2*` -> ("V4_S2", 1)). -- Parse the type chain inside the parens (e.g. `V4_S2*` -> ("V4_S2", 1)).
local type_name, depth, after_chain = parse_type_chain(inner, 1) local type_name, depth, after_chain = parse_type_chain(inner, 1)
if not type_name then return nil, 0, pos end if not type_name then return nil, 0, pos end
local end_check = duffle.skip_ws_and_cmt(inner, after_chain) local end_check = duffle.skip_ws_and_cmt(inner, after_chain)
if end_check <= #inner then return nil, 0, pos end if end_check <= #inner then return nil, 0, pos end
return type_name, depth, duffle.skip_ws_and_cmt(body, after_close) return type_name, depth, duffle.skip_ws_and_cmt(body, after_close)
end end
@@ -1098,11 +1095,11 @@ end
--- ---
--- Diagnostic-only path: a following `(...)` is recorded as an invalid parenthesized-form marker so the annotation rule can emit a precise "parenthesized form" diagnostic. --- Diagnostic-only path: a following `(...)` is recorded as an invalid parenthesized-form marker so the annotation rule can emit a precise "parenthesized form" diagnostic.
--- The parenthesized form stays diagnostic; the bare form alone carries the runtime stamp. --- The parenthesized form stays diagnostic; the bare form alone carries the runtime stamp.
--- @param source string --- @param source string
--- @param pos integer --- @param pos integer
--- @param ident_end integer --- @param ident_end integer
--- @param line_of fun(pos: integer): integer --- @param line_of fun(pos: integer): integer
--- @param out SourceScan --- @param out SourceScan
--- @return integer -- source cursor position to resume from --- @return integer -- source cursor position to resume from
local function parse_dbg_skip_marker(source, pos, ident_end, line_of, out) local function parse_dbg_skip_marker(source, pos, ident_end, line_of, out)
local marker_kind = source:sub(pos, ident_end - 1) local marker_kind = source:sub(pos, ident_end - 1)
@@ -1134,15 +1131,15 @@ end
-- Parse `atom_dbg_reg_default(R_X, <type>...)`; -- 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]`. -- 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) local function parse_atom_dbg_reg_default(source, pos, ident_end, line_of, out)
local inner, after_paren = read_parens_after(source, ident_end) local inner, after_paren = read_parens_after(source, ident_end)
if not inner then return after_paren end if not inner then return after_paren end
local args = duffle.split_top_level_commas(inner) local args = duffle.split_top_level_commas(inner)
if #args < 1 then if #args < 1 then
-- Annotation pass surfaces this; we still consume the marker. -- Annotation pass surfaces this; we still consume the marker.
return after_paren return after_paren
end end
local reg_name = duffle.trim(args[1]) local reg_name = duffle.trim(args[1])
local type_part = args[2] or "void" local type_part = args[2] or "void"
local type_name, depth = parse_type_chain(type_part, 1) local type_name, depth = parse_type_chain(type_part, 1)
if not type_name then type_name, depth = duffle.trim(type_part), 0 end if not type_name then type_name, depth = duffle.trim(type_part), 0 end
out.types[reg_name] = { out.types[reg_name] = {
@@ -1161,23 +1158,23 @@ local function parse_atom_dbg_reg_default(source, pos, ident_end, line_of, out)
end end
--- Parse: `MipsAtom_(<name>) [atom_info(<binds>, <reads>, <writes>)] { <body> }` --- Parse: `MipsAtom_(<name>) [atom_info(<binds>, <reads>, <writes>)] { <body> }`
--- @param source string --- @param source string
--- @param pos integer --- @param pos integer
--- @param ident_end integer --- @param ident_end integer
--- @param line_of fun(pos: integer): integer --- @param line_of fun(pos: integer): integer
--- @param out SourceScan --- @param out SourceScan
--- @return integer --- @return integer
local function parse_mips_atom(source, pos, ident_end, line_of, out) local function parse_mips_atom(source, pos, ident_end, line_of, out)
local inner, after_paren, open_paren = read_parens_after(source, ident_end) local inner, after_paren, open_paren = read_parens_after(source, ident_end)
if not inner then return after_paren end if not inner then return after_paren end
local raw_name = duffle.read_ident(inner, 1) local raw_name = duffle.read_ident(inner, 1)
-- Lookahead for atom_info(...) between `)` and `{`. Captures sub-calls; updates brace search start. -- Lookahead for atom_info(...) between `)` and `{`. Captures sub-calls; updates brace search start.
local brace_search_pos = after_paren local brace_search_pos = after_paren
local lookahead = duffle.skip_ws_and_cmt(source, after_paren) local lookahead = duffle.skip_ws_and_cmt(source, after_paren)
local look_ident, look_end = duffle.read_ident(source, lookahead) local look_ident, look_end = duffle.read_ident(source, lookahead)
if look_ident == "atom_info" then if look_ident == "atom_info" then
local info_open = duffle.skip_ws_and_cmt(source, look_end) local info_open = duffle.skip_ws_and_cmt(source, look_end)
if source:sub(info_open, info_open) == "(" then if source:sub(info_open, info_open) == "(" then
local info_inner, info_after = duffle.read_parens(source, info_open) local info_inner, info_after = duffle.read_parens(source, info_open)
@@ -1221,7 +1218,7 @@ local function parse_mips_atom(source, pos, ident_end, line_of, out)
end end
end end
local body, after_brace, body_off = find_body_braces(source, brace_search_pos, open_paren + 1) local body, after_brace, body_off = find_body_braces(source, brace_search_pos, open_paren + 1)
if not body then return after_brace end if not body then return after_brace end
if raw_name and raw_name ~= "" then if raw_name and raw_name ~= "" then
register_atom(out, "atom", line_of(pos), raw_name, body, body_off, raw_name, pos, after_paren, source) register_atom(out, "atom", line_of(pos), raw_name, body, body_off, raw_name, pos, after_paren, source)
@@ -1231,20 +1228,20 @@ local function parse_mips_atom(source, pos, ident_end, line_of, out)
end end
--- Parse: `MipsAtomComp_(<name>) { <body> }` --- Parse: `MipsAtomComp_(<name>) { <body> }`
--- @param source string --- @param source string
--- @param pos integer --- @param pos integer
--- @param ident_end integer --- @param ident_end integer
--- @param line_of fun(pos: integer): integer --- @param line_of fun(pos: integer): integer
--- @param out SourceScan --- @param out SourceScan
--- @return integer --- @return integer
local function parse_mips_atom_comp(source, pos, ident_end, line_of, out) local function parse_mips_atom_comp(source, pos, ident_end, line_of, out)
local inner, after_paren, open_paren = read_parens_after(source, ident_end) local inner, after_paren, open_paren = read_parens_after(source, ident_end)
if not inner then return after_paren end if not inner then return after_paren end
local raw_name = duffle.read_ident(inner, 1) local raw_name = duffle.read_ident(inner, 1)
if not raw_name then return open_paren + 1 end if not raw_name then return open_paren + 1 end
local body, after_brace, body_off = find_body_braces(source, after_paren, open_paren + 1) local body, after_brace, body_off = find_body_braces(source, after_paren, open_paren + 1)
if not body then return after_brace end if not body then return after_brace end
local name = strip_ac_prefix(raw_name) local name = strip_ac_prefix(raw_name)
register_atom(out, "comp_bare", line_of(pos), name, body, body_off, raw_name, pos, after_paren, source) register_atom(out, "comp_bare", line_of(pos), name, body, body_off, raw_name, pos, after_paren, source)
@@ -1253,14 +1250,14 @@ local function parse_mips_atom_comp(source, pos, ident_end, line_of, out)
end end
--- Parse: `MipsAtomComp_Proc_(<name>, { <body> })` — body is inside the LAST `{` in args. --- Parse: `MipsAtomComp_Proc_(<name>, { <body> })` — body is inside the LAST `{` in args.
--- @param source string --- @param source string
--- @param pos integer --- @param pos integer
--- @param ident_end integer --- @param ident_end integer
--- @param line_of fun(pos: integer): integer --- @param line_of fun(pos: integer): integer
--- @param out SourceScan --- @param out SourceScan
--- @return integer --- @return integer
local function parse_mips_atom_comp_proc(source, pos, ident_end, line_of, out) local function parse_mips_atom_comp_proc(source, pos, ident_end, line_of, out)
local inner, after_paren, open_paren = read_parens_after(source, ident_end) local inner, after_paren, open_paren = read_parens_after(source, ident_end)
if not inner then return after_paren end if not inner then return after_paren end
-- Find the LAST `{` in inner (the body brace, not any potential embedded braces in expressions). -- Find the LAST `{` in inner (the body brace, not any potential embedded braces in expressions).
@@ -1277,21 +1274,20 @@ local function parse_mips_atom_comp_proc(source, pos, ident_end, line_of, out)
if close_pos > #inner + 1 then return after_paren end if close_pos > #inner + 1 then return after_paren end
local raw_name = inner:match("^%s*([%w_]+)") or "?" local raw_name = inner:match("^%s*([%w_]+)") or "?"
local name = strip_ac_prefix(raw_name) local name = strip_ac_prefix(raw_name)
-- Position of body[1] in source = open_paren + 1 (start of inner) + last_brace_pos + 1 (past '{'). -- 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 local body_off = open_paren + 2 + last_brace_pos
register_atom(out, "comp_proc", line_of(pos), name, body, body_off, raw_name, pos, after_paren, source) register_atom(out, "comp_proc", line_of(pos), name, body, body_off, raw_name, pos, after_paren, source)
return after_paren return after_paren
end end
--- Parse: `MipsCode code_<name> { <body> }` (raw atom form — offsets pass only). --- Parse: `MipsCode code_<name> { <body> }` (raw atom form — offsets pass only).
--- @param source string --- @param source string
--- @param pos integer --- @param pos integer
--- @param ident_end integer --- @param ident_end integer
--- @param line_of fun(pos: integer): integer --- @param line_of fun(pos: integer): integer
--- @param out SourceScan --- @param out SourceScan
--- @return integer --- @return integer
local function parse_mips_code(source, pos, ident_end, line_of, out) local function parse_mips_code(source, pos, ident_end, line_of, out)
local next_pos = duffle.skip_ws_and_cmt(source, ident_end) local next_pos = duffle.skip_ws_and_cmt(source, ident_end)
@@ -1300,8 +1296,8 @@ local function parse_mips_code(source, pos, ident_end, line_of, out)
return ident_end return ident_end
end end
local atom_name = next_ident:sub(6) local atom_name = next_ident:sub(6)
local body, after_brace, body_off = find_body_braces(source, next_after, ident_end) local body, after_brace, body_off = find_body_braces(source, next_after, ident_end)
if not body then return after_brace end if not body then return after_brace end
register_raw_atom(out, line_of(pos), atom_name, body, body_off, atom_name, pos) register_raw_atom(out, line_of(pos), atom_name, body, body_off, atom_name, pos)
@@ -1320,13 +1316,13 @@ end
--- pointer_depth = 0 --- pointer_depth = 0
--- } -- byte_size + per-field offset/byte_size set by the propagation pass. --- } -- byte_size + per-field offset/byte_size set by the propagation pass.
--- Also populates `out.binds[]` IFF `name:sub(1, 6) == "Binds_"`. --- Also populates `out.binds[]` IFF `name:sub(1, 6) == "Binds_"`.
--- @param body string --- @param body string
--- @param name string --- @param name string
--- @param pos integer --- @param pos integer
--- @param line_of fun(pos: integer): integer --- @param line_of fun(pos: integer): integer
--- @param out SourceScan --- @param out SourceScan
local function register_struct_type(body, name, pos, line_of, out) local function register_struct_type(body, name, pos, line_of, out)
local fields = parse_struct_body_fields(body) local fields = parse_struct_body_fields(body)
local source_pos = line_of(pos) local source_pos = line_of(pos)
out.type_name_registry[name] = { out.type_name_registry[name] = {
name = name, name = name,
@@ -1352,11 +1348,11 @@ end
--- Register an Enum_ entry in type_name_registry. --- Register an Enum_ entry in type_name_registry.
--- Local helper for parse_typedef_binds. Captures the underlying type (1st arg of `Enum_(<underlying>, <name>)`) and the body fields. --- Local helper for parse_typedef_binds. Captures the underlying type (1st arg of `Enum_(<underlying>, <name>)`) and the body fields.
--- @param underlying string --- @param underlying string
--- @param name string --- @param name string
--- @param body string --- @param body string
--- @param pos integer --- @param pos integer
--- @param line_of fun(pos: integer): integer --- @param line_of fun(pos: integer): integer
--- @param out SourceScan --- @param out SourceScan
local function register_enum_type(underlying, name, body, pos, line_of, out) local function register_enum_type(underlying, name, body, pos, line_of, out)
local fields = parse_enum_body_fields(body) local fields = parse_enum_body_fields(body)
out.type_name_registry[name] = { out.type_name_registry[name] = {
@@ -1375,18 +1371,18 @@ end
--- Captures the underlying type ident (LHS of `typedef <type> <alias>;`) and exposes it through the registry. --- Captures the underlying type ident (LHS of `typedef <type> <alias>;`) and exposes it through the registry.
--- The propagation pass follows the underlying_type chain to resolve byte_size. --- The propagation pass follows the underlying_type chain to resolve byte_size.
--- @param underlying string --- @param underlying string
--- @param name string --- @param name string
--- @param pos integer --- @param pos integer
--- @param line_of fun(pos: integer): integer --- @param line_of fun(pos: integer): integer
--- @param out SourceScan --- @param out SourceScan
local function register_typedef_alias(underlying, name, pos, line_of, out) local function register_typedef_alias(underlying, name, pos, line_of, out)
out.type_name_registry[name] = { out.type_name_registry[name] = {
name = name, name = name,
kind = "typedef", kind = "typedef",
underlying_type = underlying, underlying_type = underlying,
source_line = line_of(pos), source_line = line_of(pos),
source_file = out._source_file, source_file = out._source_file,
pointer_depth = 0, pointer_depth = 0,
} }
end end
@@ -1396,29 +1392,29 @@ end
--- 1. `typedef Struct_(<name>) { <body> } <alias>;` adds to type_name_registry (kind="struct"). --- 1. `typedef Struct_(<name>) { <body> } <alias>;` adds to type_name_registry (kind="struct").
--- Binds_* aliases also land in out.binds[]. --- Binds_* aliases also land in out.binds[].
--- 2. `typedef Enum_(<underlying>, <name>) { <body> } <alias>;` --- 2. `typedef Enum_(<underlying>, <name>) { <body> } <alias>;`
--- adds to type_name_registry (kind="enum"). --- Adds to type_name_registry (kind="enum").
--- 3. `typedef <type> <alias>;` simple typedef alias. --- 3. `typedef <type> <alias>;` simple typedef alias.
--- Adds to type_name_registry (kind="typedef"). --- Adds to type_name_registry (kind="typedef").
--- 4. `typedef <type> TSet_(<name>);` duffle TSet_ convention. --- 4. `typedef <type> TSet_(<name>);` duffle TSet_ convention.
--- Strips TSet_ wrapper; adds to type_name_registry (kind="typedef") with underlying_type=<type>. --- Strips TSet_ wrapper; adds to type_name_registry (kind="typedef") with underlying_type=<type>.
--- ---
--- All four shapes also attach an "unrelated" debug-skip marker (the existing behavior — typedef declarations don't carry atom_dbg_skip). --- All four shapes also attach an "unrelated" debug-skip marker (the existing behavior — typedef declarations don't carry atom_dbg_skip).
--- @param source string --- @param source string
--- @param pos integer --- @param pos integer
--- @param ident_end integer --- @param ident_end integer
--- @param line_of fun(pos: integer): integer --- @param line_of fun(pos: integer): integer
--- @param out SourceScan --- @param out SourceScan
--- @return integer --- @return integer
local function parse_typedef_binds(source, pos, ident_end, line_of, out) local function parse_typedef_binds(source, pos, ident_end, line_of, out)
local after_typedef = duffle.skip_ws_and_cmt(source, ident_end) local after_typedef = duffle.skip_ws_and_cmt(source, ident_end)
local id2, id2_end = duffle.read_ident(source, after_typedef) local id2, id2_end = duffle.read_ident(source, after_typedef)
if not id2 then return ident_end end if not id2 then return ident_end end
-- ── Shape 1: `typedef Struct_(<name>) { <body> } <alias>;` ──────────── -- ── Shape 1: `typedef Struct_(<name>) { <body> } <alias>;` ────────────
if id2 == "Struct_" then if id2 == "Struct_" then
local inner, after_paren, open_paren = read_parens_after(source, id2_end, id2_end) local inner, after_paren, open_paren = read_parens_after(source, id2_end, id2_end)
if not inner then return id2_end end if not inner then return id2_end end
local name = duffle.trim(inner) local name = duffle.trim(inner)
local body, after_brace = find_body_braces(source, after_paren, open_paren + 1) local body, after_brace = find_body_braces(source, after_paren, open_paren + 1)
if not body then return after_brace end if not body then return after_brace end
@@ -1428,7 +1424,7 @@ local function parse_typedef_binds(source, pos, ident_end, line_of, out)
-- ── Shape 2: `typedef Enum_(<underlying>, <name>) { <body> } <alias>;` -- ── Shape 2: `typedef Enum_(<underlying>, <name>) { <body> } <alias>;`
elseif id2 == "Enum_" then elseif id2 == "Enum_" then
local inner, after_paren, open_paren = read_parens_after(source, id2_end, id2_end) local inner, after_paren, open_paren = read_parens_after(source, id2_end, id2_end)
if not inner then return id2_end end if not inner then return id2_end end
-- Split `inner` on the first top-level comma into (<underlying>, <name>). -- Split `inner` on the first top-level comma into (<underlying>, <name>).
local args = duffle.split_top_level_commas(inner) local args = duffle.split_top_level_commas(inner)
@@ -1436,7 +1432,7 @@ local function parse_typedef_binds(source, pos, ident_end, line_of, out)
local underlying = duffle.trim(args[1]) local underlying = duffle.trim(args[1])
local name = duffle.trim(args[2]) local name = duffle.trim(args[2])
local body, after_brace = find_body_braces(source, after_paren, open_paren + 1) local body, after_brace = find_body_braces(source, after_paren, open_paren + 1)
if not body then return after_brace end if not body then return after_brace end
register_enum_type(underlying, name, body, pos, line_of, out) register_enum_type(underlying, name, body, pos, line_of, out)
attach_debug_skip_marker(out, "unrelated") attach_debug_skip_marker(out, "unrelated")
@@ -1469,11 +1465,10 @@ local function parse_typedef_binds(source, pos, ident_end, line_of, out)
-- Shape 4 (TSet_ at id2 position): no preceding underlying span. -- Shape 4 (TSet_ at id2 position): no preceding underlying span.
if id2 == "TSet_" then if id2 == "TSet_" then
local inner, after_paren = read_parens_after(source, id2_end, id2_end) local inner, after_paren = read_parens_after(source, id2_end, id2_end)
if not inner then return id2_end end if not inner then return id2_end end
local tset_name = duffle.trim(inner) local tset_name = duffle.trim(inner)
-- Empty underlying span is acceptable; the TSet_ wrapper itself -- Empty underlying span is acceptable; the TSet_ wrapper itself encodes the alias identity (per the duffle TSet_ convention).
-- encodes the alias identity (per the duffle TSet_ convention).
register_typedef_alias("", tset_name, pos, line_of, out) register_typedef_alias("", tset_name, pos, line_of, out)
attach_debug_skip_marker(out, "unrelated") attach_debug_skip_marker(out, "unrelated")
return after_paren return after_paren
@@ -1491,13 +1486,13 @@ local function parse_typedef_binds(source, pos, ident_end, line_of, out)
while scan < semi_pos do while scan < semi_pos do
scan = duffle.skip_ws_and_cmt(source, scan) scan = duffle.skip_ws_and_cmt(source, scan)
if scan >= semi_pos then break end if scan >= semi_pos then break end
local id, id_end = duffle.read_ident(source, scan) local id, id_end = duffle.read_ident(source, scan)
if not id then if not id then
scan = scan + 1 scan = scan + 1
elseif id == "TSet_" then elseif id == "TSet_" then
-- Shape 4 (TSet_ at non-id2 position): grab the parenthesized argument. -- Shape 4 (TSet_ at non-id2 position): grab the parenthesized argument.
local inner, after_paren = read_parens_after(source, id_end, id_end) local inner, after_paren = read_parens_after(source, id_end, id_end)
if inner then if inner then
tset_arg = duffle.trim(inner) tset_arg = duffle.trim(inner)
tset_arg_end = after_paren tset_arg_end = after_paren
tset_pos = scan tset_pos = scan
@@ -1957,7 +1952,7 @@ local function merge_named_with_sites(registry, name, new_entry, site, collision
registry[name].sites = { site } registry[name].sites = { site }
return return
end end
local existing = registry[name] local existing = registry[name]
local new_shape = shape_fn(new_entry) local new_shape = shape_fn(new_entry)
local old_shape = shape_fn(existing) local old_shape = shape_fn(existing)
if new_shape == old_shape and new_shape ~= "" then if new_shape == old_shape and new_shape ~= "" then
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -93,7 +93,7 @@ end
--- Load the authored `word_count.metadata.h` into `ctx.shared.corpus.word_counts`. --- Load the authored `word_count.metadata.h` into `ctx.shared.corpus.word_counts`.
--- Generated `.macs.h` files are OUTPUT artifacts and are NOT scanned as inputs. --- Generated `.macs.h` files are OUTPUT artifacts and are NOT scanned as inputs.
--- Current component counts are computed and inserted by `passes/components.lua` --- Current component counts are computed and inserted by `passes/components.lua`
--- after the components pass iterates `corpus.source_order` and writes each source's `<dir_basename>.macs.h` file. --- after the components pass iterates `corpus.source_order` and writes each source-directory's `gen/macs.h` file.
--- ---
--- Contract: --- Contract:
--- * `ctx.shared.corpus` MUST exist (canonical corpus ownership). --- * `ctx.shared.corpus` MUST exist (canonical corpus ownership).
Binary file not shown.
+49 -25
View File
@@ -16,36 +16,60 @@
-- Companion: scripts/gdb/gdb_tape_atoms.gdb (covers GPRs + atom-aware stepping). -- Companion: scripts/gdb/gdb_tape_atoms.gdb (covers GPRs + atom-aware stepping).
local function register_handlers() local function register_handlers()
if not PCSX.WebServer then PCSX.WebServer = {} end if not PCSX.WebServer then PCSX.WebServer = {} end
if not PCSX.WebServer.Handlers then PCSX.WebServer.Handlers = {} end if not PCSX.WebServer.Handlers then PCSX.WebServer.Handlers = {} end
-- ── GTE state ── -- ── GTE state ──
PCSX.WebServer.Handlers.gte = function(req) PCSX.WebServer.Handlers.gte = function(req)
local r = PCSX.getRegisters() local r = PCSX.getRegisters()
local out = { "pc=0x" .. string.format("%x", r.pc) } local out = { "pc=0x" .. string.format("%x", r.pc) }
for i = 0, 31 do for i = 0, 31 do
out[#out + 1] = string.format("D[%d]=0x%08x C[%d]=0x%08x", out[#out + 1] = string.format("D[%d]=0x%08x C[%d]=0x%08x",
i, r.CP2D.r[i], i, r.CP2C.r[i]) i, r.CP2D.r[i], i, r.CP2C.r[i])
end end
return table.concat(out, "\n") return table.concat(out, "\n")
end end
-- ── GP state (pointer to existing endpoints) ── -- ── GP state (pointer to existing endpoints) ──
-- pcsx-redux's Lua GPU API exposes only takeScreenShot(); no GPUSTAT / GP0 / GP1 command log / display state. -- 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). -- 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) PCSX.WebServer.Handlers.gp = function(req)
local out = { local out = {
"gpu_screenshot_png=http://localhost:8080/api/v1/state/still", "gpu_screenshot_png=http://localhost:8080/api/v1/state/still",
"vram_raw=http://localhost:8080/api/v1/gpu/vram/raw (1MB VRAM)", "vram_raw=http://localhost:8080/api/v1/gpu/vram/raw (1MB VRAM)",
"gpustat=NOT_AVAILABLE_VIA_LUA", "gpustat=NOT_AVAILABLE_VIA_LUA",
"gp_command_log=NOT_AVAILABLE_VIA_LUA (use pcsx-redux Debug > GPU Logger)", "gp_command_log=NOT_AVAILABLE_VIA_LUA (use pcsx-redux Debug > GPU Logger)",
"hint_run_emulator_unpaused_for_screenshot", "hint_run_emulator_unpaused_for_screenshot",
} }
return table.concat(out, "\n") return table.concat(out, "\n")
end end
end end
local ok, err = pcall(register_handlers) local ok, err = pcall(register_handlers)
if ok then print("[pcsx_debug_helper] handlers registered: gte, gp") if ok then print("[pcsx_debug_helper] handlers registered: gte, gp")
else print("[pcsx_debug_helper] registration failed: " .. tostring(err)) else print("[pcsx_debug_helper] registration failed: " .. tostring(err))
end 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
+39 -50
View File
@@ -16,7 +16,7 @@
-- Bootstrap: load `duffle_paths.lua` via this script's own path. -- Bootstrap: load `duffle_paths.lua` via this script's own path.
-- Use `arg[0]` when this file is the entry script (`arg[0]` ends in "ps1_meta.lua"); -- Use `arg[0]` when this file is the entry script (`arg[0]` ends in "ps1_meta.lua");
-- fall back to `debug.getinfo(1, "S").source` when this file is being dofile()'d or require()'d (in which case `arg[0]` is the *caller's* path, not ours). -- fall back to `debug.getinfo(1, "S").source` when this file is being dofile()'d or require()'d (in which case `arg[0]` is the *caller's* path).
-- That single statement: (a) sets `package.path` + `package.cpath`, (b) at the bottom returns `require("duffle")`. -- That single statement: (a) sets `package.path` + `package.cpath`, (b) at the bottom returns `require("duffle")`.
-- So the dofile's return value is the duffle module. -- So the dofile's return value is the duffle module.
local _is_entry_script = arg and arg[0] and arg[0]:match("ps1_meta%.lua$") ~= nil local _is_entry_script = arg and arg[0] and arg[0]:match("ps1_meta%.lua$") ~= nil
@@ -54,45 +54,44 @@ local PASS_FLAG_DISPATCH_KEY = "__pass__"
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
--- @class PassDescriptor --- @class PassDescriptor
--- @field module string -- module name passed to require() --- @field module string -- Module name passed to require()
--- @field kind string -- "shared" | "header-output" | "validation" | "diagnostic" | "report" --- @field kind string -- "shared" | "header-output" | "validation" | "diagnostic" | "report"
--- -- Report severity is independent from process exit policy (see PASS_KIND_STOP_ON_ERROR). --- -- Report severity is independent from process exit policy (see PASS_KIND_STOP_ON_ERROR).
--- @field deps string[] -- names of upstream passes --- @field deps string[] -- Names of upstream passes
--- @field groups string[]? -- OPTIONAL build-phase groups this pass is a root of --- @field groups string[]? -- OPTIONAL build-phase groups this pass is a root of (e.g. { "pre-link" }, { "post-link" }); absent ⇒ dependency-only
--- -- (e.g. { "pre-link" }, { "post-link" }); absent ⇒ dependency-only
--- @class SourceFile --- @class SourceFile
--- @field path string -- absolute path to the source file --- @field path string -- Absolute path to the source file
--- @field text string -- the full source text --- @field text string -- Full source text
--- @field dir string -- the directory containing the source --- @field dir string -- Directory containing the source
--- @field basename string -- filename without extension --- @field basename string -- Filename without extension
--- @class PassCtx --- @class PassCtx
--- @field metadata_path string -- path to word_count.metadata.h --- @field metadata_path string -- Path to word_count.metadata.h
--- @field shared table -- cross-pass shared state --- @field shared table -- Cross-pass shared state
--- @field shared.corpus table -- canonical authored-source/project projection --- @field shared.corpus table -- Authored-source/project projection
--- @field out_root string -- output root (e.g. "build/gen") --- @field out_root string -- Output root (e.g. "build/gen")
--- @field project_root string -- PS1 repository root --- @field project_root string -- PS1 repository root
--- @field flags table -- CLI flags + per-pass stash --- @field flags table -- CLI flags + per-pass stash
--- @field verbose boolean -- if true, log diagnostic info --- @field verbose boolean -- If true, log diagnostic info
--- @class Finding --- @class Finding
--- @field line integer -- source line (or 0 for pass-level) --- @field line integer -- Source line (or 0 for pass-level)
--- @field msg string -- finding message --- @field msg string -- Finding message
--- @class PassResult --- @class PassResult
--- @field outputs PassOutputEntry[] -- emitted file paths --- @field outputs PassOutputEntry[] -- Emitted file paths
--- @field errors Finding[] -- build-stops (per-pass kind policy) --- @field errors Finding[] -- Build-stops (per-pass kind policy)
--- @field warnings Finding[] -- informational --- @field warnings Finding[] -- Informational
--- @class ParsedArgs --- @class ParsedArgs
--- @field requested_set string[] -- pass names to run (explicit --all expanded) --- @field requested_set string[] -- Pass names to run (explicit --all expanded)
--- @field sources string[] -- exact --source values, retained in CLI order --- @field sources string[] -- Exact --source values, retained in CLI order
--- @field unity_root string|nil -- --unity-root value; mutually exclusive with sources --- @field unity_root string|nil -- --unity-root value; mutually exclusive with sources
--- @field metadata string -- --metadata value --- @field metadata string -- --metadata value
--- @field out_root string -- --out-root value (default "build/gen") --- @field out_root string -- --out-root value (default "build/gen")
--- @field project_root string -- PS1 repository root (derived from metadata by default) --- @field project_root string -- PS1 repository root (derived from metadata by default)
--- @field verbose boolean -- if true, log diagnostic info --- @field verbose boolean -- If true, log diagnostic info
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- PASSES Table -- PASSES Table
@@ -138,7 +137,7 @@ local PASSES = {
["static-analysis"] = { ["static-analysis"] = {
module = "passes.static_analysis", module = "passes.static_analysis",
-- "diagnostic" — every `error`/`warning` finding is written to the report file; -- "diagnostic" — every `error`/`warning` finding is written to the report file;
-- the orchestrator does NOT exit non-zero on these findings (see PASS_KIND_STOP_ON_ERROR). -- The orchestrator does NOT exit non-zero on these findings (see PASS_KIND_STOP_ON_ERROR).
-- Report severity is independent from process exit policy. -- Report severity is independent from process exit policy.
kind = "diagnostic", kind = "diagnostic",
deps = {"scan-source", "word-counts", "components", "emission-model"}, deps = {"scan-source", "word-counts", "components", "emission-model"},
@@ -163,12 +162,12 @@ local PASSES = {
} }
-- ──────────────────────────────────────────────────────────────────────────── -- ────────────────────────────────────────────────────────────────────────────
-- Phase-root selection: derive the sorted set of roots belonging to a named build-phase group, then append them to `args.requested_set`. -- Phase-root selection: Derive the sorted set of roots belonging to a named build-phase group, then append them to `args.requested_set`.
-- topo_sort closes the transitive deps from there; dispatch_passes runs every resolved pass without phase-filtering. -- topo_sort closes the transitive deps from there; dispatch_passes runs every resolved pass without phase-filtering.
-- ──────────────────────────────────────────────────────────────────────────── -- ────────────────────────────────────────────────────────────────────────────
--- @param group_name string -- the build-phase group ("pre-link" | "post-link") --- @param group_name string -- Build-phase group ("pre-link" | "post-link")
--- @return string[] -- sorted root pass names belonging to that group --- @return string[] -- Sorted root pass names belonging to that group
local function roots_for_group(group_name) local function roots_for_group(group_name)
local names = {} local names = {}
for name, pass in pairs(PASSES) do for name, pass in pairs(PASSES) do
@@ -206,7 +205,7 @@ end
-- Report severity is independent from process exit policy. -- Report severity is independent from process exit policy.
-- A "diagnostic" pass still writes every `error`/`warning` finding into its report file, -- A "diagnostic" pass still writes every `error`/`warning` finding into its report file,
-- but `report_validation_errors` returns early for non-stopping kinds, so nothing is printed to stderr and the orchestrator does not exit non-zero. -- but `report_validation_errors` returns early for non-stopping kinds, so nothing is printed to stderr and the orchestrator does not exit non-zero.
-- Adding a new pass kind requires listing it here explicitly; an unknown kind must not silently fall back to "true". -- Adding a new pass kind requires listing it here explicitly; An unknown kind must not silently fall back to "true".
local PASS_KIND_STOP_ON_ERROR = { local PASS_KIND_STOP_ON_ERROR = {
["shared"] = false, ["shared"] = false,
["header-output"] = true, ["header-output"] = true,
@@ -216,8 +215,7 @@ local PASS_KIND_STOP_ON_ERROR = {
} }
-- Closed set of CLI flags -> pass names. -- Closed set of CLI flags -> pass names.
-- Per-pass flags (e.g. --word-counts) live here; phase flags (--pre-link, --post-link, --all) -- Per-pass flags (e.g. --word-counts); phase flags (--pre-link, --post-link, --all) are within FLAG_HANDLERS because they own side effects or invoke group-derivation logic.
-- live in FLAG_HANDLERS because they own side effects or invoke group-derivation logic.
-- dwarf-injection is *also* a per-pass opt-in flag, but its selection + opt-in state are both owned by the explicit FLAG_HANDLERS entry below -- dwarf-injection is *also* a per-pass opt-in flag, but its selection + opt-in state are both owned by the explicit FLAG_HANDLERS entry below
-- (it sets args.flags.dwarf_injection and appends "dwarf-injection" to requested_set), so it is intentionally absent from this table. -- (it sets args.flags.dwarf_injection and appends "dwarf-injection" to requested_set), so it is intentionally absent from this table.
local PASS_FLAG_TO_NAME = { local PASS_FLAG_TO_NAME = {
@@ -246,7 +244,6 @@ end
-- Per-flag handlers. Each handler takes (args, argv, arg_idx) and returns the new arg_idx (so multi-arg flags like --source FILE advance it). -- Per-flag handlers. Each handler takes (args, argv, arg_idx) and returns the new arg_idx (so multi-arg flags like --source FILE advance it).
-- Returning nil + os.exit() handles termination flags (--help). -- Returning nil + os.exit() handles termination flags (--help).
local FLAG_HANDLERS = {} local FLAG_HANDLERS = {}
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
@@ -272,9 +269,9 @@ PASS_FLAGS:
Or pick any subset: Or pick any subset:
--scan-source Scan sources into the fat SourceScan payload --scan-source Scan sources into the fat SourceScan payload
--word-counts Load metadata.h + scan for existing .macs.h --word-counts Load metadata.h + scan for existing .macs.h
--components Generate <module>/gen/<basename>.macs.h --components Generate <srcdir>/gen/macs.h (per-directory aggregation)
--validate Run atom annotation DSL validation --validate Run atom annotation DSL validation
--offsets Generate <module>/gen/<basename>.offsets.h --offsets Generate <srcdir>/gen/offsets.h (per-directory aggregation)
--atoms-source-map Generate <basename>.atoms.sourcemap.txt per source --atoms-source-map Generate <basename>.atoms.sourcemap.txt per source
--dwarf-injection [opt-in] Select the post-link dwarf-injection pass + set the opt-in flag. Requires --elf. --dwarf-injection [opt-in] Select the post-link dwarf-injection pass + set the opt-in flag. Requires --elf.
--static-analysis Static analysis: GTE pipeline-fill, mac_yield, ABI handoff, cycle budget --static-analysis Static analysis: GTE pipeline-fill, mac_yield, ABI handoff, cycle budget
@@ -317,8 +314,7 @@ local function require_flag_value(argv, arg_idx, flag)
local next_known = type(value) == "string" local next_known = type(value) == "string"
and (FLAG_HANDLERS[value] ~= nil or PASS_FLAG_TO_NAME[value] ~= nil) and (FLAG_HANDLERS[value] ~= nil or PASS_FLAG_TO_NAME[value] ~= nil)
if value == nil or next_known then if value == nil or next_known then
io.stderr:write("ps1_meta: " .. flag .. " requires " io.stderr:write("ps1_meta: " .. flag .. " requires " .. FLAG_VALUE_NAMES[flag] .. "\n")
.. FLAG_VALUE_NAMES[flag] .. "\n")
os.exit(EXIT_INTERNAL_ERROR) os.exit(EXIT_INTERNAL_ERROR)
end end
return value, arg_idx + 1 return value, arg_idx + 1
@@ -328,12 +324,10 @@ end
-- Termination flags like --help call os.exit() instead. -- Termination flags like --help call os.exit() instead.
-- Populated AFTER print_help so the --help handler can reference it as an upvalue (Lua resolves locals at closure-call time, -- Populated AFTER print_help so the --help handler can reference it as an upvalue (Lua resolves locals at closure-call time,
-- but if the closure is defined before the local, it falls back to _G). -- but if the closure is defined before the local, it falls back to _G).
FLAG_HANDLERS["--help"] = function(args)
print_help()
os.exit(0)
end
FLAG_HANDLERS["--verbose"] = function(args) args.verbose = true end FLAG_HANDLERS["--help"] = function(args) print_help(); os.exit(0) end
FLAG_HANDLERS["--verbose"] = function(args) args.verbose = true end
FLAG_HANDLERS["--source"] = function(args, argv, arg_idx) FLAG_HANDLERS["--source"] = function(args, argv, arg_idx)
local value, value_idx = require_flag_value(argv, arg_idx, "--source") local value, value_idx = require_flag_value(argv, arg_idx, "--source")
args.sources[#args.sources + 1] = value args.sources[#args.sources + 1] = value
@@ -490,14 +484,13 @@ end
--- @param args ParsedArgs --- @param args ParsedArgs
--- @return PassCtx --- @return PassCtx
local function build_ctx(args) local function build_ctx(args)
local normalized_project_root = duffle.normalize_path(args.project_root) local normalized_project_root = duffle.normalize_path(args.project_root)
local project_root = normalized_project_root local project_root = normalized_project_root
local project_root_is_absolute = normalized_project_root:match("^%a:/") local project_root_is_absolute = normalized_project_root:match("^%a:/")
or normalized_project_root:sub(1, 2) == "//" or normalized_project_root:sub(1, 2) == "//"
or normalized_project_root:sub(1, 1) == "/" or normalized_project_root:sub(1, 1) == "/"
if not project_root_is_absolute then if not project_root_is_absolute then
-- canonical_path_key validates ordinary relative paths and rejects -- canonical_path_key validates ordinary relative paths and rejects drive-relative paths before the absolute-path rewrite is performed.
-- drive-relative paths before the absolute-path rewrite is performed.
duffle.canonical_path_key(normalized_project_root) duffle.canonical_path_key(normalized_project_root)
project_root = duffle.normalize_path(duffle.to_absolute_path(normalized_project_root)) project_root = duffle.normalize_path(duffle.to_absolute_path(normalized_project_root))
else else
@@ -511,8 +504,7 @@ local function build_ctx(args)
project_root = project_root, project_root = project_root,
}) })
if not ok_resolve then if not ok_resolve then
io.stderr:write("ps1_meta: cannot resolve --unity-root " io.stderr:write("ps1_meta: cannot resolve --unity-root " .. tostring(args.unity_root) .. ": " .. tostring(resolved) .. "\n")
.. tostring(args.unity_root) .. ": " .. tostring(resolved) .. "\n")
os.exit(EXIT_INTERNAL_ERROR) os.exit(EXIT_INTERNAL_ERROR)
end end
resolution = resolved resolution = resolved
@@ -528,8 +520,7 @@ local function build_ctx(args)
local path = duffle.normalize_path(input_path) local path = duffle.normalize_path(input_path)
local key_ok, key_or_error = pcall(duffle.canonical_path_key, path) local key_ok, key_or_error = pcall(duffle.canonical_path_key, path)
if not key_ok then if not key_ok then
error("ps1_meta: invalid --source " .. input_path .. ": " error("ps1_meta: invalid --source " .. input_path .. ": " .. tostring(key_or_error), 0)
.. tostring(key_or_error), 0)
end end
local file = io.open(path, "r") local file = io.open(path, "r")
if not file then if not file then
@@ -627,9 +618,7 @@ local function topo_sort(passes, requested_set)
changed = false changed = false
for name, _ in pairs(needed) do for name, _ in pairs(needed) do
local pass = passes[name] local pass = passes[name]
if not pass then if not pass then error("unknown pass '" .. name .. "' requested") end
error("unknown pass '" .. name .. "' requested")
end
for _, dep in ipairs(pass.deps) do for _, dep in ipairs(pass.deps) do
if not needed[dep] then if not needed[dep] then
needed[dep] = true needed[dep] = true
+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"
}
+6 -6
View File
@@ -56,7 +56,7 @@ if (-not $msbuild_exe) {
} }
$path_pcsx_sln = join-path $path_pcsx_redux 'vsprojects\pcsx-redux.sln' $path_pcsx_sln = join-path $path_pcsx_redux 'vsprojects\pcsx-redux.sln'
& $msbuild_exe $path_pcsx_sln /p:Configuration=Release /p:Platform=x64 /p:PlatformToolset=v143 /m /v:minimal & $msbuild_exe $path_pcsx_sln /p:Configuration=Release /p:Platform=x64 /p:PlatformToolset=v143 /m /v:minimal
# Locate luajit via scoop. `luajit.exe` is on PATH via scoop's shim; # Locate luajit via scoop. `luajit.exe` is on PATH via scoop's shim;
# we use `scoop prefix` to find the install root for the include dir (needed to compile lpeg against luajit's headers). # we use `scoop prefix` to find the install root for the include dir (needed to compile lpeg against luajit's headers).
@@ -70,8 +70,8 @@ if (-not $luajit_prefix -or -not (Test-Path (Join-Path $luajit_prefix 'bin/luaji
# Discover the luajit include dir by globbing `include/luajit-*`. # Discover the luajit include dir by globbing `include/luajit-*`.
# This avoids hardcoding a specific version (e.g. `luajit-2.1`). # This avoids hardcoding a specific version (e.g. `luajit-2.1`).
$luajit_include_root = Join-Path $luajit_prefix 'include' $luajit_include_root = Join-Path $luajit_prefix 'include'
$lua_inc_dir = Get-ChildItem -Path $luajit_include_root -Directory -Filter 'luajit-*' -ErrorAction SilentlyContinue | $lua_inc_dir = Get-ChildItem -Path $luajit_include_root -Directory -Filter 'luajit-*' -ErrorAction SilentlyContinue |
Select-Object -First 1 -ExpandProperty FullName Select-Object -First 1 -ExpandProperty FullName
if (-not $lua_inc_dir) { if (-not $lua_inc_dir) {
write-error "No 'luajit-*' include dir found under '$luajit_include_root'. The scoop luajit install may be broken." write-error "No 'luajit-*' include dir found under '$luajit_include_root'. The scoop luajit install may be broken."
exit 1 exit 1
@@ -90,7 +90,7 @@ $lpeg_compile_args = @(
'-o', 'lpeg.dll' '-o', 'lpeg.dll'
) + $lpeg_sources + @('-lluajit-5.1') ) + $lpeg_sources + @('-lluajit-5.1')
push-location $path_lpeg push-location $path_lpeg
& gcc @lpeg_compile_args & gcc @lpeg_compile_args
pop-location pop-location
# ════════════════════════════════════════════════════════════════════════════ # ════════════════════════════════════════════════════════════════════════════
@@ -101,8 +101,8 @@ pop-location
$path_lfs = join-path $path_toolchain 'lfs' $path_lfs = join-path $path_toolchain 'lfs'
verify-path $path_lfs verify-path $path_lfs
$lfs_src = join-path $path_pcsx_redux 'third_party\luafilesystem\src\lfs.c' $lfs_src = join-path $path_pcsx_redux 'third_party\luafilesystem\src\lfs.c'
$lfs_dll = join-path $path_lfs 'lfs.dll' $lfs_dll = join-path $path_lfs 'lfs.dll'
$lfs_dll_import = join-path $luajit_lib_dir 'libluajit-5.1.dll.a' $lfs_dll_import = join-path $luajit_lib_dir 'libluajit-5.1.dll.a'
& gcc -O2 -shared "-I$lua_inc_dir" -o $lfs_dll $lfs_src $lfs_dll_import & gcc -O2 -shared "-I$lua_inc_dir" -o $lfs_dll $lfs_src $lfs_dll_import