From 0ad609e7c2555e00a42457b927454c1087465a06 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Thu, 9 Jul 2026 15:38:03 -0400 Subject: [PATCH] cookin --- code/duffle/gen/duffle.macs.h | 119 ++++++ ...lottes_tape.offsets.h => duffle.offsets.h} | 0 code/duffle/lottes_tape.h | 162 +++++--- code/duffle/memory.h | 1 + ...gte_tape.offsets.h => gte_hello.offsets.h} | 12 +- code/gte_hello/hello_gte.c | 10 +- code/gte_hello/hello_gte_tape.c | 27 +- code/gte_hello/tape_atom.metadata.h | 16 +- scripts/tape_atom.offset_gen.meta.lua | 5 +- scripts/tape_atom_annotation_pass.lua | 379 +++++++++++++++++- 10 files changed, 638 insertions(+), 93 deletions(-) create mode 100644 code/duffle/gen/duffle.macs.h rename code/duffle/gen/{lottes_tape.offsets.h => duffle.offsets.h} (100%) rename code/gte_hello/gen/{hello_gte_tape.offsets.h => gte_hello.offsets.h} (65%) 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))