diff --git a/code/duffle/gen/duffle.macs.h b/code/duffle/gen/duffle.macs.h
new file mode 100644
index 0000000..043c81c
--- /dev/null
+++ b/code/duffle/gen/duffle.macs.h
@@ -0,0 +1,119 @@
+#ifdef INTELLISENSE_DIRECTIVES
+#pragma once
+#endif
+// Auto-generated by tape_atom_annotation_pass.lua — DO NOT EDIT
+// Source: C:\projects\Pikuma\ps1\code\duffle\lottes_tape.h
+// Component atoms (MipsAtomComp_(ac_*)) -> macro variants (mac_*)
+// + auto word-counts (so tape_atom.metadata.h stays manual-only
+// for encoding macros).
+
+#ifndef WORD_COUNT
+#define WORD_COUNT(name, count) enum { words_##name = (count) };
+#endif
+
+#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)
+
+/* 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)
+
+/* Words: 18; Translates indices to vertex addresses and pushes them to GTE */
+#define mac_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_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_lright(R_AT, R_AT, S_(polytag_len_bits))
+WORD_COUNT(mac_insert_ot_tag_f3, 1)
+
+/* 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_lright(R_AT, R_AT, S_(polytag_len_bits))
+WORD_COUNT(mac_insert_ot_tag_g4, 1)
+
+#define mac_pack_color_word(off, code, r, g, b) \
+ load_upper_i(R_AT, (code) << 8 | (b)) \
+, or_i_self( R_AT, ((g) << 8) | (r)) \
+, store_word( R_AT, R_PrimCursor, (off))
+WORD_COUNT(mac_pack_color_word, 3)
+
+#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, 1)
+
+/* 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).
+ * The macro name declares the pipeline position; check #6 (GTE state-
+ * machine validation) verifies the call site matches the declaration. */
+#define mac_gte_store_f3_post_rtpt(...) \
+ 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_post_rtpt, 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, 4)
+
+/* 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).
+ * The macro name declares the pipeline position; check #6 (GTE state-
+ * machine validation) verifies the call site matches the declaration. */
+#define mac_gte_store_g4_p012_post_rtpt_pre_rtps(...) \
+ 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_post_rtpt_pre_rtps, 3)
+
+/* 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 — DO NOT read SXY0 here, that's the bug this name
+ * prevents).
+ * The macro name declares the pipeline position; check #6 (GTE state-
+ * machine validation) verifies the call site matches the declaration.
+ *
+ * History: this macro was named `mac_gte_store_g4_p3` until 2026-07-09
+ * when it was discovered to be reading C2_SXY0 (which held v0.screen)
+ * instead of C2_SXY2 (which holds v3.screen after RTPS). The rename
+ * encodes the pipeline position in the name so the next bug of this
+ * class is impossible. */
+#define mac_gte_store_g4_p3_post_rtps(...) \
+ gte_sw(C2_SXY2, R_PrimCursor, O_(Poly_G4,p3))
+WORD_COUNT(mac_gte_store_g4_p3_post_rtps, 1)
+
diff --git a/code/duffle/gen/lottes_tape.offsets.h b/code/duffle/gen/duffle.offsets.h
similarity index 100%
rename from code/duffle/gen/lottes_tape.offsets.h
rename to code/duffle/gen/duffle.offsets.h
diff --git a/code/duffle/lottes_tape.h b/code/duffle/lottes_tape.h
index 84cbc27..f44f08b 100644
--- a/code/duffle/lottes_tape.h
+++ b/code/duffle/lottes_tape.h
@@ -6,18 +6,36 @@
# include "gte.h"
# include "memory.h"
# include "atom_dsl.h"
+# include "gen/duffle.macs.h"
+# include "gen/duffle.offsets.h"
#endif
typedef U4 const MipsCode;
-#define MipsAtom_(sym) MipsCode tmpl(code,sym) [] align_(4) =
+typedef Slice_(MipsCode);
+typedef Slice_MipsCode MipsAtom;
+#define MipsAtom_(sym) MipsCode tmpl(code,sym) [] align_(4) =
+// Bare form: file-scope declaration with hardcoded body.
+// 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) =
+// Function form: function-body block that returns a MipsAtom slice.
+// Used for components with value-args (e.g., ac_format_f3_color).
+// FI_ MipsAtom ac_X(args) MipsAtomComp_Proc_(ac_X, { body })
+// expands to:
+// FI_ MipsAtom 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); }
-#pragma region Tape Drive
-/* ---------------------------------------------------------------------------
- * TAPE DRIVE ABI & REGISTER ALIASES
- * ---------------------------------------------------------------------------
- * We map the MIPS temporary registers to a persistent global workspace.
- * The C compiler is completely unaware of these bindings.
- * ---------------------------------------------------------------------------*/
+// Auto-generated component macros (build/gen/
/.components.h)
+// are included manually by the unity build. The metaprogram puts them
+// under ./build/gen/ (not in the code dirs) so the source tree stays clean.
+
+/* Register aliases (moved up from the Tape Drive region below so that
+ * mac_yield's body and the Mips Atom Builder functions can reference
+ * them. The C compiler processes the file top-to-bottom, so the enum
+ * must be visible before any use.) */
enum {
R_AtomJmp = R_T9,
R_TapePtr = R_T8, /* The Instruction Stream Pointer */
@@ -38,6 +56,11 @@ enum {
#define R_OtBase_Code R_T6_Code
};
+#pragma region Tape Drive
+/* ---------------------------------------------------------------------------
+ * TAPE DRIVE ABI & REGISTER ALIASES (the enum moved earlier; see below)
+ * ---------------------------------------------------------------------------*/
+
/* The 'Exit' Atom */
MipsAtom_(tape_exit) { jump_reg(rret_addr), nop };
@@ -85,71 +108,104 @@ FI_ Slice_U4 tb_slice(TapeBuilder tb) { return (Sli
* These do NOT yield. They are expanded inline inside Tape Atoms.
* ---------------------------------------------------------------------------*/
-/* The 'Yield' sequence for Tape Atoms.
- * Loads the next pointer from the tape, advances the tape, and jumps.
- * Cost: ~ 4 cycles */
-#define mac_yield() \
- load_word(R_AtomJmp, R_TapePtr, 0) \
- , add_ui_self( R_TapePtr, S_(MipsCode)) \
- , jump_reg( R_AtomJmp) \
+// The 'Yield' sequence for Tape Atoms (mac_yield). Authored as a
+// bare-form component (MipsAtomComp_). The metaprogram reads the
+// body and generates a `mac_yield(...)` macro in the auto-generated
+// duffle.macs.h header. The body is the same 4-instruction yield
+// sequence as before.
+MipsAtomComp_(ac_yield) {
+ load_word(R_AtomJmp, R_TapePtr, 0)
+ , add_ui_self( R_TapePtr, S_(MipsCode))
+ , jump_reg( R_AtomJmp)
, nop
+};
/* Words: 3; Loads 3 S2 indices from the face array */
-#define mac_load_tri_indices(rId_0, rId_1, rId_2) \
- load_half_u(rId_0, R_FaceCursor, 0 * S_(S2)) \
- , load_half_u(rId_1, R_FaceCursor, 1 * S_(S2)) \
- , load_half_u(rId_2, R_FaceCursor, 2 * S_(S2))
+MipsAtomComp_(ac_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))
+};
/* Words: 18; Translates indices to vertex addresses and pushes them to GTE */
-#define mac_load_tri_verts(rId_0, rId_1, rId_2) \
- shift_lleft(R_AT, rId_0, 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, rId_1, 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, rId_2, 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)
+MipsAtomComp_(ac_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 */
-#define mac_insert_ot_tag(r_otz, prim_type) \
- shift_lleft( R_T1, r_otz, S_(U4)/2) /* T1 = r_otz * S_(U4) */ \
- , add_u_self( R_T1, R_OtBase) /* T1 = & OrderingTable[OTZ] */ \
- , load_word( R_AT, R_T1, O_(PolyTag,bf_addr_len)) /* AT = old_ot_head */ \
- , load_upper_i(R_V0, (S_(prim_type)/S_(U4) - S_(PolyTag)/S_(U4)) << polytag_len_bits) /* V0 = S_(prim_type without tag field) */ \
- , 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,bf_addr_len)) /* 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)) \
+/* 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,bf_addr_len)) /* 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,bf_addr_len)) /* 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,bf_addr_len)) /* 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,bf_addr_len)) /* 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,bf_addr_len)) /* 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,bf_addr_len)) /* 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.
* Args: off = U4 byte offset, code = GP0 cmd byte (0 for c1/c2/c3 of
* a Poly_G4), r/g/b = 8-bit RGB byte values. */
-#define mac_pack_color_word(off, code, r,g,b) \
- load_upper_i(R_AT, (code) << 8 | (b)) \
- , or_i_self( R_AT, ((g) << 8) | (r)) \
+FI_ MipsAtom ac_pack_color_word(U4 off, U4 code, U1 r, U1 g, U1 b)
+MipsAtomComp_Proc_(ac_pack_color_word, {
+ load_upper_i(R_AT, (code) << 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).
* Migrated from hello_gte_tape.c; takes RGB form per the Phase 3
* convention. */
-#define mac_format_f3_color(r,g,b) mac_pack_color_word(O_(Poly_F3,color), gp0_cmd_poly_f3, r,g,b)
+FI_ MipsAtom ac_format_f3_color(U1 r, U1 g, U1 b)
+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).
* The macro name declares the pipeline position; check #6 (GTE state-
* machine validation) verifies the call site matches the declaration. */
-#define mac_gte_store_f3_post_rtpt() \
- gte_sw(C2_SXY0, R_PrimCursor, O_(Poly_F3,p0)) \
- , gte_sw(C2_SXY1, R_PrimCursor, O_(Poly_F3,p1)) \
+MipsAtomComp_(ac_gte_store_f3_post_rtpt) {
+ 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. */
-#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) \
+FI_ MipsAtom 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.
@@ -159,10 +215,11 @@ FI_ Slice_U4 tb_slice(TapeBuilder tb) { return (Sli
* three registers aligned with v0/v1/v2 you must store before RTPS).
* The macro name declares the pipeline position; check #6 (GTE state-
* machine validation) verifies the call site matches the declaration. */
-#define mac_gte_store_g4_p012_post_rtpt_pre_rtps() \
- gte_sw(C2_SXY0, R_PrimCursor, O_(Poly_G4,p0)) \
- , gte_sw(C2_SXY1, R_PrimCursor, O_(Poly_G4,p1)) \
+MipsAtomComp_(ac_gte_store_g4_p012_post_rtpt_pre_rtps) {
+ 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
@@ -177,7 +234,9 @@ FI_ Slice_U4 tb_slice(TapeBuilder tb) { return (Sli
* instead of C2_SXY2 (which holds v3.screen after RTPS). The rename
* encodes the pipeline position in the name so the next bug of this
* class is impossible. */
-#define mac_gte_store_g4_p3_post_rtps() gte_sw(C2_SXY2, R_PrimCursor, O_(Poly_G4,p3))
+MipsAtomComp_(ac_gte_store_g4_p3_post_rtps) {
+ gte_sw(C2_SXY2, R_PrimCursor, O_(Poly_G4,p3))
+};
#pragma endregion Macro Atom Components
@@ -186,7 +245,6 @@ FI_ Slice_U4 tb_slice(TapeBuilder tb) { return (Sli
typedef Struct_(FMipsAtom512) { U4 data[512]; U4 used; };
-typedef Slice_(MipsCode); typedef Slice_MipsCode MipsAtom;
// FArena Related
typedef Relative_(FArena) Struct_(MipsAtomBuilder) { U4 start; U4 capacity; U4 used; };
// Whatever the builder is writting to should most likely coresspond
@@ -243,7 +301,7 @@ internal MipsAtom_(mips_flush_icache) {
};
typedef Struct_(Binds_SetGteWorld) {
- U4 transform;
+ M3_S2* transform;
};
internal MipsAtom_(set_gte_world) {
/* Pop matrix address from tape into R_T3 ($11) */
@@ -259,8 +317,6 @@ internal MipsAtom_(set_gte_world) {
mac_yield()
};
-// TODO(Ed): I'm not sure yet if the bindings are redundant with the floortri atom yet.
-
/* DIAGNOSTIC 1: Pure tape loop test */
internal MipsAtom_(diag_yield) { mac_yield() };
diff --git a/code/duffle/memory.h b/code/duffle/memory.h
index 2d2a72f..a911887 100644
--- a/code/duffle/memory.h
+++ b/code/duffle/memory.h
@@ -73,6 +73,7 @@ typedef Slice_(B1);
#define slice_iter(container, iter) (T_((container).ptr) iter = (container).ptr; iter != slice_end(container); ++ iter)
#define slice_arg_from_array(type, ...) & (tmpl(Slice,type)) { .ptr = array_decl(type,__VA_ARGS__), .len = array_len( array_decl(type,__VA_ARGS__)) }
+#define slice_from_array(type, array) (tmpl(Slice,type)) { .ptr = array, .len = S_(array) }
FI_ void slice_zero_(Slice s) { slice_assert(s); mem_zero(s.ptr, s.len); }
#define slice_zero(s) slice_zero_(slice_to_ut(s))
diff --git a/code/gte_hello/gen/hello_gte_tape.offsets.h b/code/gte_hello/gen/gte_hello.offsets.h
similarity index 65%
rename from code/gte_hello/gen/hello_gte_tape.offsets.h
rename to code/gte_hello/gen/gte_hello.offsets.h
index a20fdb0..669ec58 100644
--- a/code/gte_hello/gen/hello_gte_tape.offsets.h
+++ b/code/gte_hello/gen/gte_hello.offsets.h
@@ -5,20 +5,20 @@
#pragma region hello_gte_tape
-// --- atom: cube_g4_face (87 words) ---
+// --- atom: cube_g4_face (44 words) ---
-#define _atom_offset_cull_cube_g4_face_exit 48
-#define _atom_offset_bounds_chk_cube_g4_face_exit 12
+#define _atom_offset_cull_cube_g4_face_exit 25
+#define _atom_offset_bounds_chk_cube_g4_face_exit 2
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 (66 words) ---
+// --- atom: floor_f3_face (30 words) ---
-#define _atom_offset_culling_floor_f3_face_exit 29
-#define _atom_offset_bounds_chk_floor_f3_face_exit 13
+#define _atom_offset_culling_floor_f3_face_exit 15
+#define _atom_offset_bounds_chk_floor_f3_face_exit 3
enum {
atom_offset_culling_floor_f3_face_exit = _atom_offset_culling_floor_f3_face_exit,
diff --git a/code/gte_hello/hello_gte.c b/code/gte_hello/hello_gte.c
index 0896bf6..c8fce37 100644
--- a/code/gte_hello/hello_gte.c
+++ b/code/gte_hello/hello_gte.c
@@ -8,17 +8,19 @@
#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/gen/lottes_tape.offsets.h"
+# include "duffle/gen/duffle.macs.h"
+# include "duffle/gen/duffle.offsets.h"
#include "duffle/atom_dsl.h"
#include "duffle/lottes_tape.h"
# include "tape_atom.metadata.h"
-# include "gen/hello_gte_tape.offsets.h"
+# include "gen/gte_hello.offsets.h"
#include "hello_gte.h"
#include "hello_gte_tape.c"
@@ -337,9 +339,6 @@ void update(PrimitiveArena* pa, U4* ordering_buf)
m3s2_rotation (& smem.floor.rot, & smem.tform_world);
m3s2_translation(& smem.tform_world, & smem.floor.pos);
m3s2_scale (& smem.tform_world, & smem.floor.scale);
- // TODO(Ed): This can either be in the tape or here...
- // 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;
@@ -350,7 +349,6 @@ void update(PrimitiveArena* pa, U4* ordering_buf)
// Prepare the tape. (Push protocol to tape)
LP_ U4 mem_temp_tape[512];
TapeBuilder tb = tb_make(slice_ut_arr(mem_temp_tape)); tb_scope(& tb) {
- // TODO(Ed): This is bugged.
tb_emit(& tb, code_set_gte_world);
tb_data(& tb, u4_(& smem.tform_world));
diff --git a/code/gte_hello/hello_gte_tape.c b/code/gte_hello/hello_gte_tape.c
index 078e2b8..f8e4aff 100644
--- a/code/gte_hello/hello_gte_tape.c
+++ b/code/gte_hello/hello_gte_tape.c
@@ -1,9 +1,10 @@
#ifdef INTELLISENSE_DIRECTIVES
-# include "duffle/lottes_tape.h"
+# include "duffle/gen/duffle.macs.h"
+# include "duffle/gen/duffle.offsets.h"
# include "duffle/atom_dsl.h"
-# include "hello_gte.h"
+# include "duffle/lottes_tape.h"
# include "tape_atom.metadata.h"
-# include "gen/hello_gte_tape.offsets.h"
+# include "hello_gte.h"
#endif
#pragma region MACs (Mips Atom components)
@@ -17,10 +18,10 @@
#pragma region Baked Atoms
typedef Struct_(Binds_CubeTri) {
- U4 PrimCursor;
- U4 FaceCursor;
- U4 VertBase;
- U4 OtBase;
+ U4 PrimCursor;
+ V4_S2* FaceCursor;
+ V3_S2* VertBase;
+ U4* OtBase;
};
internal MipsAtom_(rbind_cube_g4_face) {
/* Pop 4 arguments from the tape directly into the workspace registers */
@@ -101,7 +102,7 @@ MipsAtom_(cube_g4_face) {
branch_equal(R_AT, R_0, atom_offset(bounds_chk, cube_g4_face_exit)), nop,
/* ── 12. Insert into Ordering Table (length = 8 words for Poly_G4) ──── */
- mac_insert_ot_tag(R_T1, Poly_G4),
+ mac_insert_ot_tag_g4(),
/* ── 13. Advance cursors & yield (both branch targets land here) ────── */
atom_label(cube_g4_face_exit)
@@ -111,10 +112,10 @@ atom_label(cube_g4_face_exit)
};
typedef Struct_(Binds_FloorTri) {
- U4 PrimCursor;
- U4 FaceCursor;
- U4 VertBase;
- U4 OtBase;
+ U4 PrimCursor;
+ V3_S2* FaceCursor;
+ V3_S2* VertBase;
+ U4* OtBase;
};
atom_region(rbind_floor_f3_face, REGION_PRIM_ARENA)
atom_group(rbind_floor_f3_face, GROUP_RENDER_FLOOR)
@@ -163,7 +164,7 @@ MipsAtom_(floor_f3_face) {
set_lt_u( R_AT, R_T1, R_AT),
branch_equal(R_AT, R_0, atom_offset(bounds_chk, floor_f3_face_exit)), nop,
/* Insert into Ordering Table Linked List */
- mac_insert_ot_tag(R_T1, Poly_F3),
+ mac_insert_ot_tag_f3(),
add_ui_self(R_PrimCursor, S_(Poly_F3)), /* Advance Prim Cursor (5 words) */
// Note(Ed): No bounds checking, should be checked before atom runs.
diff --git a/code/gte_hello/tape_atom.metadata.h b/code/gte_hello/tape_atom.metadata.h
index c6af238..19c3d8b 100644
--- a/code/gte_hello/tape_atom.metadata.h
+++ b/code/gte_hello/tape_atom.metadata.h
@@ -5,6 +5,12 @@
// Format: WORD_COUNT(MACRO_NAME, COUNT)
// One line per macro that appears in your atom sources.
//
+// This file is encoding-macros-only. The auto-generated component
+// macros (mac_X) live in duffle/gen/.macs.h (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 (or the
+// include guard order handled) to avoid WORD_COUNT redeclaration.
+//
// To regenerate: hand-count the instructions in each macro definition.
// (You'll only need to do this once per macro — they don't change often.)
#define WORD_COUNT(name, count) enum { words_##name = (count) };
@@ -47,15 +53,5 @@ WORD_COUNT(gte_cmdw_nclip, 1)
WORD_COUNT(gte_avg_sort_z3, 1)
WORD_COUNT(sub_u, 1)
WORD_COUNT(nop2, 2)
-WORD_COUNT(mac_load_tri_indices, 3)
-WORD_COUNT(mac_load_tri_verts, 18)
-WORD_COUNT(mac_format_f3_color, 3)
-WORD_COUNT(mac_gte_store_f3_post_rtpt, 3)
-WORD_COUNT(mac_pack_color_word, 3)
-WORD_COUNT(mac_format_g4_color, 12)
-WORD_COUNT(mac_gte_store_g4_p012_post_rtpt_pre_rtps, 3)
-WORD_COUNT(mac_gte_store_g4_p3_post_rtps, 1)
-WORD_COUNT(mac_insert_ot_tag, 11)
-WORD_COUNT(mac_yield, 4)
#undef WORD_COUNT
diff --git a/scripts/tape_atom.offset_gen.meta.lua b/scripts/tape_atom.offset_gen.meta.lua
index f613bf6..e0cfbd1 100644
--- a/scripts/tape_atom.offset_gen.meta.lua
+++ b/scripts/tape_atom.offset_gen.meta.lua
@@ -405,10 +405,11 @@ local function process_source(source_path, word_counts)
})
end
- local basename = basename_no_ext(source_path)
+ local basename = basename_no_ext(source_path)
+ local dir_basename = basename_no_ext(dirname(source_path))
local out_dir = dirname(source_path) .. "/gen"
ensure_dir(out_dir)
- local out_path = out_dir .. "/" .. basename .. ".offsets.h"
+ local out_path = out_dir .. "/" .. dir_basename .. ".offsets.h"
write_file(out_path, generate_header(source_path, atoms_data))
local total_branches = 0
diff --git a/scripts/tape_atom_annotation_pass.lua b/scripts/tape_atom_annotation_pass.lua
index 35653f1..26d6147 100644
--- a/scripts/tape_atom_annotation_pass.lua
+++ b/scripts/tape_atom_annotation_pass.lua
@@ -586,6 +586,367 @@ local function find_atom_names(source)
return out
end
+-- ============================================================
+-- Find the args of the function declaration that immediately precedes
+-- a MipsAtomComponent_ invocation of the given name. Returns the
+-- args string (e.g., "U4 off, U4 code, U1 r, U1 g, U1 b") or nil
+-- if no function declaration is found.
+--
+-- Convention: function form is
+-- FI_ MipsAtom ac_X(args) MipsAtomComponent_(ac_X, { body })
+-- We find the LAST occurrence of "ac_X(" before before_pos and
+-- extract the args from inside the parens.
+--
+-- No regex (per the no_regex constraint). Uses string.find with
+-- plain mode (4th arg = true) to find the name + open paren.
+-- ============================================================
+
+local function find_function_args_for(source, name, before_pos)
+ local search = source:sub(1, before_pos)
+ local name_paren = name .. "("
+ local last_idx = nil
+ local p = 1
+ while true do
+ local s = search:find(name_paren, p, true) -- plain (no regex)
+ if not s then break end
+ last_idx = s
+ p = s + #name_paren
+ end
+ if not last_idx then return nil end
+
+ -- Verify the preceding context ends with "MipsAtom" (with
+ -- possible qualifiers between). Check the last word is
+ -- "MipsAtom" (or the trimmed before ends with that token).
+ local before = search:sub(1, last_idx - 1)
+ local trimmed = duffle.trim(before)
+ if trimmed:sub(-#"MipsAtom") ~= "MipsAtom" then
+ -- Preceding context is not a function declaration.
+ -- This shouldn't happen with the convention, but guard anyway.
+ return nil
+ end
+
+ local open_paren = last_idx + #name -- position of "("
+ local inner = read_parens(source, open_paren)
+ if not inner then return nil end
+ return inner
+end
+
+-- Extract just the parameter NAMES from a function-args string
+-- (stripping type annotations). E.g.,
+-- "U4 off, U4 code, U1 r, U1 g, U1 b" -> {"off", "code", "r", "g", "b"}
+-- "U4 *ptr" -> {"ptr"}
+-- "" -> nil
+-- No regex — uses duffle.is_alnum + plain string ops.
+-- ============================================================
+
+-- ============================================================
+-- Find the contiguous comment block immediately preceding `pos` in
+-- `source`. Returns the comment text (with the `/* */` or `//` markers
+-- preserved) or an empty string if no comment is adjacent.
+-- Used to copy signature comments from the source declaration
+-- (`MipsAtomComponent_` / function decl) over to the generated
+-- `mac_X` macro, so LSP/IntelliSense displays the args doc.
+-- No regex (per the no_regex constraint).
+-- ============================================================
+
+local function preceding_comment_block(source, pos)
+ local i = pos
+ local pieces = {}
+ while true do
+ -- Skip whitespace
+ local j = i - 1
+ while j > 0 do
+ local c = source:sub(j, j)
+ if c == " " or c == "\t" or c == "\n" or c == "\r" then
+ j = j - 1
+ else
+ break
+ end
+ end
+ if j == 0 then break end
+ -- Check for /* ... */ ending at j
+ if j >= 2 and source:sub(j-1, j) == "*/" then
+ local s = source:sub(1, j - 1)
+ local last_open = nil
+ for k = #s - 1, 1, -1 do
+ if s:sub(k, k+1) == "/*" then
+ last_open = k
+ break
+ end
+ end
+ if last_open then
+ -- Include the leading whitespace+indentation before /*
+ local block_start = last_open
+ while block_start > 1 do
+ local c = source:sub(block_start - 1, block_start - 1)
+ if c == " " or c == "\t" then
+ block_start = block_start - 1
+ else
+ break
+ end
+ end
+ table.insert(pieces, 1, source:sub(block_start, j))
+ i = block_start
+ else
+ break
+ end
+ -- Check for // comment ending at j (j is at end of line, j-1 is \n)
+ elseif j >= 1 and (source:sub(j, j) == "\n" or source:sub(j, j) == "\r") then
+ -- Walk back to the start of the line
+ local line_start = j
+ while line_start > 1 and source:sub(line_start-1, line_start-1) ~= "\n" do
+ line_start = line_start - 1
+ end
+ local line = source:sub(line_start, j)
+ if line:sub(1, 2) == "//" then
+ table.insert(pieces, 1, line)
+ i = line_start - 1
+ else
+ break
+ end
+ else
+ break
+ end
+ end
+ if #pieces == 0 then return "" end
+ return table.concat(pieces, "\n")
+end
+
+-- Extract just the parameter NAMES from a function-args string
+-- (stripping type annotations). E.g.,
+-- "U4 off, U4 code, U1 r, U1 g, U1 b" -> {"off", "code", "r", "g", "b"}
+-- "U4 *ptr" -> {"ptr"}
+-- "" -> nil
+-- No regex — uses duffle.is_alnum + plain string ops.
+-- ============================================================
+
+local function extract_arg_names(args_str)
+ if not args_str or args_str == "" then return nil end
+ local names = {}
+ local tokens = duffle.split_top_level_commas(args_str)
+ for _, tok in ipairs(tokens) do
+ local trimmed = duffle.trim(tok)
+ if trimmed ~= "" then
+ -- Walk backwards from end of trimmed arg, skipping
+ -- trailing whitespace / asterisks / brackets.
+ local i = #trimmed
+ while i > 0 do
+ local c = trimmed:sub(i, i)
+ if c == " " or c == "\t" or c == "*" or c == "]" or c == "[" then
+ i = i - 1
+ else
+ break
+ end
+ end
+ -- Now find the end of the last identifier (the param name).
+ local j = i
+ while j > 0 do
+ local c = trimmed:sub(j, j)
+ if duffle.is_alnum(c) or c == "_" then
+ j = j - 1
+ else
+ break
+ end
+ end
+ local name = trimmed:sub(j + 1, i)
+ if name ~= "" then
+ names[#names + 1] = name
+ end
+ end
+ end
+ if #names == 0 then return nil end
+ return names
+end
+
+-- ============================================================
+-- Find every MipsAtomComponent_(ac_) { body } declaration in source.
+-- Supports BOTH the bare form and the function form:
+-- Bare: MipsAtomComponent_(ac_X) { body }
+-- Function: MipsAtomComponent_(ac_X, { body }) (with a preceding
+-- "FI_ MipsAtom ac_X(args)" function declaration)
+-- Returns: {line, name, body, args} where args is the function-args
+-- string (or nil for the bare form).
+-- ============================================================
+-- WORD_COUNT entry in gen/.components.h)
+-- ============================================================
+
+local function find_component_atoms(source)
+ local line_of = duffle.LineIndex(source)
+ local out = {}
+ local len = #source
+ local i = 1
+ while i <= len do
+ i = skip_ws_and_cmt(source, i); if i > len then break end
+ local ident, after = read_ident(source, i)
+ if not ident then
+ i = i + 1
+ elseif ident == "MipsAtomComp_Proc_"
+ or ident == "MipsAtomComp_" then
+ local open = skip_ws_and_cmt(source, after)
+ if source:sub(open, open) == "(" then
+ local inner, after_paren = read_parens(source, open)
+ -- Parse args: 1 arg = bare form, 2 args = function form
+ local tokens = duffle.split_top_level_commas(inner)
+ local name, body = nil, nil
+ if #tokens == 1 then
+ name = duffle.trim(tokens[1])
+ elseif #tokens == 2 then
+ name = duffle.trim(tokens[1])
+ local body_raw = duffle.trim(tokens[2])
+ -- Strip leading { and trailing } if present
+ if #body_raw >= 2
+ and body_raw:sub(1, 1) == "{"
+ and body_raw:sub(-1) == "}" then
+ body = duffle.trim(body_raw:sub(2, -2))
+ else
+ body = body_raw
+ end
+ end
+ if name and name:sub(1, 3) == "ac_" then
+ -- Find the function args (preceding function decl).
+ -- For the bare form this returns nil (no function).
+ local args = find_function_args_for(source, name, open)
+ -- Capture the preceding comment block (signature doc).
+ -- Walk back from `i` (position of the identifier start)
+ -- so the walk-back goes through whitespace+comment and
+ -- stops AT the comment (not at the identifier chars).
+ local comment = preceding_comment_block(source, i)
+ if body == nil then
+ -- Bare form: body is the brace block AFTER the parens.
+ local brace = scan_to_char(source, "{", after_paren)
+ if brace then
+ local body_content, after_brace = read_braces(source, brace)
+ out[#out + 1] = {
+ line = line_of(i),
+ name = name:sub(4), -- strip "ac_" prefix
+ body = body_content,
+ args = args,
+ comment = comment,
+ }
+ i = after_brace
+ else
+ i = open + 1
+ end
+ else
+ -- Function form: body is the second arg (already extracted).
+ out[#out + 1] = {
+ line = line_of(i),
+ name = name:sub(4), -- strip "ac_" prefix
+ body = body,
+ args = args,
+ comment = comment,
+ }
+ i = after_paren
+ end
+ else
+ i = open + 1
+ end
+ else
+ i = open + 1
+ end
+ else
+ i = after
+ end
+ end
+ return out
+end
+
+-- ============================================================
+-- Emit a per-directory generated header with mac_X(...) macros
+-- derived from MipsAtomComponent_ declarations + auto word-counts.
+-- Output: /gen/.components.h
+-- ============================================================
+
+local function emit_component_macros_h(source_path, components)
+ if #components == 0 then return end
+
+ local dir = duffle.dirname(source_path)
+ local dir_basename = duffle.basename_no_ext(dir)
+ -- Components header stays in the source dir (used by the codebase).
+ local out_dir = dir .. "/gen"
+ local out_path = out_dir .. "/" .. dir_basename .. ".macs.h"
+
+ local lines = {
+ -- #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.
+ "#ifdef INTELLISENSE_DIRECTIVES",
+ "#pragma once",
+ "#endif",
+ "// Auto-generated by tape_atom_annotation_pass.lua — DO NOT EDIT",
+ "// Source: " .. source_path,
+ "// Component atoms (MipsAtomComp_(ac_*)) -> macro variants (mac_*)",
+ "// + auto word-counts (so tape_atom.metadata.h stays manual-only",
+ "// for encoding macros).",
+ "",
+ -- Self-contained: define WORD_COUNT if not already defined.
+ -- The metadata file (tape_atom.metadata.h) defines it as
+ -- enum { words_##name = (count) };
+ -- We use the same definition here so the auto-generated
+ -- entries below expand to compile-time constants whether
+ -- the metadata file is included first or not.
+ "#ifndef WORD_COUNT",
+ "#define WORD_COUNT(name, count) enum { words_##name = (count) };",
+ "#endif",
+ "",
+ }
+
+ for _, c in ipairs(components) do
+ -- Emit the signature comment (if any) above the macro.
+ -- This is the same comment that preceded the MipsAtomComponent_
+ -- declaration in the source; LSP/IntelliSense shows it on the
+ -- generated mac_X macro.
+ if c.comment and c.comment ~= "" then
+ for line in (c.comment .. "\n"):gmatch("([^\n]*)\n") do
+ lines[#lines + 1] = line
+ end
+ end
+
+ -- Split body by top-level commas; filter empty tokens.
+ local tokens = {}
+ for _, t in ipairs(duffle.split_top_level_commas(c.body)) do
+ local trimmed = duffle.trim(t)
+ if trimmed ~= "" then tokens[#tokens + 1] = trimmed end
+ end
+ local n = #tokens
+
+ -- Determine the macro signature: with function args (function
+ -- form) or variadic-ignored (bare form).
+ local arg_names = extract_arg_names(c.args)
+ local sig
+ if arg_names and #arg_names > 0 then
+ sig = table.concat(arg_names, ", ")
+ else
+ sig = "..."
+ end
+
+ if n > 0 then
+ -- Emit the mac_() macro
+ lines[#lines + 1] = "#define mac_" .. c.name .. "(" .. sig .. ") \\"
+ lines[#lines + 1] = "\t" .. tokens[1] .. " \\"
+ for j = 2, n do
+ lines[#lines + 1] = ",\t" .. tokens[j] .. " \\"
+ end
+ -- Strip the trailing line-continuation on the last body line.
+ -- The last 2 chars are always " \" (space + backslash).
+ -- No regex — just trim with string.sub.
+ local last = lines[#lines]
+ if last:sub(-2) == " \\" then
+ lines[#lines] = last:sub(1, -3)
+ end
+ end
+
+ -- Emit the WORD_COUNT(mac_, N) entry.
+ lines[#lines + 1] = "WORD_COUNT(mac_" .. c.name .. ", " .. n .. ")"
+ lines[#lines + 1] = ""
+ end
+
+ duffle.ensure_dir(out_dir)
+ duffle.write_file(out_path, table.concat(lines, "\n") .. "\n")
+ print(string.format(" -> %s", out_path))
+end
+
local function find_atom_annotations(source)
local line_of = duffle.LineIndex(source)
local annots = {}
@@ -1056,7 +1417,8 @@ local function main(args)
for i = 2, #args do
local source_path = args[i]
local basename = basename_no_ext(source_path)
- local out_dir = dirname(source_path) .. "/gen"
+ local dir_basename = basename_no_ext(dirname(source_path))
+ local out_dir = dirname(source_path) .. "/../../build/gen/" .. dir_basename
local out_txt = out_dir .. "/" .. basename .. ".annotations.txt"
local out_err = out_dir .. "/" .. basename .. ".errors.h"
@@ -1107,10 +1469,21 @@ local function main(args)
if f then f:close(); os.remove(stale) end
end
end
+
+ -- Emit the per-directory component-macros header (gen/.components.h)
+ -- if this source has any MipsAtomComponent_ declarations. Independent of
+ -- has_atoms: a header can have components without having full atoms.
+ -- (TODO: pass source text into validate() to avoid the double-read.)
+ local source_text = duffle.read_file(source_path)
+ local components = find_component_atoms(source_text)
+ if #components > 0 then
+ emit_component_macros_h(source_path, components)
+ end
end
- -- Write project-level summary.
- local summary_path = dirname(args[2]) .. "/gen/annotation_validation.txt"
+ -- Write project-level summary. Goes to build/gen/ (reporting cruft),
+ -- not the source dir.
+ local summary_path = dirname(args[2]) .. "/../../build/gen/annotation_validation.txt"
ensure_dir(dirname(summary_path))
write_file(summary_path, render_project_report(all_results))
print(string.format("[summary] %s\n", summary_path))