#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; used by MMX4 and 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); } };