Просмотр исходного кода

Load Ember movie texture definitions before native registration

Millie 3 дней назад
Родитель
Сommit
8ff050be9f

+ 18 - 5
Sunrise/src/client/hooks/ember_movies/ember_movies.cpp

@@ -26,7 +26,7 @@ Owner firstCompleted{};
 std::uint64_t key{}, began{};
 unsigned movie{};
 Status state{};
-bool acquired{}, stopRequested{}, attempted{}, escapeHeld{};
+bool acquired{}, stopRequested{}, attempted{}, escapeHeld{}, finishing{};
 Playback playback{};
 MovieResource resource{};
 void* decoderOwner{};
@@ -78,6 +78,12 @@ void release() {
     presentation.store(false);
     if (acquired) { api.release(api.manager()); acquired=false; }
 }
+void finish() {
+    if (!resource.release()) return;
+    state=Status::complete; watching.store(false); finishing=false; report("complete",lastDecoderState);
+    if (movie==1) firstCompleted=owner;
+    else if (firstCompleted==owner) orbit_return::arm(owner);
+}
 void fail(const char* reason) {
     // Stop only our exact decoder asset, never another movie's playback.
     if (acquired) {
@@ -102,7 +108,7 @@ bool request(Owner next, std::uint64_t nextKey, unsigned index, bool stop) noexc
     } else if (!acquired && !resource.held() && state!=Status::queued && state!=Status::preparing && state!=Status::playing) {
         if (index==1 || !(next==owner)) firstCompleted={};
         owner=next; key=nextKey; movie=index; state=Status::queued; began=GetTickCount64();
-        stopRequested=false; playback={}; decoderOwner=nullptr; lastDecoderState=-1;
+        stopRequested=false; finishing=false; playback={}; decoderOwner=nullptr; lastDecoderState=-1;
         escapeHeld=(GetAsyncKeyState(VK_ESCAPE)&0x8000)!=0;
         watching.store(true); report("queued"); accepted=true;
     }
@@ -130,6 +136,9 @@ void poll(std::int32_t region, std::int32_t step) noexcept {
     if (region!=0 || step!=38) { fail("world_changed"); ReleaseSRWLockExclusive(&lock); return; }
     if (!frameReady.load()) { fail("frame_observer_unavailable"); ReleaseSRWLockExclusive(&lock); return; }
     if (!uiReady.load()) { fail("movie_ui_unavailable"); ReleaseSRWLockExclusive(&lock); return; }
+    // Native release may clear/reuse the decoder. Once EOF is observed, wait
+    // only for our resource cleanup; never reinterpret that cleared decoder.
+    if (finishing) { finish(); ReleaseSRWLockExclusive(&lock); return; }
     if (!resolve()) { state=Status::failed; watching.store(false); ReleaseSRWLockExclusive(&lock); return; }
     auto* manager=api.manager();
     auto* decoder=api.decoder();
@@ -138,6 +147,9 @@ void poll(std::int32_t region, std::int32_t step) noexcept {
         if (!resource.held() && !resource.begin(assets[movie-1])) {
             fail("resource_request_failed"); ReleaseSRWLockExclusive(&lock); return;
         }
+        if (!resource.advance()) {
+            fail("surface_dependency_failed"); ReleaseSRWLockExclusive(&lock); return;
+        }
         if (resource.state()==3 || resource.state()<0) {
             fail("resource_load_failed"); ReleaseSRWLockExclusive(&lock); return;
         }
@@ -178,9 +190,10 @@ void poll(std::int32_t region, std::int32_t step) noexcept {
         api.stop(manager); stopRequested=false; report("stop_requested",decoderState);
     }
     if (observed==Status::complete) {
-        release(); state=observed; watching.store(!resource.release()); report("complete",decoderState);
-        if (movie==1) firstCompleted=owner;
-        else if (firstCompleted==owner) orbit_return::arm(owner);
+        release();
+        // Lua submits movie two as soon as completion is visible. Finish native
+        // unregistration first, so that submission never races retained resources.
+        finishing=true; finish();
     }
     else if (observed==Status::failed) fail("decoder_failed_or_replaced");
     else state=observed;

+ 71 - 9
Sunrise/src/client/hooks/ember_movies/resources.cpp

@@ -88,6 +88,22 @@ bool stream_ready(std::uint32_t handle) {
     const auto location=item-(read<std::uintptr_t>(item+8)&mask);
     return movie_stream_ready(read<std::uint32_t>(item),read<std::uint32_t>(item+4),location);
 }
+int root_state(std::uint32_t handle) {
+    if (handle==0xFFFFFFFFU || !status) return -1;
+    auto* root=blob(handle);return root ? status(root) : -1;
+}
+bool definitions_ready(bool requireBuffers) {
+    for (unsigned i=0;i<movie_surface_definitions.size();++i) {
+        const auto tag=movie_surface_definitions[i];
+        const auto item=datum(tag);
+        if (!item) return false;
+        const auto at=reinterpret_cast<std::uintptr_t>(blob(tag));
+        if (!movie_definition_resident(read<std::uint32_t>(item),read<std::uint32_t>(item+4),at)
+            || !movie_definition_matches(i,read<std::uint8_t>(at),read<std::uint8_t>(at+6))) return false;
+        if (requireBuffers && read<std::uintptr_t>(at+8)==0) return false;
+    }
+    return true;
+}
 }
 bool sunburn_resident() noexcept {
     if (!resolve()) return false;
@@ -101,6 +117,7 @@ bool MovieResource::begin(std::uint32_t asset) noexcept {
     create(mgr,&root_,8,2,0,"mission_ember_movie");
     if (!held()) return false;
     auto* root=blob(root_);if (!root) return false;
+    retiringSurfaces_=false;
     // Retain metadata plus the compact stream mapping consumed by 41A160.
     // Its native kind-1 load initializes file offset/patch/size without copying the movie.
     for (const auto tag : movie_metadata(asset)) {
@@ -109,18 +126,49 @@ bool MovieResource::begin(std::uint32_t asset) noexcept {
     }
     const std::uint32_t streamRequest[]{movie_resource_kind,movie_stream(asset)};
     add(root,streamRequest);
-    for (const auto tag : movie_surfaces) {
+    // Native registration 1204163 immediately reads the referenced definition.
+    // A single flat batch cannot order that callback after the definition load.
+    for (const auto tag : movie_surface_definitions) {
         const std::uint32_t request[]{movie_resource_kind,tag};
         add(root,request);
     }
     submit(mgr,root_);asset_=asset;
     core::log::writef(core::log::Channel::client,core::log::Level::info,
-        "ev=ember_movie result=resource_requested asset=%08X root=%08X kind=1 metadata=4 stream=%08X surfaces=6",asset_,root_,movie_stream(asset_));
+        "ev=ember_movie result=resource_requested asset=%08X root=%08X kind=1 metadata=4 stream=%08X definitions=6",asset_,root_,movie_stream(asset_));
     return true;
 }
+bool MovieResource::advance() noexcept {
+    if (!held() || retiringSurfaces_) return false;
+    if (surfaces_!=0xFFFFFFFFU) return true;
+    __try {
+        const auto value=root_state(root_);
+        if (value==3 || value<0) return false;
+        if (value!=2) return true;
+        if (!definitions_ready(false)) return false;
+        auto* mgr=manager();
+        create(mgr,&surfaces_,8,2,0,"mission_ember_movie_surfaces");
+        if (surfaces_==0xFFFFFFFFU) return false;
+        auto* root=blob(surfaces_);if (!root) return false;
+        // 1204581 binds each raw buffer to definition+8. Both the buffer callback
+        // and the container registration require the retained definition first.
+        for (auto tag : movie_surface_buffers) {
+            const std::uint32_t request[]{movie_resource_kind,tag};add(root,request);
+        }
+        for (auto tag : movie_surfaces) {
+            const std::uint32_t request[]{movie_resource_kind,tag};add(root,request);
+        }
+        submit(mgr,surfaces_);
+        core::log::writef(core::log::Channel::client,core::log::Level::info,
+            "ev=ember_movie result=surface_dependencies_ready asset=%08X root=%08X buffers=6 containers=6",asset_,surfaces_);
+        return true;
+    } __except(EXCEPTION_EXECUTE_HANDLER) { return false; }
+}
 int MovieResource::state() const noexcept {
     if (!held() || !status) return -1;
-    __try { auto* root=blob(root_);return root ? status(root) : -1; }
+    __try {
+        const auto value=root_state(root_);
+        return value!=2 ? value : surfaces_==0xFFFFFFFFU ? 1 : root_state(surfaces_);
+    }
     __except(EXCEPTION_EXECUTE_HANDLER) { return -1; }
 }
 bool MovieResource::ready() const noexcept {
@@ -142,6 +190,7 @@ bool MovieResource::ready() const noexcept {
 bool MovieResource::prepare_surfaces() noexcept {
     if (state()!=2 || !publishSurfaces || !surfaceRegistrations) return false;
     __try {
+        if (!definitions_ready(true)) return false;
         for (unsigned i=0;i<movie_surfaces.size();++i) {
             auto* container=blob(movie_surfaces[i],0x80806B91U);
             if (!container || read<std::uint32_t>(reinterpret_cast<std::uintptr_t>(container))
@@ -158,11 +207,24 @@ bool MovieResource::prepare_surfaces() noexcept {
 }
 bool MovieResource::release() noexcept {
     if (!held()) return true;
-    const auto value=state();
-    if (!resource_can_release(value)) return false;
-    destroy(manager(),root_);
-    core::log::writef(core::log::Channel::client,core::log::Level::info,
-        "ev=ember_movie result=resource_released asset=%08X root=%08X",asset_,root_);
-    root_=0xFFFFFFFFU;asset_=0;return true;
+    __try {
+        if (surfaces_!=0xFFFFFFFFU) {
+            if (!resource_can_release(root_state(surfaces_))) return false;
+            destroy(manager(),surfaces_);surfaces_=0xFFFFFFFFU;retiringSurfaces_=true;
+            core::log::writef(core::log::Channel::client,core::log::Level::info,
+                "ev=ember_movie result=surfaces_retiring asset=%08X",asset_);
+            return false; // Native unregister/release callbacks run asynchronously.
+        }
+        if (retiringSurfaces_) {
+            if (!movie_surfaces_absent(read<SurfaceRegistrations>(surfaceRegistrations))) return false;
+            for (auto tag : movie_surfaces) if (blob(tag,0x80806B91U)) return false;
+            for (auto tag : movie_surface_buffers) if (blob(tag)) return false;
+        }
+        if (!resource_can_release(root_state(root_))) return false;
+        destroy(manager(),root_);
+        core::log::writef(core::log::Channel::client,core::log::Level::info,
+            "ev=ember_movie result=resource_released asset=%08X root=%08X",asset_,root_);
+        root_=0xFFFFFFFFU;asset_=0;retiringSurfaces_=false;return true;
+    } __except(EXCEPTION_EXECUTE_HANDLER) { return false; }
 }
 }

+ 5 - 2
Sunrise/src/client/hooks/ember_movies/resources.h

@@ -2,11 +2,14 @@
 #include <cstdint>
 namespace sunrise::client::hooks::ember_movies {
 bool sunburn_resident() noexcept;
-// Native root owns the complete dependency graph until playback has released it.
+// Load definitions before the callbacks which dereference them. Release the
+// dependent containers/buffers before releasing the definitions they use.
 class MovieResource {
-    std::uint32_t root_{0xFFFFFFFFU}, asset_{};
+    std::uint32_t root_{0xFFFFFFFFU}, surfaces_{0xFFFFFFFFU}, asset_{};
+    bool retiringSurfaces_{};
 public:
     bool begin(std::uint32_t asset) noexcept;
+    bool advance() noexcept; // nonblocking: start dependent load only after definitions settle
     int state() const noexcept; // native root: 1 pending, 2 ready, 3 failed
     bool ready() const noexcept;
     bool prepare_surfaces() noexcept; // frame-owned publication after the native player is idle

+ 24 - 0
Sunrise/src/client/hooks/ember_movies/surface_rules.h

@@ -8,11 +8,35 @@ constexpr std::array<std::uint32_t,6> movie_surfaces{
     0x80BCA022U,0x80BCA025U,0x80BCA028U,0x80BCA02BU,0x80BCA02EU,0x80BCA031U};
 constexpr std::array<std::uint32_t,6> movie_surface_definitions{
     0x80BCA021U,0x80BCA024U,0x80BCA026U,0x80BCA029U,0x80BCA02CU,0x80BCA02FU};
+constexpr std::array<std::uint32_t,6> movie_surface_buffers{
+    0x80BCA020U,0x80BCA023U,0x80BCA027U,0x80BCA02AU,0x80BCA02DU,0x80BCA030U};
+// Type-19 definitions expand from eight package bytes to a 16-byte native
+// allocation. The raw buffer loader later fills definition+8 (native 1204581).
+constexpr bool movie_definition_resident(std::uint32_t size, std::uint32_t typeInfo,
+                                         std::uintptr_t pointer) noexcept {
+    return size==0xC0000010U
+        && (typeInfo&0x3FFFFU)==0x44FBU && pointer!=0;
+}
+constexpr bool movie_definition_matches(unsigned index, std::uint8_t slot,
+                                        std::uint8_t format) noexcept {
+    return index<6 && slot==index+1 && format==(index<2 ? 0x7F : 0x23);
+}
 struct SurfaceRegistration {
     std::uint32_t count{}, entries[3]{}, selected{0xFFFFFFFFU};
 };
 static_assert(sizeof(SurfaceRegistration)==20);
 using SurfaceRegistrations=std::array<SurfaceRegistration,8>;
+constexpr bool movie_surfaces_absent(const SurfaceRegistrations& rows) noexcept {
+    for (const auto& row : rows) {
+        if (row.count>3) return false;
+        for (auto definition : movie_surface_definitions) {
+            if (row.selected==definition) return false;
+            for (unsigned i=0;i<row.count;++i)
+                if (row.entries[i]==definition) return false;
+        }
+    }
+    return true;
+}
 constexpr std::uint32_t selected_surface(const SurfaceRegistration& r) noexcept {
     return r.count>0 && r.count<=3 ? r.entries[r.count-1] : 0xFFFFFFFFU;
 }

+ 14 - 0
docs/mission-ember-final-corrections.md

@@ -1,5 +1,19 @@
 # 1AU: movie loader, surge audio and escape explosions
 
+## Freeze captured on `a47adbd`: surface registration dereferenced an unloaded definition
+
+The final escape trigger queued STM at t=289137 and submitted its resource root at t=289153. No playback submission followed. All six containers were resident, but all six definitions were unloaded (`FEFE0000 / type_info 001044FB / blob 0`). Scanning the frozen process found the render thread's saved exception: **C0000005 at game+1204163, RAX=0, RBX=80BCA021, RDX=4**. That instruction reads the definition's slot byte during container registration. This establishes the fault directly; neither decoding nor orbit return had begun.
+
+Evidence is preserved in `build/first-encounter-audit/a47adbd-surface-exception.txt`, `a47adbd-frozen-surfaces.txt`, `a47adbd-fiber/surface-exception-stack.bin`, and the `a47adbd-live-20260906-201858` log directory. The process was inspected without launching, stopping or modifying the game.
+
+The corrected loader uses two asynchronous roots. The first retains metadata, the compact movie stream mapping and the six definitions. Only after native completion and validation of each 16-byte type-19 definition, slot and format does it submit the second root containing the six raw backing buffers and six registration containers. Merely inserting definitions earlier in the same unordered batch would not establish this dependency.
+
+The backing buffer tags are `80BCA020`, `80BCA023`, `80BCA027`, `80BCA02A`, `80BCA02D`, and `80BCA030`. Their package type is `254FB`, paired with definition type `44FB`; both use native request kind 1. Native `1202430` expands the eight-byte definition to 16 bytes. The type-19 buffer callback at `1204581` fills definition+8, which `1184660` reads through `4A6340`. Playback now requires all six non-null backing pointers as well as the matching registrations and published selections.
+
+Cleanup releases the dependent root first and waits for the native container registrations, selections and backing records to disappear before releasing the definitions. Completion is not exposed to Lua until this cleanup finishes, preventing the next movie from being refused while the previous resources are retained. Once native EOF has been observed, cleanup no longer reads the released decoder. The immediate return-to-orbit selection remains after movie two completes; there is no added banner delay.
+
+The captured free-list/invalid-definition cases and outstanding-registration cleanup cases pass regression tests. All 24 portable tests and the native/package ABI verifier pass. Visible video, hidden HUD, both movie transitions and the orbit arrival still require a fresh live run of this correction.
+
 ## Correction after the active black-picture trace
 
 The manual replay established the failure directly: valid 1920×800 Y/U/V frames were present in native CPU buffers, and the UI command stream included the native movie command (`1B`, with packed header flags). A decoded frame from `picture-20260906-194230/` visibly contains the cutscene. The six GPU video surface slots were null during playback. Their native registration rows had **count 0 / selected FFFFFFFF**, with only stale candidate values. The prior raw metadata request did not retain the six surface containers named by `80BCA032`.

+ 22 - 0
tests/ending_retirement_test.cpp

@@ -65,12 +65,29 @@ int main() {
     assert(orbit.observe(160001,inApex)==OA::select);
     auto replaced=inOrbit;replaced.step=38;replaced.sameWorld=false;
     assert(orbit.observe(160002,replaced)==OA::canceled);
+    // a47adbd: container 80BCA022 loaded, but definition 80BCA021 was
+    // FEFE0000 / 001044FB / null. Native 1204163 faulted reading its slot.
+    assert(!movies::movie_definition_resident(0xFEFE0000,0x1044FB,0));
+    assert(!movies::movie_definition_resident(0xFEFE0000,0x1044FB,0x1234000));
+    assert(!movies::movie_definition_resident(0xC0000010,0x44FB,0));
+    assert(!movies::movie_definition_resident(0xC0000008,0x44FB,0x1234000));
+    assert(!movies::movie_definition_resident(0xC0000010,0x254FB,0x1234000)); // raw buffer is not a definition
+    assert(movies::movie_definition_resident(0xC0000010,0x44FB,0x1234000));
+    assert(movies::movie_definition_resident(0xC0000010,0x4044FB,0x1234000)); // native residency flags
+    for (unsigned i=0;i<6;++i) {
+        assert(movies::movie_definition_matches(i,i+1,i<2 ? 0x7F : 0x23));
+        assert(!movies::movie_definition_matches(i,0,i<2 ? 0x7F : 0x23));
+    }
+    assert(!movies::movie_definition_matches(0,1,0x23));
+    assert(!movies::movie_definition_matches(6,7,0x23));
     movies::SurfaceRegistrations surfaces{};
     for (unsigned i=1;i<=6;++i) surfaces[i].entries[0]=movies::movie_surface_definitions[i-1];
     // Live black-video capture: old candidate handles remain, but no container
     // holds a registration and every selected surface is FFFFFFFF.
     assert(!movies::movie_surfaces_registered(surfaces));
+    assert(movies::movie_surfaces_absent(surfaces)); // stale candidates do not retain references
     for (unsigned i=1;i<=6;++i) surfaces[i].count=1;
+    assert(!movies::movie_surfaces_absent(surfaces)); // hold definitions until native unregister
     assert(movies::movie_surfaces_registered(surfaces));
     assert(!movies::movie_surfaces_selected(surfaces));
     for (unsigned i=1;i<=6;++i) surfaces[i].selected=surfaces[i].entries[0];
@@ -86,6 +103,11 @@ int main() {
     assert(!movies::movie_surfaces_registered(surfaces)); // do not publish an unrelated pending slot
     surfaces[7].selected=123;
     assert(movies::movie_surfaces_selected(surfaces));
+    auto removed=surfaces;
+    for (unsigned i=1;i<=6;++i) removed[i].count=0;
+    assert(!movies::movie_surfaces_absent(removed)); // selection is still using the definition
+    for (unsigned i=1;i<=6;++i) removed[i].selected=0xFFFFFFFF;
+    assert(movies::movie_surfaces_absent(removed));
     // Native kind 2 resolves shared-tag records; these ordinary movie tags require 1.
     static_assert(movies::movie_resource_kind==1);
     assert((movies::movie_metadata(0x80BCA001)==std::array<std::uint32_t,4>{0x80BCA001,0x80BCA000,0x80B9EB33,0x80BCA032}));

+ 20 - 1
tests/verify_ember_movie_native.py

@@ -50,6 +50,16 @@ assert surface + 13 + struct.unpack_from('<i', data, surface + 9)[0] == 0x2E800B
 target(0xB5F4C5, 9, surface)  # native world activation publishes the surface registration stacks
 target(0x1184660, 0x37, 0x1202C20)  # renderer fetches selected surface definitions
 assert data[0x116A070:0x116A077] == bytes.fromhex('48 83 38 00 0f 95 c0')  # missing surface => skip GPU upload
+# Captured a47adbd exception: C0000005, RIP 1204163, RAX=0, RBX=80BCA021,
+# RDX=4. The registration callback reads the unloaded definition's slot directly.
+assert data[0x1204160:0x1204167] == bytes.fromhex('48 2b c1 48 0f be 08')
+# Native type-19 definitions need 16 bytes, not their eight-byte package size.
+assert struct.unpack_from('<I',data,0x1202488+18*4)[0] == 0x1202478
+assert data[0x1202478:0x120247F] == bytes.fromhex('ba 10 00 00 00 8b c2')
+# The type-19 raw buffer callback fills definition+8; the renderer reads that pointer.
+assert struct.unpack_from('<I',data,0x12045D0+18*4)[0] == 0x1204581
+assert data[0x12045BF:0x12045C6] == bytes.fromhex('48 2b c1 4c 89 40 08')
+assert data[0x4A6340:0x4A6345] == bytes.fromhex('48 8b 41 08 c3')
 ui = signature('Sunrise/src/client/hooks/bootflow/ember_movie_ui.cpp', 'sig')
 assert ui == 0x132BD80
 target(0x132B890, 0x353, 0x1278FF0)  # native movie command is queued before either UI layer
@@ -104,6 +114,12 @@ if len(sys.argv) > 2:
                           (0x80BCA022, 0x80806B91), (0x80BCA025, 0x80806B91),
                           (0x80BCA028, 0x80806B91), (0x80BCA02B, 0x80806B91),
                           (0x80BCA02E, 0x80806B91), (0x80BCA031, 0x80806B91),
+                          (0x80BCA021, 0x80BCA020), (0x80BCA024, 0x80BCA023),
+                          (0x80BCA026, 0x80BCA027), (0x80BCA029, 0x80BCA02A),
+                          (0x80BCA02C, 0x80BCA02D), (0x80BCA02F, 0x80BCA030),
+                          (0x80BCA020, 0x80BCA021), (0x80BCA023, 0x80BCA024),
+                          (0x80BCA027, 0x80BCA026), (0x80BCA02A, 0x80BCA029),
+                          (0x80BCA02D, 0x80BCA02C), (0x80BCA030, 0x80BCA02F),
                           (0x80BCA034, 0xFFFFFFFF), (0x80C7C000, 0xFFFFFFFF)]:
         # Tag package IDs include the bank: 80BCAxxx belongs to package 01E5.
         package = (tag >> 13) & 0x3FF
@@ -117,7 +133,10 @@ if len(sys.argv) > 2:
         assert type_info & 0xF000 != 0x2000, (hex(tag), hex(type_info))
         if expected == 0xFFFFFFFF:
             assert (type_info & 0x30000) == 0x10000 and (type_info >> 6) & 0x3F == 24
-    print('Installed movie metadata, compact streams, six surface containers and kind-1 package types verified.')
+        if 0x80BCA020 <= expected <= 0x80BCA030:
+            definitions = {0x80BCA021,0x80BCA024,0x80BCA026,0x80BCA029,0x80BCA02C,0x80BCA02F}
+            assert (type_info & 0x3FFFF) == (0x44FB if tag in definitions else 0x254FB)
+    print('Movie metadata, streams, surface containers, definitions and raw buffers all use kind 1.')
     tags = repo / 'build/first-encounter-audit/tags'
     catalog = (tags / '80BCA032.bin').read_bytes()
     for i, (container, definition) in enumerate(zip(