diff --git a/.gitignore b/.gitignore index 7a0f114824..d3ef2e0e18 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,8 @@ tools/n64sym/n64sym tools/n64sym/n64symdump tools/**/*.exe website/ref/ +mips64-* +bin/ ## OSX junk .DS_Store diff --git a/include/rdpq.h b/include/rdpq.h index 6baa38cc00..626bef27a4 100644 --- a/include/rdpq.h +++ b/include/rdpq.h @@ -232,6 +232,21 @@ enum { #define RDPQ_CFG_AUTOSCISSOR (1 << 3) ///< Configuration flag: enable automatic generation of SET_SCISSOR commands on render target change #define RDPQ_CFG_DEFAULT (0xFFFF) ///< Configuration flag: default configuration +/** + * @brief Configuration flag: enable "frozen blocks" recording. + * + * When set at the time a block is recorded via #rspq_block_begin_frozen, the + * block snapshots the current RDP render state. + * At playback time (#rspq_block_run_frozen), the snapshot is compared against the live RDP state. + * If any state that would have changed the recorded commands has drifted, + * the block is reported stale and not executed the caller is expected to re-record it. + * + * This flag is intentionally excluded from #RDPQ_CFG_DEFAULT: it is an opt-in + * recording-time toggle and is meaningful only between + * #rspq_block_begin_frozen and #rspq_block_end_frozen. + */ +#define RDPQ_CFG_FROZEN_BLOCKS (1 << 16) + ///@cond // Used in inline functions as part of the autosync engine. Not part of public API. #define AUTOSYNC_TILE(n) (1 << (0+(n))) // Autosync state: Bit used for tile N @@ -1533,6 +1548,57 @@ inline void rdpq_set_combiner_raw(uint64_t comb) { */ uint64_t rdpq_get_combiner_raw(void); +/** @name Frozen-block staleness reasons + * + * Bits returned by #rdpq_block_stale_reasons indicating which piece of the + * recorded RDP render state has drifted from the live state since the block + * was recorded. + * + * @{ + */ +#define RDPQ_BLOCK_STALE_SOM (1 << 0) ///< Frozen block stale: SOM (any bit) changed +#define RDPQ_BLOCK_STALE_CC (1 << 1) ///< Frozen block stale: combiner formula changed +#define RDPQ_BLOCK_STALE_CC_MIPMASK (1 << 2) ///< Frozen block stale: combiner mipmap mask changed +#define RDPQ_BLOCK_STALE_BLENDER (1 << 3) ///< Frozen block stale: blender step (fog or blender) changed +#define RDPQ_BLOCK_STALE_SCISSOR (1 << 4) ///< Frozen block stale: scissor rectangle changed +#define RDPQ_BLOCK_STALE_FILL (1 << 5) ///< Frozen block stale: fill color or target bitdepth changed +#define RDPQ_BLOCK_STALE_FOG (1 << 6) ///< Frozen block stale: fog enable (SOMX_FOG) changed (subset of SOM) +#define RDPQ_BLOCK_STALE_AA (1 << 7) ///< Frozen block stale: AA enable changed (subset of SOM) +#define RDPQ_BLOCK_STALE_CYCLE_TYPE (1 << 8) ///< Frozen block stale: cycle type (1cyc/2cyc/fill/copy) changed (subset of SOM) +#define RDPQ_BLOCK_STALE_MIPMAP (1 << 9) ///< Frozen block stale: mipmap interpolation state changed (subset of SOM) +#define RDPQ_BLOCK_STALE_UNKNOWN (1 << 31) ///< Frozen block stale: live RDP state is unknown to CPU (re-anchor required) +/** @} */ + +///@cond +typedef struct rspq_block_s rspq_block_t; +///@endcond + +/** + * @brief Check whether a frozen block's recorded RDP state still matches the live state. + * + * Returns 0 if the block is fresh and can be safely executed, or a bitmask of + * #RDPQ_BLOCK_STALE_* values indicating which pieces of state have changed + * since the block was recorded with #rspq_block_begin_frozen. + * + * For non-frozen blocks, returns 0 (no recorded snapshot to compare against). + * + * @param block The block to check + * @return 0 if fresh, or a bitmask of RDPQ_BLOCK_STALE_* reasons + * + * @see #rspq_block_begin_frozen + * @see #rspq_block_run_frozen + */ +int rdpq_block_stale_reasons(rspq_block_t *block); + +/** + * @brief Return a short human-readable name for a single #RDPQ_BLOCK_STALE_* reason bit, + * useful for debug logging. + * + * @param reason_bit A single RDPQ_BLOCK_STALE_* bit (not a bitmask) + * @return Static string naming the reason, or "?" if unknown + */ +const char *rdpq_block_stale_reason_str(int reason_bit); + /** * @brief Add a fence to synchronize RSP with RDP commands. * @@ -1607,51 +1673,105 @@ void rdpq_exec(void *buffer, int size); */ void rdpq_call_deferred(void (*func)(void *), void *arg); +/** @brief Mask for the RDP command count in the #rdpq_write @p flags argument. */ +#define RDPQ_WRITE_COUNT_MASK 0xFF + +/** @brief #rdpq_write flag: the number of expected generated RDP commands from this RSP command + * If this is not known, use RDPQ_WRITE_COUNT_UNKNOWN +*/ +#define RDPQ_WRITE_COUNT(amount) (amount) + /** - * @brief Enqueue a RSP command that also generates RDP commands. - * + * @brief #rdpq_write flag: the number of generated RDP commands is unbounded + * (or simply too high to bound). Replaces the legacy "-1" count value. + */ +#define RDPQ_WRITE_COUNT_UNKNOWN (1<<8) +/** + * @name #rdpq_write flags: declare which RDP render state the RSP command reads from DMEM. + * + * Declare these for RSP commands whose execution depends on the current RDP render state stored in DMEM. + * Inside a frozen block, mode-setting rdpq calls write resolved RDP commands directly to + * the static buffer and bypass the RSP-side resolver, leaving DMEM stale. + * These flags make #rdpq_write flush the relevant DMEM state before the command, so it observes the correct render state. + * + * Only the requested state groups are synced, and only on the first reading command after it changed. + * Outside frozen blocks these are no-ops. + * + * @{ + */ +/** @brief Reads RDPQ_OTHER_MODES (SOM). */ +#define RDPQ_WRITE_READS_OTHER_MODES (1<<9) +/** @brief Reads RDPQ_COMBINER (+ mipmap mask). */ +#define RDPQ_WRITE_READS_COMBINER (1<<10) +/** @brief Reads the blender steps (RDPQ_MODE_BLENDER_STEPS). */ +#define RDPQ_WRITE_READS_BLENDER (1<<11) +/** @brief Reads RDPQ_SCISSOR_RECT. */ +#define RDPQ_WRITE_READS_SCISSOR (1<<12) +/** @brief Reads color registers (RDPQ_FILL_COLOR). */ +#define RDPQ_WRITE_READS_COLORS (1<<13) +/** @brief Reads all RDP render state (combination of all RDPQ_WRITE_READS_* groups). */ +#define RDPQ_WRITE_READS_RDP_STATE (RDPQ_WRITE_READS_OTHER_MODES | RDPQ_WRITE_READS_COMBINER | \ + RDPQ_WRITE_READS_BLENDER | RDPQ_WRITE_READS_SCISSOR | \ + RDPQ_WRITE_READS_COLORS) +/** @} */ + +/** + * @brief Enqueue a RSP command that also generates or reads RDP state. + * * This function is similar to #rspq_write: it enqueues a RSP command in the * RSP command queue for later execution by RSP. The main difference is that - * this macro also declares that the RSP command is going to generate RDP - * commands as part of its execution. - * + * this macro also declares RDP-related semantics of the RSP command through + * the @p flags argument. + * * RSP commands in overlays can generate RDP commands by including rsp_rdqp.inc * and calling RDPQ_Send (or RDPQ_Write8 / RDPQ_Write16 / RDPQ_Finalize). If - * they do, they must enqueued using #rdpq_write instead of #rspq_write. - * + * they do, they must be enqueued using #rdpq_write instead of #rspq_write. + * * It is important to know that the RSP command is going to generate RDP commands * because the space for them needs to be allocated in the static buffer in * blocks. When wrongly using #rspq_write instead of #rdpq_write, the command * will work correctly outside of blocks but might fail in surprising ways * when called within blocks. - * - * In some cases, it is not possible to know beforehand how many RDP commands - * will be generated. In these case, @p num_rdp_commands should be the maximum - * possible value in words. If the number is quite high and potentially - * unbounded, pass the special value "-1". - * - * @param num_rdp_commands Maximum number of RDP 8-byte commands that will be - * generated by the RSP command. Use -1 if the number - * is unbounded and potentially high. - * @param ovl_id ID of the overlay for the command (see #rspq_write) - * @param cmd_id ID of the command (see #rspq_write) - * + * + * The @p flags argument encodes: + * - In the low 8 bits (#RDPQ_WRITE_COUNT_MASK): the maximum number of RDP + * 8-byte commands generated by the RSP command. + * - #RDPQ_WRITE_COUNT_UNKNOWN: the count is unbounded / too high to bound. + * - #RDPQ_WRITE_READS_RDP_STATE: the RSP command reads RDP render state from + * DMEM (triggers a state flush inside frozen blocks). + * + * For backward compatibility, passing a plain count works (e.g. `2`), and the + * legacy value `-1` is still accepted as a synonym for #RDPQ_WRITE_COUNT_UNKNOWN. + * + * @param flags Bitmask of RDP-related semantics (see above). A bare integer + * is interpreted as the RDP command count. + * @param ovl_id ID of the overlay for the command (see #rspq_write) + * @param cmd_id ID of the command (see #rspq_write) + * * @see #rspq_write - * + * * @note Some RDP commands are made of multiple 64 bit words. For the purpose - * of #rdpq_write, please treat @p num_rdp_commands as it was the - * "number of 64-bit words". So for instance if the RSP command generates - * a single RDP TEXTURE_RECTANGLE command, pass 2 as @p num_rdp_commands. - * + * of #rdpq_write, please treat the count as the "number of 64-bit words". + * So for instance if the RSP command generates a single RDP + * TEXTURE_RECTANGLE command, pass 2 as the count. + * * @hideinitializer */ -#define rdpq_write(num_rdp_commands, ovl_id, cmd_id, ...) ({ \ - int __num_rdp_commands = (num_rdp_commands); \ +#define rdpq_write(flags, ovl_id, cmd_id, ...) ({ \ + int __wflags = (flags); \ + /* Backward-compat: "-1" used to mean "unbounded count" (and nothing else). \ + * Normalize it before testing any other flag bits. */ \ + if (__wflags == -1) __wflags = RDPQ_WRITE_COUNT_UNKNOWN | RDPQ_WRITE_READS_RDP_STATE; \ + int __num_rdp_commands = (__wflags & RDPQ_WRITE_COUNT_UNKNOWN) \ + ? -1 : (__wflags & RDPQ_WRITE_COUNT_MASK); \ if (!__builtin_constant_p(__num_rdp_commands) || __num_rdp_commands != 0) { \ if (__builtin_expect(rspq_block != NULL, 0)) { \ __rdpq_block_reserve(__num_rdp_commands); \ } \ } \ + if (__wflags & RDPQ_WRITE_READS_RDP_STATE) { \ + __rdpq_frozen_sync_dmem(__wflags & RDPQ_WRITE_READS_RDP_STATE); \ + } \ rspq_write(ovl_id, cmd_id, ##__VA_ARGS__); \ }) @@ -1659,7 +1779,8 @@ void rdpq_call_deferred(void (*func)(void *), void *arg); // Declarations used by rdpq_write, not part of the public API. typedef struct rspq_block_s rspq_block_t; extern rspq_block_t *rspq_block; -extern void __rdpq_block_reserve(int); \ +extern void __rdpq_block_reserve(int); +extern void __rdpq_frozen_sync_dmem(unsigned int groups); /// @endcond diff --git a/include/rspq.h b/include/rspq.h index 974bc92d9f..50684c49b0 100644 --- a/include/rspq.h +++ b/include/rspq.h @@ -878,6 +878,42 @@ void rspq_block_begin_reuse(rspq_block_t *reuse_block); */ rspq_block_t* rspq_block_end(void); +/** + * @brief Begin recording of a frozen block. + * + * The block captures the live RDP state at this point as its baseline, + * at playback time the baseline is compared against the live state, + * and a mismatch causes #rspq_block_run_frozen to return false without executing. + * + * The caller is expected to handle staleness by re-recording. + * + * @param reuse_block Existing block allocation to reuse (or NULL for a new one) + * + * @see #rspq_block_begin_reuse + * @see #rspq_block_end_frozen + * @see #rspq_block_run_frozen + */ +void rspq_block_begin_frozen(rspq_block_t *reuse_block); + +/** + * @brief Finish recording of a frozen block. + * + * Pairs with #rspq_block_begin_frozen. Returns the recorded block, which + * carries the captured RDP state baseline for the staleness check. + */ +rspq_block_t *rspq_block_end_frozen(void); + +/** + * @brief Run a frozen block, checking that its recorded RDP state baseline + * still matches the live state. + * For easier use, this function allows passing in NULL as the block, which will return false + * + * @return true on success (block was executed), + * or false if the block was not executed because its recorded assumptions no longer hold. + * Use #rdpq_block_stale_reasons to inspect which specific state bits drifted. + */ +bool rspq_block_run_frozen(rspq_block_t *block); + /** * @brief Sets the target for a placeholder in a block * diff --git a/src/rdpq/rdpq.c b/src/rdpq/rdpq.c index eb7fb81930..68134ed7ab 100644 --- a/src/rdpq/rdpq.c +++ b/src/rdpq/rdpq.c @@ -407,9 +407,18 @@ static uint32_t rdpq_config; /** @brief RDP block management state */ rdpq_block_state_t rdpq_block_state; +/** @brief Frozen-block DMEM staleness mask (global, persists across blocks). */ +uint16_t __rdpq_frozen_dmem_pending; + +/** @brief Frozen-block deferred-mode flag (see rdpq_internal.h). */ +bool __rdpq_frozen_mode_pending; + /** @brief Tracking state of RDP */ rdpq_tracking_t rdpq_tracking; +/** @brief CPU-side mirror of RDP render state (see rdpq_state_mirror_t). */ +rdpq_state_mirror_t rdpq_state_mirror; + /** @brief Syncpoint ID at the moment of last SYNC_FULL. Used to implement #rdpq_call_deferred. */ volatile int __rdpq_syncpoint_at_syncfull; @@ -479,6 +488,11 @@ void rdpq_init() rdpq_tracking.autosync = 0; rdpq_tracking.mode_freeze = false; + // Initialize the CPU mirror to match the DMEM defaults that will be set + // up by the rdpq_set_*_raw calls below. Mirror updates inside those calls + // then keep it in sync. + memset(&rdpq_state_mirror, 0, sizeof(rdpq_state_mirror)); + // Register an interrupt handler for DP interrupts, and activate them. register_DP_handler(__rdpq_interrupt); set_DP_interrupt(1); @@ -548,6 +562,11 @@ void rdpq_exec(void *buffer, int size) void *end = buffer + size; rspq_int_write(RSPQ_CMD_RDP_SET_BUFFER, PhysicalAddr(end), PhysicalAddr(buffer), PhysicalAddr(end)); + + // The injected buffer can contain arbitrary RDP commands (including + // SET_OTHER_MODES / SET_COMBINE) that bypass the CPU mirror. The mirror + // can no longer be trusted until the caller re-anchors via rdpq_set_mode_*. + rdpq_state_mirror.unknown = 1; } /** @brief Assert handler for RSP asserts (see "RSP asserts" documentation in rsp.h) */ @@ -595,6 +614,9 @@ extern inline void __rdpq_tracking_state_reset(rdpq_tracking_t *state); /** @brief Autosync engine: mark certain resources as in use */ extern inline void __rdpq_autosync_use(uint32_t res); +/** @brief Flush stale frozen-block DMEM state before a block CALL is enqueued */ +extern inline void __rdpq_block_run_prepare(rdpq_block_t *block); + /** * @brief Autosync engine: mark certain resources as being changed. * @@ -605,6 +627,12 @@ extern inline void __rdpq_autosync_use(uint32_t res); * The SYNC command will then reset the "use" status of each respective resource. */ void __rdpq_autosync_change(uint32_t res) { + // Before changing RDP render state through the RSP pipeline (whose handlers read the current state from DMEM), + // sync any DMEM state left stale by a previously-run frozen block. + if (__builtin_expect(__rdpq_frozen_dmem_pending && !rdpq_block_state.frozen, 0)) { + __rdpq_frozen_sync_dmem(RDPQ_WRITE_READS_RDP_STATE); + } + res &= rdpq_tracking.autosync; if (res) { if ((res & AUTOSYNC_TILES) && (rdpq_config & RDPQ_CFG_AUTOSYNCTILE)) @@ -648,6 +676,16 @@ void __rdpq_block_begin() // Save the tracking state (to be recovered when the block is done) rdpq_block_state.previous_tracking = rdpq_tracking; + // Save the CPU mirror. The mirror keeps advancing during recording so it + // reflects the post-state of the block, at __rdpq_block_end we capture it + // onto the block and restore this saved copy. + rdpq_block_state.previous_mirror = rdpq_state_mirror; + + rdpq_block_state.frozen = (rdpq_config & RDPQ_CFG_FROZEN_BLOCKS) != 0; + + __rdpq_frozen_mode_pending = false; + __rdpq_frozen_dmem_pending = 0; + // Set for unknown state (like if we just run another unknown block: we lost track of the RDP state) __rdpq_block_run_no_rdp(); } @@ -668,6 +706,10 @@ void __rdpq_block_recycle(rdpq_block_t *head) memset(st, 0, sizeof(*st)); st->previous_tracking = rdpq_tracking; + st->previous_mirror = rdpq_state_mirror; + st->frozen = (rdpq_config & RDPQ_CFG_FROZEN_BLOCKS) != 0; + __rdpq_frozen_mode_pending = false; + __rdpq_frozen_dmem_pending = 0; __rdpq_block_run_no_rdp(); st->first_node = head; @@ -783,13 +825,32 @@ rdpq_block_t* __rdpq_block_end() struct rdpq_block_state_s *st = &rdpq_block_state; rdpq_block_t *ret = st->first_node; + // stop coalescing modes in frozen blocks and flush snything pending out + if (st->frozen) { + __rdpq_frozen_flush_pending_mode(); + } + // Save the current autosync state in the first node of the RDP block. // This makes it easy to recover it when the block is run - if (st->first_node) + if (st->first_node) { st->first_node->tracking = rdpq_tracking; + // Capture the CPU mirror as the block's post-state. When the block is + // later run, the live mirror is updated to this value. + st->first_node->mirror_post = rdpq_state_mirror; + // For frozen blocks, persist the pre-state snapshot (taken at begin, + // saved in previous_mirror) so playback can compare against it. + st->first_node->frozen = st->frozen; + if (st->frozen) + st->first_node->mirror_pre = st->previous_mirror; + } // Recover tracking state before the block creation started rdpq_tracking = st->previous_tracking; + rdpq_state_mirror = st->previous_mirror; + + // Clear the frozen flag now that recording is over. + // Without this, the next rdpq mode call outside any block recording would take the frozen-emit path. + st->frozen = false; // NOTE: no rspq command is enqueued at the end of block. Specifically, // there is no RSPQ_CMD_RDP_SET_BUFFER to switch back to the dynamic RDP buffers. @@ -810,7 +871,7 @@ rdpq_block_t* __rdpq_block_end() void __rdpq_block_run_with_rdp(rdpq_block_t *block) { // We have run a block that contains rdpq commands. - // During creation, we tracked some state for the block + // During creation, we tracked some state for the block // and saved it into the block structure; set it as current, // because from now on we can assume the block would and the // state of the engine must match the state at the end of the block. @@ -825,6 +886,13 @@ void __rdpq_block_run_with_rdp(rdpq_block_t *block) if (rdpq_tracking.cycle_type_frozen == 0) rdpq_tracking.cycle_type_frozen = prev.cycle_type_frozen; + if (block->frozen) { + rdpq_state_mirror = block->mirror_post; + // Raw RDP commands were committed without the RSP knowing about it, + // mark DMEM render state pending so it is lazily re-published when next read. + __rdpq_frozen_dmem_pending = RDPQ_WRITE_READS_RDP_STATE; + } + // The called block has switched static buffer. Adjust our state to set // our buffer as pending; if a new RDP command is issued, we will switch // back to it. @@ -844,16 +912,19 @@ void __rdpq_block_run_no_rdp(void) } /** - * @brief Notify that a rspq block was run. - * - * The block might or might not contain RDP commands. + * @brief Notify that a rspq block was run. + * + * The block might or might not contain RDP commands. * This is the case for a block placeholder. */ void __rdpq_block_run_maybe_rdp(void) { - rdpq_tracking_t prev = rdpq_tracking; __rdpq_block_run_no_rdp(); + // Placeholder blocks can execute arbitrary rdpq commands; the CPU cannot + // predict the resulting state, so mark the mirror as unknown. + rdpq_state_mirror.unknown = 1; + // The called block has switched static buffer. Adjust our state to set // our buffer as pending; if a new RDP command is issued, we will switch // back to it. @@ -950,6 +1021,14 @@ void __rdpq_block_update(volatile uint32_t *wptr) uint32_t phys_new = PhysicalAddr(wptr); st->wptr = wptr; + // Frozen blocks: an RDP command was just written to the static buffer, + // bypassing the RSP-side resolver. DMEM rdpq state is now stale; mark all + // groups pending so the next RDPQ_WRITE_READS_* command flushes the ones it + // reads. (Coarse: a single passthrough may have touched only one group, but + // re-publishing an unchanged group is harmless and keeps this hot path cheap.) + if (st->frozen) + __rdpq_frozen_dmem_pending = RDPQ_WRITE_READS_RDP_STATE; + assertf((phys_old & 0x7) == 0, "old not aligned to 8 bytes: %lx", phys_old); assertf((phys_new & 0x7) == 0, "new not aligned to 8 bytes: %lx", phys_new); @@ -1035,6 +1114,77 @@ __attribute__((noinline)) void __rdpq_fixup_write8_syncchange(uint32_t cmd_id, uint32_t w0, uint32_t w1, uint32_t autosync) { __rdpq_autosync_change(autosync); + + // SET_TEXTURE_IMAGE / SET_Z_IMAGE: rdpq's "fixup" is just the lookup-table + // address resolution done by RDPQCmd_SetFixupImage (rsp_rdpq.S). When the + // address-table index is 0 (no placeholder surface), the resolution is a + // no-op and we emit a raw command. Non-zero index (placeholder surfaces) + // is forbidden in frozen blocks — the address-table lives in DMEM and can't + // be resolved at record time. Use a block placeholder instead. + if (rdpq_block_state.frozen + && (cmd_id == RDPQ_CMD_SET_TEXTURE_IMAGE || cmd_id == RDPQ_CMD_SET_Z_IMAGE)) { + assertf((w1 & 0x3C000000) == 0, + "placeholder surface cannot be used inside a frozen block; use a block placeholder (RSPQ_BLOCK_PLACEHOLDER_0..6) instead"); + rdpq_passthrough_write((cmd_id, w0, w1)); + return; + } + + // Mirror + frozen-block gate for the SET_PRIM_COLOR_COMPONENT fixup. + // The RSP handler (RDPQCmd_SetPrimColorComponent) reads RDPQ_PRIM_COLOR_EX, + // merges the new component (selector in bits 16-17 of w0: 0=rgba, 1=primlod, + // 2=minlod), and emits a raw SET_PRIM_COLOR (opcode 0xFA). We replicate + // that logic on CPU using the mirror. + if (cmd_id == RDPQ_CMD_SET_PRIM_COLOR_COMPONENT) { + uint32_t sel = (w0 >> 16) & 0x3; + uint32_t prev = (uint32_t)rdpq_state_mirror.prim_color_ex; + uint32_t merged; + if (sel == 0) { // rgba: replace bits 0-15 + merged = (prev & 0x0000FFFF) | (w0 & 0xFFFF0000); + rdpq_state_mirror.prim_color_rgba = w1; + } else if (sel == 1) { // primlod: replace bits 0-7 + merged = (prev & 0x0000FF00) | (w0 & 0xFFFF00FF); + } else { // minlod: replace bits 8-15 + merged = (prev & 0x000000FF) | (w0 & 0xFFFFFF00); + } + rdpq_state_mirror.prim_color_ex = merged; + + if (rdpq_block_state.frozen) { + // Emit raw SET_PRIM_COLOR (RDP opcode 0xFA = RDPQ_OVL_ID + 0x3A). + // RDPQ_CMD_SET_PRIM_COLOR is the existing passthrough cmd id. + rdpq_passthrough_write((RDPQ_CMD_SET_PRIM_COLOR, + merged & 0x0000FFFF, rdpq_state_mirror.prim_color_rgba)); + return; + } + } + + // Auto-TMEM tile setup. RSP-side RDPQCmd_AutoTmem_SetTile takes the SET_TILE + // command in w0/w1, resolves the auto address (using ADDR or ADDR_PREV based + // on the REUSE bit), folds it into the cmd word, lowers the autotmem limit + // for "wide" formats (RGBA32/YUV16/CI4/CI8 use upper half of TMEM), and + // forwards as a raw SET_TILE (opcode 0xF5). We mirror all of that on CPU. + if (cmd_id == RDPQ_CMD_AUTOTMEM_SET_TILE) { + bool reuse = (w0 & (1u << 18)) != 0; + uint16_t auto_addr = reuse ? rdpq_state_mirror.autotmem_addr_prev + : rdpq_state_mirror.autotmem_addr; + // tmem_addr offset (already in units of 8 bytes) is in bits 0-8. + uint32_t resolved_w0 = (w0 & ~0x1FFu) | ((w0 + auto_addr) & 0x1FFu); + + // Format-driven limit lowering. Format field is bits 19-23 of w0 and + // encodes (fmt<<2)|size. Lower limit to 2048/8 for RGBA32, YUV16, CI4, CI8. + uint32_t fmt5 = (resolved_w0 >> 19) & 0x1F; + if (fmt5 == ((0<<2)|3) || fmt5 == ((1<<2)|2) || + fmt5 == ((2<<2)|0) || fmt5 == ((2<<2)|1)) { + rdpq_state_mirror.autotmem_limit_lo = 1; + } + + if (rdpq_block_state.frozen) { + // Emit raw SET_TILE (RDP opcode 0xF5 = RDPQ_OVL_ID + 0x35). + rdpq_passthrough_write((RDPQ_CMD_SET_TILE, + resolved_w0 & 0x00FFFFFF, w1)); + return; + } + } + rdpq_write(1, RDPQ_OVL_ID, cmd_id, w0, w1); } @@ -1058,14 +1208,48 @@ void __rdpq_set_scissor(uint32_t w0, uint32_t w1) // NOTE: We can't optimize this away into a standard SET_SCISSOR, even if // we track the cycle type, because the RSP must always know the current // scissoring rectangle. So we must always go through the fixup. + rdpq_state_mirror.scissor = ((uint64_t)w0 << 32) | (uint64_t)w1; + + if (rdpq_block_state.frozen) { + // CPU port of RDPQ_WriteSetScissor (rsp_rdpq.inc:719): in FILL/COPY + // mode the right edge is subtracted by 1 subpixel before emitting. + uint32_t sc_lo = w1; + if ((rdpq_state_mirror.som & SOM_CYCLE_MASK) >= SOM_CYCLE_COPY) + sc_lo -= (1u << 12); + rdpq_passthrough_write((RDPQ_CMD_SET_SCISSOR, w0, sc_lo)); + return; + } rdpq_write(1, RDPQ_OVL_ID, RDPQ_CMD_SET_SCISSOR_EX, w0, w1); } +/* Pack RGBA8888 to RGBA5551 + duplicate in upper half, matches + * RDPQ_WriteSetFillColor (rsp_rdpq.inc:749). */ +static inline uint32_t __rdpq_fill_pack_rgba5551(uint32_t rgba) +{ + uint32_t r = (rgba >> (24 + (8-5) - 11)) & (0x1Fu << 11); + uint32_t g = (rgba >> (16 + (8-5) - 6)) & (0x1Fu << 6); + uint32_t b = (rgba >> ( 8 + (8-5) - 1)) & (0x1Fu << 1); + uint32_t a = (rgba >> ( 0 + (8-1) - 0)) & (0x01u << 0); + uint32_t v16 = r | g | b | a; + return v16 | (v16 << 16); +} + /** @brief Out-of-line implementation of #rdpq_set_fill_color */ __attribute__((noinline)) void __rdpq_set_fill_color(uint32_t w1) { __rdpq_autosync_change(AUTOSYNC_PIPE); + rdpq_state_mirror.fill_color = w1; + + if (rdpq_block_state.frozen) { + // CPU port: bitdepth 3 (32bpp) sends raw, otherwise pack to RGBA5551 x2. + uint32_t emit_val = ((rdpq_state_mirror.target_bitdepth & 3) == 3) + ? w1 + : __rdpq_fill_pack_rgba5551(w1); + // RDP SET_FILL_COLOR opcode = 0xF7. RDPQ_CMD_SET_FILL_COLOR (0x37) + 0xC0 = 0xF7. + rdpq_passthrough_write((RDPQ_CMD_SET_FILL_COLOR, 0, emit_val)); + return; + } rdpq_write(1, RDPQ_OVL_ID, RDPQ_CMD_SET_FILL_COLOR_32, 0, w1); } @@ -1073,9 +1257,36 @@ void __rdpq_set_fill_color(uint32_t w1) __attribute__((noinline)) void __rdpq_set_color_image(uint32_t w0, uint32_t w1, uint32_t sw0, uint32_t sw1) { + __rdpq_autosync_change(AUTOSYNC_PIPE); + // Bitdepth (2-bit format size code) lives at bits [20:19] of w0. + rdpq_state_mirror.target_bitdepth = (w0 >> 19) & 0x3; + + // RDPQCmd_SetColorImage (rsp_rdpq.S:368) does: save bitdepth, fixup the + // lookup-table address (same RDPQ_FixupAddress as SetFixupImage), emit raw + // SET_COLOR_IMAGE, then re-emit fill color repacked for the new bitdepth + // (via RDPQ_WriteSetFillColor). In frozen mode we replicate this on CPU. + if (rdpq_block_state.frozen) { + // Placeholder surfaces (non-zero lookup index) are forbidden in frozen + // blocks — the address-table lives in DMEM and can't be resolved at + // record time. Use a block placeholder instead. + assertf((w1 & 0x3C000000) == 0, + "placeholder surface cannot be used inside a frozen block; use a block placeholder (RSPQ_BLOCK_PLACEHOLDER_0..6) instead"); + // Emit raw SET_COLOR_IMAGE (RDP opcode 0xFF, cmd_id 0x3F + 0xC0). + rdpq_passthrough_write((RDPQ_CMD_SET_COLOR_IMAGE, w0, w1)); + // Re-emit fill color repacked for the new bitdepth, matching the + // RSP tail of RDPQCmd_SetColorImage (RDPQ_WriteSetFillColor). + uint32_t packed = ((rdpq_state_mirror.target_bitdepth & 3) == 3) + ? rdpq_state_mirror.fill_color + : __rdpq_fill_pack_rgba5551(rdpq_state_mirror.fill_color); + rdpq_passthrough_write((RDPQ_CMD_SET_FILL_COLOR, 0, packed)); + // Auto-scissor: __rdpq_set_scissor is already frozen-gated. + if (rdpq_config & RDPQ_CFG_AUTOSCISSOR) + __rdpq_set_scissor(sw0, sw1); + return; + } + // SET_COLOR_IMAGE on RSP always generates an additional SET_FILL_COLOR, // so make sure there is space for it in case of a static buffer (in a block). - __rdpq_autosync_change(AUTOSYNC_PIPE); rdpq_write(2, RDPQ_OVL_ID, RDPQ_CMD_SET_COLOR_IMAGE, w0, w1); if (rdpq_config & RDPQ_CFG_AUTOSCISSOR) @@ -1140,8 +1351,18 @@ void __rdpq_set_other_modes(uint32_t w0, uint32_t w1) { __rdpq_autosync_change(AUTOSYNC_PIPE); - // SOM might also generate a SET_SCISSOR. Make sure to reserve space for it. - rdpq_write(2, RDPQ_OVL_ID, RDPQ_CMD_SET_OTHER_MODES, w0, w1); + // Mirror the full SOM (low 24 of w0 = high 24 of SOM, w1 = low 32). + rdpq_state_mirror.som = ((uint64_t)(w0 & 0x00FFFFFF) << 32) | (uint64_t)w1; + + if (rdpq_block_state.frozen) { + // Match RSP behavior (RDPQCmd_SetOtherModes -> RDPQ_FinalizeOtherModes + // also emits SET_SCISSOR with cycle adjustment). + extern void __rdpq_frozen_emit_raw_som_and_scissor(void); + __rdpq_frozen_emit_raw_som_and_scissor(); + } else { + // SOM might also generate a SET_SCISSOR. Make sure to reserve space for it. + rdpq_write(2, RDPQ_OVL_ID, RDPQ_CMD_SET_OTHER_MODES, w0, w1); + } if (w0 & (1 << (SOM_CYCLE_SHIFT-32+1))) rdpq_tracking.cycle_type_known = 2; @@ -1149,14 +1370,39 @@ void __rdpq_set_other_modes(uint32_t w0, uint32_t w1) rdpq_tracking.cycle_type_known = 1; } +/** @brief Apply a SOM partial update (mask + value) to the CPU mirror. + * + * Matches the RSP handler for RDPQ_CMD_MODIFY_OTHER_MODES: the command takes a + * 4-byte offset (0 = high half, 4 = low half), an inverted mask, and the value. + */ +void __rdpq_mirror_change_som(uint32_t w0, uint32_t w1, uint32_t w2) +{ + uint32_t offset = w0 & 0x7; + uint32_t mask = ~w1; + uint32_t val = w2; + if (offset == 0) + rdpq_state_mirror.som = (rdpq_state_mirror.som & ~((uint64_t)mask << 32)) + | ((uint64_t)(val & mask) << 32); + else + rdpq_state_mirror.som = (rdpq_state_mirror.som & ~(uint64_t)mask) + | (uint64_t)(val & mask); +} + /** @brief Out-of-line implementation of #rdpq_change_other_modes_raw */ __attribute__((noinline)) void __rdpq_change_other_modes(uint32_t w0, uint32_t w1, uint32_t w2) { __rdpq_autosync_change(AUTOSYNC_PIPE); - // SOM might also generate a SET_SCISSOR. Make sure to reserve space for it. - rdpq_write(2, RDPQ_OVL_ID, RDPQ_CMD_MODIFY_OTHER_MODES, w0, w1, w2); + __rdpq_mirror_change_som(w0, w1, w2); + + if (rdpq_block_state.frozen) { + extern void __rdpq_frozen_emit_raw_som_and_scissor(void); + __rdpq_frozen_emit_raw_som_and_scissor(); + } else { + // SOM might also generate a SET_SCISSOR. Make sure to reserve space for it. + rdpq_write(2, RDPQ_OVL_ID, RDPQ_CMD_MODIFY_OTHER_MODES, w0, w1, w2); + } if ((w0 == 0) && (w1 & (1 << (SOM_CYCLE_SHIFT-32+1)))) { if (w2 & (1 << (SOM_CYCLE_SHIFT-32+1))) @@ -1168,22 +1414,149 @@ void __rdpq_change_other_modes(uint32_t w0, uint32_t w1, uint32_t w2) uint64_t rdpq_get_other_modes_raw(void) { + // The mirror is the source of truth when known. Fall back to the slow + // DMEM read only if state visibility was lost (e.g. via rdpq_exec or + // placeholder block playback). + if (!rdpq_state_mirror.unknown) + return rdpq_state_mirror.som; rsp_queue_t *state = __rspq_get_state(); return state->rdp_mode.other_modes; } uint64_t rdpq_get_combiner_raw(void) { + if (!rdpq_state_mirror.unknown) + return rdpq_state_mirror.cc; rsp_queue_t *state = __rspq_get_state(); return state->rdp_mode.combiner; } +int rdpq_block_stale_reasons(rspq_block_t *block) +{ + // Non-frozen blocks have no recorded snapshot to compare against, so + // they are by definition "fresh" the RSP-side mode tracking handles + // any state drift. + if (!block || !block->rdp_block || !block->rdp_block->frozen) + return 0; + + const rdpq_state_mirror_t *snap = &block->rdp_block->mirror_pre; + const rdpq_state_mirror_t *live = &rdpq_state_mirror; + int reasons = 0; + + // The mirror "unknown" flag is itself part of the mirror state; if the + // snapshot and live state were both reached through identical code paths + // the flag matches and we fall through to per-field comparison. If they + // diverge, report it as a sentinel reason. + if (snap->unknown != live->unknown) + reasons |= RDPQ_BLOCK_STALE_UNKNOWN; + + uint64_t som_xor = snap->som ^ live->som; + if (som_xor) { + reasons |= RDPQ_BLOCK_STALE_SOM; + if (som_xor & SOMX_FOG) reasons |= RDPQ_BLOCK_STALE_FOG; + if (som_xor & ((uint64_t)SOM_AA_ENABLE | SOMX_AA_REDUCED)) reasons |= RDPQ_BLOCK_STALE_AA; + if (som_xor & SOM_CYCLE_MASK) reasons |= RDPQ_BLOCK_STALE_CYCLE_TYPE; + if (som_xor & (SOMX_LOD_INTERP_MASK | SOM_TEXTURE_LOD | SOMX_NUMLODS_MASK)) + reasons |= RDPQ_BLOCK_STALE_MIPMAP; + } + if (snap->cc != live->cc) reasons |= RDPQ_BLOCK_STALE_CC; + if (snap->cc_mipmask != live->cc_mipmask) reasons |= RDPQ_BLOCK_STALE_CC_MIPMASK; + if (snap->blender_steps[0] != live->blender_steps[0] || + snap->blender_steps[1] != live->blender_steps[1]) reasons |= RDPQ_BLOCK_STALE_BLENDER; + if (snap->scissor != live->scissor) reasons |= RDPQ_BLOCK_STALE_SCISSOR; + if (snap->fill_color != live->fill_color || + snap->target_bitdepth != live->target_bitdepth) reasons |= RDPQ_BLOCK_STALE_FILL; + + return reasons; +} + +void rspq_block_begin_frozen(rspq_block_t *reuse_block) +{ + // Briefly enable the frozen-blocks config flag so __rdpq_block_begin / + // __rdpq_block_recycle capture the frozen intent on the block; the flag + // can then be restored, frozenness lives on rdpq_block_state.frozen. + uint32_t prev = rdpq_config_enable(RDPQ_CFG_FROZEN_BLOCKS); + rspq_block_begin_reuse(reuse_block); + rdpq_config_set(prev); +} + +rspq_block_t *rspq_block_end_frozen(void) +{ + return rspq_block_end(); +} + +bool rspq_block_run_frozen(rspq_block_t *block) +{ + if (!block || rdpq_block_stale_reasons(block) != 0) { + return false; + } + rspq_block_run(block); + return true; +} + +const char *rdpq_block_stale_reason_str(int reason_bit) +{ + switch (reason_bit) { + case RDPQ_BLOCK_STALE_SOM: return "SOM"; + case RDPQ_BLOCK_STALE_CC: return "CC"; + case RDPQ_BLOCK_STALE_CC_MIPMASK: return "CC_MIPMASK"; + case RDPQ_BLOCK_STALE_BLENDER: return "BLENDER"; + case RDPQ_BLOCK_STALE_SCISSOR: return "SCISSOR"; + case RDPQ_BLOCK_STALE_FILL: return "FILL"; + case RDPQ_BLOCK_STALE_FOG: return "FOG"; + case RDPQ_BLOCK_STALE_AA: return "AA"; + case RDPQ_BLOCK_STALE_CYCLE_TYPE: return "CYCLE_TYPE"; + case RDPQ_BLOCK_STALE_MIPMAP: return "MIPMAP"; + case RDPQ_BLOCK_STALE_UNKNOWN: return "UNKNOWN"; + default: return "?"; + } +} + +/* CPU mirror of RDPQCmd_AutoTmem_SetAddr semantics — must stay in sync. + * + * value == 0 : begin (increment enabled; if was 0, reset addr=0/addr_prev=0/limit=4096/8) + * value == -1 : end (decrement enabled; must reach 0 cleanly) + * value > 0 : grow (addr_prev = addr; addr += value; assert addr <= limit) + */ +static void __rdpq_mirror_autotmem_setaddr(int16_t value) +{ + if (value < 0) { + assertf(rdpq_state_mirror.autotmem_enabled > 0, + "rdpq_set_tile_autotmem(-1) without matching begin"); + rdpq_state_mirror.autotmem_enabled--; + } else if (value == 0) { + if (rdpq_state_mirror.autotmem_enabled++ == 0) { + rdpq_state_mirror.autotmem_addr = 0; + rdpq_state_mirror.autotmem_addr_prev = 0; + rdpq_state_mirror.autotmem_limit_lo = 0; + } + } else { + assertf(rdpq_state_mirror.autotmem_enabled > 0, + "rdpq_set_tile_autotmem(%d) without matching begin", value); + rdpq_state_mirror.autotmem_addr_prev = rdpq_state_mirror.autotmem_addr; + rdpq_state_mirror.autotmem_addr += (uint16_t)value; + uint16_t limit = rdpq_state_mirror.autotmem_limit_lo ? 2048 / 8 : 4096 / 8; + assertf(rdpq_state_mirror.autotmem_addr <= limit, + "auto-TMEM full: addr=%u limit=%u", + rdpq_state_mirror.autotmem_addr, limit); + } +} + void rdpq_set_tile_autotmem(int16_t tmem_bytes) { if (tmem_bytes >= 0) { assertf((tmem_bytes % 8) == 0 , "tmem_bytes must be a multiple of 8"); tmem_bytes /= 8; } + + __rdpq_mirror_autotmem_setaddr(tmem_bytes); + + // Frozen recording: the CPU mirror is the source of truth for autotmem + // allocation, the RSP-side state is not consulted because we'll CPU-resolve + // SET_TILE addresses ourselves. Skip the rspq overlay cmd entirely. + if (rdpq_block_state.frozen) + return; + rspq_write(RDPQ_OVL_ID, RDPQ_CMD_AUTOTMEM_SET_ADDR, (uint16_t)tmem_bytes); } diff --git a/src/rdpq/rdpq_internal.h b/src/rdpq/rdpq_internal.h index 09104c4d06..e3cd1a052b 100644 --- a/src/rdpq/rdpq_internal.h +++ b/src/rdpq/rdpq_internal.h @@ -62,19 +62,59 @@ typedef struct { extern rdpq_tracking_t rdpq_tracking; +/** + * @brief CPU-side mirror of the RDP render state. + * + * This struct shadows the subset of DMEM-resident rdpq state that determines + * what RDP commands the RSP would emit when resolving the render mode. The + * mirror is the authoritative source of truth on CPU for "what state the + * RDP will be in once all currently-queued rspq commands have executed". + * + * Every CPU-side rdpq call that programs SOM, CC, blender, scissor, fill + * color or color image bitdepth updates this mirror before writing the rspq + * command. The mirror is also kept current across block playback: at + * #rspq_block_end the mirror is captured into rdpq_block_t::mirror_post, + * and #__rdpq_block_run_with_rdp re-applies it when the block runs. + * + * This is the foundation for the "frozen blocks" feature (see + * FROZEN_BLOCKS_PLAN.md): a snapshot of the mirror at block-begin time will + * later be compared against the live mirror to detect staleness. + */ +typedef struct { + uint64_t som; ///< Mirror of RDPQ_OTHER_MODES (incl. SOMX_* flags) + uint64_t cc; ///< Mirror of RDPQ_COMBINER (user value, may carry 2PASS marker) + uint64_t cc_mipmask; ///< Mirror of RDPQ_COMBINER_MIPMAPMASK + uint64_t scissor; ///< Mirror of RDPQ_SCISSOR_RECT (raw SET_SCISSOR) + uint32_t blender_steps[2]; ///< Mirror of RDPQ_MODE_BLENDER_STEPS (fog step, blender step) + uint32_t fill_color; ///< Mirror of RDPQ_FILL_COLOR (32-bit packed) + uint32_t prim_color_ex; ///< Mirror of RDPQ_PRIM_COLOR_EX (minlod/primlod + selector bits, no top byte) + uint32_t prim_color_rgba; ///< Mirror of RDPQ_PRIM_COLOR_RGBA (packed RGBA8888) + uint16_t autotmem_addr; ///< Mirror of RDPQ_AUTOTMEM_ADDR (current autotmem allocation, units of 8 bytes) + uint16_t autotmem_addr_prev;///< Mirror of RDPQ_AUTOTMEM_ADDR_PREV (snapshot before last increment, for REUSE) + uint8_t autotmem_enabled : 4; ///< Mirror of RDPQ_AUTOTMEM_ENABLED (reentrant counter, 0-15) + uint8_t target_bitdepth : 2; ///< Mirror of RDPQ_TARGET_BITDEPTH (low 2 bits of fmt, 0-3) + uint8_t unknown : 1; ///< Sentinel: live RDP state is unknown / has drifted from this mirror + uint8_t autotmem_limit_lo : 1; ///< Mirror of RDPQ_AUTOTMEM_LIMIT: 0=4096/8, 1=2048/8 (lowered for 32bpp/YUV/CI) +} rdpq_state_mirror_t; + +extern rdpq_state_mirror_t rdpq_state_mirror; + /** * @brief A buffer that piggybacks onto rspq_block_t to store RDP commands - * + * * In rspq blocks, raw RDP commands are not stored as passthroughs for performance. * Instead, they are stored in a parallel buffer in RDRAM and the RSP block contains * commands to send (portions of) this buffer directly to RDP via DMA. This saves * memory bandwidth compared to doing passthrough for every command. - * + * * Since the buffer can grow during creation, it is stored as a linked list of buffers. */ typedef struct rdpq_block_s { rdpq_block_t *next; ///< Link to next buffer (or NULL if this is the last one for this block) rdpq_tracking_t tracking; ///< Tracking state at the end of a block (this is populated only on the first link) + rdpq_state_mirror_t mirror_post; ///< CPU mirror of RDP state at end of block (populated only on the first link) + rdpq_state_mirror_t mirror_pre; ///< CPU mirror snapshot at block-begin (only meaningful when @c frozen is set) + bool frozen; ///< True if recorded under #RDPQ_CFG_FROZEN_BLOCKS (eligible for staleness checks) uint32_t cmds[] __attribute__((aligned(8))); ///< RDP commands } rdpq_block_t; @@ -108,8 +148,50 @@ typedef struct rdpq_block_state_s { * @brief Tracking state before starting building the block. */ rdpq_tracking_t previous_tracking; + /** + * @brief CPU mirror state before block recording started. + * + * The mirror keeps advancing during recording (so it reflects the post-state + * of the block being recorded). At #__rdpq_block_end this saved value is + * restored to the live mirror, so that the act of recording a block does + * not leak in-block state changes to the surrounding scope. + */ + rdpq_state_mirror_t previous_mirror; + /** + * @brief True if the current recording session is a frozen block. + * + * Set by #__rdpq_block_begin / #__rdpq_block_recycle when + * #RDPQ_CFG_FROZEN_BLOCKS is enabled. Causes the snapshot at begin to be + * persisted onto the block (rdpq_block_t::mirror_pre) and unlocks + * staleness checks at playback time. + */ + bool frozen; } rdpq_block_state_t; +extern rdpq_block_state_t rdpq_block_state; + +/** + * @brief Frozen blocks: global bitmask of RDP state groups stale in DMEM. + * + * Set (to all RDPQ_WRITE_READS_* groups) whenever a frozen-block RDP command + * is written to the static buffer (mode change, scissor, fill, texture, + * etc.), marking that DMEM no longer reflects the RDP render state. A + * RDPQ_WRITE_READS_* command flushes the groups it requests (via + * #__rdpq_frozen_sync_dmem) and clears those bits, so repeated reads of the + * same group (e.g. per-triangle) are cheap. + */ +extern uint16_t __rdpq_frozen_dmem_pending; + +/** + * @brief Frozen blocks: sets if resolved render mode is deferred, awaiting the next draw. + * + * Inside a frozen block, mode changes (combiner/blender/SOM) do not emit immediately. + * They advance the CPU mirror and set this flag, + * the resolved SET_OTHER_MODES + SET_COMBINE pair is emitted by #__rdpq_frozen_flush_pending_mode. + */ +extern bool __rdpq_frozen_mode_pending; +void __rdpq_frozen_flush_pending_mode(void); + void __rdpq_block_begin(); void __rdpq_block_recycle(rdpq_block_t *head); rdpq_block_t* __rdpq_block_end(); @@ -120,6 +202,8 @@ void __rdpq_block_run_maybe_rdp(void); void __rdpq_block_next_buffer(void); void __rdpq_block_update(volatile uint32_t *wptr); void __rdpq_block_reserve(int num_rdp_commands); +void __rdpq_frozen_publish_post_state(unsigned int groups); +void __rdpq_frozen_sync_dmem(unsigned int groups); /** Close rdpq_attach subsystem */ void __rdpq_attach_close(void); @@ -144,6 +228,24 @@ inline void __rdpq_tracking_state_reset(rdpq_tracking_t *state) { inline void __rdpq_autosync_use(uint32_t res) { rdpq_tracking.autosync |= res; + // Frozen-block mode coalescing: a pipe-using command (draw) is about to be written, + // so flush any deferred resolved mode into the static RDP buffer first. + if (__builtin_expect((res & AUTOSYNC_PIPE) && __rdpq_frozen_mode_pending, 0)) { + __rdpq_frozen_flush_pending_mode(); + } +} + +/** + * @brief Notify the rdpq engine that a rspq block is about to run, before its CALL command is enqueued. + * This intern *may* flush out pending DMEM states form a frozen blocks + */ +inline void __rdpq_block_run_prepare(rdpq_block_t *block) +{ + if (__builtin_expect( + __rdpq_frozen_dmem_pending && !block->frozen && !rdpq_block_state.frozen, 0) + ) { + __rdpq_frozen_sync_dmem(RDPQ_WRITE_READS_RDP_STATE); + } } void __rdpq_autosync_change(uint32_t res); diff --git a/src/rdpq/rdpq_mode.c b/src/rdpq/rdpq_mode.c index 36287b34ed..870ca89458 100644 --- a/src/rdpq/rdpq_mode.c +++ b/src/rdpq/rdpq_mode.c @@ -9,10 +9,324 @@ #include "rspq.h" #include "rdpq_internal.h" #include "yuv.h" +#include "rdpq_macros.h" -/** +/* ------------------------------------------------------------------------ + * Frozen-block resolver + * + * These tables and the __rdpq_resolve_mode() function are the C port of the + * RSP-side RDPQ_UpdateRenderMode (rsp_rdpq.inc). They take the + * CPU mirror of the user-input render state and produce the final RDP + * SET_OTHER_MODES + SET_COMBINE values, exactly as the RSP would. + * + * The resolver is invoked from rdpq mode helpers when recording a frozen + * block, to write fully resolved RDP commands directly into the static RDP + * buffer (bypassing the RSP-side mode pipeline). + * + * Constants below must remain in sync with rsp_rdpq.inc. + * ------------------------------------------------------------------------ */ + +#define _COMBINER_TEX_SHADE_NOID (RDPQ_COMBINER_TEX_SHADE & 0x00FFFFFFFFFFFFFFULL) +#define _COMBINER_SHADE_NOID (RDPQ_COMBINER_SHADE & 0x00FFFFFFFFFFFFFFULL) + +#define _COMBINER_TEX_SHADE_FOG RDPQ_COMBINER1((TEX0,0,SHADE,0), (0,0,0,TEX0)) +#define _COMBINER_SHADE_FOG RDPQ_COMBINER1((0,0,0,SHADE), (0,0,0,1)) + +/* Interpolated mipmap combiners (already masked by RDPQ_COMB0_MASK and tagged + * with RDPQ_COMBINER_2PASS, matching COMBINER_MIPMAPS in rsp_rdpq.inc). */ +#define _COMBINER_MIPMAP_TRILINEAR \ + ((RDPQ_COMBINER2((TEX1,TEX0,LOD_FRAC,TEX0),(TEX1,TEX0,LOD_FRAC,TEX0),(0,0,0,0),(0,0,0,0)) \ + & RDPQ_COMB0_MASK) | RDPQ_COMBINER_2PASS) +#define _COMBINER_MIPMAP_SHQ \ + ((RDPQ_COMBINER2((TEX0,TEX1,K5,0),(0,0,0,TEX1),(0,0,0,0),(0,0,0,0)) \ + & RDPQ_COMB0_MASK) | RDPQ_COMBINER_2PASS) + +static const uint64_t _COMBINER_MIPMAPS[2] = { + _COMBINER_MIPMAP_TRILINEAR, + _COMBINER_MIPMAP_SHQ, +}; + +static const uint32_t _AA_BLEND_TABLE[4] = { + (uint32_t)SOM_COVERAGE_DEST_ZAP, /* AA=0 BL=0 */ + (uint32_t)SOM_COVERAGE_DEST_ZAP, /* AA=0 BL=1 */ + (uint32_t)(SOM_BLALPHA_CVG | SOM_COVERAGE_DEST_CLAMP), /* AA=1 BL=0 */ + (uint32_t)(SOM_COLOR_ON_CVG_OVERFLOW | SOM_COVERAGE_DEST_WRAP), /* AA=1 BL=1 */ +}; + +static const uint32_t _AA_BLEND_MASK_C = + (uint32_t)(SOM_COVERAGE_DEST_MASK | SOM_BLEND_MASK | SOM_BLALPHA_MASK | SOM_COLOR_ON_CVG_OVERFLOW); + +static const uint32_t _AA_BLEND_DEFAULT_FORMULA[2] = { + (uint32_t)(RDPQ_BLENDER((IN_RGB, IN_ALPHA, MEMORY_RGB, MEMORY_CVG))), + (uint32_t)(RDPQ_BLENDER((IN_RGB, IN_ALPHA, MEMORY_RGB, MEMORY_CVG)) & ~SOM_READ_ENABLE), +}; + +/* Resolved RDP mode values, ready to be written into the static RDP buffer. + * The SOM/CC values do NOT include the RDP opcode in the top byte, the + * emit helper adds it via rdpq_passthrough_write. */ +typedef struct { + uint64_t som; /* Final SOM (low 56 bits meaningful; top byte 0) */ + uint64_t cc; /* Final CC (low 56 bits meaningful; top byte 0) */ + bool emit_cc; /* false in FILL/COPY mode (emit only SOM) */ +} __rdpq_resolved_t; + +__rdpq_resolved_t __rdpq_resolve_mode(const rdpq_state_mirror_t *m) +{ + __rdpq_resolved_t out; + uint64_t som = m->som; + uint64_t cc = m->cc; + + /* FILL/COPY cycle: emit only SOM, no CC (matches the RSP-side asm test of + * bit 53, set when cycle is COPY or FILL). */ + if ((som & SOM_CYCLE_MASK) >= SOM_CYCLE_COPY) { + out.som = som & 0x00FFFFFFFFFFFFFFULL & ~SOMX_UPDATE_FREEZE; + out.cc = 0; + out.emit_cc = false; + return out; + } + + uint64_t cc_1cyc = cc; + uint64_t cc_2cyc = cc; + bool comb_2pass = (cc & RDPQ_COMBINER_2PASS) != 0; + + if (!comb_2pass) { + /* 1-pass combiner fog substitution? */ + if (som & SOMX_FOG) { + uint64_t cc_noid = cc & 0x00FFFFFFFFFFFFFFULL; + if (cc_noid == _COMBINER_TEX_SHADE_NOID) + cc = _COMBINER_TEX_SHADE_FOG; + else if (cc_noid == _COMBINER_SHADE_NOID) + cc = _COMBINER_SHADE_FOG; + } + + uint8_t lod_interp = (uint8_t)((som & SOMX_LOD_INTERP_MASK) >> SOMX_LOD_INTERP_SHIFT); + if (lod_interp != 0) { + /* Interpolated mipmap: apply mipmask, OR in lookup. Force 2-cycle. */ + cc = (cc & m->cc_mipmask) | _COMBINER_MIPMAPS[lod_interp - 1]; + cc_2cyc = cc; + /* cc_1cyc unused. need_2cyc will be true */ + } else { + cc_1cyc = cc; + cc_2cyc = cc & RDPQ_COMB0_MASK; + /* Nearest mipmapping (SOM_TEXTURE_LOD) forces 2-cycle. */ + if (som & SOM_TEXTURE_LOD) + cc_2cyc |= RDPQ_COMBINER_2PASS; + } + } else { + cc_2cyc = cc; + } + + /* Blender step merge */ + uint32_t step0 = m->blender_steps[0]; /* fog */ + uint32_t step1 = m->blender_steps[1]; /* blender */ + bool bkg_blending = (step1 != 0); + + if (step1 == 0 && (som & SOM_AA_ENABLE)) { + int aa_reduced_idx = (som & SOMX_AA_REDUCED) ? 1 : 0; + step1 = _AA_BLEND_DEFAULT_FORMULA[aa_reduced_idx]; + step0 &= ~(uint32_t)SOM_BLENDING; + } + + uint32_t blend_1cyc, blend_2cyc; + if (step0 == 0 || step1 == 0) { + uint32_t single = step0 | step1; + blend_2cyc = single & (uint32_t)SOM_BLEND1_MASK; + blend_1cyc = single & (uint32_t)SOM_BLEND0_MASK; + } else { + blend_1cyc = 0; + blend_2cyc = (step0 & (uint32_t)SOM_BLEND0_MASK) + | (step1 & (uint32_t)SOM_BLEND1_MASK) + | (uint32_t)SOMX_BLEND_2PASS; + } + + /* YUV bilinear fixup */ + if ((som & (SOM_TF_MASK | SOM_SAMPLE_MASK)) == (SOM_SAMPLE_BILINEAR | SOM_TF0_YUV | SOM_TF1_YUV)) + som |= SOM_TF1_YUVTEX0 | SOM_TF0_RGB; + + /* 1cyc vs 2cyc selection */ + bool need_2cyc = (blend_2cyc & (uint32_t)SOMX_BLEND_2PASS) || (cc_2cyc & RDPQ_COMBINER_2PASS); + uint64_t cc_final; + uint32_t blend_final; + if (need_2cyc) { + cc_final = cc_2cyc; + blend_final = blend_2cyc; + som = (som & ~SOM_CYCLE_MASK) | SOM_CYCLE_2; + } else { + cc_final = cc_1cyc; + blend_final = blend_1cyc; + som = (som & ~SOM_CYCLE_MASK) | SOM_CYCLE_1; + } + + /* AA + coverage configuration */ + int aa_idx = ((som & SOM_AA_ENABLE) ? 2 : 0) | (bkg_blending ? 1 : 0); + uint32_t aa_bits = _AA_BLEND_TABLE[aa_idx] | blend_final; + som = (som & ~(uint64_t)_AA_BLEND_MASK_C) | (uint64_t)(aa_bits & _AA_BLEND_MASK_C); + + /* AA + alpha-compare interaction */ + if ((som & SOMX_ALPHACOMPARE) && (som & SOM_BLALPHA_CVG)) { + som |= SOM_BLALPHA_CVG_TIMES_CC; + som &= ~SOM_ALPHACOMPARE_MASK; + } + + /* ZMODE auto-toggle: when ZMODE is STANDARD or TRANSPARENT (bit 10 clear), + * set bit 11 iff blending is active. */ + if (!(som & (1ULL << 10))) { + if (som & SOM_BLENDING) + som |= (2ULL << 10); + else + som &= ~(2ULL << 10); + } + + /* Mask out SOMX_UPDATE_FREEZE: it's an RSP-only flag that tells the RSP + * resolver to skip recalculation (batched mode). The RDP must never see + * this bit, which would map to Force Blend in SET_OTHER_MODES. */ + out.som = som & 0x00FFFFFFFFFFFFFFULL & ~SOMX_UPDATE_FREEZE; + out.cc = cc_final & 0x00FFFFFFFFFFFFFFULL; + out.emit_cc = true; + return out; +} + +/* ------------------------------------------------------------------------ + * Frozen-block emission helpers + * + * Called from the mode-emit hot paths when recording into a frozen block. + * They write resolved SET_OTHER_MODES (+ SET_COMBINE) directly into the + * static RDP buffer, replacing the RSP-side mode pipeline at run time. + * ------------------------------------------------------------------------ */ + +void __rdpq_frozen_emit_resolved_mode(void) +{ + __rdpq_resolved_t r = __rdpq_resolve_mode(&rdpq_state_mirror); + + /* The rdpq_passthrough_write macro packs the cmd id into the top byte of + * the first word, producing a valid RDP command in the static buffer: + * RDPQ_CMD_SET_OTHER_MODES = 0x2F -> top byte 0xC + 0x2F = 0xEF + * RDPQ_CMD_SET_COMBINE_MODE_RAW = 0x3C -> top byte 0xC + 0x3C = 0xFC + */ + if (r.emit_cc) + rdpq_passthrough_write((RDPQ_CMD_SET_COMBINE_MODE_RAW, + (uint32_t)(r.cc >> 32) & 0x00FFFFFF, (uint32_t)r.cc)); + rdpq_passthrough_write((RDPQ_CMD_SET_OTHER_MODES, + (uint32_t)(r.som >> 32) & 0x00FFFFFF, (uint32_t)r.som)); +} + +/* Emit just SET_OTHER_MODES from the current mirror SOM (no resolution). + * Used by the raw set_other_modes / change_other_modes paths. + * + * Matches RSP behavior: RDPQCmd_SetOtherModes / ModifyOtherModes (without the + * 1<<15 recalc flag) fall through to RDPQ_FinalizeOtherModes which also + * emits SET_SCISSOR (cycle-adjusted from RDPQ_SCISSOR_RECT). We follow suit. */ +/* Publish the requested groups of the frozen block's render state to DMEM, so + * the RSP-side mode tracking stays consistent. Emits a sequence of internal + * RSPQ_CMD_WRITE_WORD commands targeting only the rdpq DMEM slots covered by + * @p groups (a mask of RDPQ_WRITE_READS_* bits). + * + * The RDPQ_OTHER_MODES value pushed is the resolver output (matching what + * RSP would have produced) merged with the mirror's top byte (SOMX_* flags). + * The other slots (RDPQ_COMBINER, BLENDER_STEPS, etc.) are user inputs, so + * they go through verbatim from the mirror. */ +void __rdpq_frozen_publish_post_state(unsigned int groups) +{ + if (groups & RDPQ_WRITE_READS_OTHER_MODES) { + __rdpq_resolved_t r = __rdpq_resolve_mode(&rdpq_state_mirror); + /* DMEM RDPQ_OTHER_MODES keeps the user's top byte (SOMX flags); the low + * 56 bits are the resolved SOM. */ + uint64_t som_dmem = (rdpq_state_mirror.som & 0xFF00000000000000ULL) + | (r.som & 0x00FFFFFFFFFFFFFFULL); + uint32_t off_om = offsetof(rsp_queue_t, rdp_mode.other_modes); + rspq_int_write(RSPQ_CMD_WRITE_WORD, off_om + 0, (uint32_t)(som_dmem >> 32)); + rspq_int_write(RSPQ_CMD_WRITE_WORD, off_om + 4, (uint32_t)som_dmem); + } + + if (groups & RDPQ_WRITE_READS_COMBINER) { + uint32_t off_cc = offsetof(rsp_queue_t, rdp_mode.combiner); + uint32_t off_cc_mm = offsetof(rsp_queue_t, rdp_mode.combiner_mipmapmask); + rspq_int_write(RSPQ_CMD_WRITE_WORD, off_cc + 0, (uint32_t)(rdpq_state_mirror.cc >> 32)); + rspq_int_write(RSPQ_CMD_WRITE_WORD, off_cc + 4, (uint32_t)rdpq_state_mirror.cc); + rspq_int_write(RSPQ_CMD_WRITE_WORD, off_cc_mm + 0, (uint32_t)(rdpq_state_mirror.cc_mipmask >> 32)); + rspq_int_write(RSPQ_CMD_WRITE_WORD, off_cc_mm + 4, (uint32_t)rdpq_state_mirror.cc_mipmask); + } + + if (groups & RDPQ_WRITE_READS_BLENDER) { + uint32_t off_bs0 = offsetof(rsp_queue_t, rdp_mode.blend_step0); + uint32_t off_bs1 = offsetof(rsp_queue_t, rdp_mode.blend_step1); + rspq_int_write(RSPQ_CMD_WRITE_WORD, off_bs0, rdpq_state_mirror.blender_steps[0]); + rspq_int_write(RSPQ_CMD_WRITE_WORD, off_bs1, rdpq_state_mirror.blender_steps[1]); + } + + if (groups & RDPQ_WRITE_READS_SCISSOR) { + /* DMEM RDPQ_SCISSOR_RECT stores the *full* SET_SCISSOR command, with the + * 0xED RDP opcode byte in the high byte of word 0. The CPU mirror, however, + * only stores the data bits (XH/YH/XL/YL), because that is what userland + * rdpq_set_scissor() passes to __rdpq_set_scissor(). + * The RSP-side RDPQCmd_SetScissorEx handler ORs in 0xED before writing to DMEM. + * We must do the same here, otherwise we wipe the opcode byte and subsequent + * RDPQ_FinalizeOtherModes / RDPQCmd_PopMode loads (which copy DMEM straight + * to the staging area) end up emitting a NOP-looking command to the RDP. */ + uint32_t off_scissor = offsetof(rsp_queue_t, rdp_scissor_rect); + rspq_int_write(RSPQ_CMD_WRITE_WORD, off_scissor + 0, + 0xED000000u | (uint32_t)(rdpq_state_mirror.scissor >> 32)); + rspq_int_write(RSPQ_CMD_WRITE_WORD, off_scissor + 4, (uint32_t)rdpq_state_mirror.scissor); + } + + if (groups & RDPQ_WRITE_READS_COLORS) { + uint32_t off_fill = offsetof(rsp_queue_t, rdp_fill_color); + rspq_int_write(RSPQ_CMD_WRITE_WORD, off_fill, rdpq_state_mirror.fill_color); + + /* PRIM_COLOR_EX / PRIM_COLOR_RGBA live in the rdpq overlay state (rsp_rdpq.S) + * and are not part of rsp_queue_t, so they are not republished here. + * Consequence: rdpq_set_prim_lod_frac / set_detail_factor called *after* a + * frozen block will see DMEM RDPQ_PRIM_COLOR_EX from the previous non-frozen + * call. Acceptable since these partial-update APIs are rarely interleaved + * across frozen-block boundaries; the staleness check on the CPU mirror + * catches genuine drift on re-record. */ + } + + /* RDPQ_TARGET_BITDEPTH is 1 byte sharing a word with RDPQ_SYNCFULL_ONGOING; + * not republished here (frozen blocks rarely change render target). */ +} + +/* Called via #rdpq_write for commands flagged RDPQ_WRITE_READS_* (e.g. t3d + * triangles). During frozen-block recording the material's RDP commands have + * already been written to the static buffer but DMEM is stale; this emits + * WRITE_WORD commands into the block buffer for the requested-and-still-dirty + * groups, so interleaved RSP commands see the correct RSP-side state at + * playback time. The flushed groups are cleared so repeated reads are cheap. + * + * Outside frozen-block recording this is a no-op (DMEM is maintained by the + * RSP-side resolver). */ +void __rdpq_frozen_sync_dmem(unsigned int groups) +{ + unsigned int todo = groups & __rdpq_frozen_dmem_pending; + if (todo) { + __rdpq_frozen_publish_post_state(todo); + __rdpq_frozen_dmem_pending &= ~todo; + } +} + +void __rdpq_frozen_emit_scissor_adjusted(void) +{ + /* Scissor: write current mirror.scissor, adjusting bottom-right by -1 + * subpixel in FILL/COPY mode (matches RDPQ_WriteSetScissor). */ + uint64_t sc = rdpq_state_mirror.scissor; + uint32_t sc_lo = (uint32_t)sc; + if ((rdpq_state_mirror.som & SOM_CYCLE_MASK) >= SOM_CYCLE_COPY) + sc_lo -= (1u << 12); /* -1 subpixel on XL (10.2 fixed at bits 23:12) */ + rdpq_passthrough_write((RDPQ_CMD_SET_SCISSOR, + (uint32_t)(sc >> 32) & 0x00FFFFFF, sc_lo)); +} + +void __rdpq_frozen_emit_raw_som_and_scissor(void) +{ + uint64_t som = rdpq_state_mirror.som & 0x00FFFFFFFFFFFFFFULL; + rdpq_passthrough_write((RDPQ_CMD_SET_OTHER_MODES, + (uint32_t)(som >> 32) & 0x00FFFFFF, (uint32_t)som)); + __rdpq_frozen_emit_scissor_adjusted(); +} + +/** * @brief Like #rdpq_write, but for mode commands. - * + * * During freeze (#rdpq_mode_begin), mode commands don't emit RDP commands * as they are batched instead, so we can avoid reserving space in the * RDP static buffer in blocks. @@ -21,9 +335,21 @@ rdpq_write(rdpq_tracking.mode_freeze ? num_frozen_rdp_commands : num_rdp_commands, ##__VA_ARGS__); \ }) -/** +/* Emit the resolved mode pair that was deferred by the coalescing logic, if any. + * Called from the draw hook (#__rdpq_autosync_use) and at block end. */ +void __rdpq_frozen_flush_pending_mode(void) +{ + if (__rdpq_frozen_mode_pending) { + __rdpq_frozen_emit_resolved_mode(); + __rdpq_frozen_mode_pending = false; + } +} + +extern void __rdpq_mirror_change_som(uint32_t w0, uint32_t w1, uint32_t w2); + +/** * @brief Write a fixup that changes the current render mode (8-byte command) - * + * * All the mode fixups always need to update the RDP render mode * and thus generate two RDP commands: SET_COMBINE and SET_OTHER_MODES. */ @@ -31,6 +357,30 @@ __attribute__((noinline)) void __rdpq_fixup_mode(uint32_t cmd_id, uint32_t w0, uint32_t w1) { __rdpq_autosync_change(AUTOSYNC_PIPE); + + // CPU mirror: 8-byte mode fixups carry either a 2-pass combiner, a blender + // formula, or a fog formula in w0|w1. Dispatch on cmd_id. + switch (cmd_id) { + case RDPQ_CMD_SET_COMBINE_MODE_2PASS: + rdpq_state_mirror.cc = ((uint64_t)(w0 & 0x00FFFFFF) << 32) | (uint64_t)w1 + | RDPQ_COMBINER_2PASS; + break; + case RDPQ_CMD_SET_BLENDING_MODE: + rdpq_state_mirror.blender_steps[1] = w1; + break; + case RDPQ_CMD_SET_FOG_MODE: + rdpq_state_mirror.blender_steps[0] = w1; + break; + } + + // Frozen-block recording: emit a fully-resolved SET_COMBINE+SET_OTHER_MODES + // pair into the static RDP buffer, bypassing the RSP-side mode pipeline. + // During a mode_begin/end batch the emit is deferred and coalesced. + if (rdpq_block_state.frozen) { + __rdpq_frozen_mode_pending = true; + return; + } + rdpq_mode_write(2, 0, RDPQ_OVL_ID, cmd_id, w0, w1); // COMBINE+SOM } @@ -39,8 +389,18 @@ __attribute__((noinline)) void __rdpq_fixup_mode3(uint32_t cmd_id, uint32_t w0, uint32_t w1, uint32_t w2) { __rdpq_autosync_change(AUTOSYNC_PIPE); - rdpq_mode_write(2, 0, RDPQ_OVL_ID, cmd_id, w0, w1, w2); // COMBINE+SOM + // CPU mirror: 12-byte mode fixups today are only RDPQ_CMD_MODIFY_OTHER_MODES + // with the 1<<15 recalc flag, applying a SOM partial update. + if (cmd_id == RDPQ_CMD_MODIFY_OTHER_MODES) + __rdpq_mirror_change_som(w0, w1, w2); + + if (rdpq_block_state.frozen) { + __rdpq_frozen_mode_pending = true; + return; + } + + rdpq_mode_write(2, 0, RDPQ_OVL_ID, cmd_id, w0, w1, w2); // COMBINE+SOM } /** @brief Write a fixup that changes the current render mode (16-byte command) */ @@ -48,6 +408,19 @@ __attribute__((noinline)) void __rdpq_fixup_mode4(uint32_t cmd_id, uint32_t w0, uint32_t w1, uint32_t w2, uint32_t w3) { __rdpq_autosync_change(AUTOSYNC_PIPE); + + // CPU mirror: 16-byte mode fixup is the 1-pass combiner setter, carrying + // the user combiner (w0|w1) and the mipmap mask (w2|w3). + if (cmd_id == RDPQ_CMD_SET_COMBINE_MODE_1PASS) { + rdpq_state_mirror.cc = ((uint64_t)(w0 & 0x00FFFFFF) << 32) | (uint64_t)w1; + rdpq_state_mirror.cc_mipmask = ((uint64_t)(w2 & 0x00FFFFFF) << 32) | (uint64_t)w3; + } + + if (rdpq_block_state.frozen) { + __rdpq_frozen_mode_pending = true; + return; + } + rdpq_mode_write(2, 0, RDPQ_OVL_ID, cmd_id, w0, w1, w2, w3); // COMBINE+SOM } @@ -56,13 +429,56 @@ __attribute__((noinline)) void __rdpq_reset_render_mode(uint32_t w0, uint32_t w1, uint32_t w2, uint32_t w3) { __rdpq_autosync_change(AUTOSYNC_PIPE); + + // CPU mirror: ResetRenderMode rewrites both CC (w0|w1) and SOM (w2|w3), + // and resets the mipmap mask. (It is used by rdpq_set_mode_standard / + // _copy / _fill / _yuv, which then optionally also call rdpq_mode_combiner + // to overwrite cc/mipmask through __rdpq_fixup_mode4.) + rdpq_state_mirror.cc = ((uint64_t)(w0 & 0x00FFFFFF) << 32) | (uint64_t)w1; + rdpq_state_mirror.cc_mipmask = 0; + rdpq_state_mirror.som = ((uint64_t)(w2 & 0x00FFFFFF) << 32) | (uint64_t)w3; + rdpq_state_mirror.blender_steps[0] = 0; + rdpq_state_mirror.blender_steps[1] = 0; + + if (rdpq_block_state.frozen) { + // Reset emits SET_SCISSOR (with cycle adjustment) to match the RSP path. + __rdpq_frozen_emit_scissor_adjusted(); + // The resolved CC+SOM emit is deferred if inside a mode_begin/end batch. + __rdpq_frozen_mode_pending = true; + return; + } + // ResetRenderMode can generate: SCISSOR+COMBINE+SOM when not frozen, // or just SCISSOR when frozen. rdpq_mode_write(3, 1, RDPQ_OVL_ID, RDPQ_CMD_RESET_RENDER_MODE, w0, w1, w2, w3); } +/* CPU-side shadow of the RSP mode stack (RDPQ_MODE_STACK in DMEM has 3 slots). + * Mirrors what RSP push/pop does, so the CPU mirror stays consistent with + * DMEM across rdpq_mode_push / rdpq_mode_pop calls (used by e.g. rdpq_clear + * and rdpq_attach to wrap a temporary mode change). */ +#define RDPQ_MIRROR_STACK_SLOTS 3 +static rdpq_state_mirror_t __rdpq_mirror_stack[RDPQ_MIRROR_STACK_SLOTS]; +static int __rdpq_mirror_stack_depth = 0; + void rdpq_mode_push(void) { + // Shadow the push: save the current mirror so the matching pop can + // restore it. If the stack overflows, mark unknown (matches the existing + // RSP-side assertion behavior, which would also fail). + if (__rdpq_mirror_stack_depth < RDPQ_MIRROR_STACK_SLOTS) + __rdpq_mirror_stack[__rdpq_mirror_stack_depth++] = rdpq_state_mirror; + else + rdpq_state_mirror.unknown = 1; + + // Frozen-block recording: the CPU shadow stack is the source of truth for + // mode state across push/pop, so the RSP-side stack is irrelevant, the + // block emits raw RDP commands directly and doesn't rely on RDPQ_MODE_STACK. + // Skip the rspq overlay command (which would cost an overlay switch) and + // rely on publish-post-state to resync DMEM at block end. + if (rdpq_block_state.frozen) + return; + // Push is not a RDP passthrough/fixup command, it's just a standard // RSP command. Use rspq_write. rspq_write(RDPQ_OVL_ID, RDPQ_CMD_PUSH_RENDER_MODE); @@ -71,10 +487,30 @@ void rdpq_mode_push(void) void rdpq_mode_pop(void) { __rdpq_autosync_change(AUTOSYNC_PIPE); + + // Restore the saved mirror to match what RSP-side pop will load from + // RDPQ_MODE_STACK. Without this, cycle-type bits and other SOM state + // diverge from DMEM after any push/pop pair (e.g. rdpq_clear wraps a + // FILL-mode op in push/pop, so without restore the mirror stays at FILL). + if (__rdpq_mirror_stack_depth > 0) + rdpq_state_mirror = __rdpq_mirror_stack[--__rdpq_mirror_stack_depth]; + else + rdpq_state_mirror.unknown = 1; + + rdpq_tracking.cycle_type_known = 0; + + // Frozen recording: re-emit the popped state (scissor + resolved mode) + // directly as raw RDP commands. No rspq overlay command needed. The + // resolved mode emit is coalesced if inside a mode_begin/end batch. + if (rdpq_block_state.frozen) { + __rdpq_frozen_emit_scissor_adjusted(); + __rdpq_frozen_mode_pending = true; + return; + } + // ModePop can generate: SCISSOR+COMBINE+SOM when not frozen, // or just SCISSOR when frozen. rdpq_mode_write(3, 1, RDPQ_OVL_ID, RDPQ_CMD_POP_RENDER_MODE); - rdpq_tracking.cycle_type_known = 0; } /** @brief Like #rdpq_set_mode_fill, but without fill color configuration */ @@ -142,20 +578,24 @@ void rdpq_set_mode_yuv(bool bilinear) { void rdpq_mode_begin(void) { - // Freeze render mode updates. We call rdpq_change_other_modes_raw here - // (instead of __rdpq_mode_change_som) because there will be no RDP - // commands emitted from this call. rdpq_tracking.mode_freeze = true; rdpq_tracking.cycle_type_frozen = 0; - __rdpq_mode_change_som(SOMX_UPDATE_FREEZE, SOMX_UPDATE_FREEZE); + + // nop in frozen blocks, they will batch modes automatically + if (!rdpq_block_state.frozen) { + __rdpq_mode_change_som(SOMX_UPDATE_FREEZE, SOMX_UPDATE_FREEZE); + } } void rdpq_mode_end(void) { - // Unfreeze render mode updates and recalculate new render mode. rdpq_tracking.mode_freeze = false; rdpq_tracking.cycle_type_known = rdpq_tracking.cycle_type_frozen; - __rdpq_mode_change_som(SOMX_UPDATE_FREEZE, 0); + + // nop in frozen blocks, they will batch modes automatically + if (!rdpq_block_state.frozen) { + __rdpq_mode_change_som(SOMX_UPDATE_FREEZE, 0); + } } diff --git a/src/rspq/rspq.c b/src/rspq/rspq.c index 361012756e..0302f89fc4 100644 --- a/src/rspq/rspq.c +++ b/src/rspq/rspq.c @@ -1337,6 +1337,12 @@ rspq_block_t* rspq_block_end(void) { assertf(rspq_block, "a block was not being created"); + // Frozen blocks: do NOT eagerly publish the post-state to DMEM here. + // The DMEM staleness left by the block is republished lazily at run time, + // by the first rdpq command after the block that reads RDP state. + // Any pending state accumulated while recording is either already baked or re-established at block run. + __rdpq_frozen_dmem_pending = 0; + // Terminate the block with a RET command, encoding // the nesting level which is used as stack slot by RSP. rspq_append1(rspq_cur_pointer, RSPQ_CMD_RET, rspq_block->nesting_level<<2); @@ -1344,12 +1350,15 @@ rspq_block_t* rspq_block_end(void) // Switch back to the normal display list rspq_switch_context(&lowpri); + // __rdpq_block_end may allocate RDP buffers. Temporarily clear rspq_block + // so that rspq_next_buffer takes the main-queue path on overflow. + rspq_block_t *b = rspq_block; + rspq_block = NULL; + // Save pointer to rdpq block (if any) - rspq_block->rdp_block = __rdpq_block_end(); + b->rdp_block = __rdpq_block_end(); // Return the created block - rspq_block_t *b = rspq_block; - rspq_block = NULL; return b; } @@ -1421,6 +1430,10 @@ void rspq_block_run(rspq_block_t *block) assertf(block->nesting_level < block->min_ph_level, "Block nesting level overlaps with used placeholders"); + // flush any pending DMEM state out (e.g. a frozen block ran before we now submit a normal block) + if (block->rdp_block) + __rdpq_block_run_prepare(block->rdp_block); + // Write the CALL op. The second argument is the nesting level // which is used as stack slot in the RSP to save the current // pointer position.