diff --git a/runtime/include/render_pass_sprite_scene.hpp b/runtime/include/render_pass_sprite_scene.hpp new file mode 100644 index 00000000..73bc6907 --- /dev/null +++ b/runtime/include/render_pass_sprite_scene.hpp @@ -0,0 +1,226 @@ +#pragma once +#include "cpu_state.h" +#include "mod_plugins.h" +#include "render_pass_motion.h" +#include "psx_cycle_freeze.h" +#include "pgxp.h" +#include "gpu.h" +#include +#include + +extern "C" uint8_t* memory_get_ram_ptr(void); +extern "C" uint8_t* memory_get_scratchpad_ptr(void); +extern "C" uint32_t memory_get_ram_bytes(void); + +/* Replay of a double-buffered sprite scene, with identified 16.16 actor and + * layer coordinates. Layout describes an audited guest ABI, including OT size + * and display-bank prefixes. Capture/replay never runs the gameplay update. + * Late HUD/effect packets are preserved, the existing VSync wait runs once, + * and all temporary guest changes remain inside the render-pass sandbox. + * Derived from the MMX6 native scene implementation; also used by MMX5. + */ +template +class PSXSpriteSceneReplay { +static constexpr uint32_t Draw=Layout::Draw; +static constexpr uint32_t End=Layout::End; +static constexpr uint32_t Camera=Layout::Camera; +static constexpr uint32_t Actor=Layout::Actor; +static constexpr uint32_t VSync=Layout::VSync; +static constexpr uint32_t Base=Layout::Base; +static constexpr uint32_t Stride=Layout::Stride; +static constexpr uint32_t Layers=Layout::Layers; +static constexpr uint32_t PutDrawEnv=Layout::PutDrawEnv; +static constexpr uint32_t DrawOTag=Layout::DrawOTag; +static constexpr uint32_t DrawSync=Layout::DrawSync; +inline static CPUState entry{}; +inline static std::vector ram; +inline static uint8_t scratch[1024]{}; +inline static PSXMotionSet* motion=nullptr; +inline static PSXMotionStats stats{}; +inline static uint32_t vblanks=0, buffer=0, alpha=0; +inline static bool capturing=false, ready=false, waiting=false; +inline static uint32_t draw_heads[Layout::Buckets]{}; +struct LatePacket { uint32_t address;std::vector words; }; +inline static std::vector late[Layout::Buckets]; + +static bool capture_late_packets() { + // Other scene tasks can prepend a fade or overlay after the world wrapper + // returns. Preserve those exact current-frame packets in their OT buckets. + // Only a bounded prefix ending at the captured world head is accepted. + uint32_t total=0; + for(uint32_t i=0;imemory_get_ram_bytes()-4 || ++total>4096)return false; + const uint32_t tag=psx_mod_read_word(node),words=1+(tag>>24); + if(words*4>memory_get_ram_bytes()-node)return false; + LatePacket packet{node,{}};packet.words.reserve(words); + for(uint32_t j=0;j>24)!=0xE5 || + (psx_mod_read_word(offset_other+4)>>24)!=0xE5 || + (psx_mod_read_word(clip_other+4)>>24)!=0xE3 || + (psx_mod_read_word(clip_other+8)>>24)!=0xE4)return false; + psx_mod_write_word(offset+4,psx_mod_read_word(offset_other+4)); + psx_mod_write_word(clip+4,psx_mod_read_word(clip_other+4)); + psx_mod_write_word(clip+8,psx_mod_read_word(clip_other+8)); + return true; +} + +static uint32_t call(CPUState* cpu,uint32_t fn,uint32_t a0) { + const CPUState saved=*cpu; + cpu->pc=0;cpu->gpr[4]=a0; + psx_dispatch_call(cpu,fn,saved.gpr[31]); + const uint32_t result=cpu->gpr[2];*cpu=saved;return result; +} +static void reset() { capturing=ready=false;psx_motion_invalidate(motion); } +static void tick(){if(!g_psx_render_pass_active)++vblanks;} +static void begin(CPUState* cpu,uint32_t) { + if(g_psx_render_pass_active)return; + const uint32_t next=psx_mod_read_word(Layout::CurrentBuffer); + if(next!=Base && next!=Base+Stride){reset();return;} + if(!motion)motion=psx_motion_set_create(4096); + if(!motion)return; + if(capturing)reset(); + buffer=next;entry=*cpu; + ram.resize(memory_get_ram_bytes()); + std::memcpy(ram.data(),memory_get_ram_ptr(),ram.size()); + std::memcpy(scratch,memory_get_scratchpad_ptr(),sizeof scratch); + psx_motion_begin(motion,vblanks);capturing=true;ready=false; +} +static void position(uint32_t addr,uint32_t identity) { + if(g_psx_render_pass_active) { + int32_t value; + if(psx_motion_blend_at(motion,PSX_MOTION_SCALAR,addr, + double(alpha)/65536.0,&value)==1) + psx_mod_write_word(addr,uint32_t(value)); + } else if(capturing) { + if(!psx_motion_track(motion,PSX_MOTION_SCALAR,addr,identity))reset(); + } +} +static void cameras(CPUState*,uint32_t) { + if(!capturing && !g_psx_render_pass_active)return; + // After the layer initializer, before streaming and tile construction. + // Parent layers are captured separately; their authored offsets stay intact. + for(uint32_t i=0;i<3;++i) { + const uint32_t b=Layers+i*0x54; + if(!psx_mod_read_byte(b+3))continue; + const uint32_t id=psx_motion_identity(psx_mod_read_word(b+0x18), + psx_mod_read_word(b+0x1C),psx_mod_read_byte(b+0x52)); + position(b+8,id);position(b+12,id); + } +} +static void actor(CPUState* cpu,uint32_t) { + if(!capturing && !g_psx_render_pass_active)return; + const uint32_t b=cpu->gpr[4], phys=b&0x1FFFFFFF; + if(phys>memory_get_ram_bytes()-0x50 || (b&3))return; + // Negative camera indices are absolute screen coordinates (HUD/menus). + const int8_t layer=int8_t(psx_mod_read_byte(b+0x14)); + if(layer<0 || layer>=3)return; + const uint32_t id=psx_motion_identity(psx_mod_read_word(b+0x38), + psx_mod_read_word(b+0x3C),uint32_t(layer)); + position(b+8,id);position(b+12,id); +} +static void end(CPUState*,uint32_t) { + if(g_psx_render_pass_active || !capturing)return; + capturing=false; + for(uint32_t i=0;igpr[31]!=Layout::WaitReturn || cpu->gpr[4]!=0)return; + if(!ready)return; + ready=false; + const uint32_t other=buffer==Base?Base+Stride:Base; + if(psx_mod_read_word(Layout::CurrentBuffer)!=buffer || + psx_mod_read_word(buffer)!=psx_mod_read_word(other+0x14) || + psx_mod_read_word(buffer+4)!=psx_mod_read_word(other+0x18))return; + if(!capture_late_packets()){psx_mod_counter_add("sprite_scene.fr.overlay_rejected",1);return;} + // Let the preceding display flip be captured before opening this frame's + // generation. VSync(1) is only a query, so a plan before VSync(0)'s wait + // otherwise overwrites its predecessor before that predecessor is shown. + // Execute the original wait once and finish the intercepted function; + // running its body again would add a guest VBlank and slow gameplay. + waiting=true; + psx_dispatch_call(cpu,VSync,cpu->gpr[31]); + waiting=false; + ready=true; + PSXModRenderPassFrame frame{};frame.struct_size=sizeof frame; + frame.period_vblanks=stats.ticks; + frame.x=psx_mod_read_half(buffer);frame.y=psx_mod_read_half(buffer+2); + frame.w=psx_mod_read_half(buffer+4);frame.h=psx_mod_read_half(buffer+6); + psx_mod_counter_add("sprite_scene.fr.passes",psx_mod_render_pass_frame(cpu,&frame,pass,nullptr)); + ready=false; + psx_mod_finish_function(cpu); +} +static void activate() { + reset();vblanks=0; + psx_mod_activate_render_pass_rate(Layout::Package, + "frame-interpolation","rate",PSX_MOD_RENDER_PASS_FLIP_PENDING); +} + +public: +static void install(const char* id) { + psx_mod_register_activation_plugin(id,activate); + psx_mod_register_vblank_plugin(id,tick); + psx_mod_register_savestate_plugin(id,reset); + psx_mod_register_function_entry_plugin(id,Draw,begin); + psx_mod_register_function_entry_plugin(id,Actor,actor); + psx_mod_register_function_entry_plugin(id,VSync,finish); + psx_mod_register_instruction_plugin(id,Camera,Layout::CameraInstruction,cameras); + psx_mod_register_instruction_plugin(id,End,0x03E00008,end); +} +};