diff --git a/runtime/include/render_pass_ot_relocation.hpp b/runtime/include/render_pass_ot_relocation.hpp new file mode 100644 index 00000000..e2d54a75 --- /dev/null +++ b/runtime/include/render_pass_ot_relocation.hpp @@ -0,0 +1,287 @@ +#pragma once +#include +#include +#include +#include + +// Preserve the real frame's work outside a bounded drawing section. Matrix +// replay may change clipping and core packet layout. Tail relocation accepts +// only proved append-only packet chains attached through zero-length OT heads. +// Packet allocations may contain padding; only linked packet words are copied. +class PSXOTRelocation { +public: + struct Node { uint32_t address, header; }; + struct Word { uint32_t address, value; }; + struct Range { uint32_t first, last; }; + std::vector prefix, core, complete; + std::vector outputs; + std::vector payloads; + std::vector changes; + std::vector prefix_payload; + std::vector headers; + std::vector after_core; + uint32_t cursor=0, final_cursor=0, ordering_table=0; + uint32_t refusal=0; + bool refuse(uint32_t line) {refusal=line;return false;} + bool captured=false, ready=false; + std::vector splice_writes; + uint32_t relocated_first=0, relocated_last=0, relocated_destination=0; + uint32_t arena_begin=0; + + static uint32_t word(const uint8_t* ram, uint32_t address) { + uint32_t value; + std::memcpy(&value,ram+(address&0x1fffffu),4); + return value; + } + static bool walk(const uint8_t* ram, uint32_t bytes, uint32_t ot, + std::vector& nodes) { + nodes.clear(); + if(bytes!=0x200000u)return false; + uint32_t address=ot&0xffffffu; + for(unsigned n=0;n<32768;++n) { + if(address==0xffffffu) { + std::vector spans; + for(const auto& node:nodes) + spans.push_back({node.address,node.address+4u+(node.header>>24)*4u}); + std::sort(spans.begin(),spans.end(),[](const Range& a,const Range& b){return a.first=bytes || bytes-address<4u) return false; + const uint32_t header=word(ram,address); + const uint32_t payload=(header>>24)*4u; + if(payload>bytes-address-4u) return false; + nodes.push_back({address,header}); + address=header&0xffffffu; + } + return false; + } + void clear() {captured=ready=false;after_core.clear();changes.clear();prefix_payload.clear();outputs.clear();payloads.clear();} + bool begin(const uint8_t* ram,uint32_t bytes,uint32_t ot,uint32_t initial_cursor=0) { + clear();ordering_table=ot;arena_begin=initial_cursor&0x1fffffu; + if((arena_begin&3u) || arena_begin>=bytes)return false; + if(!walk(ram,bytes,ot,prefix))return false; + for(const auto& node:prefix) + for(uint32_t offset=4;offset<=((node.header>>24)*4u);offset+=4) + prefix_payload.push_back({node.address+offset,word(ram,node.address+offset)}); + return true; + } + bool end(const uint8_t* ram,uint32_t bytes,uint32_t current_cursor) { + if(!walk(ram,bytes,ordering_table,core)) return false; + // This candidate owns newly appended packets only. Reject a core that + // reuses a prefix payload instead of silently restoring stale precision. + for(const auto& saved:prefix_payload) + if(word(ram,saved.address)!=saved.value)return false; + headers=core; + std::sort(headers.begin(),headers.end(),[](const Node& a,const Node& b){return a.addressaddress!=prior.address || + ((current->header^prior.header)&0xff000000u))return false; + } + std::vector prefix_addresses; + for(const auto& node:prefix) prefix_addresses.push_back(node.address); + std::sort(prefix_addresses.begin(),prefix_addresses.end()); + for(const auto& node:core) { + const uint32_t length=(node.header>>24)*4u; + if(length)payloads.push_back({node.address+4u,node.address+4u+length}); + if(length && !std::binary_search(prefix_addresses.begin(),prefix_addresses.end(),node.address)) + outputs.push_back({node.address+4u,node.address+4u+length}); + } + std::sort(outputs.begin(),outputs.end(),[](const Range& a,const Range& b){return a.first merged; + for(const auto& range:outputs) { + if(!merged.empty() && range.first<=merged.back().last) + merged.back().last=std::max(merged.back().last,range.last); + else merged.push_back(range); + } + outputs.swap(merged); + std::sort(payloads.begin(),payloads.end(),[](const Range& a,const Range& b){return a.firstlast)return false; + } + cursor=current_cursor; + headers=core; + std::sort(headers.begin(),headers.end(),[](const Node& a,const Node& b){return a.addresslast; + } + bool prepare(const uint8_t* ram,uint32_t bytes,uint32_t producer_end=0) { + final_cursor=producer_end&0x1fffffu; + ready=false;changes.clear(); + if(!captured || after_core.size()!=bytes || (bytes&3u) || + !walk(ram,bytes,ordering_table,complete)) return false; + for(uint32_t address=0;addresslast)return false; + const auto header=std::lower_bound(headers.begin(),headers.end(),address, + [](const Node& node,uint32_t word){return node.addressaddress==address && + ((value^header->header)&0xff000000u))return false; + changes.push_back({address,value}); + } + ready=true; + return true; + } + bool matches(const uint8_t* ram,uint32_t bytes,uint32_t ot,uint32_t current_cursor) const { + if(!ready || ot!=ordering_table || cursor!=current_cursor || after_core.size()!=bytes) return false; + for(const auto& node:core) + if(word(ram,node.address)!=node.header) return false; + return true; + } + // Only append-only tail packets and ordinary zero-length OT head inserts + // may move. This prepares all writes before touching the replay's RAM. + bool prepare_splice(const uint8_t* ram,uint32_t bytes,uint32_t ot, + uint32_t current_cursor,uint32_t cursor_word) { + refusal=0;splice_writes.clear();relocated_first=relocated_last=relocated_destination=0; + if(!ready || ot!=ordering_table || after_core.size()!=bytes || + (cursor_word&0x1fffffu)>bytes-4u || word(ram,cursor_word)!=current_cursor)return refuse(__LINE__); + const uint32_t old_first=cursor&0x1fffffu, new_first=current_cursor&0x1fffffu; + if((old_first|new_first)&3u || old_first=bytes || new_first>=bytes)return refuse(__LINE__); + std::vector live; + if(!walk(ram,bytes,ot,live))return refuse(__LINE__); + auto sorted=[](std::vector nodes){ + std::sort(nodes.begin(),nodes.end(),[](const Node&a,const Node&b){return a.address& nodes,uint32_t address)->const Node* { + auto at=std::lower_bound(nodes.begin(),nodes.end(),address, + [](const Node&n,uint32_t a){return n.addressaddress==address?&*at:nullptr; + }; + // Static prefix data must survive; its links may point at newly emitted + // core packets. Every zero-length OT node retains its identity. + for(const auto& node:prefix) { + const Node* actual=find(now,node.address); + if(!actual || ((actual->header^node.header)&0xff000000u))return refuse(__LINE__); + } + for(const auto& saved:prefix_payload) + if(word(ram,saved.address)!=saved.value)return refuse(__LINE__); + for(const auto& node:old)if(!(node.header>>24)) { + const Node* actual=find(now,node.address); + if(!actual || actual->header>>24)return refuse(__LINE__); + } + std::vector added; + for(const auto& node:complete)if(!find(old,node.address))added.push_back(node); + added=sorted(added); + if(added.empty())return refuse(__LINE__); + uint32_t end=old_first; + for(const auto& node:added) { + // The original allocator can reserve more bytes than DMA reads + // (e.g. an 8-byte DrawMode in a 16-byte allocation). Validate + // linked packets, not uninitialized padding between allocations. + if(node.address>24))return refuse(__LINE__); + end=node.address+4u+(node.header>>24)*4u; + if(end>bytes || (final_cursor && end>final_cursor))return refuse(__LINE__); + } + if(final_cursor) { + if(final_cursor=bytes)return refuse(__LINE__); + end=final_cursor; + } + const uint32_t length=end-old_first; + if(length>bytes-new_first)return refuse(__LINE__); + const uint32_t new_end=new_first+length; + for(const auto& node:live) { + const uint32_t node_end=node.address+4u+(node.header>>24)*4u; + if(node.address=old_first && address terminals; + for(const auto& change:changes) { + const Node* prior=find(old,change.address); + if(!prior)continue; + if(prior->header>>24 || change.value>>24 || !is_tail(change.value&0xffffffu))return refuse(__LINE__); + const Node* current=find(now,change.address); + if(!current || current->header>>24)return refuse(__LINE__); + uint32_t link=change.value&0xffffffu; + unsigned visited=0; + while(is_tail(link)) { + const Node* node=find(added,link); + if(!node || ++visited>added.size())return refuse(__LINE__); + const uint32_t next=node->header&0xffffffu; + if(!is_tail(next)) { + if(next!=(prior->header&0xffffffu))return refuse(__LINE__); + terminals.push_back({node->address,current->header&0xffffffu}); + } + link=next; + } + splice_writes.push_back({change.address,relocate(change.value&0xffffffu)}); + } + if(terminals.empty())return refuse(__LINE__); + // Every appended packet belongs to one proved head-insert chain. + std::vector owned; + for(const auto& change:changes)if(find(old,change.address)) { + uint32_t link=change.value&0xffffffu; + while(is_tail(link)) { + owned.push_back(link); + if(owned.size()>added.size())return refuse(__LINE__); + const Node* node=find(added,link); + if(!node)return refuse(__LINE__); + link=node->header&0xffffffu; + } + } + std::sort(owned.begin(),owned.end()); + if(owned.size()!=added.size() || std::adjacent_find(owned.begin(),owned.end())!=owned.end())return refuse(__LINE__); + for(const auto& node:added) { + uint32_t link=node.header&0xffffffu; + if(is_tail(link))link=relocate(link); + else { + auto terminal=std::find_if(terminals.begin(),terminals.end(),[&](const Word&w){return w.address==node.address;}); + if(terminal==terminals.end())return refuse(__LINE__); + link=terminal->value; + } + splice_writes.push_back({relocate(node.address),(node.header&0xff000000u)|link}); + for(uint32_t offset=4;offset<=((node.header>>24)*4u);offset+=4) { + uint32_t value=word(after_core.data(),node.address+offset); + const auto change=std::lower_bound(changes.begin(),changes.end(),node.address+offset, + [](const Word& w,uint32_t address){return w.addressaddress==node.address+offset)value=change->value; + splice_writes.push_back({relocate(node.address+offset),value}); + } + } + for(const auto& change:changes) { + if(is_tail(change.address) || find(old,change.address))continue; + if(change.address==(cursor_word&0x1fffffu)) { + if((change.value&0x1fffffu)!=end)return refuse(__LINE__); + splice_writes.push_back({change.address,(current_cursor&0xffe00000u)|new_end}); + } else { + if(word(ram,change.address)!=word(after_core.data(),change.address))return refuse(__LINE__); + splice_writes.push_back(change); + } + } + relocated_first=old_first;relocated_last=end;relocated_destination=new_first; + return true; + } + uint32_t shadow_destination(uint32_t address) const { + return address>=relocated_first && address + +static int failures=0; +#define CHECK(c) do {if(!(c)){std::fprintf(stderr,"FAIL line %d: %s\n",__LINE__,#c);++failures;}}while(0) +static void put(std::vector& ram,uint32_t address,uint32_t value) { + std::memcpy(ram.data()+address,&value,4); +} +int main() { + std::vector ram(0x200000); + constexpr uint32_t OT=0x1000,Prefix=0x1100,Core=0x1200,Tail=0x1400; + put(ram,OT,Prefix);put(ram,Prefix,0x02ffffffu); + put(ram,Prefix+4,0x64000000);put(ram,Prefix+8,0x000a000b); + PSXOTRelocation journal; + CHECK(journal.begin(ram.data(),ram.size(),OT)); + put(ram,OT,Core);put(ram,Core,0x02000000u|Prefix); + put(ram,Core+4,0x20000000);put(ram,Core+8,0x000c000d); + CHECK(journal.end(ram.data(),ram.size(),0x1300)); + const auto core=ram; + put(ram,OT,Tail);put(ram,Tail,0x02000000u|Core); + put(ram,Tail+4,0x64000000);put(ram,Tail+8,0x00100020); + CHECK(journal.prepare(ram.data(),ram.size())); + ram=core;put(ram,Core+8,0x000d000e); // interpolated core coordinates + CHECK(journal.matches(ram.data(),ram.size(),OT,0x1300)); + CHECK(!journal.matches(ram.data(),ram.size(),OT,0x1304)); + CHECK(!journal.matches(ram.data(),ram.size(),OT+4,0x1300)); + put(ram,Core,0x02000000u|Tail); + CHECK(!journal.matches(ram.data(),ram.size(),OT,0x1300)); + ram=core;put(ram,Core+8,0x000d000e);journal.apply(ram.data()); + CHECK(PSXOTRelocation::word(ram.data(),OT)==Tail); + CHECK(PSXOTRelocation::word(ram.data(),Core+8)==0x000d000e); + CHECK(PSXOTRelocation::word(ram.data(),Tail+8)==0x00100020); + + // Tail must never overwrite interpolated output, or grow a core packet. + ram=core;put(ram,Core+8,0x10000000); + CHECK(!journal.prepare(ram.data(),ram.size())); + ram=core;put(ram,Core,0x03000000u|Prefix); + CHECK(!journal.prepare(ram.data(),ram.size())); + ram=core;put(ram,OT,0x300000); // out-of-RAM link + CHECK(!journal.prepare(ram.data(),ram.size())); + ram=core;put(ram,OT,OT); // cycle, bounded traversal + CHECK(!journal.prepare(ram.data(),ram.size())); + ram=core; + CHECK(journal.begin(ram.data(),ram.size(),OT)); + put(ram,Prefix+8,0x11112222); // in-place core reuse is unsupported + CHECK(!journal.end(ram.data(),ram.size(),0x1300)); + ram=core;put(ram,Core,0x02000000u|(Core+4)); + put(ram,Core+4,0x00ffffffu); // linked header aliases a packet payload + std::vector aliased; + CHECK(!PSXOTRelocation::walk(ram.data(),ram.size(),OT,aliased)); + // Variable core packet layout: relocate a contiguous two-packet HUD tail + // and splice its terminal onto the current OT head, preserving live core. + ram.assign(0x200000,0); + put(ram,OT,Prefix);put(ram,Prefix,0x02ffffffu); + put(ram,Prefix+4,0x64000000);put(ram,Prefix+8,0x000a000b); + PSXOTRelocation relocated; + CHECK(relocated.begin(ram.data(),ram.size(),OT,0x80001200)); + put(ram,OT,Core);put(ram,Core,0x02000000u|Prefix); + put(ram,Core+4,0x20000000);put(ram,Core+8,0x000c000d); + put(ram,0x1800,0x80001300); + CHECK(relocated.end(ram.data(),ram.size(),0x80001300)); + const auto recorded_core=ram; + put(ram,0x1300,0x02000000u|Core); + put(ram,0x1304,0x64000000);put(ram,0x1308,0x00100020); + put(ram,0x130c,0x02001300u); + put(ram,0x1310,0x60000000);put(ram,0x1314,0x00300040); + put(ram,OT,0x130c);put(ram,0x1800,0x80001318); + CHECK(relocated.prepare(ram.data(),ram.size())); + ram=recorded_core; + put(ram,OT,0x1220);put(ram,0x1220,0x02000000u|Prefix); + put(ram,0x1224,0x20000000);put(ram,0x1228,0x000e000f); + put(ram,0x1800,0x80001320); + CHECK(relocated.prepare_splice(ram.data(),ram.size(),OT,0x80001320,0x1800)); + CHECK(relocated.shadow_destination(0x1308)==0x1328); + relocated.apply_splice(ram.data()); + CHECK(PSXOTRelocation::word(ram.data(),OT)==0x132c); + CHECK(PSXOTRelocation::word(ram.data(),0x1320)==(0x02000000u|0x1220)); + CHECK(PSXOTRelocation::word(ram.data(),0x132c)==0x02001320u); + CHECK(PSXOTRelocation::word(ram.data(),0x1228)==0x000e000f); + CHECK(PSXOTRelocation::word(ram.data(),0x1800)==0x80001338); + CHECK(!relocated.prepare_splice(ram.data(),ram.size(),OT+4,0x80001320,0x1800)); + ram=recorded_core;put(ram,Prefix+8,0x00010002); + CHECK(!relocated.prepare_splice(ram.data(),ram.size(),OT,0x80001320,0x1800)); + ram=recorded_core; + CHECK(!relocated.prepare_splice(ram.data(),ram.size(),OT,0x80001200,0x1800)); + CHECK(!relocated.prepare_splice(ram.data(),ram.size(),OT,0x801ffffc,0x1800)); + ram=recorded_core;put(ram,0x1800,0x800012e0); + CHECK(relocated.prepare_splice(ram.data(),ram.size(),OT,0x800012e0,0x1800)); + relocated.apply_splice(ram.data()); + CHECK(PSXOTRelocation::word(ram.data(),OT)==0x12ec); + CHECK(PSXOTRelocation::word(ram.data(),0x12e0)==(0x02000000u|Core)); + // DrawMode's allocator reserves 16 bytes but DMA reads 8. Stale padding + // must neither be interpreted as a tag nor copied onto live core output. + ram=recorded_core; + put(ram,0x1300,0x01000000u|Core); + put(ram,0x1304,0xe1000220); + put(ram,0x1308,0xffffffffu);put(ram,0x130c,0xaaaaaaaa); + put(ram,0x1310,0x02001300u); + put(ram,0x1314,0x64000000);put(ram,0x1318,0x00100020); + put(ram,OT,0x1310);put(ram,0x1800,0x80001320); + CHECK(relocated.prepare(ram.data(),ram.size(),0x80001320)); + ram=recorded_core;put(ram,0x1800,0x80001340); + put(ram,0x1348,0x12345678);put(ram,0x134c,0x87654321); + CHECK(relocated.prepare_splice(ram.data(),ram.size(),OT,0x80001340,0x1800)); + relocated.apply_splice(ram.data()); + CHECK(PSXOTRelocation::word(ram.data(),OT)==0x1350); + CHECK(PSXOTRelocation::word(ram.data(),0x1340)==(0x01000000u|Core)); + CHECK(PSXOTRelocation::word(ram.data(),0x1348)==0x12345678); + CHECK(PSXOTRelocation::word(ram.data(),0x134c)==0x87654321); + CHECK(PSXOTRelocation::word(ram.data(),0x1800)==0x80001360); + ram=recorded_core;put(ram,0x1800,0x80001340); + // A packet outside the actual producer interval is still refused. + CHECK(relocated.prepare_splice(ram.data(),ram.size(),OT,0x801ffff0,0x1800)==false); + // A middle-list insertion into a primitive header is unsupported even if + // the complete DMA graph remains structurally valid. + ram=recorded_core; + put(ram,0x1300,0x02000000u|Prefix); + put(ram,0x1304,0x64000000);put(ram,0x1308,0x00100020); + put(ram,Core,0x02001300u);put(ram,0x1800,0x8000130c); + CHECK(relocated.prepare(ram.data(),ram.size())); + ram=recorded_core;put(ram,0x1800,0x80001320); + CHECK(!relocated.prepare_splice(ram.data(),ram.size(),OT,0x80001320,0x1800)); + std::printf("OT relocation guards: %s\n",failures?"FAIL":"PASS"); + return failures?1:0; +}