mirror of
https://github.com/Ed94/pikuma_ps1.git
synced 2026-09-22 17:20:04 +00:00
Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
57fdb9e037 | ||
|
|
b5953a723b | ||
|
|
888ffce859 | ||
|
|
7289e7c89c | ||
|
|
54a5bb9a31 | ||
|
|
e0f4ac873d | ||
|
|
f17fa9165e | ||
|
|
8282f8e902 | ||
|
|
9eb696ece8 |
@@ -15,6 +15,10 @@
|
|||||||
* These do NOT yield. They are expanded inline inside Tape Atoms.
|
* These do NOT yield. They are expanded inline inside Tape Atoms.
|
||||||
* ---------------------------------------------------------------------------*/
|
* ---------------------------------------------------------------------------*/
|
||||||
// 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).
|
||||||
#define mac_yield(...) \
|
#define mac_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)) \
|
||||||
@@ -22,6 +26,18 @@
|
|||||||
, nop
|
, nop
|
||||||
WORD_COUNT(mac_yield, 4)
|
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 */
|
/* atom_dbg_skip */
|
||||||
/* Words: 3; Loads 3 S2 indices from the face array */
|
/* Words: 3; Loads 3 S2 indices from the face array */
|
||||||
#define mac_load_tri_indices(...) \
|
#define mac_load_tri_indices(...) \
|
||||||
@@ -61,7 +77,7 @@ WORD_COUNT(mac_gte_load_tri_verts, 18)
|
|||||||
, load_word( R_AT, R_T1, O_(PolyTag,code)) /* AT = old_ot_head */ \
|
, 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 */ \
|
, 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 */ \
|
, mask_upper( R_AT, R_AT, S_(PolyTag_len_bits)) /* Strip upper 8 bits (length from prev cell) → keep only low 24 */ \
|
||||||
, or( R_AT, R_AT, R_V0) /* Merge length */ \
|
, 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) */ \
|
, 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_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)) \
|
, shift_lright(R_AT, R_AT, S_(PolyTag_len_bits)) \
|
||||||
@@ -76,7 +92,7 @@ WORD_COUNT(mac_insert_ot_tag_f3, 11)
|
|||||||
, load_word( R_AT, R_T1, O_(PolyTag,code)) /* AT = old_ot_head */ \
|
, 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 */ \
|
, 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 */ \
|
, mask_upper( R_AT, R_AT, S_(PolyTag_len_bits)) /* Strip upper 8 bits (length from prev cell) → keep only low 24 */ \
|
||||||
, or( R_AT, R_AT, R_V0) /* Merge length */ \
|
, 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) */ \
|
, 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_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)) \
|
, shift_lright(R_AT, R_AT, S_(PolyTag_len_bits)) \
|
||||||
|
|||||||
+45
-96
@@ -10,10 +10,10 @@
|
|||||||
# include "gen/duffle.offsets.h"
|
# include "gen/duffle.offsets.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
typedef U4 const MipsCode;
|
typedef U4 const MipsCode; // Underlying type to mips asm words.
|
||||||
typedef Slice_(MipsCode);
|
typedef Slice_(MipsCode);
|
||||||
typedef Slice_MipsCode MipsAtom;
|
|
||||||
|
|
||||||
|
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) =
|
#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).
|
// Used for components with no args (e.g., ac_load_tri_indices) or identifier-args (hardcoded register names).
|
||||||
@@ -23,9 +23,9 @@ typedef Slice_MipsCode MipsAtom;
|
|||||||
#define MipsAtomComp_(sym) MipsCode sym [] align_(4) =
|
#define MipsAtomComp_(sym) MipsCode sym [] align_(4) =
|
||||||
|
|
||||||
// Used for components with value-args (e.g., ac_format_f3_color).
|
// Used for components with value-args (e.g., ac_format_f3_color).
|
||||||
// FI_ MipsAtom ac_X(args) MipsAtomComp_Proc_(ac_X, { body })
|
// FI_ Slice_MipsCode ac_X(args) MipsAtomComp_Proc_(ac_X, { body })
|
||||||
// expands to:
|
// expands to:
|
||||||
// FI_ MipsAtom ac_X(args) { MipsCode ac_X[] align_(4) = { body }; return slice_from_array(MipsCode, ac_X); }
|
// 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); }
|
#define MipsAtomComp_Proc_(sym, ...) { MipsCode sym [] align_(4) = __VA_ARGS__; return slice_from_array(MipsCode, sym); }
|
||||||
|
|
||||||
// Auto-generated component macros (<module>/gen/<dir>/<dir>.macs.h) are included manually by the unity build.
|
// Auto-generated component macros (<module>/gen/<dir>/<dir>.macs.h) are included manually by the unity build.
|
||||||
@@ -59,12 +59,12 @@ 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_TScratch11 = R_V1,
|
||||||
// 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_TScratch10 = R_V0,
|
// R_TScratch12 = R_A0,
|
||||||
// R_TScratch10 = R_V1,
|
// R_TScratch13 = R_A1,
|
||||||
// R_TScratch11 = R_A0,
|
// R_TScratch14 = R_A3,
|
||||||
// R_TScratch12 = R_A1,
|
|
||||||
// R_TScratch13 = R_A3,
|
|
||||||
// TODO(Ed): Review S0-S7, they are technically avaialble, we just have to snapshot them at the ABI boundary.
|
// 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): 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)
|
// TODO(Ed): Sort out the other available registers... (Not sure how much is left avail)
|
||||||
@@ -74,31 +74,27 @@ enum {
|
|||||||
/* ---------------------------------------------------------------------------
|
/* ---------------------------------------------------------------------------
|
||||||
* TAPE DRIVE ABI & REGISTER ALIASES (the enum moved earlier; see below)
|
* TAPE DRIVE ABI & REGISTER ALIASES (the enum moved earlier; see below)
|
||||||
* ---------------------------------------------------------------------------*/
|
* ---------------------------------------------------------------------------*/
|
||||||
|
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): 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?
|
||||||
/* Generalized Tape Engine Runner */
|
/* Generalized Tape Engine Runner */
|
||||||
FI_ void tape_run(Slice_MipsCode 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(
|
||||||
// Removed, not needed
|
|
||||||
/*add_ui( R_SP, R_SP, -MipsStackAlignment)*/ /* Allocate stack space */
|
|
||||||
/*, store_word( R_RA, R_SP, 0)*/ /* Safely backup $ra to the stack */
|
|
||||||
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 */
|
||||||
, add_ui_self(R_TapePtr, S_(MipsCode)) /* Advance tape */
|
, add_ui_self(R_TapePtr, S_(MipsAtom)) /* Advance tape */
|
||||||
, call_reg( R_AtomJmp) /* jalr $t9 */
|
, call_reg( R_AtomJmp) /* jalr $t9 */
|
||||||
, nop /* Branch delay slot */
|
, nop /* Branch delay slot */
|
||||||
)
|
)
|
||||||
// Removed, not needed
|
|
||||||
/*, load_word( R_RA, R_SP, 0)* /* Restore $ra from stack */
|
|
||||||
/*, add_ui_self(R_SP, MipsStackAlignment)*/ /* Deallocate stack space */
|
|
||||||
asm_rpins, r_use(tape_ptr)
|
asm_rpins, r_use(tape_ptr)
|
||||||
asm_clobber:
|
asm_clobber:
|
||||||
rlit(R_AT)
|
rlit(R_AT),
|
||||||
, rlit(R_T0), rlit(R_T1), rlit(R_T2), rlit(R_T3), rlit(R_T4)
|
rlit(R_V0), rlit(R_V1),
|
||||||
, rlit(R_T5), rlit(R_T6), rlit(R_T7), rlit(R_T8)
|
rlit(R_T0), rlit(R_T1), rlit(R_T2), rlit(R_T3), rlit(R_T4),
|
||||||
, clb_mem_drain
|
rlit(R_T5), rlit(R_T6), rlit(R_T7), rlit(R_T8),
|
||||||
|
clb_mem_drain
|
||||||
); }
|
); }
|
||||||
|
|
||||||
typedef Relative_(FArena) Struct_(TapeBuilder) { U4 ptr; U4 capacity; U4 used; };
|
typedef Relative_(FArena) Struct_(TapeBuilder) { U4 ptr; U4 capacity; U4 used; };
|
||||||
@@ -106,12 +102,13 @@ FI_ void tb_init(TapeBuilder* tb, FArena* arena) { tb->ptr = arena->start
|
|||||||
FI_ TapeBuilder tb_make_old( FArena* arena) { return (TapeBuilder){ arena->start, 0 }; }
|
FI_ TapeBuilder tb_make_old( FArena* arena) { return (TapeBuilder){ arena->start, 0 }; }
|
||||||
FI_ TapeBuilder tb_make(Slice mem) { return (TapeBuilder){ mem.ptr, mem.len, 0 }; }
|
FI_ TapeBuilder tb_make(Slice mem) { return (TapeBuilder){ mem.ptr, mem.len, 0 }; }
|
||||||
|
|
||||||
#define tb_emit_(tb, atom) tb_emit(tb, atom)
|
|
||||||
FI_ void tb_emit(TapeBuilder* tb, MipsCode* atom) { u4_r(tb->ptr)[tb->used] = u4_(atom); ++ tb->used; }
|
FI_ void tb_emit(TapeBuilder* tb, MipsCode* atom) { u4_r(tb->ptr)[tb->used] = u4_(atom); ++ tb->used; }
|
||||||
FI_ void tb_data(TapeBuilder* tb, U4 data) { u4_r(tb->ptr)[tb->used] = u4_(data); ++ tb->used; }
|
FI_ void tb_data(TapeBuilder* tb, U4 data) { u4_r(tb->ptr)[tb->used] = u4_(data); ++ tb->used; }
|
||||||
|
#define tb_emit_(atom) tb_emit(& tb, atom)
|
||||||
|
#define tb_data_(field, data) tb_data(& tb, u4_(data))
|
||||||
|
|
||||||
FI_ Slice_MipsCode tb_end (TapeBuilder* tb) { tb_emit(tb,tape_exit); return (Slice_MipsCode){ C_(U4*,tb->ptr), tb->used }; }
|
FI_ Tape tb_end (TapeBuilder* tb) { tb_emit(tb,tape_exit); return (Tape){ C_(U4*,tb->ptr), tb->used }; }
|
||||||
FI_ Slice_MipsCode tb_slice(TapeBuilder tb) { return (Slice_MipsCode){ C_(U4*,tb.ptr), tb.used }; }
|
FI_ Tape tb_slice(TapeBuilder tb) { return (Tape){ C_(U4*,tb.ptr), tb.used }; }
|
||||||
#define tb_scope(tb) for(U4 tbs_once=0;tbs_once==0;++tbs_once,tb_emit(tb,tape_exit))
|
#define tb_scope(tb) for(U4 tbs_once=0;tbs_once==0;++tbs_once,tb_emit(tb,tape_exit))
|
||||||
|
|
||||||
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])); }
|
||||||
@@ -125,13 +122,27 @@ FI_ void tb_scope_run_end(TapeBuilder* tb) { tb_emit(tb,tape_exit); tape_run(tb_
|
|||||||
* These do NOT yield. They are expanded inline inside Tape Atoms.
|
* These do NOT yield. They are expanded inline inside Tape Atoms.
|
||||||
* ---------------------------------------------------------------------------*/
|
* ---------------------------------------------------------------------------*/
|
||||||
|
|
||||||
// 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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
atom_dbg_skip MipsAtomComp_(ac_yield_load) {
|
||||||
|
load_word(R_AtomJmp, R_TapePtr, 0),
|
||||||
|
};
|
||||||
|
|
||||||
|
atom_dbg_skip MipsAtomComp_(ac_yield_tail) {
|
||||||
|
add_ui_self(R_TapePtr, S_(MipsCode)),
|
||||||
|
jump_reg( R_AtomJmp), nop,
|
||||||
|
};
|
||||||
|
|
||||||
enum {
|
enum {
|
||||||
R_PrimCursor = R_T7 atom_reg atom_type(U4*), /* VRAM output cursor (primitive buffer) */
|
R_PrimCursor = R_T7 atom_reg atom_type(U4*), /* VRAM output cursor (primitive buffer) */
|
||||||
R_FaceCursor = R_T4 atom_reg atom_type(V4_S2*), /* Cube face-index cursor (V4_S2*); floor context switches to V3_S2* via atom_phase */
|
R_FaceCursor = R_T4 atom_reg atom_type(V4_S2*), /* Cube face-index cursor (V4_S2*); floor context switches to V3_S2* via atom_phase */
|
||||||
@@ -165,7 +176,7 @@ MipsAtomComp_(ac_insert_ot_tag_f3) {
|
|||||||
load_word( R_AT, R_T1, O_(PolyTag,code)), // AT = old_ot_head
|
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
|
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
|
mask_upper( R_AT, R_AT, S_(PolyTag_len_bits)), // Strip upper 8 bits (length from prev cell) → keep only low 24
|
||||||
or( R_AT, R_AT, R_V0), // Merge length
|
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)
|
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_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)),
|
shift_lright(R_AT, R_AT, S_(PolyTag_len_bits)),
|
||||||
@@ -180,7 +191,7 @@ MipsAtomComp_(ac_insert_ot_tag_g4) {
|
|||||||
load_word( R_AT, R_T1, O_(PolyTag,code)), // AT = old_ot_head
|
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
|
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
|
mask_upper( R_AT, R_AT, S_(PolyTag_len_bits)), // Strip upper 8 bits (length from prev cell) → keep only low 24
|
||||||
or( R_AT, R_AT, R_V0), // Merge length
|
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)
|
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_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)),
|
shift_lright(R_AT, R_AT, S_(PolyTag_len_bits)),
|
||||||
@@ -189,7 +200,7 @@ MipsAtomComp_(ac_insert_ot_tag_g4) {
|
|||||||
|
|
||||||
/* Words: 3; Emits one (cmd|color) word to R_PrimCursor at the given
|
/* Words: 3; Emits one (cmd|color) word to R_PrimCursor at the given
|
||||||
* byte offset. Internal helper used by the *_format_*_color macros. */
|
* byte offset. Internal helper used by the *_format_*_color macros. */
|
||||||
FI_ MipsAtom ac_pack_color_word(U4 off, U4 cmd, U1 r, U1 g, U1 b)
|
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, {
|
atom_dbg_skip MipsAtomComp_Proc_(ac_pack_color_word, {
|
||||||
load_upper_i(R_AT, (cmd) << 8 | (b)),
|
load_upper_i(R_AT, (cmd) << 8 | (b)),
|
||||||
or_i_self( R_AT, ((g) << 8) | (r)),
|
or_i_self( R_AT, ((g) << 8) | (r)),
|
||||||
@@ -198,7 +209,7 @@ atom_dbg_skip MipsAtomComp_Proc_(ac_pack_color_word, {
|
|||||||
|
|
||||||
/* Words: 3; Emits the F3 command+color word (cmd byte | BLUE | GREEN | RED)
|
/* 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). */
|
* Args: _r, _g, _b are 8-bit RGB byte values (not raw 16-bit fields). */
|
||||||
FI_ MipsAtom ac_format_f3_color(U1 r, U1 g, U1 b)
|
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) })
|
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.
|
/* Words: 3; Stores the 3 transformed (V2_S2 screen) vertices to the F3.
|
||||||
@@ -211,7 +222,7 @@ atom_dbg_skip MipsAtomComp_(ac_gte_store_f3) {
|
|||||||
|
|
||||||
/* Words: 12; Emits the four (code|color) words of a Poly_G4.
|
/* 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. */
|
* Args: rN,gN,bN are 8-bit RGB byte values for each of the 4 vertices. */
|
||||||
FI_ MipsAtom ac_format_g4_color(
|
FI_ Slice_MipsCode ac_format_g4_color(
|
||||||
U1 r0, U1 g0, U1 b0,
|
U1 r0, U1 g0, U1 b0,
|
||||||
U1 r1, U1 g1, U1 b1,
|
U1 r1, U1 g1, U1 b1,
|
||||||
U1 r2, U1 g2, U1 b2,
|
U1 r2, U1 g2, U1 b2,
|
||||||
@@ -265,7 +276,7 @@ FI_ void atombuilder_end(MipsAtomBuilder_R ab) {
|
|||||||
mem_bump(ab->start, ab->capacity, & ab->used, S_(ac_yield));
|
mem_bump(ab->start, ab->capacity, & ab->used, S_(ac_yield));
|
||||||
}
|
}
|
||||||
|
|
||||||
#define mipsatom_from_builder(ab) (MipsAtom){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
|
||||||
|
|
||||||
@@ -319,66 +330,4 @@ internal MipsAtom_(set_gte_world) atom_info(
|
|||||||
mac_yield()
|
mac_yield()
|
||||||
};
|
};
|
||||||
|
|
||||||
/* 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()
|
|
||||||
};
|
|
||||||
|
|
||||||
#pragma endregion Baked Mips Atoms
|
#pragma endregion Baked Mips Atoms
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ enum {
|
|||||||
v3s2_byteoff = 3, // log2(8), used with shift_left_logical op for index via byte offset.
|
v3s2_byteoff = 3, // log2(8), used with shift_left_logical op for index via byte offset.
|
||||||
};
|
};
|
||||||
|
|
||||||
|
typedef Array_(U1, 2);
|
||||||
typedef Array_(U4, 2);
|
typedef Array_(U4, 2);
|
||||||
typedef Array_(S2, 2);
|
typedef Array_(S2, 2);
|
||||||
typedef Array_(S2, 3);
|
typedef Array_(S2, 3);
|
||||||
@@ -22,6 +23,7 @@ typedef S2 A3x3_S2[3][3];
|
|||||||
typedef Struct_(Extent2_S2) { S2 width; S2 height; };
|
typedef Struct_(Extent2_S2) { S2 width; S2 height; };
|
||||||
typedef Struct_(Extent2_S4) { S4 width; S4 height; };
|
typedef Struct_(Extent2_S4) { S4 width; S4 height; };
|
||||||
|
|
||||||
|
typedef Struct_(V2_U1) { U1 x; U1 y; };
|
||||||
typedef Struct_(V2_S2) { S2 x; S2 y; };
|
typedef Struct_(V2_S2) { S2 x; S2 y; };
|
||||||
typedef Struct_(V2_S4) { S4 x; S4 y; };
|
typedef Struct_(V2_S4) { S4 x; S4 y; };
|
||||||
typedef Struct_(V3_S2) { S2 x; S2 y; S2 z; S2 pad; };
|
typedef Struct_(V3_S2) { S2 x; S2 y; S2 z; S2 pad; };
|
||||||
|
|||||||
@@ -67,8 +67,8 @@ typedef Slice_(B1);
|
|||||||
#define slice_end(slice) ((slice).ptr + (slice).len)
|
#define slice_end(slice) ((slice).ptr + (slice).len)
|
||||||
#define S_slice(s) ((s).len * S_((s).ptr[0]))
|
#define S_slice(s) ((s).len * S_((s).ptr[0]))
|
||||||
|
|
||||||
#define slice_ut(ptr,len) slice_ut_(u4_(ptr), u4_(len))
|
#define slice_ut(ptr,len) slice_ut_(u4_(ptr), u4_(len))
|
||||||
#define slice_ut_arr(a) slice_ut_(u4_(a), S_(a))
|
#define slice_ut_arr(a) slice_ut_(u4_(a), S_(a))
|
||||||
#define slice_to_ut(s) slice_ut_(u4_((s).ptr), S_slice(s))
|
#define slice_to_ut(s) slice_ut_(u4_((s).ptr), S_slice(s))
|
||||||
|
|
||||||
#define slice_iter(container, iter) (T_((container).ptr) iter = (container).ptr; iter != slice_end(container); ++ iter)
|
#define slice_iter(container, iter) (T_((container).ptr) iter = (container).ptr; iter != slice_end(container); ++ iter)
|
||||||
|
|||||||
+24
-6
@@ -336,10 +336,10 @@ enum { _BitOffsets = 0
|
|||||||
|
|
||||||
/* Logic Opcodes */
|
/* Logic Opcodes */
|
||||||
|
|
||||||
#define and(rd, rs, rt) enc_r(op_special, (rs), (rt), (rd), 0, fc_and)
|
#define and_u(rd, rs, rt) enc_r(op_special, (rs), (rt), (rd), 0, fc_and)
|
||||||
#define or(rd, rs, rt) enc_r(op_special, (rs), (rt), (rd), 0, fc_or)
|
#define or_u(rd, rs, rt) enc_r(op_special, (rs), (rt), (rd), 0, fc_or)
|
||||||
#define xor(rd, rs, rt) enc_r(op_special, (rs), (rt), (rd), 0, fc_xor)
|
#define xor_u(rd, rs, rt) enc_r(op_special, (rs), (rt), (rd), 0, fc_xor)
|
||||||
#define nor(rd, rs, rt) enc_r(op_special, (rs), (rt), (rd), 0, fc_nor)
|
#define nor_u(rd, rs, rt) enc_r(op_special, (rs), (rt), (rd), 0, fc_nor)
|
||||||
|
|
||||||
#define or_u_self(rd_rs, rt) enc_r(op_special, (rd_rs), (rt), (rd_rs), 0, fc_or)
|
#define or_u_self(rd_rs, rt) enc_r(op_special, (rd_rs), (rt), (rd_rs), 0, fc_or)
|
||||||
|
|
||||||
@@ -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) --- */
|
||||||
|
|||||||
+54
-19
@@ -3,24 +3,26 @@
|
|||||||
# include "dsl.h"
|
# include "dsl.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
/* PSX button bit positions — 1:1 with PSX-SPX docs at docs/psx-spx/docs/controllersandmemorycards.md:405-421.
|
||||||
|
* Wire is active-low (0 = pressed).
|
||||||
|
* The decoder atom computes buttons = (~raw_buttons) & 0xFFFF; the active-low-to-active-high inversion is applied bit-by-bit. */
|
||||||
enum {
|
enum {
|
||||||
Bit_(Pad_L2, 0),
|
Bit_(Pad_Select, 0),
|
||||||
Bit_(Pad_R2, 1),
|
Bit_(Pad_L3, 1),
|
||||||
Bit_(Pad_L1, 2),
|
Bit_(Pad_R3, 2),
|
||||||
Bit_(Pad_R1, 3),
|
Bit_(Pad_Start, 3),
|
||||||
Bit_(Pad_Triangle, 4),
|
Bit_(Pad_Up, 4),
|
||||||
Bit_(Pad_Circle, 5),
|
Bit_(Pad_Right, 5),
|
||||||
Bit_(Pad_Cross, 6),
|
Bit_(Pad_Down, 6),
|
||||||
Bit_(Pad_Square, 7),
|
Bit_(Pad_Left, 7),
|
||||||
Bit_(Pad_Select, 8),
|
Bit_(Pad_L2, 8),
|
||||||
Bit_(Unused_PadI, 9),
|
Bit_(Pad_R2, 9),
|
||||||
Bit_(Unused_PadJ, 10),
|
Bit_(Pad_L1, 10),
|
||||||
Bit_(Pad_Start, 11),
|
Bit_(Pad_R1, 11),
|
||||||
Bit_(Pad_Up, 12),
|
Bit_(Pad_Triangle, 12),
|
||||||
Bit_(Pad_Right, 13),
|
Bit_(Pad_Circle, 13),
|
||||||
Bit_(Pad_Down, 14),
|
Bit_(Pad_Cross, 14),
|
||||||
Bit_(Pad_Left, 15),
|
Bit_(Pad_Square, 15),
|
||||||
};
|
};
|
||||||
|
|
||||||
enum {
|
enum {
|
||||||
@@ -33,6 +35,39 @@ enum {
|
|||||||
#define pad0_(btn_id) (btn_id << Pad0)
|
#define pad0_(btn_id) (btn_id << Pad0)
|
||||||
#define pad1_(btn_id) (btn_id << Pad1)
|
#define pad1_(btn_id) (btn_id << Pad1)
|
||||||
|
|
||||||
void pad_init(U4 mode) asm("PadInit");
|
/* ============================================================
|
||||||
U4 pad_read(U4 id) asm("PadRead");
|
* BIOS pad-buffer subsystem: docs/psx-spx/docs/kernelbios.md (B(12h) + B(13h))
|
||||||
|
* ============================================================ */
|
||||||
|
|
||||||
|
enum {
|
||||||
|
PAD_BIOS_RAW_SIZE = 0x22,
|
||||||
|
};
|
||||||
|
typedef Struct_(PadBiosRaw) {
|
||||||
|
U1 bytes[PAD_BIOS_RAW_SIZE];
|
||||||
|
};
|
||||||
|
|
||||||
|
typedef Enum_(U4, PadStatus) {
|
||||||
|
PadStatus_Disconnected,
|
||||||
|
PadStatus_Digital,
|
||||||
|
PadStatus_AnalogStick,
|
||||||
|
PadStatus_AnalogPad,
|
||||||
|
PadStatus_Unsupported,
|
||||||
|
PadStatus_Pending,
|
||||||
|
PadStatus_Invalid,
|
||||||
|
};
|
||||||
|
|
||||||
|
/* PadState — per-port normalized runtime state.
|
||||||
|
* Field order is chosen so that the 4 axes (left_x, left_y, right_x, right_y)
|
||||||
|
* form a contiguous 4-byte block at offset 8, allowing a single `store_word` to clear-or-write all 4 axes in one MIPS instruction.
|
||||||
|
* The struct size stays 12 bytes (unchanged from the prior order,
|
||||||
|
* which left the C compiler to insert 1 byte of trailing pad to reach the 4-byte struct alignment). */
|
||||||
|
typedef Struct_(PadState) {
|
||||||
|
PadStatus status; /* offset 0, size 4 (U4) */
|
||||||
|
U2 buttons; /* offset 4, size 2 */
|
||||||
|
U1 id; /* offset 6, size 1 */
|
||||||
|
U1 pad; /* offset 7, size 1 — explicit pad to align the axes block */
|
||||||
|
U1 left_x; /* offset 8, size 1 — store_word target (4-byte aligned) */
|
||||||
|
U1 left_y; /* offset 9, size 1 */
|
||||||
|
U1 right_x; /* offset 10, size 1 */
|
||||||
|
U1 right_y; /* offset 11, size 1 */
|
||||||
|
};
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ WORD_COUNT(call_reg, 1)
|
|||||||
WORD_COUNT(call_addr, 1)
|
WORD_COUNT(call_addr, 1)
|
||||||
WORD_COUNT(branch_le_zero, 1)
|
WORD_COUNT(branch_le_zero, 1)
|
||||||
WORD_COUNT(branch_equal, 1)
|
WORD_COUNT(branch_equal, 1)
|
||||||
|
WORD_COUNT(branch_ne, 1)
|
||||||
WORD_COUNT(add_ui, 1)
|
WORD_COUNT(add_ui, 1)
|
||||||
WORD_COUNT(set_lt_u, 1)
|
WORD_COUNT(set_lt_u, 1)
|
||||||
WORD_COUNT(set_lt_s, 1)
|
WORD_COUNT(set_lt_s, 1)
|
||||||
@@ -29,7 +30,9 @@ WORD_COUNT(set_lt_si, 1)
|
|||||||
WORD_COUNT(set_lt_ui, 1)
|
WORD_COUNT(set_lt_ui, 1)
|
||||||
WORD_COUNT(load_word, 1)
|
WORD_COUNT(load_word, 1)
|
||||||
WORD_COUNT(load_half_u, 1)
|
WORD_COUNT(load_half_u, 1)
|
||||||
|
WORD_COUNT(load_byte_u, 1)
|
||||||
WORD_COUNT(store_word, 1)
|
WORD_COUNT(store_word, 1)
|
||||||
|
WORD_COUNT(store_byte, 1)
|
||||||
WORD_COUNT(add_ui_self, 1)
|
WORD_COUNT(add_ui_self, 1)
|
||||||
WORD_COUNT(add_u_self, 1)
|
WORD_COUNT(add_u_self, 1)
|
||||||
WORD_COUNT(add_u, 1)
|
WORD_COUNT(add_u, 1)
|
||||||
@@ -37,6 +40,7 @@ WORD_COUNT(or_i, 1)
|
|||||||
WORD_COUNT(or_i_self, 1)
|
WORD_COUNT(or_i_self, 1)
|
||||||
WORD_COUNT(or_u, 1)
|
WORD_COUNT(or_u, 1)
|
||||||
WORD_COUNT(or_u_self, 1)
|
WORD_COUNT(or_u_self, 1)
|
||||||
|
WORD_COUNT(nor_u, 1)
|
||||||
WORD_COUNT(shift_lleft, 1)
|
WORD_COUNT(shift_lleft, 1)
|
||||||
WORD_COUNT(shift_lleft_self, 1)
|
WORD_COUNT(shift_lleft_self, 1)
|
||||||
WORD_COUNT(shift_lright, 1)
|
WORD_COUNT(shift_lright, 1)
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ global SMemory smem;
|
|||||||
extern SMemory smem;
|
extern SMemory smem;
|
||||||
|
|
||||||
// TODO(Ed):
|
// TODO(Ed):
|
||||||
FI_ U4* spad_warm(MipsAtom atom) {
|
FI_ U4* spad_warm(Slice_MipsCode atom) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,68 @@
|
|||||||
|
|
||||||
#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()
|
||||||
|
};
|
||||||
|
|
||||||
typedef Struct_(Binds_CubeTri) {
|
typedef Struct_(Binds_CubeTri) {
|
||||||
U4 PrimCursor;
|
U4 PrimCursor;
|
||||||
V4_S2* FaceCursor;
|
V4_S2* FaceCursor;
|
||||||
@@ -63,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(
|
||||||
@@ -122,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,
|
||||||
|
|||||||
@@ -9,6 +9,12 @@
|
|||||||
#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 */
|
/* atom_dbg_skip */
|
||||||
#define mac_store_v2s2(rt_x, rt_y, base, offset) \
|
#define mac_store_v2s2(rt_x, rt_y, base, offset) \
|
||||||
store_half(rt_x, base, offset + O_(V2_S2,x)) \
|
store_half(rt_x, base, offset + O_(V2_S2,x)) \
|
||||||
|
|||||||
@@ -5,19 +5,9 @@
|
|||||||
#pragma region hello_joypad.tape
|
#pragma region hello_joypad.tape
|
||||||
|
|
||||||
|
|
||||||
// --- atom: pad_input_demo (24 words) ---
|
// --- atom: cube_g4_face (76 words) ---
|
||||||
|
|
||||||
#define _atom_offset_pad_left_exit_pad_left 6
|
#define _atom_offset_cull_cube_g4_face_exit 41
|
||||||
#define _atom_offset_pad_right_exit_pad_right 6
|
|
||||||
|
|
||||||
enum {
|
|
||||||
atom_offset_pad_left_exit_pad_left = _atom_offset_pad_left_exit_pad_left,
|
|
||||||
atom_offset_pad_right_exit_pad_right = _atom_offset_pad_right_exit_pad_right,
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- atom: cube_g4_face (77 words) ---
|
|
||||||
|
|
||||||
#define _atom_offset_cull_cube_g4_face_exit 42
|
|
||||||
#define _atom_offset_bounds_chk_cube_g4_face_exit 24
|
#define _atom_offset_bounds_chk_cube_g4_face_exit 24
|
||||||
|
|
||||||
enum {
|
enum {
|
||||||
@@ -35,5 +25,49 @@ enum {
|
|||||||
atom_offset_bounds_chk_floor_f3_face_exit = _atom_offset_bounds_chk_floor_f3_face_exit,
|
atom_offset_bounds_chk_floor_f3_face_exit = _atom_offset_bounds_chk_floor_f3_face_exit,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --- atom: pad_bios_snapshot (77 words) ---
|
||||||
|
|
||||||
|
#define _atom_offset_snap_root_skip_disconnected 8
|
||||||
|
#define _atom_offset_disconnected_snap_end 60
|
||||||
|
#define _atom_offset_case_2_id_dispatch 8
|
||||||
|
#define _atom_offset_pending_snap_end 50
|
||||||
|
#define _atom_offset_id_dispatch_try_analog_stick 11
|
||||||
|
#define _atom_offset_id_dispatch_snap_end 37
|
||||||
|
#define _atom_offset_try_analog_stick_try_analog_pad 12
|
||||||
|
#define _atom_offset_analog_stick_snap_end 23
|
||||||
|
#define _atom_offset_try_analog_pad_try_unsupported 11
|
||||||
|
#define _atom_offset_analog_pad_snap_end 9
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- atom: pad_apply_input (59 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 23
|
||||||
|
#define _atom_offset_end_low_exit_stick 11
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
#pragma endregion hello_joypad.tape
|
#pragma endregion hello_joypad.tape
|
||||||
|
|
||||||
|
|||||||
@@ -111,34 +111,26 @@ enum {
|
|||||||
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;
|
||||||
Ent_Floor floor;
|
Ent_Floor floor;
|
||||||
|
|
||||||
U4 pad_state;
|
PadBiosRaw pad_raw[2];
|
||||||
|
PadState pad[2];
|
||||||
|
|
||||||
U4_V scratchpad; // d-cache
|
U4_V scratchpad; // d-cache
|
||||||
};
|
};
|
||||||
global SMemory smem;
|
global SMemory smem;
|
||||||
extern SMemory smem;
|
extern SMemory smem;
|
||||||
|
|
||||||
#define pad0_signal_(btn_id) smem.pad_state & pad0_(btn_id)
|
|
||||||
#define pad1_signal_(btn_id) smem.pad_state & pad1_(btn_id)
|
|
||||||
|
|
||||||
|
|
||||||
// TODO(Ed):
|
|
||||||
FI_ U4* spad_warm(MipsAtom atom) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
I_ B1* prim__alloc(U4 type_width, Str8 type_name) {
|
I_ B1* prim__alloc(U4 type_width, Str8 type_name) {
|
||||||
gknown PrimitiveArena* pa = & smem.primitives;
|
gknown PrimitiveArena* pa = & smem.primitives;
|
||||||
gknown B1* buf = (B1*) r_(smem.primitives.buf)[smem.active_buf_id];
|
gknown B1* buf = (B1*) r_(smem.primitives.buf)[smem.active_buf_id];
|
||||||
@@ -149,6 +141,77 @@ I_ B1* prim__alloc(U4 type_width, Str8 type_name) {
|
|||||||
}
|
}
|
||||||
#define prim_alloc(type) (type*)prim__alloc(S_(type), slit( stringify(type)))
|
#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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void gp_screen_init_c11(DoubleBuffer* screen_buf, S4* active_buf_id)
|
void gp_screen_init_c11(DoubleBuffer* screen_buf, S4* active_buf_id)
|
||||||
{
|
{
|
||||||
reset_graph(0);
|
reset_graph(0);
|
||||||
@@ -193,23 +256,19 @@ void gp_display_frame(DoubleBuffer* screen_buf, S4* active_buf_id, U4* ordering_
|
|||||||
active_buf_id[0] = ! active_buf_id[0]; // Swap current buffer
|
active_buf_id[0] = ! active_buf_id[0]; // Swap current buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
void render(void) {
|
|
||||||
}
|
|
||||||
|
|
||||||
GCC_OPTIMIZATION_DISABLE
|
GCC_OPTIMIZATION_DISABLE
|
||||||
void update(PrimitiveArena* pa, U4* ordering_buf)
|
void update(PrimitiveArena* pa, U4* ordering_buf)
|
||||||
{
|
{
|
||||||
TapeBuilder tb = tb_make(slice_ut_arr(smem.MemTape));
|
TapeBuilder tb = tb_make(slice_ut_arr(smem.MemTape));
|
||||||
|
|
||||||
smem.pad_state = pad_read(0);
|
if (0) // Pad Input (dead — kept for the source-as-written record; references the deleted `pad_state` field)
|
||||||
|
|
||||||
if (0) // Pad Input
|
|
||||||
{
|
{
|
||||||
if (pad0_signal_(Pad_Left)) {
|
(void)Pad_Left; (void)Pad_Right; /* suppress unused-token warnings */
|
||||||
|
if (false) {
|
||||||
smem.cube.rot.y += 30;
|
smem.cube.rot.y += 30;
|
||||||
smem.floor.rot.y += 5;
|
smem.floor.rot.y += 5;
|
||||||
}
|
}
|
||||||
if (pad0_signal_(Pad_Right)) {
|
if (false) {
|
||||||
smem.cube.rot.y -= 30;
|
smem.cube.rot.y -= 30;
|
||||||
smem.floor.rot.y -= 5;
|
smem.floor.rot.y -= 5;
|
||||||
}
|
}
|
||||||
@@ -217,10 +276,18 @@ void update(PrimitiveArena* pa, U4* ordering_buf)
|
|||||||
if (1) // Pad Input (Tape version)
|
if (1) // Pad Input (Tape version)
|
||||||
{
|
{
|
||||||
tb.used = 0; tb_scope_run(& tb) {
|
tb.used = 0; tb_scope_run(& tb) {
|
||||||
tb_emit(& tb, pad_input_demo);
|
/* BIOS-owned polling: per-frame snapshot of both ports. */
|
||||||
tb_data(& tb, smem.pad_state);
|
tb_emit_(pad_bios_snapshot);
|
||||||
tb_data(& tb, u4_(& smem.cube.rot));
|
tb_data_(raw, & smem.pad_raw[0]);
|
||||||
tb_data(& tb, u4_(& smem.floor.rot));
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -411,25 +478,12 @@ 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) {
|
||||||
|
}
|
||||||
|
|
||||||
int main(void)
|
int main(void)
|
||||||
{
|
{
|
||||||
smem = (SMemory){0};
|
smem = (SMemory){0};
|
||||||
@@ -440,7 +494,6 @@ int main(void)
|
|||||||
ent_cube128_init(& smem.cube.verts, & smem.cube.faces); {
|
ent_cube128_init(& smem.cube.verts, & smem.cube.faces); {
|
||||||
Ent_Cube* cube = & smem.cube;
|
Ent_Cube* cube = & smem.cube;
|
||||||
cube->rot = v3s2(0, 0, 0);
|
cube->rot = v3s2(0, 0, 0);
|
||||||
// cube->pos = v3s4(0, 0, 900);
|
|
||||||
cube->scale = v3s4_fp_one();
|
cube->scale = v3s4_fp_one();
|
||||||
cube->accel = v3s4(0, 1, 0);
|
cube->accel = v3s4(0, 1, 0);
|
||||||
cube->pos = v3s4(0, -400, 1800);
|
cube->pos = v3s4(0, -400, 1800);
|
||||||
@@ -454,13 +507,15 @@ int main(void)
|
|||||||
}
|
}
|
||||||
TapeBuilder tb = tb_make(slice_ut_arr(smem.MemTape)); {
|
TapeBuilder tb = tb_make(slice_ut_arr(smem.MemTape)); {
|
||||||
reset_graph(0);
|
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 U4* io_base_addr rgcc(R_IO_BaseAddr) = u4_r(IO_BASE_ADDR);
|
||||||
register DoubleBuffer* screen_buf rgcc(R_ScreenBuf) = & smem.screen_buf;
|
register DoubleBuffer* screen_buf rgcc(R_ScreenBuf) = & smem.screen_buf;
|
||||||
tb.used = 0; tb_scope_run(& tb) {
|
tb.used = 0; tb_scope_run(& tb) {
|
||||||
tb_emit(& tb, screen_env_init);
|
tb_emit(& tb, screen_env_init);
|
||||||
tb_emit(& tb, gp_screen_init);
|
tb_emit(& tb, gp_screen_init);
|
||||||
}
|
}
|
||||||
pad_init(0);
|
|
||||||
}
|
}
|
||||||
while (1) {
|
while (1) {
|
||||||
gknown S4* active_buf_id = & smem.active_buf_id;
|
gknown S4* active_buf_id = & smem.active_buf_id;
|
||||||
|
|||||||
@@ -3,18 +3,23 @@
|
|||||||
# include "duffle/dsl.h"
|
# include "duffle/dsl.h"
|
||||||
# include "duffle/math.h"
|
# include "duffle/math.h"
|
||||||
# include "duffle/gp.h"
|
# include "duffle/gp.h"
|
||||||
|
# include "duffle/pad.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
enum {
|
enum {
|
||||||
PrimitiveBuff_Len = 4096,
|
// PrimitiveBuff_Len = 4096,
|
||||||
OrderingTbl_Len = 2048
|
// OrderingTbl_Len = 2048,
|
||||||
|
PrimitiveBuff_Len = 131072,
|
||||||
|
OrderingTbl_Len = 8192,
|
||||||
};
|
};
|
||||||
|
|
||||||
#define ScreenRes_X 320
|
enum {
|
||||||
#define ScreenRes_Y 240
|
ScreenRes_X = 320,
|
||||||
#define ScreenZ 320
|
ScreenRes_Y = 240,
|
||||||
#define ScreenRes_CenterX (ScreenRes_X >> 1)
|
ScreenZ = 320,
|
||||||
#define ScreenRes_CenterY (ScreenRes_Y >> 1)
|
ScreenRes_CenterX = (ScreenRes_X >> 1),
|
||||||
|
ScreenRes_CenterY = (ScreenRes_Y >> 1),
|
||||||
|
};
|
||||||
|
|
||||||
enum {
|
enum {
|
||||||
fp_one = (1 << 12),
|
fp_one = (1 << 12),
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
# include "duffle/gen/duffle.macs.h"
|
# include "duffle/gen/duffle.macs.h"
|
||||||
# include "duffle/gen/duffle.offsets.h"
|
# include "duffle/gen/duffle.offsets.h"
|
||||||
# include "duffle/atom_dsl.h"
|
# include "duffle/atom_dsl.h"
|
||||||
# include "duffle/pad.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/word_count.metadata.h"
|
# include "duffle/word_count.metadata.h"
|
||||||
# include "psyq.h"
|
# include "psyq.h"
|
||||||
# include "gen/hello_joypad.offsets.h"
|
# include "gen/hello_joypad.offsets.h"
|
||||||
@@ -16,33 +16,37 @@
|
|||||||
|
|
||||||
#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_ MipsAtom ac_store_v2s2(U4 rt_x, U4 rt_y, U4 base, U4 offset) atom_dbg_skip MipsAtomComp_Proc_(ac_store_v2s2, {
|
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_x, base, offset + O_(V2_S2,x)),
|
||||||
store_half(rt_y, base, offset + O_(V2_S2,y)),
|
store_half(rt_y, base, offset + O_(V2_S2,y)),
|
||||||
})
|
})
|
||||||
|
|
||||||
FI_ MipsAtom 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, {
|
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_x, base, offset + O_(Rect_S2,x)),
|
||||||
store_half(rt_y, base, offset + O_(Rect_S2,y)),
|
store_half(rt_y, base, offset + O_(Rect_S2,y)),
|
||||||
store_half(rt_width, base, offset + O_(Rect_S2,width)),
|
store_half(rt_width, base, offset + O_(Rect_S2,width)),
|
||||||
store_half(rt_height, base, offset + O_(Rect_S2,height)),
|
store_half(rt_height, base, offset + O_(Rect_S2,height)),
|
||||||
})
|
})
|
||||||
|
|
||||||
FI_ MipsAtom ac_store_rgb8(U1 rr, U1 rg, U1 rb, U4 base, U4 offset) atom_dbg_skip MipsAtomComp_Proc_(ac_store_rgb8, {
|
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(rr, base, offset + O_(DrawEnv,initial_bg_color.r)),
|
||||||
store_byte(rg, base, offset + O_(DrawEnv,initial_bg_color.g)),
|
store_byte(rg, base, offset + O_(DrawEnv,initial_bg_color.g)),
|
||||||
store_byte(rb, base, offset + O_(DrawEnv,initial_bg_color.b)),
|
store_byte(rb, base, offset + O_(DrawEnv,initial_bg_color.b)),
|
||||||
})
|
})
|
||||||
|
|
||||||
FI_ MipsAtom ac_gcmd_push(U4 cmd, U4 reg_transfer, U4 reg_base, U2 port)
|
FI_ Slice_MipsCode ac_gcmd_push(U4 cmd, U4 reg_transfer, U4 reg_base, U2 port)
|
||||||
MipsAtomComp_Proc_(ac_gcmd_push, {
|
MipsAtomComp_Proc_(ac_gcmd_push, {
|
||||||
load_upper_i(reg_transfer, cmd >> 16),
|
load_upper_i(reg_transfer, cmd >> 16),
|
||||||
or_i_self( reg_transfer, cmd & 0xFFFF),
|
or_i_self( reg_transfer, cmd & 0xFFFF),
|
||||||
store_word( reg_transfer, reg_base, port),
|
store_word( reg_transfer, reg_base, port),
|
||||||
})
|
})
|
||||||
|
|
||||||
FI_ MipsAtom 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)).
|
||||||
// Sequence per libpsyx PutDispEnv: DrawArea TL → DrawArea BR → Mask → DrawArea TL → DrawArea BR
|
// Sequence per libpsyx PutDispEnv: DrawArea TL → DrawArea BR → Mask → DrawArea TL → DrawArea BR
|
||||||
@@ -53,7 +57,7 @@ MipsAtomComp_Proc_(ac_put_disp_env, {
|
|||||||
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),
|
||||||
})
|
})
|
||||||
|
|
||||||
FI_ MipsAtom ac_put_draw_env(U4 reg_transfer, U4 reg_base, U2 port)
|
FI_ Slice_MipsCode ac_put_draw_env(U4 reg_transfer, U4 reg_base, U2 port)
|
||||||
MipsAtomComp_Proc_(ac_put_draw_env, {
|
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.
|
* ORIGIN: each code word corresponds to the EXACT value libpsyx's PutDrawEnv function would compute for the same DrawEnv settings.
|
||||||
@@ -198,47 +202,6 @@ internal MipsAtom_(gp_screen_init) atom_info(atom_phase(screen_init), atom_reads
|
|||||||
mac_yield(),
|
mac_yield(),
|
||||||
};
|
};
|
||||||
|
|
||||||
enum {
|
|
||||||
R_PadState = R_T4 atom_reg atom_type(U4),
|
|
||||||
R_PadSignal = R_T0 atom_reg atom_type(U4),
|
|
||||||
R_CubeRot = R_T1 atom_reg atom_type(V3_S2*),
|
|
||||||
R_FloorRot = R_T2 atom_reg atom_type(V3_S2*),
|
|
||||||
};
|
|
||||||
typedef Struct_(Binds_PadInputDemo) {
|
|
||||||
U4 pad_state;
|
|
||||||
V3_S2* cube_rot;
|
|
||||||
V3_S2* floor_rot;
|
|
||||||
};
|
|
||||||
internal MipsAtom_(pad_input_demo) atom_info(atom_bind(Binds_PadInputDemo)
|
|
||||||
, atom_reads(R_PadState, R_CubeRot, R_FloorRot)
|
|
||||||
, atom_writes(R_CubeRot, R_FloorRot)
|
|
||||||
) {
|
|
||||||
load_word(R_PadState, R_TapePtr, O_(Binds_PadInputDemo,pad_state)),
|
|
||||||
load_word(R_CubeRot, R_TapePtr, O_(Binds_PadInputDemo,cube_rot)),
|
|
||||||
load_word(R_FloorRot, R_TapePtr, O_(Binds_PadInputDemo,floor_rot)),
|
|
||||||
add_ui_self( R_TapePtr, S_(Binds_PadInputDemo)),
|
|
||||||
|
|
||||||
and_i(R_PadSignal, R_PadState, pad0_(Pad_Left)), branch_le_zero(R_PadSignal, atom_offset(pad_left, exit_pad_left)),
|
|
||||||
load_half( R_T5, R_CubeRot, O_(V3_S2,y)), // BD-Slot occupied
|
|
||||||
load_half( R_T6, R_FloorRot, O_(V3_S2,y)),
|
|
||||||
add_si( R_T5, R_T5, 30),
|
|
||||||
add_si( R_T6, R_T6, 5),
|
|
||||||
store_half(R_T5, R_CubeRot, O_(V3_S2,y)),
|
|
||||||
store_half(R_T6, R_FloorRot, O_(V3_S2,y)),
|
|
||||||
atom_label(exit_pad_left)
|
|
||||||
|
|
||||||
and_i(R_PadSignal, R_PadState, pad0_(Pad_Right)), branch_le_zero(R_PadSignal, atom_offset(pad_right, exit_pad_right)),
|
|
||||||
load_half( R_T5, R_CubeRot, O_(V3_S2,y)), // BD-Slot occupied
|
|
||||||
load_half( R_T6, R_FloorRot, O_(V3_S2,y)),
|
|
||||||
add_si( R_T5, R_T5, -30),
|
|
||||||
add_si( R_T6, R_T6, -5),
|
|
||||||
store_half(R_T5, R_CubeRot, O_(V3_S2,y)),
|
|
||||||
store_half(R_T6, R_FloorRot, O_(V3_S2,y)),
|
|
||||||
atom_label(exit_pad_right)
|
|
||||||
|
|
||||||
mac_yield(),
|
|
||||||
};
|
|
||||||
|
|
||||||
typedef Struct_(Binds_CubeTri) {
|
typedef Struct_(Binds_CubeTri) {
|
||||||
U4 PrimCursor;
|
U4 PrimCursor;
|
||||||
V4_S2* FaceCursor;
|
V4_S2* FaceCursor;
|
||||||
@@ -274,7 +237,10 @@ MipsAtom_(cube_g4_face) atom_info(atom_phase(cube_g4),
|
|||||||
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)),
|
||||||
@@ -376,4 +342,302 @@ internal MipsAtom_(sync_primitive_arena) atom_info(atom_bind(Binds_SyncPrimitive
|
|||||||
mac_yield()
|
mac_yield()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* ----- 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(snap_end)
|
||||||
|
/* NOT mac_yield() — R_AtomJmp was already loaded in the BD-slot of the case-exit branch. */
|
||||||
|
mac_yield_tail(),
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ----- 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(exit_stick)
|
||||||
|
/* 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
|
||||||
|
|||||||
@@ -0,0 +1,659 @@
|
|||||||
|
|
||||||
|
#if 0 /* ac_pad_sio_write_pad_state — superseded by pad_bios_snapshot */
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
* raw_sio_pad_poll_20260802 — superseded by bios_pad_buffer_snapshot_20260803.
|
||||||
|
* The doomed raw-SIO production atoms (ac_pad_sio_write_pad_state,
|
||||||
|
* pad_sio_init, pad_sio_step, pad_sio_diag_pin, pad_sio_diag_byte_exchange)
|
||||||
|
* reference symbols that were removed from code/duffle/pad.h during
|
||||||
|
* Phase 1. Each is wrapped in a narrow `#if 0` so the C compile skips
|
||||||
|
* the body while the source-as-written text stays in place for the
|
||||||
|
* Phase 5.1 deletion pass. The wrap is removed (and the bodies are
|
||||||
|
* deleted) by Phase 5.1 of this track.
|
||||||
|
* ============================================================ */
|
||||||
|
|
||||||
|
* Writes the per-port PadState in 5 instructions plus 4 store_word calls (status,
|
||||||
|
* buttons, left_x/y/right_x/right_y packed, attempt). The provisional decode publishes
|
||||||
|
* 0x0000FFFF buttons + centered axes on every path until response-byte decode lands.
|
||||||
|
*
|
||||||
|
* Args:
|
||||||
|
* status_val - the PadSioStatus enum value to publish
|
||||||
|
* state_ptr_reg - the PadState* base (R_PadState at the call site)
|
||||||
|
* scratch_reg - scratch register for the value being stored (e.g., R_T0)
|
||||||
|
*
|
||||||
|
* Emits 9 instructions (status/buttons/axes/attempt stores plus the
|
||||||
|
* two-instruction zero-extended buttons load).
|
||||||
|
*/
|
||||||
|
FI_ Slice_MipsCode ac_pad_sio_write_pad_state(U4 status_val, U4 state_ptr_reg, U4 scratch_reg)
|
||||||
|
MipsAtomComp_Proc_(ac_pad_sio_write_pad_state, {
|
||||||
|
add_ui(scratch_reg, R_0, status_val),
|
||||||
|
store_word(scratch_reg, state_ptr_reg, O_(PadState,status)),
|
||||||
|
/* FIX 2026-08-02: buttons = 0x0000FFFF = "no buttons pressed" in
|
||||||
|
* libetc convention. Build it with LUI + ORI so addiu does not
|
||||||
|
* sign-extend 0xFFFF to 0xFFFFFFFF. */
|
||||||
|
load_upper_i(scratch_reg, 0x0000),
|
||||||
|
or_i(scratch_reg, scratch_reg, 0xFFFF),
|
||||||
|
store_word(scratch_reg, state_ptr_reg, O_(PadState,buttons)),
|
||||||
|
add_ui(scratch_reg, R_0, 0x80808080),
|
||||||
|
store_word(scratch_reg, state_ptr_reg, O_(PadState,left_x)),
|
||||||
|
add_ui(scratch_reg, R_0, 0),
|
||||||
|
store_word(scratch_reg, state_ptr_reg, O_(PadState,attempt))
|
||||||
|
})
|
||||||
|
#endif /* end ac_pad_sio_write_pad_state wrap */
|
||||||
|
|
||||||
|
/* ----- pad_sio_init -----
|
||||||
|
* Boot-time SIO0 init. Caller pins R_T6 = sio_base_addr0.
|
||||||
|
* Issues SIO CTRL=0x0040 (reset), MODE=0x000D, BAUD=0x0088.
|
||||||
|
* (Phase 2 fills the body.)
|
||||||
|
*/
|
||||||
|
#if 0 /* pad_sio_init — superseded by pad_bios_init_start (Phase 1.3) */
|
||||||
|
internal MipsAtom_(pad_sio_init) atom_info(atom_phase(pad_init)
|
||||||
|
, atom_reads(R_T5, R_T6)
|
||||||
|
, atom_writes(R_T5, R_T6)
|
||||||
|
) {
|
||||||
|
/* FIX 2026-08-02: explicitly load the KSEG1 base into R_T6 at the top of
|
||||||
|
* the atom body. The rgcc(R_PadSioBase) binding in main() pins R_T6 = base
|
||||||
|
* when main() runs, but $12 is caller-saved per the O32 ABI — when tape_run
|
||||||
|
* is invoked, R_T6 is fair game. The atom body cannot rely on the value. */
|
||||||
|
load_upper_i(R_T6, pad_IO_KSEG1_BASE >> 16), /* R_T6 high 16 = 0xBF80 */
|
||||||
|
or_i(R_T6, R_T6, pad_IO_KSEG1_BASE & 0xFFFF), /* R_T6 = 0xBF800000 */
|
||||||
|
|
||||||
|
/* SIO CTRL = 0x0040 (reset) */
|
||||||
|
add_ui(R_T5, R_0, pad_SIO_CTRL_RESET),
|
||||||
|
store_half(R_T5, R_T6, pad_SIO_CTRL_OFFSET),
|
||||||
|
/* SIO MODE = 0x000D (MUL1, 8-bit, no parity, idle-high) */
|
||||||
|
add_ui(R_T5, R_0, pad_SIO_MODE_INIT),
|
||||||
|
store_half(R_T5, R_T6, pad_SIO_MODE_OFFSET),
|
||||||
|
/* SIO BAUD = 0x0088 (~250 kHz) */
|
||||||
|
add_ui(R_T5, R_0, pad_SIO_BAUD_INIT),
|
||||||
|
store_half(R_T5, R_T6, pad_SIO_BAUD_OFFSET),
|
||||||
|
mac_yield(),
|
||||||
|
};
|
||||||
|
#endif /* end pad_sio_init wrap */
|
||||||
|
|
||||||
|
/* ----- pad_sio_step -----
|
||||||
|
* Per-frame bounded raw-SIO transaction. Reads PadState pointers + SIO
|
||||||
|
* base addresses from Binds_PadSioStep; writes per-port status +
|
||||||
|
* buttons + axes into smem.pad[0..1].
|
||||||
|
* Body shape (per spec §"Transaction model (per port, per pad_sio_step)"):
|
||||||
|
* port 0: CTRL=CLEANUP → settle → CTRL=port-select → settle → exchange 5
|
||||||
|
* bytes (addr + 0x42 0x00 0x00 0x00) → decode → write PadState[0]
|
||||||
|
* → CTRL=CLEANUP.
|
||||||
|
* port 1: swap scratch regs (sio_base_addr1 → R_PadSioBase, state1 →
|
||||||
|
* R_PadState) → mirror port 0 sequence.
|
||||||
|
*
|
||||||
|
* Bounded-loop semantics: every countdown is wrapped in
|
||||||
|
* add_ui_self(R_T1, -1) + branch_ne(R_T1, R_0, ...)
|
||||||
|
* with a known maximum (pad_SIO_SETTLE_BEFORE_TX=1000, pad_SIO_SETTLE_AFTER_TX=2000,
|
||||||
|
* pad_SIO_WAIT_BUDGET=4096). The static-analysis pass currently reports
|
||||||
|
* has_loops = true; the follow-up metaprogram track that learns modeled-bounded
|
||||||
|
* loops is out of scope here (per spec §"Risks").
|
||||||
|
*
|
||||||
|
* Scratch register strategy:
|
||||||
|
* R_PadStatus = R_T4 — RESERVED for port-1 swap (holds state1)
|
||||||
|
* R_PadCountdown = R_T5 — RESERVED for port-1 swap (holds sio_base_addr1)
|
||||||
|
* R_T0 — byte-exchange value + STAT read (clobbered freely)
|
||||||
|
* R_T1 — countdown budget (clobbered freely)
|
||||||
|
* R_PadState = R_T7 — PadState* (preserved for PadState writes)
|
||||||
|
* R_PadSioBase = R_T6 — SIO base (preserved through the port)
|
||||||
|
*
|
||||||
|
* Response decode (Task 3.1 teaching scope):
|
||||||
|
* - status = PadSioStatus_Digital (hardcoded)
|
||||||
|
* - buttons = 0xFFFF (no buttons pressed in the provisional libetc
|
||||||
|
* convention; full response-byte decode is follow-up)
|
||||||
|
* - axes = 0x80808080 (centered: left_x=0x80, left_y=0x80,
|
||||||
|
* right_x=0x80, right_y=0x80)
|
||||||
|
* - attempt = 0
|
||||||
|
* - DualShock handshake (0x43 0x01 → 0x44 0x01 0x03 → 0x43 0x00) is
|
||||||
|
* follow-up scope; the hardcoded digital decode is a placeholder.
|
||||||
|
*
|
||||||
|
* Both ports raise /CS (CTRL = pad_SIO_CTRL_CLEANUP) before exit. Both ports
|
||||||
|
* treat response timeout as PadSioStatus_Disconnected per the spec §"Failure
|
||||||
|
* handling" + the canonical per-port timeout semantics.
|
||||||
|
*/
|
||||||
|
#if 0 /* pad_sio_step — superseded by pad_bios_snapshot (Phase 2.1) */
|
||||||
|
internal MipsAtom_(pad_sio_step) atom_info(atom_bind(Binds_PadSioStep)
|
||||||
|
, atom_reads(R_TapePtr, R_PadSioBase, R_PadState, R_PadStatus, R_PadCountdown)
|
||||||
|
, atom_writes(R_PadStatus, R_PadCountdown)
|
||||||
|
) {
|
||||||
|
/* FIX 2026-08-02: explicitly load KSEG1 base into R_PadSioBase (R_T6) at the
|
||||||
|
* top. The rgcc() binding in main() does NOT survive the tape_run call
|
||||||
|
* because R_T6 is caller-saved per the O32 ABI. The pad_sio_init atom
|
||||||
|
* (also in the per-frame tape) reloads R_T6 separately. */
|
||||||
|
load_upper_i(R_PadSioBase, pad_IO_KSEG1_BASE >> 16),
|
||||||
|
or_i(R_PadSioBase, R_PadSioBase, pad_IO_KSEG1_BASE & 0xFFFF),
|
||||||
|
|
||||||
|
/* Pop Binds from tape (in Binds_PadSioStep declaration order) */
|
||||||
|
load_word(R_PadState, R_TapePtr, O_(Binds_PadSioStep,state0)),
|
||||||
|
load_word(R_PadStatus, R_TapePtr, O_(Binds_PadSioStep,state1)), /* reserved for port-1 swap */
|
||||||
|
load_word(R_PadSioBase, R_TapePtr, O_(Binds_PadSioStep,sio_base_addr0)),
|
||||||
|
load_word(R_PadCountdown, R_TapePtr, O_(Binds_PadSioStep,sio_base_addr1)), /* reserved for port-1 swap */
|
||||||
|
add_ui_self(R_TapePtr, S_(Binds_PadSioStep)),
|
||||||
|
|
||||||
|
/* ============== PORT 0 TRANSACTION ============== */
|
||||||
|
/* Use R_T0 (byte value / STAT read) + R_T1 (countdown) as scratch.
|
||||||
|
* R_PadStatus (state1) + R_PadCountdown (sio_base_addr1) are preserved
|
||||||
|
* through the port-0 body and swapped into R_PadSioBase + R_PadState
|
||||||
|
* at atom_offset(port1_start, ...) below. */
|
||||||
|
|
||||||
|
/* 1. Cleanup: CTRL = 0x0010 (raise /CS, clear stale status) */
|
||||||
|
add_ui(R_T0, R_0, pad_SIO_CTRL_CLEANUP),
|
||||||
|
store_half(R_T0, R_PadSioBase, pad_SIO_CTRL_OFFSET),
|
||||||
|
/* Bounded by pad_SIO_SETTLE_BEFORE_TX = 1000 iterations. */
|
||||||
|
add_ui(R_T1, R_0, pad_SIO_SETTLE_BEFORE_TX),
|
||||||
|
atom_label(settle_pre_port0)
|
||||||
|
nop, /* BD slot */
|
||||||
|
add_ui_self(R_T1, -1),
|
||||||
|
branch_ne(R_T1, R_0, atom_offset(settle_pre_port0, settle_pre_port0)),
|
||||||
|
|
||||||
|
/* 2. Port-select: CTRL = 0x0003 (TX enable + DTR /CS) for port 0 */
|
||||||
|
add_ui(R_T0, R_0, pad_SIO_CTRL_TX_ENABLE),
|
||||||
|
or_i(R_T0, R_T0, pad_SIO_CTRL_DTR_CS), /* set /CS line low */
|
||||||
|
store_half(R_T0, R_PadSioBase, pad_SIO_CTRL_OFFSET),
|
||||||
|
/* Bounded by pad_SIO_SETTLE_AFTER_TX = 2000 iterations. */
|
||||||
|
add_ui(R_T1, R_0, pad_SIO_SETTLE_AFTER_TX),
|
||||||
|
atom_label(settle_post_port0)
|
||||||
|
nop,
|
||||||
|
add_ui_self(R_T1, -1),
|
||||||
|
branch_ne(R_T1, R_0, atom_offset(settle_post_port0, settle_post_port0)),
|
||||||
|
|
||||||
|
/* 3. Address byte (0x01) — send + RX-ready wait + read response + RX-drain confirmation */
|
||||||
|
add_ui(R_T0, R_0, pad_PROTO_ADDR),
|
||||||
|
store_byte(R_T0, R_PadSioBase, pad_SIO_DATA_OFFSET),
|
||||||
|
|
||||||
|
/* Bounded by pad_SIO_WAIT_BUDGET = 4096 iterations. */
|
||||||
|
add_ui(R_T1, R_0, pad_SIO_WAIT_BUDGET),
|
||||||
|
atom_label(wait_ack0_port0)
|
||||||
|
load_half_u(R_T0, R_PadSioBase, pad_SIO_STAT_OFFSET),
|
||||||
|
nop,
|
||||||
|
and_i(R_T0, R_T0, pad_SIO_STAT_RX_NOT_EMPTY),
|
||||||
|
branch_ne(R_T0, R_0, atom_offset(wait_ack0_port0, ack0_received_port0)),
|
||||||
|
add_ui_self(R_T1, -1),
|
||||||
|
atom_label(continue_wait_ack0_port0)
|
||||||
|
branch_ne(R_T1, R_0, atom_offset(continue_wait_ack0_port0, wait_ack0_port0)),
|
||||||
|
/* RX timeout → mark disconnected; skip to port 1 */
|
||||||
|
mac_pad_sio_write_pad_state(PadSioStatus_Disconnected, R_PadState, R_T0),
|
||||||
|
atom_label(skip_port0_from_ack0)
|
||||||
|
branch_equal(R_0, R_0, atom_offset(skip_port0_from_ack0, port1_start)),
|
||||||
|
|
||||||
|
atom_label(ack0_received_port0)
|
||||||
|
/* Read open-bus response byte 0 — discard per docs/psx-spx §controllersandmemorycards.md */
|
||||||
|
load_byte_u(R_T0, R_PadSioBase, pad_SIO_DATA_OFFSET),
|
||||||
|
|
||||||
|
/* Confirm RX FIFO drained before sending byte 1. Bounded by pad_SIO_WAIT_BUDGET = 4096 iterations. */
|
||||||
|
add_ui(R_T1, R_0, pad_SIO_WAIT_BUDGET),
|
||||||
|
atom_label(wait_ackrel0_port0)
|
||||||
|
load_half_u(R_T0, R_PadSioBase, pad_SIO_STAT_OFFSET),
|
||||||
|
nop,
|
||||||
|
and_i(R_T0, R_T0, pad_SIO_STAT_RX_NOT_EMPTY),
|
||||||
|
branch_equal(R_T0, R_0, atom_offset(wait_ackrel0_port0, ack_released_port0)),
|
||||||
|
add_ui_self(R_T1, -1),
|
||||||
|
atom_label(continue_wait_ackrel0_port0)
|
||||||
|
branch_ne(R_T1, R_0, atom_offset(continue_wait_ackrel0_port0, wait_ackrel0_port0)),
|
||||||
|
/* RX-drain timeout → disconnected; skip to port 1 */
|
||||||
|
mac_pad_sio_write_pad_state(PadSioStatus_Disconnected, R_PadState, R_T0),
|
||||||
|
atom_label(skip_port0_from_ackrel0)
|
||||||
|
branch_equal(R_0, R_0, atom_offset(skip_port0_from_ackrel0, port1_start)),
|
||||||
|
|
||||||
|
atom_label(ack_released_port0)
|
||||||
|
|
||||||
|
/* === Byte 1 (port 0): send 0x42 (cmd read) + RX-ready wait + read response + RX-drain confirmation === */
|
||||||
|
/* Bounded by pad_SIO_WAIT_BUDGET = 4096 iterations. */
|
||||||
|
add_ui(R_T0, R_0, pad_PROTO_CMD_READ),
|
||||||
|
store_byte(R_T0, R_PadSioBase, pad_SIO_DATA_OFFSET),
|
||||||
|
add_ui(R_T1, R_0, pad_SIO_WAIT_BUDGET),
|
||||||
|
atom_label(wait_ack1_port0)
|
||||||
|
load_half_u(R_T0, R_PadSioBase, pad_SIO_STAT_OFFSET),
|
||||||
|
nop,
|
||||||
|
and_i(R_T0, R_T0, pad_SIO_STAT_RX_NOT_EMPTY),
|
||||||
|
branch_ne(R_T0, R_0, atom_offset(wait_ack1_port0, ack1_received_port0)),
|
||||||
|
add_ui_self(R_T1, -1),
|
||||||
|
atom_label(continue_wait_ack1_port0)
|
||||||
|
branch_ne(R_T1, R_0, atom_offset(continue_wait_ack1_port0, wait_ack1_port0)),
|
||||||
|
mac_pad_sio_write_pad_state(PadSioStatus_Disconnected, R_PadState, R_T0),
|
||||||
|
atom_label(skip_port0_from_ack1)
|
||||||
|
branch_equal(R_0, R_0, atom_offset(skip_port0_from_ack1, port1_start)),
|
||||||
|
|
||||||
|
atom_label(ack1_received_port0)
|
||||||
|
/* Read response ID byte — discarded for teaching scope (decode hardcoded). */
|
||||||
|
load_byte_u(R_T0, R_PadSioBase, pad_SIO_DATA_OFFSET),
|
||||||
|
|
||||||
|
/* RX FIFO drain wait. Bounded by pad_SIO_WAIT_BUDGET = 4096 iterations. */
|
||||||
|
add_ui(R_T1, R_0, pad_SIO_WAIT_BUDGET),
|
||||||
|
atom_label(wait_ackrel1_port0)
|
||||||
|
load_half_u(R_T0, R_PadSioBase, pad_SIO_STAT_OFFSET),
|
||||||
|
nop,
|
||||||
|
and_i(R_T0, R_T0, pad_SIO_STAT_RX_NOT_EMPTY),
|
||||||
|
branch_equal(R_T0, R_0, atom_offset(wait_ackrel1_port0, ack_released1_port0)),
|
||||||
|
add_ui_self(R_T1, -1),
|
||||||
|
atom_label(continue_wait_ackrel1_port0)
|
||||||
|
branch_ne(R_T1, R_0, atom_offset(continue_wait_ackrel1_port0, wait_ackrel1_port0)),
|
||||||
|
mac_pad_sio_write_pad_state(PadSioStatus_Disconnected, R_PadState, R_T0),
|
||||||
|
atom_label(skip_port0_from_ackrel1)
|
||||||
|
branch_equal(R_0, R_0, atom_offset(skip_port0_from_ackrel1, port1_start)),
|
||||||
|
|
||||||
|
atom_label(ack_released1_port0)
|
||||||
|
|
||||||
|
/* === Byte 2 (port 0): send 0x00 + RX-ready wait + read response + RX-drain confirmation === */
|
||||||
|
/* Bounded by pad_SIO_WAIT_BUDGET = 4096 iterations. */
|
||||||
|
add_ui(R_T0, R_0, 0x00),
|
||||||
|
store_byte(R_T0, R_PadSioBase, pad_SIO_DATA_OFFSET),
|
||||||
|
add_ui(R_T1, R_0, pad_SIO_WAIT_BUDGET),
|
||||||
|
atom_label(wait_ack2_port0)
|
||||||
|
load_half_u(R_T0, R_PadSioBase, pad_SIO_STAT_OFFSET),
|
||||||
|
nop,
|
||||||
|
and_i(R_T0, R_T0, pad_SIO_STAT_RX_NOT_EMPTY),
|
||||||
|
branch_ne(R_T0, R_0, atom_offset(wait_ack2_port0, ack2_received_port0)),
|
||||||
|
add_ui_self(R_T1, -1),
|
||||||
|
atom_label(continue_wait_ack2_port0)
|
||||||
|
branch_ne(R_T1, R_0, atom_offset(continue_wait_ack2_port0, wait_ack2_port0)),
|
||||||
|
mac_pad_sio_write_pad_state(PadSioStatus_Disconnected, R_PadState, R_T0),
|
||||||
|
atom_label(skip_port0_from_ack2)
|
||||||
|
branch_equal(R_0, R_0, atom_offset(skip_port0_from_ack2, port1_start)),
|
||||||
|
|
||||||
|
atom_label(ack2_received_port0)
|
||||||
|
load_byte_u(R_T0, R_PadSioBase, pad_SIO_DATA_OFFSET),
|
||||||
|
|
||||||
|
/* Bounded by pad_SIO_WAIT_BUDGET = 4096 iterations. */
|
||||||
|
add_ui(R_T1, R_0, pad_SIO_WAIT_BUDGET),
|
||||||
|
atom_label(wait_ackrel2_port0)
|
||||||
|
load_half_u(R_T0, R_PadSioBase, pad_SIO_STAT_OFFSET),
|
||||||
|
nop,
|
||||||
|
and_i(R_T0, R_T0, pad_SIO_STAT_RX_NOT_EMPTY),
|
||||||
|
branch_equal(R_T0, R_0, atom_offset(wait_ackrel2_port0, ack_released2_port0)),
|
||||||
|
add_ui_self(R_T1, -1),
|
||||||
|
atom_label(continue_wait_ackrel2_port0)
|
||||||
|
branch_ne(R_T1, R_0, atom_offset(continue_wait_ackrel2_port0, wait_ackrel2_port0)),
|
||||||
|
mac_pad_sio_write_pad_state(PadSioStatus_Disconnected, R_PadState, R_T0),
|
||||||
|
atom_label(skip_port0_from_ackrel2)
|
||||||
|
branch_equal(R_0, R_0, atom_offset(skip_port0_from_ackrel2, port1_start)),
|
||||||
|
|
||||||
|
atom_label(ack_released2_port0)
|
||||||
|
|
||||||
|
/* === Byte 3 (port 0): send 0x00 + RX-ready wait + read response + RX-drain confirmation === */
|
||||||
|
/* Bounded by pad_SIO_WAIT_BUDGET = 4096 iterations. */
|
||||||
|
add_ui(R_T0, R_0, 0x00),
|
||||||
|
store_byte(R_T0, R_PadSioBase, pad_SIO_DATA_OFFSET),
|
||||||
|
add_ui(R_T1, R_0, pad_SIO_WAIT_BUDGET),
|
||||||
|
atom_label(wait_ack3_port0)
|
||||||
|
load_half_u(R_T0, R_PadSioBase, pad_SIO_STAT_OFFSET),
|
||||||
|
nop,
|
||||||
|
and_i(R_T0, R_T0, pad_SIO_STAT_RX_NOT_EMPTY),
|
||||||
|
branch_ne(R_T0, R_0, atom_offset(wait_ack3_port0, ack3_received_port0)),
|
||||||
|
add_ui_self(R_T1, -1),
|
||||||
|
atom_label(continue_wait_ack3_port0)
|
||||||
|
branch_ne(R_T1, R_0, atom_offset(continue_wait_ack3_port0, wait_ack3_port0)),
|
||||||
|
mac_pad_sio_write_pad_state(PadSioStatus_Disconnected, R_PadState, R_T0),
|
||||||
|
atom_label(skip_port0_from_ack3)
|
||||||
|
branch_equal(R_0, R_0, atom_offset(skip_port0_from_ack3, port1_start)),
|
||||||
|
|
||||||
|
atom_label(ack3_received_port0)
|
||||||
|
load_byte_u(R_T0, R_PadSioBase, pad_SIO_DATA_OFFSET),
|
||||||
|
|
||||||
|
/* Bounded by pad_SIO_WAIT_BUDGET = 4096 iterations. */
|
||||||
|
add_ui(R_T1, R_0, pad_SIO_WAIT_BUDGET),
|
||||||
|
atom_label(wait_ackrel3_port0)
|
||||||
|
load_half_u(R_T0, R_PadSioBase, pad_SIO_STAT_OFFSET),
|
||||||
|
nop,
|
||||||
|
and_i(R_T0, R_T0, pad_SIO_STAT_RX_NOT_EMPTY),
|
||||||
|
branch_equal(R_T0, R_0, atom_offset(wait_ackrel3_port0, ack_released3_port0)),
|
||||||
|
add_ui_self(R_T1, -1),
|
||||||
|
atom_label(continue_wait_ackrel3_port0)
|
||||||
|
branch_ne(R_T1, R_0, atom_offset(continue_wait_ackrel3_port0, wait_ackrel3_port0)),
|
||||||
|
mac_pad_sio_write_pad_state(PadSioStatus_Disconnected, R_PadState, R_T0),
|
||||||
|
atom_label(skip_port0_from_ackrel3)
|
||||||
|
branch_equal(R_0, R_0, atom_offset(skip_port0_from_ackrel3, port1_start)),
|
||||||
|
|
||||||
|
atom_label(ack_released3_port0)
|
||||||
|
|
||||||
|
/* === Byte 4 (FINAL, port 0): send 0x00 + RX-not-empty wait + read final byte === */
|
||||||
|
/* Bounded by pad_SIO_WAIT_BUDGET = 4096 iterations. */
|
||||||
|
add_ui(R_T0, R_0, 0x00),
|
||||||
|
store_byte(R_T0, R_PadSioBase, pad_SIO_DATA_OFFSET),
|
||||||
|
add_ui(R_T1, R_0, pad_SIO_WAIT_BUDGET),
|
||||||
|
atom_label(wait_rx4_port0)
|
||||||
|
load_half_u(R_T0, R_PadSioBase, pad_SIO_STAT_OFFSET),
|
||||||
|
nop,
|
||||||
|
and_i(R_T0, R_T0, pad_SIO_STAT_RX_NOT_EMPTY),
|
||||||
|
branch_ne(R_T0, R_0, atom_offset(wait_rx4_port0, rx4_received_port0)),
|
||||||
|
add_ui_self(R_T1, -1),
|
||||||
|
atom_label(continue_wait_rx4_port0)
|
||||||
|
branch_ne(R_T1, R_0, atom_offset(continue_wait_rx4_port0, wait_rx4_port0)),
|
||||||
|
mac_pad_sio_write_pad_state(PadSioStatus_Disconnected, R_PadState, R_T0),
|
||||||
|
atom_label(skip_port0_from_rx4)
|
||||||
|
branch_equal(R_0, R_0, atom_offset(skip_port0_from_rx4, port1_start)),
|
||||||
|
|
||||||
|
atom_label(rx4_received_port0)
|
||||||
|
load_byte_u(R_T0, R_PadSioBase, pad_SIO_DATA_OFFSET), /* discard final byte */
|
||||||
|
|
||||||
|
/* === RESPONSE DECODE (hardcoded for teaching scope) ===
|
||||||
|
* Per the plan §"Phase 3 task 3.1" + spec §"Architecture":
|
||||||
|
* - Full decode (buttons/axes from response bytes) is follow-up scope.
|
||||||
|
* - Teaching scope: hardcode digital poll response.
|
||||||
|
* status = PadSioStatus_Digital
|
||||||
|
* buttons = 0x0000FFFF (no buttons pressed — placeholder)
|
||||||
|
* axes = 0x80808080 (left_x=0x80, left_y=0x80, right_x=0x80, right_y=0x80)
|
||||||
|
* attempt = 0
|
||||||
|
*/
|
||||||
|
atom_label(decode_port0)
|
||||||
|
mac_pad_sio_write_pad_state(PadSioStatus_Digital, R_PadState, R_T0),
|
||||||
|
|
||||||
|
/* /CS cleanup: raise /CS, clear stale status before exiting port 0. */
|
||||||
|
add_ui(R_T0, R_0, pad_SIO_CTRL_CLEANUP),
|
||||||
|
store_half(R_T0, R_PadSioBase, pad_SIO_CTRL_OFFSET),
|
||||||
|
|
||||||
|
/* ============== PORT 1 SETUP ============== */
|
||||||
|
/* Swap: R_PadCountdown holds sio_base_addr1; R_PadStatus holds state1. */
|
||||||
|
atom_label(port1_start)
|
||||||
|
add_u(R_PadSioBase, R_0, R_PadCountdown), /* sio_base_addr1 → R_PadSioBase */
|
||||||
|
add_u(R_PadState, R_0, R_PadStatus), /* state1 → R_PadState */
|
||||||
|
|
||||||
|
/* ============== PORT 1 TRANSACTION (mirror of port 0) ============== */
|
||||||
|
/* R_PadStatus + R_PadCountdown are no longer reserved (port 1 is the
|
||||||
|
* last transaction); we still use R_T0/R_T1 as scratch to match port 0. */
|
||||||
|
|
||||||
|
/* 1. Cleanup: CTRL = 0x0010 (raise /CS, clear stale status) */
|
||||||
|
add_ui(R_T0, R_0, pad_SIO_CTRL_CLEANUP),
|
||||||
|
store_half(R_T0, R_PadSioBase, pad_SIO_CTRL_OFFSET),
|
||||||
|
/* Bounded by pad_SIO_SETTLE_BEFORE_TX = 1000 iterations. */
|
||||||
|
add_ui(R_T1, R_0, pad_SIO_SETTLE_BEFORE_TX),
|
||||||
|
atom_label(settle_pre_port1)
|
||||||
|
nop,
|
||||||
|
add_ui_self(R_T1, -1),
|
||||||
|
branch_ne(R_T1, R_0, atom_offset(settle_pre_port1, settle_pre_port1)),
|
||||||
|
|
||||||
|
/* 2. Port-select: CTRL = 0x0003 | (1 << 13) (port 1 select) */
|
||||||
|
add_ui(R_T0, R_0, pad_SIO_CTRL_TX_ENABLE),
|
||||||
|
or_i(R_T0, R_T0, pad_SIO_CTRL_DTR_CS),
|
||||||
|
or_i(R_T0, R_T0, 1 << 13), /* port 1 select bit (CTRL bit 13 = port select) */
|
||||||
|
store_half(R_T0, R_PadSioBase, pad_SIO_CTRL_OFFSET),
|
||||||
|
/* Bounded by pad_SIO_SETTLE_AFTER_TX = 2000 iterations. */
|
||||||
|
add_ui(R_T1, R_0, pad_SIO_SETTLE_AFTER_TX),
|
||||||
|
atom_label(settle_post_port1)
|
||||||
|
nop,
|
||||||
|
add_ui_self(R_T1, -1),
|
||||||
|
branch_ne(R_T1, R_0, atom_offset(settle_post_port1, settle_post_port1)),
|
||||||
|
|
||||||
|
/* 3. Address byte (0x01) — send + RX-ready wait + read response + RX-drain confirmation */
|
||||||
|
add_ui(R_T0, R_0, pad_PROTO_ADDR),
|
||||||
|
store_byte(R_T0, R_PadSioBase, pad_SIO_DATA_OFFSET),
|
||||||
|
|
||||||
|
/* Bounded by pad_SIO_WAIT_BUDGET = 4096 iterations. */
|
||||||
|
add_ui(R_T1, R_0, pad_SIO_WAIT_BUDGET),
|
||||||
|
atom_label(wait_ack0_port1)
|
||||||
|
load_half_u(R_T0, R_PadSioBase, pad_SIO_STAT_OFFSET),
|
||||||
|
nop,
|
||||||
|
and_i(R_T0, R_T0, pad_SIO_STAT_RX_NOT_EMPTY),
|
||||||
|
branch_ne(R_T0, R_0, atom_offset(wait_ack0_port1, ack0_received_port1)),
|
||||||
|
add_ui_self(R_T1, -1),
|
||||||
|
atom_label(continue_wait_ack0_port1)
|
||||||
|
branch_ne(R_T1, R_0, atom_offset(continue_wait_ack0_port1, wait_ack0_port1)),
|
||||||
|
mac_pad_sio_write_pad_state(PadSioStatus_Disconnected, R_PadState, R_T0),
|
||||||
|
atom_label(skip_port1_from_ack0)
|
||||||
|
branch_equal(R_0, R_0, atom_offset(skip_port1_from_ack0, end_atom)),
|
||||||
|
|
||||||
|
atom_label(ack0_received_port1)
|
||||||
|
load_byte_u(R_T0, R_PadSioBase, pad_SIO_DATA_OFFSET),
|
||||||
|
|
||||||
|
/* Bounded by pad_SIO_WAIT_BUDGET = 4096 iterations. */
|
||||||
|
add_ui(R_T1, R_0, pad_SIO_WAIT_BUDGET),
|
||||||
|
atom_label(wait_ackrel0_port1)
|
||||||
|
load_half_u(R_T0, R_PadSioBase, pad_SIO_STAT_OFFSET),
|
||||||
|
nop,
|
||||||
|
and_i(R_T0, R_T0, pad_SIO_STAT_RX_NOT_EMPTY),
|
||||||
|
branch_equal(R_T0, R_0, atom_offset(wait_ackrel0_port1, ack_released_port1)),
|
||||||
|
add_ui_self(R_T1, -1),
|
||||||
|
atom_label(continue_wait_ackrel0_port1)
|
||||||
|
branch_ne(R_T1, R_0, atom_offset(continue_wait_ackrel0_port1, wait_ackrel0_port1)),
|
||||||
|
mac_pad_sio_write_pad_state(PadSioStatus_Disconnected, R_PadState, R_T0),
|
||||||
|
atom_label(skip_port1_from_ackrel0)
|
||||||
|
branch_equal(R_0, R_0, atom_offset(skip_port1_from_ackrel0, end_atom)),
|
||||||
|
|
||||||
|
atom_label(ack_released_port1)
|
||||||
|
|
||||||
|
/* === Byte 1 (port 1): send 0x42 (cmd read) + RX-ready wait + read response + RX-drain confirmation === */
|
||||||
|
/* Bounded by pad_SIO_WAIT_BUDGET = 4096 iterations. */
|
||||||
|
add_ui(R_T0, R_0, pad_PROTO_CMD_READ),
|
||||||
|
store_byte(R_T0, R_PadSioBase, pad_SIO_DATA_OFFSET),
|
||||||
|
add_ui(R_T1, R_0, pad_SIO_WAIT_BUDGET),
|
||||||
|
atom_label(wait_ack1_port1)
|
||||||
|
load_half_u(R_T0, R_PadSioBase, pad_SIO_STAT_OFFSET),
|
||||||
|
nop,
|
||||||
|
and_i(R_T0, R_T0, pad_SIO_STAT_RX_NOT_EMPTY),
|
||||||
|
branch_ne(R_T0, R_0, atom_offset(wait_ack1_port1, ack1_received_port1)),
|
||||||
|
add_ui_self(R_T1, -1),
|
||||||
|
atom_label(continue_wait_ack1_port1)
|
||||||
|
branch_ne(R_T1, R_0, atom_offset(continue_wait_ack1_port1, wait_ack1_port1)),
|
||||||
|
mac_pad_sio_write_pad_state(PadSioStatus_Disconnected, R_PadState, R_T0),
|
||||||
|
atom_label(skip_port1_from_ack1)
|
||||||
|
branch_equal(R_0, R_0, atom_offset(skip_port1_from_ack1, end_atom)),
|
||||||
|
|
||||||
|
atom_label(ack1_received_port1)
|
||||||
|
load_byte_u(R_T0, R_PadSioBase, pad_SIO_DATA_OFFSET),
|
||||||
|
|
||||||
|
/* Bounded by pad_SIO_WAIT_BUDGET = 4096 iterations. */
|
||||||
|
add_ui(R_T1, R_0, pad_SIO_WAIT_BUDGET),
|
||||||
|
atom_label(wait_ackrel1_port1)
|
||||||
|
load_half_u(R_T0, R_PadSioBase, pad_SIO_STAT_OFFSET),
|
||||||
|
nop,
|
||||||
|
and_i(R_T0, R_T0, pad_SIO_STAT_RX_NOT_EMPTY),
|
||||||
|
branch_equal(R_T0, R_0, atom_offset(wait_ackrel1_port1, ack_released1_port1)),
|
||||||
|
add_ui_self(R_T1, -1),
|
||||||
|
atom_label(continue_wait_ackrel1_port1)
|
||||||
|
branch_ne(R_T1, R_0, atom_offset(continue_wait_ackrel1_port1, wait_ackrel1_port1)),
|
||||||
|
mac_pad_sio_write_pad_state(PadSioStatus_Disconnected, R_PadState, R_T0),
|
||||||
|
atom_label(skip_port1_from_ackrel1)
|
||||||
|
branch_equal(R_0, R_0, atom_offset(skip_port1_from_ackrel1, end_atom)),
|
||||||
|
|
||||||
|
atom_label(ack_released1_port1)
|
||||||
|
|
||||||
|
/* === Byte 2 (port 1): send 0x00 + RX-ready wait + read response + RX-drain confirmation === */
|
||||||
|
/* Bounded by pad_SIO_WAIT_BUDGET = 4096 iterations. */
|
||||||
|
add_ui(R_T0, R_0, 0x00),
|
||||||
|
store_byte(R_T0, R_PadSioBase, pad_SIO_DATA_OFFSET),
|
||||||
|
add_ui(R_T1, R_0, pad_SIO_WAIT_BUDGET),
|
||||||
|
atom_label(wait_ack2_port1)
|
||||||
|
load_half_u(R_T0, R_PadSioBase, pad_SIO_STAT_OFFSET),
|
||||||
|
nop,
|
||||||
|
and_i(R_T0, R_T0, pad_SIO_STAT_RX_NOT_EMPTY),
|
||||||
|
branch_ne(R_T0, R_0, atom_offset(wait_ack2_port1, ack2_received_port1)),
|
||||||
|
add_ui_self(R_T1, -1),
|
||||||
|
atom_label(continue_wait_ack2_port1)
|
||||||
|
branch_ne(R_T1, R_0, atom_offset(continue_wait_ack2_port1, wait_ack2_port1)),
|
||||||
|
mac_pad_sio_write_pad_state(PadSioStatus_Disconnected, R_PadState, R_T0),
|
||||||
|
atom_label(skip_port1_from_ack2)
|
||||||
|
branch_equal(R_0, R_0, atom_offset(skip_port1_from_ack2, end_atom)),
|
||||||
|
|
||||||
|
atom_label(ack2_received_port1)
|
||||||
|
load_byte_u(R_T0, R_PadSioBase, pad_SIO_DATA_OFFSET),
|
||||||
|
|
||||||
|
/* Bounded by pad_SIO_WAIT_BUDGET = 4096 iterations. */
|
||||||
|
add_ui(R_T1, R_0, pad_SIO_WAIT_BUDGET),
|
||||||
|
atom_label(wait_ackrel2_port1)
|
||||||
|
load_half_u(R_T0, R_PadSioBase, pad_SIO_STAT_OFFSET),
|
||||||
|
nop,
|
||||||
|
and_i(R_T0, R_T0, pad_SIO_STAT_RX_NOT_EMPTY),
|
||||||
|
branch_equal(R_T0, R_0, atom_offset(wait_ackrel2_port1, ack_released2_port1)),
|
||||||
|
add_ui_self(R_T1, -1),
|
||||||
|
atom_label(continue_wait_ackrel2_port1)
|
||||||
|
branch_ne(R_T1, R_0, atom_offset(continue_wait_ackrel2_port1, wait_ackrel2_port1)),
|
||||||
|
mac_pad_sio_write_pad_state(PadSioStatus_Disconnected, R_PadState, R_T0),
|
||||||
|
atom_label(skip_port1_from_ackrel2)
|
||||||
|
branch_equal(R_0, R_0, atom_offset(skip_port1_from_ackrel2, end_atom)),
|
||||||
|
|
||||||
|
atom_label(ack_released2_port1)
|
||||||
|
|
||||||
|
/* === Byte 3 (port 1): send 0x00 + RX-ready wait + read response + RX-drain confirmation === */
|
||||||
|
/* Bounded by pad_SIO_WAIT_BUDGET = 4096 iterations. */
|
||||||
|
add_ui(R_T0, R_0, 0x00),
|
||||||
|
store_byte(R_T0, R_PadSioBase, pad_SIO_DATA_OFFSET),
|
||||||
|
add_ui(R_T1, R_0, pad_SIO_WAIT_BUDGET),
|
||||||
|
atom_label(wait_ack3_port1)
|
||||||
|
load_half_u(R_T0, R_PadSioBase, pad_SIO_STAT_OFFSET),
|
||||||
|
nop,
|
||||||
|
and_i(R_T0, R_T0, pad_SIO_STAT_RX_NOT_EMPTY),
|
||||||
|
branch_ne(R_T0, R_0, atom_offset(wait_ack3_port1, ack3_received_port1)),
|
||||||
|
add_ui_self(R_T1, -1),
|
||||||
|
atom_label(continue_wait_ack3_port1)
|
||||||
|
branch_ne(R_T1, R_0, atom_offset(continue_wait_ack3_port1, wait_ack3_port1)),
|
||||||
|
mac_pad_sio_write_pad_state(PadSioStatus_Disconnected, R_PadState, R_T0),
|
||||||
|
atom_label(skip_port1_from_ack3)
|
||||||
|
branch_equal(R_0, R_0, atom_offset(skip_port1_from_ack3, end_atom)),
|
||||||
|
|
||||||
|
atom_label(ack3_received_port1)
|
||||||
|
load_byte_u(R_T0, R_PadSioBase, pad_SIO_DATA_OFFSET),
|
||||||
|
|
||||||
|
/* Bounded by pad_SIO_WAIT_BUDGET = 4096 iterations. */
|
||||||
|
add_ui(R_T1, R_0, pad_SIO_WAIT_BUDGET),
|
||||||
|
atom_label(wait_ackrel3_port1)
|
||||||
|
load_half_u(R_T0, R_PadSioBase, pad_SIO_STAT_OFFSET),
|
||||||
|
nop,
|
||||||
|
and_i(R_T0, R_T0, pad_SIO_STAT_RX_NOT_EMPTY),
|
||||||
|
branch_equal(R_T0, R_0, atom_offset(wait_ackrel3_port1, ack_released3_port1)),
|
||||||
|
add_ui_self(R_T1, -1),
|
||||||
|
atom_label(continue_wait_ackrel3_port1)
|
||||||
|
branch_ne(R_T1, R_0, atom_offset(continue_wait_ackrel3_port1, wait_ackrel3_port1)),
|
||||||
|
mac_pad_sio_write_pad_state(PadSioStatus_Disconnected, R_PadState, R_T0),
|
||||||
|
atom_label(skip_port1_from_ackrel3)
|
||||||
|
branch_equal(R_0, R_0, atom_offset(skip_port1_from_ackrel3, end_atom)),
|
||||||
|
|
||||||
|
atom_label(ack_released3_port1)
|
||||||
|
|
||||||
|
/* === Byte 4 (FINAL, port 1): send 0x00 + RX-not-empty wait + read final byte === */
|
||||||
|
/* Bounded by pad_SIO_WAIT_BUDGET = 4096 iterations. */
|
||||||
|
add_ui(R_T0, R_0, 0x00),
|
||||||
|
store_byte(R_T0, R_PadSioBase, pad_SIO_DATA_OFFSET),
|
||||||
|
add_ui(R_T1, R_0, pad_SIO_WAIT_BUDGET),
|
||||||
|
atom_label(wait_rx4_port1)
|
||||||
|
load_half_u(R_T0, R_PadSioBase, pad_SIO_STAT_OFFSET),
|
||||||
|
nop,
|
||||||
|
and_i(R_T0, R_T0, pad_SIO_STAT_RX_NOT_EMPTY),
|
||||||
|
branch_ne(R_T0, R_0, atom_offset(wait_rx4_port1, rx4_received_port1)),
|
||||||
|
add_ui_self(R_T1, -1),
|
||||||
|
atom_label(continue_wait_rx4_port1)
|
||||||
|
branch_ne(R_T1, R_0, atom_offset(continue_wait_rx4_port1, wait_rx4_port1)),
|
||||||
|
mac_pad_sio_write_pad_state(PadSioStatus_Disconnected, R_PadState, R_T0),
|
||||||
|
atom_label(skip_port1_from_rx4)
|
||||||
|
branch_equal(R_0, R_0, atom_offset(skip_port1_from_rx4, end_atom)),
|
||||||
|
|
||||||
|
atom_label(rx4_received_port1)
|
||||||
|
load_byte_u(R_T0, R_PadSioBase, pad_SIO_DATA_OFFSET), /* discard final byte */
|
||||||
|
|
||||||
|
/* === RESPONSE DECODE (port 1) === */
|
||||||
|
atom_label(decode_port1)
|
||||||
|
mac_pad_sio_write_pad_state(PadSioStatus_Digital, R_PadState, R_T0),
|
||||||
|
|
||||||
|
/* /CS cleanup: raise /CS, clear stale status before exiting port 1. */
|
||||||
|
add_ui(R_T0, R_0, pad_SIO_CTRL_CLEANUP),
|
||||||
|
store_half(R_T0, R_PadSioBase, pad_SIO_CTRL_OFFSET),
|
||||||
|
|
||||||
|
atom_label(end_atom)
|
||||||
|
mac_yield(),
|
||||||
|
};
|
||||||
|
#endif /* end pad_sio_step wrap */
|
||||||
|
|
||||||
|
/* ----- pad_sio_diag_pin -----
|
||||||
|
* Per-frame diagnostic counter. The caller binds R_DiagPinScratch to
|
||||||
|
* scratch_for_atom_diag_pin for temporary gdb verification.
|
||||||
|
*/
|
||||||
|
#if 0 /* pad_sio_diag_pin — superseded (raw-SIO phase removed) */
|
||||||
|
internal MipsAtom_(pad_sio_diag_pin) atom_info(atom_phase(pad_init)
|
||||||
|
, atom_reads(R_T0, R_T1, R_DiagPinScratch)
|
||||||
|
, atom_writes(R_T0, R_T1, R_DiagPinScratch)
|
||||||
|
) {
|
||||||
|
/* FIX 2026-08-02: explicitly reload R_DiagPinScratch (R_T3 = $t3). Caller-saved
|
||||||
|
* per O32 ABI; the rgcc binding in main() does not survive tape_run. */
|
||||||
|
load_upper_i(R_DiagPinScratch, 0x8001),
|
||||||
|
or_i(R_DiagPinScratch, R_DiagPinScratch, 0xC800),
|
||||||
|
|
||||||
|
/* High half = 0xD1A6; low half increments once per atom invocation. */
|
||||||
|
load_word(R_T1, R_DiagPinScratch, 0),
|
||||||
|
nop,
|
||||||
|
add_ui(R_T1, R_T1, 1),
|
||||||
|
and_i(R_T0, R_T1, 0xFFFF),
|
||||||
|
load_upper_i(R_T1, 0xD1A6),
|
||||||
|
or_i(R_T1, R_T1, 0),
|
||||||
|
or_u(R_T1, R_T1, R_T0),
|
||||||
|
store_word(R_T1, R_DiagPinScratch, 0),
|
||||||
|
mac_yield(),
|
||||||
|
};
|
||||||
|
#endif /* end pad_sio_diag_pin wrap */
|
||||||
|
|
||||||
|
/* ----- pad_sio_diag_byte_exchange -----
|
||||||
|
* Temporary two-byte wire probe: sends 0x01 and 0x42, then stores the
|
||||||
|
* open-bus byte and response ID in scratch_for_atom_diag_pin.
|
||||||
|
*/
|
||||||
|
#if 0 /* pad_sio_diag_byte_exchange — superseded (raw-SIO phase removed) */
|
||||||
|
internal MipsAtom_(pad_sio_diag_byte_exchange) atom_info(atom_phase(pad_init)
|
||||||
|
, atom_reads(R_T0, R_T1, R_T2, R_PadSioBase, R_DiagPinScratch)
|
||||||
|
, atom_writes(R_T0, R_T1, R_T2, R_PadSioBase, R_DiagPinScratch)
|
||||||
|
) {
|
||||||
|
/* FIX 2026-08-02: explicitly reload R_DiagPinScratch (R_T3 = $t3). Caller-saved
|
||||||
|
* per O32 ABI; the rgcc binding in main() does not survive tape_run. */
|
||||||
|
load_upper_i(R_DiagPinScratch, 0x8001),
|
||||||
|
or_i(R_DiagPinScratch, R_DiagPinScratch, 0xC800),
|
||||||
|
|
||||||
|
/* FIX 2026-08-02: explicitly load KSEG1 base into R_PadSioBase (R_T6) at the
|
||||||
|
* top. The rgcc() binding in main() does NOT survive the tape_run call
|
||||||
|
* because R_T6 is caller-saved per the O32 ABI. */
|
||||||
|
load_upper_i(R_PadSioBase, pad_IO_KSEG1_BASE >> 16),
|
||||||
|
or_i(R_PadSioBase, R_PadSioBase, pad_IO_KSEG1_BASE & 0xFFFF),
|
||||||
|
|
||||||
|
add_ui(R_T0, R_0, pad_SIO_CTRL_CLEANUP),
|
||||||
|
store_half(R_T0, R_PadSioBase, pad_SIO_CTRL_OFFSET),
|
||||||
|
add_ui(R_T0, R_0, pad_SIO_CTRL_TX_ENABLE),
|
||||||
|
or_i(R_T0, R_T0, pad_SIO_CTRL_DTR_CS),
|
||||||
|
store_half(R_T0, R_PadSioBase, pad_SIO_CTRL_OFFSET),
|
||||||
|
|
||||||
|
add_ui(R_T0, R_0, pad_PROTO_ADDR),
|
||||||
|
store_byte(R_T0, R_PadSioBase, pad_SIO_DATA_OFFSET),
|
||||||
|
add_ui(R_T1, R_0, pad_SIO_WAIT_BUDGET),
|
||||||
|
atom_label(diag_wait_ack0)
|
||||||
|
load_half_u(R_T0, R_PadSioBase, pad_SIO_STAT_OFFSET),
|
||||||
|
nop,
|
||||||
|
and_i(R_T0, R_T0, pad_SIO_STAT_RX_NOT_EMPTY),
|
||||||
|
branch_ne(R_T0, R_0, atom_offset(diag_wait_ack0, diag_ack0_done)),
|
||||||
|
add_ui_self(R_T1, -1),
|
||||||
|
branch_ne(R_T1, R_0, atom_offset(diag_wait_ack0, diag_wait_ack0)),
|
||||||
|
add_ui(R_T0, R_0, 0xDEADAC01),
|
||||||
|
store_word(R_T0, R_DiagPinScratch, 0),
|
||||||
|
branch_equal(R_0, R_0, atom_offset(diag_timeout_ack0, diag_timeout)),
|
||||||
|
atom_label(diag_ack0_done)
|
||||||
|
load_byte_u(R_T2, R_PadSioBase, pad_SIO_DATA_OFFSET),
|
||||||
|
add_ui(R_T0, R_0, pad_PROTO_CMD_READ),
|
||||||
|
store_byte(R_T0, R_PadSioBase, pad_SIO_DATA_OFFSET),
|
||||||
|
add_ui(R_T1, R_0, pad_SIO_WAIT_BUDGET),
|
||||||
|
atom_label(diag_wait_ack1)
|
||||||
|
load_half_u(R_T0, R_PadSioBase, pad_SIO_STAT_OFFSET),
|
||||||
|
nop,
|
||||||
|
and_i(R_T0, R_T0, pad_SIO_STAT_RX_NOT_EMPTY),
|
||||||
|
branch_ne(R_T0, R_0, atom_offset(diag_wait_ack1, diag_ack1_done)),
|
||||||
|
add_ui_self(R_T1, -1),
|
||||||
|
branch_ne(R_T1, R_0, atom_offset(diag_wait_ack1, diag_wait_ack1)),
|
||||||
|
add_ui(R_T0, R_0, 0xDEADAC02),
|
||||||
|
store_word(R_T0, R_DiagPinScratch, 0),
|
||||||
|
branch_equal(R_0, R_0, atom_offset(diag_timeout_ack1, diag_timeout)),
|
||||||
|
atom_label(diag_ack1_done)
|
||||||
|
load_byte_u(R_T0, R_PadSioBase, pad_SIO_DATA_OFFSET),
|
||||||
|
nop,
|
||||||
|
shift_lleft(R_T0, R_T0, 8),
|
||||||
|
or_u(R_T2, R_T2, R_T0),
|
||||||
|
store_word(R_T2, R_DiagPinScratch, 0),
|
||||||
|
atom_label(diag_success)
|
||||||
|
branch_equal(R_0, R_0, atom_offset(diag_success, diag_done)),
|
||||||
|
nop,
|
||||||
|
atom_label(diag_timeout_ack0)
|
||||||
|
add_ui(R_T0, R_0, 0xDEADAC01),
|
||||||
|
store_word(R_T0, R_DiagPinScratch, 0),
|
||||||
|
atom_label(diag_timeout_ack1)
|
||||||
|
add_ui(R_T0, R_0, 0xDEADAC02),
|
||||||
|
store_word(R_T0, R_DiagPinScratch, 0),
|
||||||
|
atom_label(diag_timeout)
|
||||||
|
add_ui(R_T0, R_0, 0xDEADACFF),
|
||||||
|
store_word(R_T0, R_DiagPinScratch, 0),
|
||||||
|
atom_label(diag_done)
|
||||||
|
add_ui(R_T0, R_0, pad_SIO_CTRL_CLEANUP),
|
||||||
|
store_half(R_T0, R_PadSioBase, pad_SIO_CTRL_OFFSET),
|
||||||
|
mac_yield(),
|
||||||
|
};
|
||||||
|
#endif /* end pad_sio_diag_byte_exchange wrap */
|
||||||
+7
-18
@@ -180,29 +180,18 @@ function link-modules { param([string[]]$link_modules, [string] $elf, [string[]
|
|||||||
$link_args += ($f_link_pass_through_prefix + $f_link_mapfile + $map)
|
$link_args += ($f_link_pass_through_prefix + $f_link_mapfile + $map)
|
||||||
|
|
||||||
$link_args += ($f_link_pass_through_prefix + $f_link_start_group)
|
$link_args += ($f_link_pass_through_prefix + $f_link_start_group)
|
||||||
|
# raw_sio_pad_poll_20260802 — Task 5.1c surgical library-list trim.
|
||||||
|
# The 16 removed entries (c2, card, cd, comb, ds, gs, gun, hmd, math,
|
||||||
|
# mcrd, mcx, press, sio, snd, spu, tap) had LOAD lines in the map but
|
||||||
|
# ZERO .o files pulled in — they were unused. The 5 kept libraries
|
||||||
|
# (api, c, etc, gpu, gte) are required by the C-side calls in
|
||||||
|
# hello_joypad.c (reset_graph, draw_sync, vsync, etc.).
|
||||||
$libraries = @(
|
$libraries = @(
|
||||||
"api",
|
"api",
|
||||||
"c",
|
"c",
|
||||||
"c2",
|
|
||||||
"card",
|
|
||||||
"cd",
|
|
||||||
"comb",
|
|
||||||
"ds",
|
|
||||||
"etc",
|
"etc",
|
||||||
"gpu",
|
"gpu",
|
||||||
"gs",
|
"gte"
|
||||||
"gte",
|
|
||||||
"gun",
|
|
||||||
"hmd",
|
|
||||||
"math",
|
|
||||||
"mcrd",
|
|
||||||
"mcx",
|
|
||||||
"pad",
|
|
||||||
"press",
|
|
||||||
"sio",
|
|
||||||
"snd",
|
|
||||||
"spu",
|
|
||||||
"tap"
|
|
||||||
)
|
)
|
||||||
foreach ($lib in $libraries) {
|
foreach ($lib in $libraries) {
|
||||||
$link_args += ($f_link_lib + $lib)
|
$link_args += ($f_link_lib + $lib)
|
||||||
|
|||||||
+163
-77
@@ -10,7 +10,7 @@
|
|||||||
--- * **Word-count loader** (`load_word_counts` for `WORD_COUNT(...)` metadata files).
|
--- * **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).
|
--- * **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`,
|
--- * **Domain tables** (`TAPE_ATOM_MACROS`, `GTE_PIPELINE_LATENCY`, `GP0_CMD_SIZE`, `GP0_CMD_BY_SHAPE`,
|
||||||
--- `GP0_MACRO_CONTRIB`, `INSTRUCTION_LATENCY`).
|
--- `INSTRUCTION_LATENCY`).
|
||||||
---
|
---
|
||||||
--- **Conventions**: tabs (1/level), EmmyLua annotations, no regex.
|
--- **Conventions**: tabs (1/level), EmmyLua annotations, no regex.
|
||||||
|
|
||||||
@@ -920,18 +920,26 @@ 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
|
||||||
|
-- Line-comment '// ... \n' (0x2F 0x2F): skip to (and past) the next newline, or to end-of-body.
|
||||||
|
if c == BYTE_SLASH and body:byte(scan + 1) == BYTE_SLASH then
|
||||||
|
local nl = M.find_byte(body, BYTE_NEWLINE, scan)
|
||||||
|
scan = nl and (nl + 1) or (len + 1)
|
||||||
|
-- Block-comment '/* ... */' (0x2F 0x2A): skip to (and past) the matching '*/', or to end-of-body.
|
||||||
|
elseif c == BYTE_SLASH and body:byte(scan + 1) == BYTE_STAR then
|
||||||
|
local close = body:find("*/", scan + 2, true)
|
||||||
|
scan = close and (close + 2) or (len + 1)
|
||||||
-- Group opener bytes (consume the balanced group via the matching reader): '(' = 0x28, '{' = 0x7B, '[' = 0x5B.
|
-- Group opener bytes (consume the balanced group via the matching reader): '(' = 0x28, '{' = 0x7B, '[' = 0x5B.
|
||||||
if c == BYTE_OPEN_PAREN then local _, a = M.read_parens (body, scan); scan = a
|
elseif c == BYTE_OPEN_PAREN then local _, a = M.read_parens (body, scan); scan = a
|
||||||
elseif c == BYTE_OPEN_BRACE then local _, a = M.read_braces (body, scan); scan = a
|
elseif c == BYTE_OPEN_BRACE then local _, a = M.read_braces (body, scan); scan = a
|
||||||
elseif c == BYTE_OPEN_BRACK then local _, a = M.read_brackets (body, scan); scan = a
|
elseif c == BYTE_OPEN_BRACK then local _, a = M.read_brackets (body, scan); scan = a
|
||||||
-- String-literal byte ('"' = 0x22 or '\'' = 0x27): skip past the quoted region in one shot.
|
-- String-literal byte ('"' = 0x22 or '\'' = 0x27): skip past the quoted region in one shot.
|
||||||
elseif c == BYTE_DQUOTE or c == BYTE_SQUOTE then
|
elseif c == BYTE_DQUOTE or c == BYTE_SQUOTE then
|
||||||
scan = M.skip_str_or_cmt(body, scan) + 1
|
scan = (M.skip_str_or_cmt(body, scan) or scan) + 1
|
||||||
else
|
else
|
||||||
scan = scan + 1
|
scan = scan + 1
|
||||||
end
|
end
|
||||||
@@ -1316,13 +1324,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_u"] = {1, 2, 3},
|
["and"] = {1, 2, 3},
|
||||||
["or_i"] = {1, 2},
|
["or_i"] = {1, 2},
|
||||||
["or_i_self"] = {1},
|
["or_i_self"] = {1},
|
||||||
["or_u"] = {1, 2, 3},
|
["or"] = {1, 2, 3},
|
||||||
["or_u_self"] = {1, 2},
|
["or_self"] = {1, 2},
|
||||||
["xor_i"] = {1, 2},
|
["xor_i"] = {1, 2},
|
||||||
["xor_u"] = {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},
|
||||||
@@ -1422,33 +1430,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:
|
||||||
@@ -1465,18 +1447,25 @@ M.GP0_MACRO_CONTRIB = {
|
|||||||
-- 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 (most inputs become
|
||||||
-- safe to clobber after 0-4 cycles).
|
-- 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,
|
||||||
["nop2"] = 2,
|
["nop2"] = 2,
|
||||||
["add_ui"] = 1, ["add_ui_self"] = 1,
|
["add_ui"] = 1, ["add_ui_self"] = 1,
|
||||||
["add_s"] = 1, ["add_si"] = 1,
|
["add_s"] = 1, ["add_si"] = 1,
|
||||||
["add_u"] = 1, ["add_u_self"] = 1,
|
["add_u"] = 1, ["add_u_self"] = 1,
|
||||||
["sub_u"] = 1, ["sub_s"] = 1,
|
["sub_u"] = 1, ["sub_s"] = 1,
|
||||||
["and_i"] = 1, ["and_u"] = 1,
|
["and_i"] = 1, ["and"] = 1,
|
||||||
["or_i"] = 1, ["or_i_self"] = 1,
|
["or_i"] = 1, ["or_i_self"] = 1,
|
||||||
["or_u"] = 1, ["or_u_self"] = 1,
|
["or_u"] = 1, ["or_u_self"] = 1,
|
||||||
["xor_i"] = 1, ["xor_u"] = 1,
|
["xor_i"] = 1, ["xor_u"] = 1,
|
||||||
["nor_u"] = 1,
|
["nor_u"] = 1,
|
||||||
["shift_lleft"] = 1, ["shift_lleft_self"] = 1,
|
["shift_lleft"] = 1, ["shift_lleft_self"] = 1,
|
||||||
["shift_lright"] = 1,
|
["shift_lright"] = 1,
|
||||||
@@ -1514,6 +1503,9 @@ M.INSTRUCTION_LATENCY = {
|
|||||||
["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,
|
||||||
@@ -1560,22 +1552,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,
|
||||||
@@ -1724,22 +1700,45 @@ 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.
|
||||||
{
|
{
|
||||||
@@ -1952,7 +1951,7 @@ M.GPR_VALUE_RULES = {
|
|||||||
-- Present register-form self variants. They are included here so a
|
-- Present register-form self variants. They are included here so a
|
||||||
-- known value is not needlessly lost when these encoders are used.
|
-- known value is not needlessly lost when these encoders are used.
|
||||||
add_u_self = { op = "add_u", dest = 1, sources = {1, 2}, },
|
add_u_self = { op = "add_u", dest = 1, sources = {1, 2}, },
|
||||||
or_u_self = { op = "or_u", dest = 1, sources = {1, 2}, },
|
or_u_self = { op = "or", dest = 1, sources = {1, 2}, },
|
||||||
shift_lleft_self = { op = "shift_lleft", dest = 1, source = 1, immediate = 2, },
|
shift_lleft_self = { op = "shift_lleft", dest = 1, source = 1, immediate = 2, },
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2204,10 +2203,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,
|
||||||
@@ -2217,17 +2221,77 @@ 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.
|
||||||
@@ -2253,10 +2317,20 @@ local function _project_emission_inner(root_body_entry, ctx_table)
|
|||||||
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
|
||||||
else emit_marker("offset", args[1] or "", args[2] or "", tok_line)
|
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, nil, nil, consuming_encoder, arg_pos)
|
||||||
end
|
end
|
||||||
pos = after_paren
|
pos = after_paren
|
||||||
::continue_loop::
|
::continue_loop::
|
||||||
@@ -2383,9 +2457,21 @@ 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` is the within-atom-safe
|
||||||
|
-- unconditional jump alias from `code/duffle/mips.h`; the C preprocessor expands it BEFORE
|
||||||
|
-- the metaprogram sees the source, but the raw token ident is still `jump_rel` here.
|
||||||
|
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.
|
||||||
|
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
|
||||||
|
|||||||
+133
-14
@@ -4,7 +4,7 @@
|
|||||||
--- Scanner owns `declaration_comment` and `debug_skip` on each declaration record; this pass projects both forward.
|
--- Scanner owns `declaration_comment` and `debug_skip` on each declaration record; this pass projects both forward.
|
||||||
---
|
---
|
||||||
--- Reads the pre-scanned SourceScan payload from `duffle.scan_source` for `MipsAtomComp_(ac_X)` and `MipsAtomComp_Proc_(ac_X, { body })` declarations,
|
--- 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_ MipsAtom 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 `<dir_basename>.macs.h` per source with `#define mac_X(sig) \` macros plus `WORD_COUNT(mac_X, N)` entries for downstream offset computation.
|
||||||
---
|
---
|
||||||
@@ -29,7 +29,7 @@ local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
|
|||||||
|
|
||||||
-- Atom component declaration identifiers.
|
-- Atom component declaration identifiers.
|
||||||
local ATOM_COMP_PROC = "MipsAtomComp_Proc_"
|
local ATOM_COMP_PROC = "MipsAtomComp_Proc_"
|
||||||
local MIPS_ATOM = "MipsAtom" -- prefix on the function declaration that wraps an AtomComp_Proc_
|
local MIPS_ATOM = "Slice_MipsCode" -- prefix on the function declaration that wraps an AtomComp_Proc_
|
||||||
|
|
||||||
-- Component-name prefixes.
|
-- Component-name prefixes.
|
||||||
local AC_PREFIX = "ac_" -- arg to MipsAtomComp_(ac_X); the X is the atom name
|
local AC_PREFIX = "ac_" -- arg to MipsAtomComp_(ac_X); the X is the atom name
|
||||||
@@ -97,9 +97,9 @@ local M = {}
|
|||||||
--- Returns the args string (e.g., `"U4 off, U4 code, U1 r, U1 g, U1 b"`) or nil if no function declaration is found.
|
--- 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
|
--- Convention: function form is
|
||||||
--- `FI_ MipsAtom ac_X(args) MipsAtomComp_Proc_(ac_X, { body })`
|
--- `FI_ Slice_MipsCode ac_X(args) MipsAtomComp_Proc_(ac_X, { body })`
|
||||||
--- We find the LAST occurrence of `"ac_X("` before `before_pos` and extract the args from inside the parens.
|
--- We find the LAST occurrence of `"ac_X("` before `before_pos` and extract the args from inside the parens.
|
||||||
--- We then verify the preceding context ends with `MipsAtom`
|
--- We then verify the preceding context ends with `Slice_MipsCode`
|
||||||
--- (the function-decl keyword with possible qualifiers between).
|
--- (the function-decl keyword with possible qualifiers between).
|
||||||
---
|
---
|
||||||
--- @param source string
|
--- @param source string
|
||||||
@@ -339,6 +339,118 @@ 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
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
@@ -534,25 +646,29 @@ 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.
|
||||||
@@ -632,12 +748,15 @@ function M.run(ctx)
|
|||||||
-- Use `corpus.word_counts` so the recursive lookup sees both authored-metadata entries
|
-- Use `corpus.word_counts` so the recursive lookup sees both authored-metadata entries
|
||||||
-- (loaded by word_count_eval.run) AND same-source component entries (populated earlier in this loop by `update_canonical_word_counts`).
|
-- (loaded by word_count_eval.run) AND same-source component entries (populated earlier in this loop by `update_canonical_word_counts`).
|
||||||
local counts = count_all_components(components, corpus.word_counts)
|
local counts = count_all_components(components, corpus.word_counts)
|
||||||
|
-- Derive cycle_cost + gp0_contrib from the original `MipsAtomComp_` body tokens
|
||||||
|
-- (NOT from the generated `mac_*` variants — those are written to disk above).
|
||||||
|
local metadata = compute_components_metadata(components, duffle.INSTRUCTION_LATENCY)
|
||||||
local macs_path = emit_component_macros_h(ctx, src, components, counts)
|
local macs_path = emit_component_macros_h(ctx, src, 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 (so the byte-identical `.macs.h` contract is preserved before any current-count mutation).
|
||||||
update_canonical_word_counts(corpus, components, counts)
|
update_canonical_word_counts(corpus, components, counts)
|
||||||
update_canonical_components(corpus, src, components)
|
update_canonical_components(corpus, src, components, metadata)
|
||||||
update_canonical_component_body_index(corpus, src, components, src.scan)
|
update_canonical_component_body_index(corpus, src, components, src.scan)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -73,10 +73,6 @@ 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/`
|
|
||||||
-- (red of the
|
|
||||||
-- `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_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 _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
|
||||||
@@ -840,12 +836,15 @@ 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_word(...)` call or other statement).
|
--- statements (each entry is a single `load_*` call or other statement).
|
||||||
--- @param body_tokens table[] -- the atom's pre-tokenized body statements (from atom.body_tokens)
|
--- @param 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 +852,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)
|
||||||
|
|||||||
+41
-13
@@ -57,10 +57,12 @@ local OFFSET_MACRO_COL = 44
|
|||||||
--- @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 -- the 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 -- the 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 -- the 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
|
||||||
@@ -72,7 +74,7 @@ local OFFSET_MACRO_COL = 44
|
|||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
-- 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 +82,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,
|
||||||
}
|
}
|
||||||
@@ -104,7 +108,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.
|
||||||
|
---
|
||||||
|
--- 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 labels table<string, integer>
|
||||||
--- @param branches table[]
|
--- @param branches table[]
|
||||||
--- @return BranchOffset[]
|
--- @return BranchOffset[]
|
||||||
@@ -115,11 +131,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
|
||||||
|
|||||||
+492
-100
@@ -23,7 +23,9 @@
|
|||||||
--- 4. Binding handoff: Every `atom_bind(Binds_X)` must reference a `typedef Struct_(Binds_X) { ... }` declaration.
|
--- 4. Binding handoff: Every `atom_bind(Binds_X)` must reference a `typedef Struct_(Binds_X) { ... }` declaration.
|
||||||
--- 5. GPU Port-Store Shape: Per-shape (`f3`/`f4`/`g4`/etc.) the sum of `mac_format_X_color` + `mac_gte_store_X_*` + `mac_insert_ot_tag_X` words
|
--- 5. GPU Port-Store Shape: Per-shape (`f3`/`f4`/`g4`/etc.) the sum of `mac_format_X_color` + `mac_gte_store_X_*` + `mac_insert_ot_tag_X` words
|
||||||
--- must equal the GP0 cmd's expected packet size.
|
--- must equal the GP0 cmd's expected packet size.
|
||||||
--- 6. Per-Atom Cycle Budget: Sum each atom body's instruction latencies (per `duffle.INSTRUCTION_LATENCY`); report total.
|
--- 6. Per-Atom Cycle Budget: Sum each atom body's instruction latencies — non-`mac_*` tokens look up `duffle.INSTRUCTION_LATENCY[ident]`;
|
||||||
|
--- `mac_*` tokens look up `pipe_ctx.components_by_name[bare_name].cycle_cost` (auto-derived from the original `MipsAtomComp_` body by `passes/components.lua::compute_components_metadata`).
|
||||||
|
--- Report total.
|
||||||
---
|
---
|
||||||
--- Per-source rules (registry-driven):
|
--- Per-source rules (registry-driven):
|
||||||
--- 8. enum_alias_membership: Every `R_X` referenced from `atom_dbg_reg_default`, `atom_reg_types`, `atom_type(...)`, `atom_reads`, or `atom_writes`
|
--- 8. enum_alias_membership: Every `R_X` referenced from `atom_dbg_reg_default`, `atom_reg_types`, `atom_type(...)`, `atom_reads`, or `atom_writes`
|
||||||
@@ -122,7 +124,7 @@ local OUTPUT_EXTENSION = ".static_analysis.txt"
|
|||||||
--- @field info table[] -- finding-level info (kind == "info"); distinct from per-source scanned/cycles summary rows
|
--- @field info table[] -- finding-level info (kind == "info"); distinct from per-source scanned/cycles summary rows
|
||||||
|
|
||||||
--- @alias AtomName string -- lower_snake_case atom nameMacroName string -- lower_snake_case macro identifier
|
--- @alias AtomName string -- lower_snake_case atom nameMacroName string -- lower_snake_case macro identifier
|
||||||
--- @alias CheckName string -- "transfer_hazards" | "control_transfer_delay_slot_use" | "mac_yield_uniformity" | "abi_handoff" | "gpu_portstore_shape" | "per_atom_cycle_budget" | "enum_alias_membership" | "atom_type_consistency" | "binds_no_substruct_deref"
|
--- @alias CheckName string -- "transfer_hazards" | "control_transfer_delay_slot_use" | "mac_yield_uniformity" | "yield_load_tail_pairing" | "abi_handoff" | "gpu_portstore_shape" | "per_atom_cycle_budget" | "enum_alias_membership" | "atom_type_consistency" | "binds_no_substruct_deref"
|
||||||
|
|
||||||
--- @class AtomBody
|
--- @class AtomBody
|
||||||
--- @field line integer -- source line of the atom declaration
|
--- @field line integer -- source line of the atom declaration
|
||||||
@@ -191,15 +193,21 @@ end
|
|||||||
--
|
--
|
||||||
-- The classification is stored on `atom.paths.tok_class` as an array indexed by token index (1..#tokens).
|
-- The classification is stored on `atom.paths.tok_class` as an array indexed by token index (1..#tokens).
|
||||||
-- Each entry has:
|
-- Each entry has:
|
||||||
-- ident — the leading identifier (e.g. "load_word", "gte_cmdw_rtpt", "nop", "mac_yield")
|
-- ident — the leading identifier (e.g. "load_word", "gte_cmdw_rtpt", "nop", "mac_yield")
|
||||||
-- nop_words — 0 / 1 / 2 (for "nop" / "nop2" / anything else)
|
-- nop_words — 0 / 1 / 2 (for "nop" / "nop2" / anything else)
|
||||||
-- nop_prefix — consecutive nop words ending just BEFORE this token (forward-pass pre-compute;
|
-- nop_prefix — consecutive nop words ending just BEFORE this token (forward-pass pre-compute;
|
||||||
-- makes preceding-nop lookup O(N))
|
-- makes preceding-nop lookup O(N))
|
||||||
-- is_yield — true if this token is `mac_yield` or `mac_yield(...)`
|
-- is_yield — true if this token is `mac_yield` or `mac_yield(...)`
|
||||||
-- is_atom_label — true if this token is `atom_label(name)`; label_name has the name
|
-- is_atom_label — true if this token is `atom_label(name)`; label_name has the name
|
||||||
-- is_branch — true if this token is `branch_*(...)`; branch_label has the label or false
|
-- is_branch — true if this token is `branch_*(...)` OR an unconditional-jump-with-offset (`jump(off)` / `call_addr(off)`); branch_label has the target label or false
|
||||||
-- is_load_word — true if this token starts with `load_word(`
|
-- is_unconditional_jump — true if this token is `jump` or `call_addr` (BD slot + single successor — taken only; no fall-through).
|
||||||
-- is_store_word — true if this token starts with `store_word(`
|
-- Mutually exclusive with the conditional-branch semantics; combined with `is_branch` above.
|
||||||
|
-- is_terminal_jump — true if this token is `jump_reg` / `call_reg` / `jump_link` (transfers control OUT of the current atom; the `mac_yield()` handshake ends in `jump_reg(R_AtomJmp), nop`).
|
||||||
|
-- No offset field — `atom_offset` is invalid here. Terminates the current path in the CFG.
|
||||||
|
-- is_load — true if this token starts with any of: load_word, load_half, load_half_u, load_byte,
|
||||||
|
-- load_byte_u, gte_lw, gte_lwc2. These all have MIPS load-delay semantics (the
|
||||||
|
-- destination register is volatile for 1 word after the load).
|
||||||
|
-- is_store_word — true if this token starts with `store_word(`
|
||||||
--
|
--
|
||||||
-- Checks that need the leading ident use `tok_class.ident` instead of re-matching the token string.
|
-- Checks that need the leading ident use `tok_class.ident` instead of re-matching the token string.
|
||||||
-- Checks that need "how many nops before token i" use `tok_class.nop_prefix` instead of walking backwards.
|
-- Checks that need "how many nops before token i" use `tok_class.nop_prefix` instead of walking backwards.
|
||||||
@@ -211,9 +219,11 @@ end
|
|||||||
--- @field is_yield boolean
|
--- @field is_yield boolean
|
||||||
--- @field is_atom_label boolean
|
--- @field is_atom_label boolean
|
||||||
--- @field label_name string|nil -- for atom_label(name)
|
--- @field label_name string|nil -- for atom_label(name)
|
||||||
--- @field is_branch boolean
|
--- @field is_branch boolean -- conditional branch OR unconditional-jump-with-offset
|
||||||
--- @field branch_label string|false|nil -- for branch_*(..., atom_offset(F, label))
|
--- @field is_unconditional_jump boolean -- `jump` / `call_addr` only
|
||||||
--- @field is_load_word boolean
|
--- @field is_terminal_jump boolean -- `jump_reg` / `call_reg` / `jump_link` only
|
||||||
|
--- @field branch_label string|false|nil -- for branch_*(..., atom_offset(F, label)) OR jump/call_addr
|
||||||
|
--- @field is_load boolean -- load_word | load_half | load_half_u | load_byte | load_byte_u | gte_lw | gte_lwc2
|
||||||
--- @field is_store_word boolean
|
--- @field is_store_word boolean
|
||||||
--- @field mac_format_shape string|nil -- "f3" / "g4" etc. for mac_format_X_color; nil otherwise
|
--- @field mac_format_shape string|nil -- "f3" / "g4" etc. for mac_format_X_color; nil otherwise
|
||||||
--- @field is_gte_store boolean -- ident matches `mac_gte_store_<shape>`
|
--- @field is_gte_store boolean -- ident matches `mac_gte_store_<shape>`
|
||||||
@@ -224,12 +234,38 @@ end
|
|||||||
--- @field o_arg2 string|nil -- second arg of O_(<a>, <b>) captures
|
--- @field o_arg2 string|nil -- second arg of O_(<a>, <b>) captures
|
||||||
--- @field s_arg1 string|nil -- arg of S_(<a>) captures; nil for non-S_ tokens
|
--- @field s_arg1 string|nil -- arg of S_(<a>) captures; nil for non-S_ tokens
|
||||||
|
|
||||||
|
-- The set of MIPS instruction idents that have a load-delay slot.
|
||||||
|
-- Per MIPS I R3000A: `lw`, `lh`, `lhu`, `lb`, `lbu`, `lwc2` (gte_lw).
|
||||||
|
-- Note: `lui` (load_upper_i) does NOT have a load delay on MIPS I — it's an ALU op, not a load.
|
||||||
|
-- The `load_imm_*` macros are lui + ori sequences with no per-component load delay either.
|
||||||
|
local LOAD_INSTRUCTION_IDENTS = {
|
||||||
|
load_word = true,
|
||||||
|
load_half = true,
|
||||||
|
load_half_u = true,
|
||||||
|
load_byte = true,
|
||||||
|
load_byte_u = true,
|
||||||
|
gte_lw = true,
|
||||||
|
gte_lwc2 = true,
|
||||||
|
}
|
||||||
|
|
||||||
-- Patterns for O_(<arg1>, <arg2>) and S_(<arg>) captures.
|
-- Patterns for O_(<arg1>, <arg2>) and S_(<arg>) captures.
|
||||||
-- UNANCHORED, the substring can appea anywhere in the token (e.g., `load_word(R_T0, R_TapePtr, O_(Binds_X, field))` matches at position ~24).
|
-- UNANCHORED, the substring can appea anywhere in the token (e.g., `load_word(R_T0, R_TapePtr, O_(Binds_X, field))` matches at position ~24).
|
||||||
-- The binds_name match is deferred to check_abi_handoff (which compares tc.o_arg1 == atom.info.binds).
|
-- The binds_name match is deferred to check_abi_handoff (which compares tc.o_arg1 == atom.info.binds).
|
||||||
local O_PATTERN = "O_%(([%w_]+),%s*([%w_]+)%s*%)"
|
local O_PATTERN = "O_%(([%w_]+),%s*([%w_]+)%s*%)"
|
||||||
local S_PATTERN = "S_%(([%w_]+)%s*%)"
|
local S_PATTERN = "S_%(([%w_]+)%s*%)"
|
||||||
|
|
||||||
|
-- Ident patterns for control-transfer instruction kinds:
|
||||||
|
-- * `branch_*` (conditional): `branch_equal`, `branch_ne`, `branch_lt_zero`, `branch_ge_zero`, `branch_le_zero`, `branch_gt_zero`.
|
||||||
|
-- * `jump` / `call_addr` (unconditional absolute): one immediate offset field; can carry `atom_offset(F, T)`.
|
||||||
|
-- * `jump_reg` / `call_reg` / `jump_link` (register-form): no offset field; `atom_offset` is invalid; transfers OUT of the current atom.
|
||||||
|
local BRANCH_PATTERN = "^branch_[%w_]+%s*%("
|
||||||
|
-- `jump_rel(off)` is an ergonomic alias for `branch_equal(R_0, R_0, off)` (the within-atom-safe unconditional jump — see `code/duffle/mips.h`).
|
||||||
|
-- The C preprocessor expands it BEFORE the metaprogram sees the source, but for source-level metadata consistency we still match it here and classify it as a branch_equal.
|
||||||
|
-- This keeps `consuming_encoder` canonical for any downstream tooling that consults the metadata field.
|
||||||
|
local JUMP_REL_PATTERN = "^jump_rel%s*%("
|
||||||
|
local UNCOND_JUMP_PATTERN = "^%f[%w](jump|call_addr)%f[%W]"
|
||||||
|
local TERMINAL_JUMP_PATTERN = "^%f[%w](jump_reg|call_reg|jump_link)%f[%W]"
|
||||||
|
|
||||||
local function classify_tokens(tokens)
|
local function classify_tokens(tokens)
|
||||||
local n = #tokens
|
local n = #tokens
|
||||||
local tc = {}
|
local tc = {}
|
||||||
@@ -241,12 +277,17 @@ local function classify_tokens(tokens)
|
|||||||
if ident == "nop" then nop_words = 1
|
if ident == "nop" then nop_words = 1
|
||||||
elseif ident == "nop2" then nop_words = 2 end
|
elseif ident == "nop2" then nop_words = 2 end
|
||||||
|
|
||||||
local is_yield = ident == "mac_yield"
|
-- `mac_yield_tail` is the canonical end-of-atom terminator when paired with `mac_yield_load`
|
||||||
|
-- in a preceding branch's BD-slot. It runs `addiu_self R_TapePtr; jr R_AtomJmp; nop` — the
|
||||||
|
-- "tail" half of the lego split. Treat it as a yield for the `mac_yield_uniformity` check.
|
||||||
|
local is_yield = ident == "mac_yield" or ident == "mac_yield_tail"
|
||||||
local is_atom_label = false
|
local is_atom_label = false
|
||||||
local label_name = nil
|
local label_name = nil
|
||||||
local is_branch = false
|
local is_branch = false
|
||||||
|
local is_unconditional_jump = false
|
||||||
|
local is_terminal_jump = false
|
||||||
local branch_label = nil
|
local branch_label = nil
|
||||||
local is_load_word = ident == "load_word"
|
local is_load = LOAD_INSTRUCTION_IDENTS[ident] == true
|
||||||
local is_store_word = ident == "store_word"
|
local is_store_word = ident == "store_word"
|
||||||
|
|
||||||
-- Per-check pre-computes (R3 lift).
|
-- Per-check pre-computes (R3 lift).
|
||||||
@@ -262,9 +303,21 @@ local function classify_tokens(tokens)
|
|||||||
if ident == "atom_label" then
|
if ident == "atom_label" then
|
||||||
is_atom_label = true
|
is_atom_label = true
|
||||||
label_name = tok:match("^atom_label%s*%(%s*([%w_]+)%s*%)")
|
label_name = tok:match("^atom_label%s*%(%s*([%w_]+)%s*%)")
|
||||||
elseif tok:match("^branch_[%w_]+%s*%(") then
|
elseif tok:match(BRANCH_PATTERN) or tok:match(JUMP_REL_PATTERN) then
|
||||||
|
-- Conditional branch OR `jump_rel` (the within-atom-safe unconditional jump alias).
|
||||||
|
-- Both encode a 16-bit signed relative word offset.
|
||||||
is_branch = true
|
is_branch = true
|
||||||
branch_label = tok:match("atom_offset%s*%([^,]+,%s*([%w_]+)%s*%)") or false
|
branch_label = tok:match("atom_offset%s*%([^,]+,%s*([%w_]+)%s*%)") or false
|
||||||
|
elseif tok:match(UNCOND_JUMP_PATTERN) then
|
||||||
|
-- Unconditional absolute jump / call: `jump(off)` / `call_addr(off)`.
|
||||||
|
-- One immediate offset field; can carry an `atom_offset(F, T)` marker (the offsets pass dispatches on `consuming_encoder` — see `passes/offsets.lua::compute_offsets`).
|
||||||
|
is_branch = true
|
||||||
|
is_unconditional_jump = true
|
||||||
|
branch_label = tok:match("atom_offset%s*%([^,]+,%s*([%w_]+)%s*%)") or false
|
||||||
|
elseif tok:match(TERMINAL_JUMP_PATTERN) then
|
||||||
|
-- Register-form jump / call: no offset field; `atom_offset` is invalid here (the offsets pass will error if one is supplied).
|
||||||
|
-- Transfers control OUT of the current atom — the CFG treats this as a path terminator.
|
||||||
|
is_terminal_jump = true
|
||||||
end
|
end
|
||||||
|
|
||||||
-- mac_format_X_color / mac_gte_store_<shape> / mac_insert_ot_tag_<shape> (used by check_gpu_portstore_shape).
|
-- mac_format_X_color / mac_gte_store_<shape> / mac_insert_ot_tag_<shape> (used by check_gpu_portstore_shape).
|
||||||
@@ -283,24 +336,26 @@ local function classify_tokens(tokens)
|
|||||||
if is_store_word and tok:find("R_PrimCursor", 1, true) then writes_r_prim_cursor = true end
|
if is_store_word and tok:find("R_PrimCursor", 1, true) then writes_r_prim_cursor = true end
|
||||||
|
|
||||||
tc[tok_idx] = {
|
tc[tok_idx] = {
|
||||||
ident = ident,
|
ident = ident,
|
||||||
nop_words = nop_words,
|
nop_words = nop_words,
|
||||||
nop_prefix = nop_run,
|
nop_prefix = nop_run,
|
||||||
is_yield = is_yield,
|
is_yield = is_yield,
|
||||||
is_atom_label = is_atom_label,
|
is_atom_label = is_atom_label,
|
||||||
label_name = label_name,
|
label_name = label_name,
|
||||||
is_branch = is_branch,
|
is_branch = is_branch,
|
||||||
branch_label = branch_label,
|
is_unconditional_jump = is_unconditional_jump,
|
||||||
is_load_word = is_load_word,
|
is_terminal_jump = is_terminal_jump,
|
||||||
is_store_word = is_store_word,
|
branch_label = branch_label,
|
||||||
mac_format_shape = mac_format_shape,
|
is_load = is_load,
|
||||||
is_gte_store = is_gte_store,
|
is_store_word = is_store_word,
|
||||||
is_ot_tag = is_ot_tag,
|
mac_format_shape = mac_format_shape,
|
||||||
writes_r_prim_cursor = writes_r_prim_cursor,
|
is_gte_store = is_gte_store,
|
||||||
reads_r_tape_ptr = reads_r_tape_ptr,
|
is_ot_tag = is_ot_tag,
|
||||||
o_arg1 = o_arg1,
|
writes_r_prim_cursor = writes_r_prim_cursor,
|
||||||
o_arg2 = o_arg2,
|
reads_r_tape_ptr = reads_r_tape_ptr,
|
||||||
s_arg1 = s_arg1,
|
o_arg1 = o_arg1,
|
||||||
|
o_arg2 = o_arg2,
|
||||||
|
s_arg1 = s_arg1,
|
||||||
}
|
}
|
||||||
-- Advance the nop run for the NEXT token.
|
-- Advance the nop run for the NEXT token.
|
||||||
if nop_words > 0 then nop_run = nop_run + nop_words
|
if nop_words > 0 then nop_run = nop_run + nop_words
|
||||||
@@ -346,6 +401,18 @@ end
|
|||||||
-- therefore counts ONLY words strictly between the producer and the consumer.
|
-- therefore counts ONLY words strictly between the producer and the consumer.
|
||||||
-- ─────────────────────────────────────────────────────────────────────────
|
-- ─────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
-- True iff `consumer_event` is a GTE command (gte_cmdw_* or one of the human-readable aliases
|
||||||
|
-- mapped in `duffle.GTE_COMMAND_ALIASES`). Used by the LWC2 retirement-regime dispatch in the
|
||||||
|
-- forward walker: a GTE-command consumer can read the LWC2 result in the very next slot (the GTE
|
||||||
|
-- pipeline latches the LWC2 data); any other consumer must observe the standard MIPS load delay
|
||||||
|
-- (gap >= 1).
|
||||||
|
local function is_gte_command(consumer_event)
|
||||||
|
local tok = consumer_event.encoder or consumer_event.ident or ""
|
||||||
|
if tok:sub(1, 9) == "gte_cmdw_" then return true end
|
||||||
|
local aliases = duffle.GTE_COMMAND_ALIASES or {}
|
||||||
|
return aliases[tok] ~= nil
|
||||||
|
end
|
||||||
|
|
||||||
-- True iff `consumer_word` falls inside the COP2 command's input set OR inside the producer's `fanout_to` set (for IRGB writes).
|
-- True iff `consumer_word` falls inside the COP2 command's input set OR inside the producer's `fanout_to` set (for IRGB writes).
|
||||||
-- Used by the consumer-match step of the forward walker.
|
-- Used by the consumer-match step of the forward walker.
|
||||||
local function is_cop2_consumer_of(consumer_event, destination, producer_rel)
|
local function is_cop2_consumer_of(consumer_event, destination, producer_rel)
|
||||||
@@ -381,8 +448,7 @@ local function is_cop2_consumer_of(consumer_event, destination, producer_rel)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- True iff `consumer_event` reads the GPR operand at any position the destination register occupies.
|
-- True iff `consumer_event` reads the GPR operand at any position the destination register occupies.
|
||||||
-- The read-position lookup consults `duffle.OPERAND_READ_POSITIONS`
|
-- The read-position lookup consults `duffle.OPERAND_READ_POSITIONS` for the consumer's encoder and walks each `args[pos]` to find an operand-equal match.
|
||||||
-- for the consumer's encoder and walks each `args[pos]` to find an operand-equal match.
|
|
||||||
local function is_gpr_consumer_of(consumer_event, destination)
|
local function is_gpr_consumer_of(consumer_event, destination)
|
||||||
local consumer_token = consumer_event.encoder or consumer_event.ident
|
local consumer_token = consumer_event.encoder or consumer_event.ident
|
||||||
local read_pos = duffle.OPERAND_READ_POSITIONS or {}
|
local read_pos = duffle.OPERAND_READ_POSITIONS or {}
|
||||||
@@ -449,8 +515,8 @@ local function shift_left_u4(value, amount)
|
|||||||
return wrap_u4(value * (2 ^ amount))
|
return wrap_u4(value * (2 ^ amount))
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Resolve only a standalone integer literal. Compound C expressions remain
|
-- Resolve only a standalone integer literal.
|
||||||
-- unknown by design; the analyzer must not pretend to be a C evaluator.
|
-- Compound C expressions remain unknown by design; the analyzer must not pretend to be a C evaluator.
|
||||||
local function parse_integer_literal(raw)
|
local function parse_integer_literal(raw)
|
||||||
if type(raw) ~= "string" then return nil end
|
if type(raw) ~= "string" then return nil end
|
||||||
raw = duffle.trim(raw)
|
raw = duffle.trim(raw)
|
||||||
@@ -763,9 +829,17 @@ local function analyze_hardware_relations(atom)
|
|||||||
local relation = prod.relation
|
local relation = prod.relation
|
||||||
local semantic = relation.semantic
|
local semantic = relation.semantic
|
||||||
local is_match = false
|
local is_match = false
|
||||||
if semantic == "MTC2" or semantic == "CTC2" or semantic == "LWC2" then
|
if semantic == "MTC2" or semantic == "CTC2" or semantic == "LWC2_to_GTE" or semantic == "LWC2_to_other" then
|
||||||
-- Consumer is a GTE command whose input set contains the producer's COP2 destination (or a fan-out target).
|
-- Consumer is a GTE command whose input set contains the producer's COP2 destination (or a fan-out target).
|
||||||
is_match = is_cop2_consumer_of(ev, prod.destination, relation)
|
-- LWC2_to_GTE — GTE-command consumer: gap = 0 OK (the pipeline latches the LWC2 result).
|
||||||
|
-- LWC2_to_other — non-GTE consumer: standard load delay applies.
|
||||||
|
if relation.id == "lwc2_to_gte_command" then
|
||||||
|
is_match = is_gte_command(ev) and is_cop2_consumer_of(ev, prod.destination, relation)
|
||||||
|
elseif relation.id == "lwc2_to_other_consumer" then
|
||||||
|
is_match = (not is_gte_command(ev)) and is_cop2_consumer_of(ev, prod.destination, relation)
|
||||||
|
else
|
||||||
|
is_match = is_cop2_consumer_of(ev, prod.destination, relation)
|
||||||
|
end
|
||||||
elseif semantic == "MFC2" or semantic == "CFC2" or semantic == "MFC0" then
|
elseif semantic == "MFC2" or semantic == "CFC2" or semantic == "MFC0" then
|
||||||
-- Consumer is any encoder that reads the producer's GPR destination as an operand.
|
-- Consumer is any encoder that reads the producer's GPR destination as an operand.
|
||||||
is_match = is_gpr_consumer_of(ev, prod.destination)
|
is_match = is_gpr_consumer_of(ev, prod.destination)
|
||||||
@@ -1024,7 +1098,9 @@ end
|
|||||||
-- A subsequent MFC2 (or any encoder that reads a C2 register) that picks the WRONG register for the active role emits a `result_role_mismatch` warning.
|
-- A subsequent MFC2 (or any encoder that reads a C2 register) that picks the WRONG register for the active role emits a `result_role_mismatch` warning.
|
||||||
-- For example, reading `C2_SXY0` after RTPS is wrong: the `latest_screen_xy` role is `C2_SXY2`.
|
-- For example, reading `C2_SXY0` after RTPS is wrong: the `latest_screen_xy` role is `C2_SXY2`.
|
||||||
--
|
--
|
||||||
-- Note: the OLD `gte_result_position` check also emitted table-gap info findings for `_post_<cmd>` components missing a row in `duffle.GTE_COMPONENT_RESULT_CONTRACTS`. That table-gap check was based on the `_post_<cmd>` NAMING convention rather than hardware truth, and was removed (the user did not want naming to encode ordering semantics; a proper `atom_info` directive for ordering semantics is a future TODO).
|
-- Note: the OLD `gte_result_position` check also emitted table-gap info findings for `_post_<cmd>` components missing a row in `duffle.GTE_COMPONENT_RESULT_CONTRACTS`.
|
||||||
|
-- That table-gap check was based on the `_post_<cmd>` NAMING convention rather than hardware truth, and was removed
|
||||||
|
-- (the user did not want naming to encode ordering semantics; A proper `atom_info` directive for ordering semantics is a future TODO).
|
||||||
--
|
--
|
||||||
-- The first `transfer_hazards` reader comment above records the projection contract.
|
-- The first `transfer_hazards` reader comment above records the projection contract.
|
||||||
-- ─────────────────────────────────────────────────────────────────────────
|
-- ─────────────────────────────────────────────────────────────────────────
|
||||||
@@ -1107,6 +1183,8 @@ end
|
|||||||
local function check_hazard_nop_use(atom, _pipe_ctx, findings)
|
local function check_hazard_nop_use(atom, _pipe_ctx, findings)
|
||||||
local forward = atom.paths and atom.paths.forward_state
|
local forward = atom.paths and atom.paths.forward_state
|
||||||
local events = atom.paths.word_events or {}
|
local events = atom.paths.word_events or {}
|
||||||
|
-- GPR effects table used to resolve load destinations when classifying load-delay-slot nops.
|
||||||
|
local gpr_effects = duffle.INSTRUCTION_GPR_EFFECTS or {}
|
||||||
if not events or #events == 0 then return end
|
if not events or #events == 0 then return end
|
||||||
-- Runtime-helper atoms / components (e.g. tape_exit, ac_yield) carry `debug_skip = true` from the bare
|
-- Runtime-helper atoms / components (e.g. tape_exit, ac_yield) carry `debug_skip = true` from the bare
|
||||||
-- `atom_dbg_skip` marker; their structural nops are part of the fixed handshake and not author choices.
|
-- `atom_dbg_skip` marker; their structural nops are part of the fixed handshake and not author choices.
|
||||||
@@ -1124,9 +1202,10 @@ local function check_hazard_nop_use(atom, _pipe_ctx, findings)
|
|||||||
|
|
||||||
-- Classify the nop BEFORE its event is applied to the pending state.
|
-- Classify the nop BEFORE its event is applied to the pending state.
|
||||||
if ev_ident == "nop" and prev_ev ~= nil then
|
if ev_ident == "nop" and prev_ev ~= nil then
|
||||||
-- Skip BD-slot nops: they are exclusively owned by control_transfer_delay_slot_use.
|
-- Skip BD-slot nops that are exclusively owned by control_transfer_delay_slot_use
|
||||||
|
-- (the nop after a branch/jump — covered by that check separately).
|
||||||
-- Every BD-slot nop is structural; this check never reports on it.
|
-- Every BD-slot nop is structural; this check never reports on it.
|
||||||
-- (The earlier `if not suppressed then is_bd_slot = true end` form inverted the suppression — the `mac_yield()` handshake's `jump_reg(R_AtomJmp)` was incorrectly flagged.)
|
-- (The earlier `if not suppressed then is_bd_slot = true end` form inverted the suppression - the `mac_yield()` handshake's `jump_reg(R_AtomJmp)` was incorrectly flagged.)
|
||||||
local prev_ident = prev_ev.encoder or ""
|
local prev_ident = prev_ev.encoder or ""
|
||||||
local bd_policies = duffle.CONTROL_TRANSFER_DELAY_SLOT_POLICIES or {}
|
local bd_policies = duffle.CONTROL_TRANSFER_DELAY_SLOT_POLICIES or {}
|
||||||
local is_bd_slot = bd_policies[prev_ident] ~= nil
|
local is_bd_slot = bd_policies[prev_ident] ~= nil
|
||||||
@@ -1187,20 +1266,52 @@ local function check_hazard_nop_use(atom, _pipe_ctx, findings)
|
|||||||
else
|
else
|
||||||
-- Track the slot_kind so the BD-separation case can assert the mac_yield handshake is still suppressed.
|
-- Track the slot_kind so the BD-separation case can assert the mac_yield handshake is still suppressed.
|
||||||
local slot_kind = "plain"
|
local slot_kind = "plain"
|
||||||
findings[#findings + 1] = {
|
-- MIPS load-delay slot: a `load_*` wrote a register in the previous slot, and the result
|
||||||
check = "hazard_nop_use",
|
-- is unavailable for 1 cycle. This `nop` is structurally required; classifying it as
|
||||||
kind = "info",
|
-- `modeled-required` is the correct signal (removing it would make the following
|
||||||
atom = atom.name,
|
-- instruction read the OLD value of the loaded register, a load-use hazard). The
|
||||||
line = ev_line,
|
-- `load_delay_violations` check (Concern 3) catches the actual read-side error; here
|
||||||
source = ev.def_path or ev.source or "",
|
-- we suppress the `modeled-redundant` misclassification.
|
||||||
nop_classification = "modeled-redundant",
|
-- The set of load instructions mirrors the LOAD_INSTRUCTION_IDENTS in `check_load_delay_slots`.
|
||||||
nop_word_index = ev_word,
|
local load_idents = { load_word = true, load_half = true, load_half_u = true,
|
||||||
retired_relation = nil,
|
load_byte = true, load_byte_u = true, gte_lw = true, gte_lwc2 = true }
|
||||||
slot_kind = slot_kind,
|
local is_load_delay = load_idents[prev_ident] == true
|
||||||
msg = string.format("%s at line %d: nop at word %d is modeled-redundant (no pending modeled relation)"
|
if is_load_delay then
|
||||||
, atom.name, ev_line, ev_word
|
-- Determine the destination register from the load's `writes` field.
|
||||||
),
|
local prev_writes = gpr_effects[prev_ident] and gpr_effects[prev_ident].writes or {}
|
||||||
}
|
local prev_args = prev_ev.args or {}
|
||||||
|
local load_dest = prev_writes[1] and prev_args[prev_writes[1]] or "<load-destination>"
|
||||||
|
findings[#findings + 1] = {
|
||||||
|
check = "hazard_nop_use",
|
||||||
|
kind = "info",
|
||||||
|
atom = atom.name,
|
||||||
|
line = ev_line,
|
||||||
|
source = ev.def_path or ev.source or "",
|
||||||
|
nop_classification = "modeled-required",
|
||||||
|
nop_word_index = ev_word,
|
||||||
|
retired_relation = "load_delay_slot",
|
||||||
|
producer_destination = load_dest,
|
||||||
|
consumer_token = "<would-be-consumer>",
|
||||||
|
msg = string.format("%s at line %d: nop at word %d is modeled-required (load-delay slot for %s)"
|
||||||
|
, atom.name, ev_line, ev_word, load_dest
|
||||||
|
),
|
||||||
|
}
|
||||||
|
else
|
||||||
|
findings[#findings + 1] = {
|
||||||
|
check = "hazard_nop_use",
|
||||||
|
kind = "info",
|
||||||
|
atom = atom.name,
|
||||||
|
line = ev_line,
|
||||||
|
source = ev.def_path or ev.source or "",
|
||||||
|
nop_classification = "modeled-redundant",
|
||||||
|
nop_word_index = ev_word,
|
||||||
|
retired_relation = nil,
|
||||||
|
slot_kind = slot_kind,
|
||||||
|
msg = string.format("%s at line %d: nop at word %d is modeled-redundant (no pending modeled relation)"
|
||||||
|
, atom.name, ev_line, ev_word
|
||||||
|
),
|
||||||
|
}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -1315,6 +1426,107 @@ local function check_control_transfer_delay_slot_use(atom, pipe_ctx, findings)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
-- Check #1d: load-delay slot violations (per-atom)
|
||||||
|
-- �═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
--- Walk every emitted word event of one atom. For each `is_load` event (lw / lh / lhu / lb / lbu / lwc2),
|
||||||
|
--- mark the destination register as "volatile through" the NEXT emitted slot — MIPS I R3000A load-delay
|
||||||
|
--- semantics. If any subsequent event in that 1-slot window reads the volatile register, emit a `load_delay_violation`
|
||||||
|
--- finding (severity: error — the load result is unavailable in the delay slot).
|
||||||
|
---
|
||||||
|
--- The register becomes non-volatile again at word N+2 (the load has retired), OR sooner if a non-load instruction overwrites the register
|
||||||
|
--- (the overwriter's write is the fresh producer; the load's value is shadowed and never observed by any reader).
|
||||||
|
---
|
||||||
|
--- Runtime-helper atoms / components (`debug_skip == true`) are exempt: their internal load-then-use sequences
|
||||||
|
--- are part of the fixed handshake (e.g. `ac_load_tri_indices` loads into R_T0..R_T2, but those are caller-supplied).
|
||||||
|
---
|
||||||
|
--- The walker reads `duffle.OPERAND_READ_POSITIONS[event.encoder]` to determine which args are read-source
|
||||||
|
--- (the destination of a load is in `writes`, not `reads` — see `duffle.INSTRUCTION_GPR_EFFECTS`).
|
||||||
|
--- The check is purely structural; it does not consult the GPR-value lattice (no constant propagation needed for load-delay detection — the volatility window is unconditional).
|
||||||
|
local function check_load_delay_slots(atom, pipe_ctx, findings)
|
||||||
|
if atom.kind ~= "atom" then return end
|
||||||
|
local events = atom.paths.word_events or {}
|
||||||
|
if #events == 0 then return end
|
||||||
|
if is_runtime_helper(atom) then return end
|
||||||
|
|
||||||
|
local gpr_effects = duffle.INSTRUCTION_GPR_EFFECTS or {}
|
||||||
|
local read_positions = duffle.OPERAND_READ_POSITIONS or {}
|
||||||
|
-- volatile_until[reg] = 1-based word_events index; the slot AFTER which the register is safe.
|
||||||
|
-- `nil` means "not currently volatile".
|
||||||
|
local volatile_until = {}
|
||||||
|
|
||||||
|
-- Compute the "net reads" of an event: read-positions MINUS write-positions.
|
||||||
|
-- A position that is BOTH read and written (e.g. `add_ui rt, rs, imm` where the duffle table lists position 1 as both.
|
||||||
|
-- See `duffle.OPERAND_READ_POSITIONS["add_ui"] = {1, 2}` and `INSTRUCTION_GPR_EFFECTS["add_ui"].writes = {1}` —
|
||||||
|
-- and for genuine RMW ops like `add rt, rs, rt` where position 1 IS both read+written) is not a "read" for load-delay purposes:
|
||||||
|
-- The write shadows whatever value the register previously held. Only positions that are reads WITHOUT a co-occurring write to the same register count as net reads.
|
||||||
|
local function net_reads(event_ident, args)
|
||||||
|
local effect = gpr_effects[event_ident]
|
||||||
|
local positions = read_positions[event_ident]
|
||||||
|
if not positions then return {} end
|
||||||
|
local writes_set = {}
|
||||||
|
if effect and effect.writes then
|
||||||
|
for _, pos in ipairs(effect.writes) do writes_set[pos] = true end
|
||||||
|
end
|
||||||
|
local net = {}
|
||||||
|
for _, pos in ipairs(positions) do
|
||||||
|
if not writes_set[pos] then net[#net + 1] = pos end
|
||||||
|
end
|
||||||
|
return net
|
||||||
|
end
|
||||||
|
|
||||||
|
for event_idx, event in ipairs(events) do
|
||||||
|
local event_ident = event.encoder or event.ident
|
||||||
|
local args = event.args or {}
|
||||||
|
local is_load = LOAD_INSTRUCTION_IDENTS[event_ident] == true
|
||||||
|
|
||||||
|
-- (1) Is this event reading a register that's still volatile from a previous load?
|
||||||
|
-- Skip the load instruction itself (the load's own argument list may "read" its destination via `OPERAND_READ_POSITIONS`:
|
||||||
|
-- e.g. `addiu rt, rs, imm` lists position 1 (rt) as a "read", but rt is the destination; the within-load argument list is not a separate consumer).
|
||||||
|
-- Use `net_reads` to ignore RMW positions (write shadows read within the same instruction).
|
||||||
|
if not is_load then
|
||||||
|
for _, pos in ipairs(net_reads(event_ident, args)) do
|
||||||
|
local reg = args[pos]
|
||||||
|
if type(reg) == "string" and reg:sub(1, 2) == "R_" then
|
||||||
|
local until_idx = volatile_until[reg]
|
||||||
|
if until_idx and event_idx <= until_idx then
|
||||||
|
local ev_line = line_for_word_event(event)
|
||||||
|
findings[#findings + 1] = {
|
||||||
|
atom = atom.name,
|
||||||
|
line = ev_line,
|
||||||
|
check = "load_delay_violation",
|
||||||
|
kind = "error",
|
||||||
|
msg = string.format("%s at line %d reads %s at word %d, but a prior load's "
|
||||||
|
.. "delay slot is not over until word %d; insert a `nop` between the "
|
||||||
|
.. "load and this instruction.",
|
||||||
|
atom.name, ev_line, reg, event_idx, until_idx),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- (2) Update the volatile set based on what this event writes.
|
||||||
|
local effect = gpr_effects[event_ident]
|
||||||
|
if effect and effect.writes then
|
||||||
|
for _, pos in ipairs(effect.writes) do
|
||||||
|
local reg = args[pos]
|
||||||
|
if type(reg) == "string" and reg:sub(1, 2) == "R_" then
|
||||||
|
if is_load then
|
||||||
|
-- Load: destination volatile for exactly 1 slot (the delay slot).
|
||||||
|
volatile_until[reg] = event_idx + 1
|
||||||
|
else
|
||||||
|
-- Non-load write to this register: overwrites shadow the load; the volatile state ends.
|
||||||
|
-- If another reader comes later, it sees the overwriter's value (or unknown), not the stale load value.
|
||||||
|
volatile_until[reg] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
-- Check #2: mac_yield uniformity
|
-- Check #2: mac_yield uniformity
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
@@ -1420,7 +1632,127 @@ local function check_mac_yield_uniformity(atom, pipe_ctx, findings)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
-- Check #3: Binding handoff discipline
|
-- Check #3: mac_yield_load / mac_yield_tail pairing (the lego split)
|
||||||
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
--- The lego split — `mac_yield_load()` in a branch BD-slot + `mac_yield_tail()` at the branch's target
|
||||||
|
--- label — must be used as a pair. Otherwise `R_AtomJmp` is not loaded for the tail's `jr R_AtomJmp`,
|
||||||
|
--- and the tape runtime would jump to garbage.
|
||||||
|
---
|
||||||
|
--- Rules:
|
||||||
|
--- 1. Every `mac_yield_load()` must be in a branch BD-slot (the immediately preceding token must be a branch).
|
||||||
|
--- 2. Every `mac_yield_tail()` must be the first instruction after an `atom_label()`, AND
|
||||||
|
--- at least one branch targeting that label must have `mac_yield_load()` in its BD-slot.
|
||||||
|
--- 3. `mac_yield_tail()` as the atom-end terminator (last token) is a WARNING, not an error
|
||||||
|
--- (the safe default for atom-endings is `mac_yield()` which re-loads `R_AtomJmp`).
|
||||||
|
---
|
||||||
|
--- Per-atom. Runtime-helper atoms (`debug_skip`) are exempt.
|
||||||
|
--- Takes `(atom, pipe_ctx, findings)`; `pipe_ctx` is unused.
|
||||||
|
local function check_yield_load_tail_pairing(atom, _pipe_ctx, findings)
|
||||||
|
if atom.kind ~= "atom" then return end
|
||||||
|
if is_runtime_helper(atom) then return end
|
||||||
|
|
||||||
|
local tokens = atom.paths.tokens
|
||||||
|
local line_in_body = atom.paths.line_in_body
|
||||||
|
local tc = atom.paths.tok_class
|
||||||
|
local n = #tokens
|
||||||
|
|
||||||
|
local function line_for(idx)
|
||||||
|
return atom.line + line_in_body[tokens[idx].rel]
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ── Rule 1: every `mac_yield_load()` must be in a branch BD-slot.
|
||||||
|
for tok_idx = 1, n do
|
||||||
|
local c = tc[tok_idx]
|
||||||
|
if c.ident == "mac_yield_load" then
|
||||||
|
if tok_idx < 2 or not tc[tok_idx - 1].is_branch then
|
||||||
|
local prev_ident = (tok_idx >= 2) and (tc[tok_idx - 1].ident or "?") or "<none>"
|
||||||
|
findings[#findings + 1] = {
|
||||||
|
atom = atom.name,
|
||||||
|
line = tok_idx >= 2 and line_for(tok_idx) or atom.line,
|
||||||
|
check = "yield_load_tail_pairing",
|
||||||
|
kind = "error",
|
||||||
|
msg = string.format(
|
||||||
|
"%s at line %d has `mac_yield_load()` at word %d but the previous token is `%s`, not a branch — `mac_yield_load()` must fill a branch BD-slot."
|
||||||
|
, atom.name, tok_idx >= 2 and line_for(tok_idx) or atom.line, tok_idx, prev_ident),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ── Rule 2: every `mac_yield_tail()` must be at a labeled target whose branch BD-slot is `mac_yield_load()`.
|
||||||
|
for tok_idx = 1, n do
|
||||||
|
local c = tc[tok_idx]
|
||||||
|
if c.ident ~= "mac_yield_tail" then goto continue end
|
||||||
|
|
||||||
|
-- The immediately preceding token must be an `atom_label()` (no instructions between them).
|
||||||
|
local prev_idx = tok_idx - 1
|
||||||
|
if prev_idx < 1 or not tc[prev_idx].is_atom_label then
|
||||||
|
if tok_idx == n then
|
||||||
|
-- Atom-ending case: last token is `mac_yield_tail()` without a preceding label. WARNING.
|
||||||
|
findings[#findings + 1] = {
|
||||||
|
atom = atom.name,
|
||||||
|
line = line_for(tok_idx),
|
||||||
|
check = "yield_load_tail_pairing",
|
||||||
|
kind = "warning",
|
||||||
|
msg = string.format(
|
||||||
|
"%s at line %d has `mac_yield_tail()` as the atom-end terminator. The safe default for atom-endings is `mac_yield()` (which re-loads R_AtomJmp). The split is for BD-slot fill, not atom-endings."
|
||||||
|
, atom.name, line_for(tok_idx)),
|
||||||
|
}
|
||||||
|
else
|
||||||
|
findings[#findings + 1] = {
|
||||||
|
atom = atom.name,
|
||||||
|
line = line_for(tok_idx),
|
||||||
|
check = "yield_load_tail_pairing",
|
||||||
|
kind = "error",
|
||||||
|
msg = string.format(
|
||||||
|
"%s at line %d has `mac_yield_tail()` at word %d but it's not the first instruction after an `atom_label()` — `mac_yield_tail()` must be the first token of its target label's body."
|
||||||
|
, atom.name, line_for(tok_idx), tok_idx),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
goto continue
|
||||||
|
end
|
||||||
|
|
||||||
|
local label_name = tc[prev_idx].label_name
|
||||||
|
-- Find at least one branch targeting `label_name` whose BD-slot is `mac_yield_load()`.
|
||||||
|
local found_pairing = false
|
||||||
|
for branch_idx = 1, n do
|
||||||
|
local bt = tc[branch_idx]
|
||||||
|
if bt.is_branch and bt.branch_label == label_name then
|
||||||
|
local bd_idx = branch_idx + 1
|
||||||
|
local bd_tc = bd_idx <= n and tc[bd_idx] or nil
|
||||||
|
if bd_tc and bd_tc.ident == "mac_yield_load" then
|
||||||
|
found_pairing = true
|
||||||
|
else
|
||||||
|
findings[#findings + 1] = {
|
||||||
|
atom = atom.name,
|
||||||
|
line = line_for(branch_idx),
|
||||||
|
check = "yield_load_tail_pairing",
|
||||||
|
kind = "error",
|
||||||
|
msg = string.format(
|
||||||
|
"%s at line %d has `mac_yield_tail()` at label `%s` (word %d) but the branch targeting it (at word %d) has BD-slot `%s` instead of `mac_yield_load()`."
|
||||||
|
, atom.name, line_for(branch_idx), label_name, tok_idx, branch_idx, bd_tc and bd_tc.ident or "?"),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if not found_pairing then
|
||||||
|
findings[#findings + 1] = {
|
||||||
|
atom = atom.name,
|
||||||
|
line = line_for(tok_idx),
|
||||||
|
check = "yield_load_tail_pairing",
|
||||||
|
kind = "error",
|
||||||
|
msg = string.format(
|
||||||
|
"%s at line %d has `mac_yield_tail()` at label `%s` but no branch in the body targets this label with `mac_yield_load()` in its BD-slot — R_AtomJmp would not be loaded."
|
||||||
|
, atom.name, line_for(tok_idx), label_name),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
::continue::
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
-- Check #4: Binding handoff discipline
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
--- For every atom with `atom_bind(Binds_X)`, verify the atom body reads every field of `Binds_X` from R_TapePtr (in any order)
|
--- For every atom with `atom_bind(Binds_X)`, verify the atom body reads every field of `Binds_X` from R_TapePtr (in any order)
|
||||||
@@ -1463,7 +1795,7 @@ local function check_abi_handoff(atom, pipe_ctx, findings)
|
|||||||
for tok_idx = 1, #tokens do
|
for tok_idx = 1, #tokens do
|
||||||
local tc_entry = tc[tok_idx]
|
local tc_entry = tc[tok_idx]
|
||||||
-- scan: load_word(R_*, R_TapePtr, O_(<Binds_X>, <field>))
|
-- scan: load_word(R_*, R_TapePtr, O_(<Binds_X>, <field>))
|
||||||
if tc_entry.is_load_word and tc_entry.reads_r_tape_ptr and tc_entry.o_arg1 == binds_name then
|
if tc_entry.is_load and tc_entry.reads_r_tape_ptr and tc_entry.o_arg1 == binds_name then
|
||||||
local field = tc_entry.o_arg2
|
local field = tc_entry.o_arg2
|
||||||
if field then
|
if field then
|
||||||
found_field_set[field] = true
|
found_field_set[field] = true
|
||||||
@@ -1508,14 +1840,15 @@ end
|
|||||||
-- Check #4: GPU port-store shape
|
-- Check #4: GPU port-store shape
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
--- For every baked atom body, detect which GP0 primitive it's emitting
|
--- For every baked atom body, detect which GP0 primitive it's emitting
|
||||||
--- (first `mac_format_<shape>_color` call). Sum contributions from `mac_format_X_color` + `mac_gte_store_X_post_*` + `mac_insert_ot_tag_X`.
|
--- (first `mac_format_<shape>_color` call). Sum contributions from `mac_format_X_color` + `mac_gte_store_X_post_*` + `mac_insert_ot_tag_X`.
|
||||||
--- Compare to duffle.GP0_CMD_SIZE[cmd_byte]. Mismatch = error.
|
--- Compare to duffle.GP0_CMD_SIZE[cmd_byte]. Mismatch = error.
|
||||||
---
|
---
|
||||||
--- Soft behavior (warnings):
|
--- Soft behavior (warnings):
|
||||||
--- - Atoms emitting a primitive via raw `store_word(R_PrimCursor, ...)` (no `mac_format_X_color` call) emit a "manual packet assembly" advisory.
|
--- - Atoms emitting a primitive via raw `store_word(R_PrimCursor, ...)` (no `mac_format_X_color` call) emit a "manual packet assembly" advisory.
|
||||||
--- Cannot auto-validate.
|
--- Cannot auto-validate.
|
||||||
--- - Atoms containing a `mac_<name>(...)` call whose name is not in duffle.GP0_MACRO_CONTRIB emit a "new macro; update duffle.GP0_MACRO_CONTRIB" advisory.
|
--- - Atoms containing a `mac_<name>(...)` call whose `name` is not registered in `pipe_ctx.components_by_name` emit a "new macro;
|
||||||
|
--- Not in corpus.components" advisory — the auto-derivation returned nil for that name.
|
||||||
---
|
---
|
||||||
--- Applies only to `kind = "atom"` (baked atoms). Components don't emit full primitives.
|
--- Applies only to `kind = "atom"` (baked atoms). Components don't emit full primitives.
|
||||||
local function check_gpu_portstore_shape(atom, pipe_ctx, findings)
|
local function check_gpu_portstore_shape(atom, pipe_ctx, findings)
|
||||||
@@ -1540,15 +1873,23 @@ local function check_gpu_portstore_shape(atom, pipe_ctx, findings)
|
|||||||
cmd_line = atom.line + line_in_body[tokens[tok_idx].rel]
|
cmd_line = atom.line + line_in_body[tokens[tok_idx].rel]
|
||||||
end
|
end
|
||||||
saw_format = true
|
saw_format = true
|
||||||
local n = duffle.GP0_MACRO_CONTRIB["mac_format_" .. shape .. "_color"]
|
-- gp0_contrib is auto-derived from the original `MipsAtomComp_(ac_format_<shape>_color) { body }` body
|
||||||
|
-- in `passes/components.lua::compute_components_metadata` and stored on `corpus.components`.
|
||||||
|
local comp = pipe_ctx.components_by_name["format_" .. shape .. "_color"]
|
||||||
|
local n = comp and comp.gp0_contrib
|
||||||
if n then contrib = contrib + n end
|
if n then contrib = contrib + n end
|
||||||
end
|
end
|
||||||
if tc_entry.is_gte_store then
|
if tc_entry.is_gte_store then
|
||||||
local n = duffle.GP0_MACRO_CONTRIB[tc_entry.ident]
|
-- `tc_entry.ident` is the macro-variant form (`mac_gte_store_f3`); strip the `mac_` prefix for the bare-name corpus lookup.
|
||||||
|
local bare = tc_entry.ident:sub(#"mac_" + 1)
|
||||||
|
local comp = pipe_ctx.components_by_name[bare]
|
||||||
|
local n = comp and comp.gp0_contrib
|
||||||
if n then contrib = contrib + n end
|
if n then contrib = contrib + n end
|
||||||
end
|
end
|
||||||
if tc_entry.is_ot_tag then
|
if tc_entry.is_ot_tag then
|
||||||
local n = duffle.GP0_MACRO_CONTRIB[tc_entry.ident]
|
local bare = tc_entry.ident:sub(#"mac_" + 1)
|
||||||
|
local comp = pipe_ctx.components_by_name[bare]
|
||||||
|
local n = comp and comp.gp0_contrib
|
||||||
if n then contrib = contrib + n end
|
if n then contrib = contrib + n end
|
||||||
end
|
end
|
||||||
if tc_entry.writes_r_prim_cursor then
|
if tc_entry.writes_r_prim_cursor then
|
||||||
@@ -1585,23 +1926,30 @@ end
|
|||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
--- Walk all paths through an atom body and return per-path cycle sums.
|
--- Walk all paths through an atom body and return per-path cycle sums.
|
||||||
--- Builds a tiny CFG: each token has a "next" pointer; branches have two (fall-through + taken).
|
--- Builds a tiny CFG: each token has a "next" pointer. Three control-transfer kinds are recognized (set by `classify_tokens`):
|
||||||
--- The BD-slot nop after a branch is absorbed into the branch's cost (MIPS-accurate: BD slot always runs),
|
--- * `branch_*` (conditional): 2 successors — fall-through (BD slot absorbed) + taken (if `atom_offset` target known).
|
||||||
--- and is SKIPPED when continuing down the fall-through path (otherwise we'd double-count it).
|
--- * `jump` / `call_addr` (unconditional absolute): 1 successor — taken only (BD slot absorbed into the cost).
|
||||||
|
--- * `jump_reg` / `call_reg` / `jump_link` (register-form): terminator — transfers control OUT of the current atom (e.g. `mac_yield()` ends in `jump_reg(R_AtomJmp), nop`).
|
||||||
|
---
|
||||||
|
--- The BD-slot nop after ANY of these (conditional branch, unconditional jump, terminal jump) is absorbed into the control-transfer's cost
|
||||||
|
--- (MIPS-accurate: the BD slot always runs) and is SKIPPED in the successor list (otherwise we'd double-count it).
|
||||||
---
|
---
|
||||||
--- Returns:
|
--- Returns:
|
||||||
--- cycles_min - shortest path through the body (sum of token costs)
|
--- cycles_min - shortest path through the body (sum of token costs)
|
||||||
--- cycles_max - longest path through the body
|
--- cycles_max - longest path through the body
|
||||||
--- branches - number of branches in the body
|
--- branches - number of branches in the body (conditional + unconditional-with-offset)
|
||||||
--- paths - number of distinct paths reached (terminated at mac_yield or end-of-body)
|
--- paths - number of distinct paths reached (terminated at mac_yield / terminal_jump / end-of-body)
|
||||||
--- has_loops - true iff a path re-entered a token it had visited (warning; loop bodies aren't supported)
|
--- has_loops - true iff a path re-entered a token it had visited (warning; loop bodies aren't supported)
|
||||||
--- unknown_macros - list of unique macro names not in duffle.INSTRUCTION_LATENCY
|
--- unknown_macros - list of unique ident names with no cost lookup: non-`mac_*` idents not in `duffle.INSTRUCTION_LATENCY`,
|
||||||
local function analyze_atom_paths(atom)
|
--- plus `mac_*` idents whose bare name is missing from `pipe_ctx.components_by_name` (i.e. no `MipsAtomComp_` for it).
|
||||||
|
local function analyze_atom_paths(atom, pipe_ctx)
|
||||||
local tokens = atom.paths.tokens or duffle.tokenize_body(atom.body)
|
local tokens = atom.paths.tokens or duffle.tokenize_body(atom.body)
|
||||||
local tc = atom.paths.tok_class or classify_tokens(tokens)
|
local tc = atom.paths.tok_class or classify_tokens(tokens)
|
||||||
local n = #tokens
|
local n = #tokens
|
||||||
|
|
||||||
-- Build label + branch maps from the pre-computed classification (no re-scan).
|
-- Build label + branch maps from the pre-computed classification (no re-scan).
|
||||||
|
-- `branches` keys both `branch_*` (conditional) and `jump`/`call_addr` (unconditional absolute);
|
||||||
|
-- The latter resolve via `tc[tok_idx].branch_label` the same way (the offsets pass produces a valid relative offset for both).
|
||||||
local labels = {}
|
local labels = {}
|
||||||
local branches = {}
|
local branches = {}
|
||||||
for tok_idx = 1, n do
|
for tok_idx = 1, n do
|
||||||
@@ -1615,23 +1963,46 @@ local function analyze_atom_paths(atom)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Pre-compute per-token cycle costs from the pre-computed ident (no re-match).
|
-- Pre-compute per-token cycle costs from the pre-computed ident (no re-match).
|
||||||
|
-- For non-`mac_*` tokens: lookup `duffle.INSTRUCTION_LATENCY[c.ident]` directly.
|
||||||
|
-- For `mac_*` tokens: lookup `pipe_ctx.components_by_name[bare_name].cycle_cost`, which `passes/components.lua::compute_components_metadata` derived from the originals
|
||||||
|
-- `MipsAtomComp_(ac_X) { body }` definition (sum of `INSTRUCTION_LATENCY` per emitted instruction,
|
||||||
|
-- recursing through nested `mac_*` calls). `mac_yield` is special-cased to 0 by `compute_components_metadata` (the runtime cost lands in the next atom's prologue).
|
||||||
local costs = {}
|
local costs = {}
|
||||||
local unknown_set = {}
|
local unknown_set = {}
|
||||||
for tok_idx = 1, n do
|
for tok_idx = 1, n do
|
||||||
local c = tc[tok_idx]
|
local c = tc[tok_idx]
|
||||||
local cost = duffle.INSTRUCTION_LATENCY[c.ident]
|
local ident = c.ident
|
||||||
if cost == nil then
|
local cost
|
||||||
cost = duffle.UNKNOWN_INSTRUCTION_CYCLES
|
if ident:sub(1, #"mac_") == "mac_" then
|
||||||
unknown_set[c.ident] = true
|
-- `mac_*` token: lookup corpus.components[bare_name].cycle_cost.
|
||||||
|
local bare = ident:sub(#"mac_" + 1)
|
||||||
|
local comp = pipe_ctx.components_by_name and pipe_ctx.components_by_name[bare]
|
||||||
|
if comp and comp.cycle_cost ~= nil then
|
||||||
|
cost = comp.cycle_cost
|
||||||
|
else
|
||||||
|
cost = duffle.UNKNOWN_INSTRUCTION_CYCLES
|
||||||
|
unknown_set[ident] = true
|
||||||
|
end
|
||||||
|
else
|
||||||
|
cost = duffle.INSTRUCTION_LATENCY[ident]
|
||||||
|
if cost == nil then
|
||||||
|
cost = duffle.UNKNOWN_INSTRUCTION_CYCLES
|
||||||
|
unknown_set[ident] = true
|
||||||
|
end
|
||||||
end
|
end
|
||||||
costs[tok_idx] = cost
|
costs[tok_idx] = cost
|
||||||
end
|
end
|
||||||
|
|
||||||
-- A token is a terminator if it's `mac_yield`.
|
-- Three control-transfer predicates (set by `classify_tokens`):
|
||||||
local function is_terminator(tok_idx) return tc[tok_idx].is_yield end
|
-- is_terminator — path ends here (`mac_yield` or register-form jump); empty successors.
|
||||||
|
-- is_branch — has an immediate offset (`branch_*`, `jump`, `call_addr`); 1-2 successors depending on unconditional_jump.
|
||||||
-- A token is a "branch" if the classification says so.
|
-- is_unconditional_jump — when is_branch is also true: skip fall-through (target only).
|
||||||
|
local function is_terminator(tok_idx)
|
||||||
|
local c = tc[tok_idx]
|
||||||
|
return c.is_yield or c.is_terminal_jump
|
||||||
|
end
|
||||||
local function is_branch(tok_idx) return tc[tok_idx].is_branch end
|
local function is_branch(tok_idx) return tc[tok_idx].is_branch end
|
||||||
|
local function is_unconditional_jump(tok_idx) return tc[tok_idx].is_unconditional_jump end
|
||||||
local function successors(tok_idx)
|
local function successors(tok_idx)
|
||||||
local tok = tokens[tok_idx].tok
|
local tok = tokens[tok_idx].tok
|
||||||
if is_terminator(tok_idx) then
|
if is_terminator(tok_idx) then
|
||||||
@@ -1640,11 +2011,22 @@ local function analyze_atom_paths(atom)
|
|||||||
if is_branch(tok_idx) then
|
if is_branch(tok_idx) then
|
||||||
local label = branches[tok_idx] -- may be false for literal-offset branches
|
local label = branches[tok_idx] -- may be false for literal-offset branches
|
||||||
local succ = {}
|
local succ = {}
|
||||||
-- Fall-through: skip the BD slot (tok_idx+1). Use tok_idx+2.
|
if is_unconditional_jump(tok_idx) then
|
||||||
|
-- Unconditional absolute jump / call: BD slot absorbed; single successor — the taken path.
|
||||||
|
-- The instruction word after the BD slot is unreachable in this atom's execution.
|
||||||
|
if label then
|
||||||
|
local label_pos = labels[label]
|
||||||
|
if label_pos and label_pos + 1 <= n then
|
||||||
|
succ[#succ + 1] = label_pos + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- For literal-offset jumps (label == false), the target is a non-tracked address; conservatively omit.
|
||||||
|
return succ, nil
|
||||||
|
end
|
||||||
|
-- Conditional branch: BD slot absorbed; two successors — fall-through (tok_idx+2) + taken (if known).
|
||||||
if tok_idx + 2 <= n then
|
if tok_idx + 2 <= n then
|
||||||
succ[#succ + 1] = tok_idx + 2
|
succ[#succ + 1] = tok_idx + 2
|
||||||
end
|
end
|
||||||
-- Taken: only if the branch has a known atom_offset target.
|
|
||||||
if label then
|
if label then
|
||||||
local label_pos = labels[label]
|
local label_pos = labels[label]
|
||||||
if label_pos and label_pos + 1 <= n then
|
if label_pos and label_pos + 1 <= n then
|
||||||
@@ -1680,11 +2062,11 @@ local function analyze_atom_paths(atom)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Add this token's cost. For a branch, ADD the BD-slot cost too
|
-- Add this token's cost. For ANY control-transfer (conditional branch, unconditional jump, terminal jump),
|
||||||
-- (and skip the BD slot in the successor list — already done in `successors` above for fall-through;
|
-- ADD the BD-slot cost too — MIPS-accurate: the BD slot always runs. Skip the BD slot in the successor list (already done in `successors` above;
|
||||||
-- for taken path the BD slot was at tok_idx+1 which is now skipped entirely).
|
-- for the taken path the BD slot was at tok_idx+1 which is now skipped entirely).
|
||||||
local cost = costs[tok_idx]
|
local cost = costs[tok_idx]
|
||||||
if is_branch(tok_idx) and tok_idx + 1 <= n then
|
if (is_branch(tok_idx) or is_terminator(tok_idx)) and tok_idx + 1 <= n then
|
||||||
cost = cost + costs[tok_idx + 1]
|
cost = cost + costs[tok_idx + 1]
|
||||||
end
|
end
|
||||||
local new_acc = acc + cost
|
local new_acc = acc + cost
|
||||||
@@ -1715,7 +2097,7 @@ local function analyze_atom_paths(atom)
|
|||||||
for macro_name in pairs(unknown_set) do unknown_list[#unknown_list + 1] = macro_name end
|
for macro_name in pairs(unknown_set) do unknown_list[#unknown_list + 1] = macro_name end
|
||||||
table.sort(unknown_list)
|
table.sort(unknown_list)
|
||||||
|
|
||||||
-- branch_count: number of `branch_*(...)` tokens.
|
-- branch_count: number of control-transfer tokens with an immediate offset (`branch_*` + `jump` + `call_addr`).
|
||||||
local branch_count = 0
|
local branch_count = 0
|
||||||
for _ in pairs(branches) do branch_count = branch_count + 1 end
|
for _ in pairs(branches) do branch_count = branch_count + 1 end
|
||||||
|
|
||||||
@@ -1733,9 +2115,9 @@ local function analyze_atom_paths(atom)
|
|||||||
end
|
end
|
||||||
|
|
||||||
--- Per-source check that emits one finding per unknown macro seen
|
--- Per-source check that emits one finding per unknown macro seen
|
||||||
--- (deduplicated across atoms so the warning section doesn't get spammed with N copies of "macro X not in duffle.INSTRUCTION_LATENCY").
|
--- (deduplicated across atoms so the warning section doesn't get spammed with N copies of the same diagnostic).
|
||||||
--- Per-atom: emit one finding per unknown macro seen, deduplicated across atoms
|
--- Per-atom: emit one finding per unknown macro seen, deduplicated across atoms
|
||||||
--- (so the warning section doesn't get spammed with N copies of "macro X not in duffle.INSTRUCTION_LATENCY").
|
--- (so the warning section doesn't get spammed with N copies of the same diagnostic).
|
||||||
--- Reuses `analyze_atom_paths`'s per-atom unknown_macros discovery, which walks tokens and computes per-token cycle costs.
|
--- Reuses `analyze_atom_paths`'s per-atom unknown_macros discovery, which walks tokens and computes per-token cycle costs.
|
||||||
local function check_per_atom_cycle_budget(atom, pipe_ctx, findings)
|
local function check_per_atom_cycle_budget(atom, pipe_ctx, findings)
|
||||||
local p = atom.paths or {}
|
local p = atom.paths or {}
|
||||||
@@ -1745,8 +2127,11 @@ local function check_per_atom_cycle_budget(atom, pipe_ctx, findings)
|
|||||||
findings[#findings + 1] = {
|
findings[#findings + 1] = {
|
||||||
atom = atom.name, line = atom.line,
|
atom = atom.name, line = atom.line,
|
||||||
check = "per_atom_cycle_budget", kind = "warning",
|
check = "per_atom_cycle_budget", kind = "warning",
|
||||||
msg = string.format("%s at line %d uses macro `%s` which is not in duffle.INSTRUCTION_LATENCY; "
|
msg = string.format("%s at line %d uses macro `%s` with no cycle_cost lookup; "
|
||||||
.. "cycle count will be +%d per call (best-case). Add an entry to duffle.INSTRUCTION_LATENCY."
|
.. "cycle count will be +%d per call (best-case). For `mac_*` idents, ensure the "
|
||||||
|
.. "corresponding `MipsAtomComp_(ac_X)` is in scope of the build so "
|
||||||
|
.. "`passes/components.lua::compute_components_metadata` can derive its cost; "
|
||||||
|
.. "for non-`mac_*` idents, add an entry to `duffle.INSTRUCTION_LATENCY`."
|
||||||
, atom.name, atom.line, name, duffle.UNKNOWN_INSTRUCTION_CYCLES),
|
, atom.name, atom.line, name, duffle.UNKNOWN_INSTRUCTION_CYCLES),
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
@@ -1910,7 +2295,7 @@ local function check_binds_no_substruct_deref(_src, pipe_ctx, findings)
|
|||||||
local line_in_body = a.paths and a.paths.line_in_body or {}
|
local line_in_body = a.paths and a.paths.line_in_body or {}
|
||||||
for ti = 1, #tokens do
|
for ti = 1, #tokens do
|
||||||
local tc_entry = tc[ti]
|
local tc_entry = tc[ti]
|
||||||
if (tc_entry.is_load_word or tc_entry.is_store_word)
|
if (tc_entry.is_load or tc_entry.is_store_word)
|
||||||
and tc_entry.o_arg1 and tc_entry.o_arg2 then
|
and tc_entry.o_arg1 and tc_entry.o_arg2 then
|
||||||
local type_name = tc_entry.o_arg1
|
local type_name = tc_entry.o_arg1
|
||||||
local field_name = tc_entry.o_arg2
|
local field_name = tc_entry.o_arg2
|
||||||
@@ -1969,7 +2354,9 @@ local CHECK_RULES = {
|
|||||||
{ name = "gte_role_mismatch", per_atom = check_gte_role_mismatch },
|
{ name = "gte_role_mismatch", per_atom = check_gte_role_mismatch },
|
||||||
{ name = "hazard_nop_use", per_atom = check_hazard_nop_use },
|
{ name = "hazard_nop_use", per_atom = check_hazard_nop_use },
|
||||||
{ name = "control_transfer_delay_slot_use",per_atom = check_control_transfer_delay_slot_use},
|
{ name = "control_transfer_delay_slot_use",per_atom = check_control_transfer_delay_slot_use},
|
||||||
|
{ name = "load_delay_violation", per_atom = check_load_delay_slots },
|
||||||
{ name = "mac_yield_uniformity", per_atom = check_mac_yield_uniformity },
|
{ name = "mac_yield_uniformity", per_atom = check_mac_yield_uniformity },
|
||||||
|
{ name = "yield_load_tail_pairing", per_atom = check_yield_load_tail_pairing },
|
||||||
{ name = "abi_handoff", per_atom = check_abi_handoff },
|
{ name = "abi_handoff", per_atom = check_abi_handoff },
|
||||||
{ name = "gpu_portstore_shape", per_atom = check_gpu_portstore_shape },
|
{ name = "gpu_portstore_shape", per_atom = check_gpu_portstore_shape },
|
||||||
{ name = "per_atom_cycle_budget", per_atom = check_per_atom_cycle_budget },
|
{ name = "per_atom_cycle_budget", per_atom = check_per_atom_cycle_budget },
|
||||||
@@ -1998,7 +2385,7 @@ local function build_corpus_pipe_ctx(ctx)
|
|||||||
.. "no per-source fallback is supported)", 0)
|
.. "no per-source fallback is supported)", 0)
|
||||||
end
|
end
|
||||||
-- The pipe_ctx views REFERENCE the corpus tables directly (no copies).
|
-- The pipe_ctx views REFERENCE the corpus tables directly (no copies).
|
||||||
-- Every consumer observes mutations through the corpus tables directly.
|
-- Every consumer observes mutations through the corpus tables directly.
|
||||||
return {
|
return {
|
||||||
-- Cross-source lookup tables.
|
-- Cross-source lookup tables.
|
||||||
register_alias_registry = corpus.register_alias_registry or {},
|
register_alias_registry = corpus.register_alias_registry or {},
|
||||||
@@ -2008,6 +2395,10 @@ local function build_corpus_pipe_ctx(ctx)
|
|||||||
atom_phases = corpus.atom_phases or {},
|
atom_phases = corpus.atom_phases or {},
|
||||||
binds_by_name = corpus.binds_by_name or {},
|
binds_by_name = corpus.binds_by_name or {},
|
||||||
atoms_by_name = corpus.atoms_by_name or {},
|
atoms_by_name = corpus.atoms_by_name or {},
|
||||||
|
-- Per-component metadata (cycle_cost + gp0_contrib) auto-derived from the original
|
||||||
|
-- `MipsAtomComp_` body by `passes/components.lua::compute_components_metadata`.
|
||||||
|
-- Keyed by bare name (e.g. `format_f3_color`, `gte_store_f3`); the `mac_` prefix at call sites is stripped before lookup.
|
||||||
|
components_by_name = corpus.components or {},
|
||||||
-- Corpus-wide ordered list of atom_info records (source-order + duplicates).
|
-- Corpus-wide ordered list of atom_info records (source-order + duplicates).
|
||||||
atom_infos_list = corpus.atom_infos or {},
|
atom_infos_list = corpus.atom_infos or {},
|
||||||
-- Corpus-wide collisions (recorded by scan_source.merge_corpus_registries).
|
-- Corpus-wide collisions (recorded by scan_source.merge_corpus_registries).
|
||||||
@@ -2059,15 +2450,16 @@ local function validate(ctx, src, corpus_pipe_ctx)
|
|||||||
atom_infos_list = atom_infos or {},
|
atom_infos_list = atom_infos or {},
|
||||||
register_alias_registry = corpus_pipe_ctx.register_alias_registry,
|
register_alias_registry = corpus_pipe_ctx.register_alias_registry,
|
||||||
type_name_registry = corpus_pipe_ctx.type_name_registry,
|
type_name_registry = corpus_pipe_ctx.type_name_registry,
|
||||||
|
-- Per-component metadata (cycle_cost + gp0_contrib) auto-derived from the original `MipsAtomComp_` body by `passes/components.lua::compute_components_metadata`.
|
||||||
|
components_by_name = corpus_pipe_ctx.components_by_name,
|
||||||
}
|
}
|
||||||
-- Shared cross-source component-body index is owned by the corpus
|
-- Shared cross-source component-body index is owned by the corpus (`corpus.component_body_index`, populated by `passes/components.lua`).
|
||||||
-- (`corpus.component_body_index`, populated by `passes/components.lua`).
|
|
||||||
-- Per-atom checks consume the corpus-owned index directly.
|
-- Per-atom checks consume the corpus-owned index directly.
|
||||||
pipe_ctx.component_body_index = (corpus and corpus.component_body_index) or {}
|
pipe_ctx.component_body_index = (corpus and corpus.component_body_index) or {}
|
||||||
|
|
||||||
--- Per-atom pipeline. ONE iteration of atoms; the 5 check_* functions + analyze_atom_paths all run here, sharing a single tokenize_body + build_body_line_index per body.
|
--- Per-atom pipeline. ONE iteration of atoms; the 5 check_* functions + analyze_atom_paths all run here, sharing a single tokenize_body + build_body_line_index per body.
|
||||||
--- Every piece of state derived from an atom body lives on `atom.paths` (per-atom mega-struct);
|
--- Every piece of state derived from an atom body lives on `atom.paths` (per-atom mega-struct);
|
||||||
--- readers (analyze_atom_paths, the 5 checks, the renderers) all consume `atom.paths`, not the raw `atoms` list.
|
--- readers (analyze_atom_paths, the 5 checks, the renderers) all consume `atom.paths`.
|
||||||
--- Each `check_*` function accepts one atom and its shared context.
|
--- Each `check_*` function accepts one atom and its shared context.
|
||||||
--- Per-source rules run once after this loop completes (no parallel dispatch table).
|
--- Per-source rules run once after this loop completes (no parallel dispatch table).
|
||||||
---
|
---
|
||||||
@@ -2093,7 +2485,7 @@ local function validate(ctx, src, corpus_pipe_ctx)
|
|||||||
a.paths.tok_class = classify_tokens(a.paths.tokens)
|
a.paths.tok_class = classify_tokens(a.paths.tokens)
|
||||||
|
|
||||||
-- analyze_atom_paths fills the *cycles / branches / has_loops / unknown_macros* fields of a.paths.
|
-- analyze_atom_paths fills the *cycles / branches / has_loops / unknown_macros* fields of a.paths.
|
||||||
analyze_atom_paths(a)
|
analyze_atom_paths(a, pipe_ctx)
|
||||||
|
|
||||||
-- Run the single forward walker for transfer-hazard policy.
|
-- Run the single forward walker for transfer-hazard policy.
|
||||||
-- Runs once per atom BEFORE the CHECK_RULES per-atom dispatch so the `transfer_hazards` reader (`check_transfer_hazards`) can
|
-- Runs once per atom BEFORE the CHECK_RULES per-atom dispatch so the `transfer_hazards` reader (`check_transfer_hazards`) can
|
||||||
|
|||||||
Reference in New Issue
Block a user