Ver código fonte

Load Ember movie stream descriptors and correct beam poses

Millie 2 dias atrás
pai
commit
4b37549be2

+ 11 - 0
Sunrise/src/client/hooks/ember_movies/readiness_rules.h

@@ -9,6 +9,17 @@ constexpr std::array<std::uint32_t,4> movie_metadata(std::uint32_t asset) noexce
     if (asset!=0x80BCA001U && asset!=0x80BCA003U) return {};
     if (asset!=0x80BCA001U && asset!=0x80BCA003U) return {};
     return {asset,asset-1,asset==0x80BCA001U ? 0x80B9EB33U : 0x80B9EB34U,0x80BCA032U};
     return {asset,asset-1,asset==0x80BCA001U ? 0x80B9EB33U : 0x80B9EB34U,0x80BCA032U};
 }
 }
+constexpr std::uint32_t movie_stream(std::uint32_t asset) noexcept {
+    return asset==0x80BCA001U ? 0x80BCA034U : asset==0x80BCA003U ? 0x80C7C000U : 0xFFFFFFFFU;
+}
+// Stream datums hold size plus a packed package offset/patch id, not a CPU blob.
+// Native 3591B0 initializes this mapping without allocating the whole media file.
+constexpr bool movie_stream_ready(std::uint32_t size, std::uint32_t typeInfo,
+                                   std::uint64_t location) noexcept {
+    return (size&0xC0000000U)==0xC0000000U && (size&0x3FFFFFFFU)>0
+        && ((typeInfo>>6)&0x3FU)==24 && (typeInfo&0x30000U)==0x10000U
+        && location<=0xFFFFFFFFULL && (location&0xFFFFFF00ULL)!=0;
+}
 constexpr bool movie_resources_ready(int rootState,std::uint32_t asset,bool wrapperResident,
 constexpr bool movie_resources_ready(int rootState,std::uint32_t asset,bool wrapperResident,
     std::uint32_t header,bool headerResident,std::uint32_t media) noexcept {
     std::uint32_t header,bool headerResident,std::uint32_t media) noexcept {
     return rootState==2 && (asset==0x80BCA001U || asset==0x80BCA003U)
     return rootState==2 && (asset==0x80BCA001U || asset==0x80BCA003U)

+ 23 - 10
Sunrise/src/client/hooks/ember_movies/resources.cpp

@@ -54,18 +54,28 @@ std::uintptr_t row(std::uint32_t handle) {
     const auto table=read<std::uintptr_t>(head);
     const auto table=read<std::uintptr_t>(head);
     return table ? table+64*pool : 0;
     return table ? table+64*pool : 0;
 }
 }
-void* blob(std::uint32_t handle,std::uint32_t expectedClass=0) {
-    const auto pool=row(handle);if (!pool) return nullptr;
+std::uintptr_t datum(std::uint32_t handle) {
+    const auto pool=row(handle);if (!pool) return 0;
     const auto storage=read<std::uintptr_t>(pool+8);
     const auto storage=read<std::uintptr_t>(pool+8);
     const auto stride=read<std::uint32_t>(pool+48);
     const auto stride=read<std::uint32_t>(pool+48);
-    if (!storage || !stride) return nullptr;
-    const auto item=storage+stride*static_cast<std::uintptr_t>(handle&0x1FFFU);
+    if (!storage || !stride) return 0;
+    return storage+stride*static_cast<std::uintptr_t>(handle&0x1FFFU);
+}
+void* blob(std::uint32_t handle,std::uint32_t expectedClass=0) {
+    const auto item=datum(handle);if (!item) return nullptr;
     // Unloaded package entries contain FEFE free-list markers, not a definition.
     // Unloaded package entries contain FEFE free-list markers, not a definition.
     if (expectedClass && read<std::uint32_t>(item)!=expectedClass) return nullptr;
     if (expectedClass && read<std::uint32_t>(item)!=expectedClass) return nullptr;
-    const auto mask=static_cast<std::uintptr_t>(static_cast<std::intptr_t>(read<std::int32_t>(pool+52)));
+    const auto mask=static_cast<std::uintptr_t>(static_cast<std::intptr_t>(read<std::int32_t>(row(handle)+52)));
     const auto at=item-(read<std::uintptr_t>(item+8)&mask);
     const auto at=item-(read<std::uintptr_t>(item+8)&mask);
     return reinterpret_cast<void*>(at);
     return reinterpret_cast<void*>(at);
 }
 }
+bool stream_ready(std::uint32_t handle) {
+    const auto item=datum(handle);if (!item) return false;
+    const auto mask=static_cast<std::uintptr_t>(static_cast<std::intptr_t>(read<std::int32_t>(row(handle)+52)));
+    // Never dereference this value: it encodes the file offset and patch id.
+    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);
+}
 }
 }
 bool sunburn_resident() noexcept {
 bool sunburn_resident() noexcept {
     if (!resolve()) return false;
     if (!resolve()) return false;
@@ -79,15 +89,17 @@ bool MovieResource::begin(std::uint32_t asset) noexcept {
     create(mgr,&root_,8,2,0,"mission_ember_movie");
     create(mgr,&root_,8,2,0,"mission_ember_movie");
     if (!held()) return false;
     if (!held()) return false;
     auto* root=blob(root_);if (!root) return false;
     auto* root=blob(root_);if (!root) return false;
-    // Explicitly retain the small metadata records dereferenced by 41A810 and
-    // the subtitle reader. Native playback streams the 640MB video entry itself.
+    // 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)) {
     for (const auto tag : movie_metadata(asset)) {
         const std::uint32_t request[]{movie_resource_kind,tag};
         const std::uint32_t request[]{movie_resource_kind,tag};
         add(root,request);
         add(root,request);
     }
     }
+    const std::uint32_t streamRequest[]{movie_resource_kind,movie_stream(asset)};
+    add(root,streamRequest);
     submit(mgr,root_);asset_=asset;
     submit(mgr,root_);asset_=asset;
     core::log::writef(core::log::Channel::client,core::log::Level::info,
     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",asset_,root_);
+        "ev=ember_movie result=resource_requested asset=%08X root=%08X kind=1 metadata=4 stream=%08X",asset_,root_,movie_stream(asset_));
     return true;
     return true;
 }
 }
 int MovieResource::state() const noexcept {
 int MovieResource::state() const noexcept {
@@ -106,8 +118,9 @@ bool MovieResource::ready() const noexcept {
         const auto metadata=movie_metadata(asset_);
         const auto metadata=movie_metadata(asset_);
         if (read<std::uint32_t>(reinterpret_cast<std::uintptr_t>(wrapper)+12)!=metadata[2]
         if (read<std::uint32_t>(reinterpret_cast<std::uintptr_t>(wrapper)+12)!=metadata[2]
             || !blob(metadata[2],0x80809A88U) || !blob(metadata[3],0x80806B8FU)) return false;
             || !blob(metadata[2],0x80809A88U) || !blob(metadata[3],0x80806B8FU)) return false;
-        return movie_resources_ready(2,asset_,true,header,info!=nullptr,
-            info ? read<std::uint32_t>(reinterpret_cast<std::uintptr_t>(info)+0x18) : 0xFFFFFFFFU);
+        const auto media=info ? read<std::uint32_t>(reinterpret_cast<std::uintptr_t>(info)+0x18) : 0xFFFFFFFFU;
+        return movie_resources_ready(2,asset_,true,header,info!=nullptr,media)
+            && media==movie_stream(asset_) && stream_ready(media);
     } __except(EXCEPTION_EXECUTE_HANDLER) { return false; }
     } __except(EXCEPTION_EXECUTE_HANDLER) { return false; }
 }
 }
 bool MovieResource::release() noexcept {
 bool MovieResource::release() noexcept {

+ 16 - 2
docs/mission-ember-final-corrections.md

@@ -1,6 +1,20 @@
 # 1AU: movie loader, surge audio and escape explosions
 # 1AU: movie loader, surge audio and escape explosions
 
 
-## Current status
+## Latest playtest: metadata ready, stream missing
+
+The user confirms `13f07ee` no longer freezes, but neither ending plays. The log records movie 1 resources ready and playback submitted at t=256404, decoder state 1 at t=256487, state 7 at t=256527, then failure and resource release at t=256598. The escape trigger and movie dispatch are working; the native decoder never reaches playing state 5.
+
+A read-only capture after the failure found stream tag `80BCA034` still in a free-list state: datum `FEFE0035 / type_info 0`, decoding to location 0. The movie headers were resident. Native video I/O `41A160` consumes the media datum through `3597C0` (open package/patch) and `357DA0` (file offset and length). The previous bridge explicitly requested four metadata records but omitted this stream mapping.
+
+The native load job `3591B0`, branch `3592C6`, handles `(type_info & 30000) == 10000` by publishing **offset | patch id** and **length | C0000000** with `351D00`, then returning before the ordinary allocation/read branch. Requesting the media tag initializes this compact mapping; it does **not** allocate a buffer for the full movie. The prior assumption that omitting this request was needed to preserve streaming was wrong.
+
+Both movies now include their exact media tag as a fifth kind-1 root request: STM `80BCA034`, CNN `80C7C000`. Playback additionally requires the video-family/type bits, an encoded nonzero length and valid package location. The mapping is never dereferenced as a CPU pointer. The resource root holds it until native playback releases its reference. Tests cover the captured free-list datum, wrong types, empty lengths and invalid locations, and the native/package check verifies this stream initialization branch and both authored media entries.
+
+The user's same test establishes the opposite beam pose mapping from the prior implementation: **close = normal, open = surge**. Both beam devices now follow that mapping at entry, warning, cooling and shutdown. The encounter clock and alarm's surge callback remain unchanged.
+
+Evidence: `build/first-encounter-audit/13f07ee-decoder-failure.log`. The latest change is compiled and checked offline; successful rendered playback is still awaiting the next run.
+
+## Previous correction and validation
 
 
 The latest changes address the captured `bc3e912` movie-loader crash, couple the alarm request to the beam surge, and forward escape triggers into the authored explosion scene. Release compilation, 24 portable tests, all five Lua mission suites, native ABI/package checks, and scene schema/content checks pass. **Rendered ending playback, audible alignment and visible explosions still require a fresh game test.** Offline verification does not establish those outcomes.
 The latest changes address the captured `bc3e912` movie-loader crash, couple the alarm request to the beam surge, and forward escape triggers into the authored explosion scene. Release compilation, 24 portable tests, all five Lua mission suites, native ABI/package checks, and scene schema/content checks pass. **Rendered ending playback, audible alignment and visible explosions still require a fresh game test.** Offline verification does not establish those outcomes.
 
 
@@ -19,7 +33,7 @@ Native `426920` derives the correct request kind from each package entry: kind 2
 | `80B9EB33`, `80B9EB34` subtitle metadata | `80809A88` | `1019` | 1 |
 | `80B9EB33`, `80B9EB34` subtitle metadata | `80809A88` | `1019` | 1 |
 | `80BCA032` shared movie metadata | `80806B8F` | `103B` | 1 |
 | `80BCA032` shared movie metadata | `80806B8F` | `103B` | 1 |
 
 
-The correction creates one native asynchronous root and adds the selected wrapper, its header, its subtitle metadata and `80BCA032`, each using `{1, tag}`. It retains that root through playback. The large media entry is streamed by the native player, not loaded wholesale into the root.
+The correction creates one native asynchronous root and adds the selected wrapper, its header, its subtitle metadata and `80BCA032`, each using `{1, tag}`. It retains that root through playback. The media mapping must also be included, as corrected above; its loader path preserves native streaming.
 
 
 Playback requires native root state 2, matching resident wrapper/header classes, the expected subtitle reference and resident metadata, and a present media reference. Only then call the original acquire/play pair. Both native completion and Escape stopping must finish before releasing the root. Pending roots are polled; the frame never invokes a global I/O drain. Failed or timed-out preparation never completes the mission.
 Playback requires native root state 2, matching resident wrapper/header classes, the expected subtitle reference and resident metadata, and a present media reference. Only then call the original acquire/play pair. Both native completion and Escape stopping must finish before releasing the root. Pending roots are polled; the frame never invokes a global I/O drain. Failed or timed-out preparation never completes the mission.
 
 

+ 1 - 1
docs/mission-ember-prerendered-ending.md

@@ -15,7 +15,7 @@ The placement's entity reference is at +0x30. Both entities contain class 808065
 
 
 ## Required residency correction
 ## Required residency correction
 
 
-The installed `4f5c706` bridge froze on its first live STM request: the resource lookup read address 0x8 at `349D2C`. Both movie wrappers and headers were registered but unloaded. The subsequent `bc3e912` loader also failed: its kind-2 request sent the ordinary movie tag down the shared-resource path, leaving a handle at `FFFFFFFF` before a worker dereferenced it at `3374C6`. The current bridge uses the package-verified kind 1 and pins the small movie, header, subtitle and shared metadata tags before playback. See [the correction evidence and validation](mission-ember-final-corrections.md). Rendered playback remains unconfirmed until a fresh game test.
+The installed `4f5c706` bridge froze on its first live STM request: the resource lookup read address 0x8 at `349D2C`. Both movie wrappers and headers were registered but unloaded. The subsequent `bc3e912` loader also failed: its kind-2 request sent the ordinary movie tag down the shared-resource path, leaving a handle at `FFFFFFFF` before a worker dereferenced it at `3374C6`. The current bridge uses the package-verified kind 1 and pins the movie, header, subtitle and shared metadata tags plus the compact video-stream mapping before playback. The `13f07ee` test reached decoder preparation but failed with state 7; it had omitted that stream mapping. Native `3591B0` initializes the media datum without copying the full video into RAM. See [the correction evidence and validation](mission-ember-final-corrections.md). Rendered playback remains unconfirmed until a fresh game test.
 
 
 ## Native playback bridge
 ## Native playback bridge
 
 

+ 3 - 5
scripts/mission_ember/apex.lua

@@ -69,12 +69,10 @@ return function(m, a, ending)
     -- `ring_device_sense` in the log carries their native position and power.
     -- `ring_device_sense` in the log carries their native position and power.
     local structure_objects = {"SPECOPS_APEX_RING_CORE_OBJECT", "SPECOPS_APEX_RING_RING_OBJECT"}
     local structure_objects = {"SPECOPS_APEX_RING_CORE_OBJECT", "SPECOPS_APEX_RING_RING_OBJECT"}
     local beam_devices = {"SPECOPS_APEX_RING_LASER_DEVICE", "SPECOPS_APEX_RING_RING_DEVICE"}
     local beam_devices = {"SPECOPS_APEX_RING_LASER_DEVICE", "SPECOPS_APEX_RING_RING_DEVICE"}
-    -- These carry the same inverted pose as the landing and clamshell bridges, where the
-    -- authored position-0 lane is the driven end rather than the resting one. Confirmed in game:
-    -- the resting drive rendered as the surge and the driven end as the weapon's normal state,
-    -- so resting is `open` and the surge drives to `close`.
+    -- The latest live test establishes the beam's poses: close is normal, open is surge.
+    -- Keep this mapping separate from the encounter clock and shutter cooling state.
     local function beam_pose(c, surging, snap)
     local function beam_pose(c, surging, snap)
-        for _, name in ipairs(beam_devices) do a.device(c, name, not surging, snap) end
+        for _, name in ipairs(beam_devices) do a.device(c, name, surging, snap) end
     end
     end
     -- Firing: the beam is present and both of its devices are powered.
     -- Firing: the beam is present and both of its devices are powered.
     local function beam(c, s, firing, snap)
     local function beam(c, s, firing, snap)

+ 13 - 1
tests/ending_retirement_test.cpp

@@ -26,7 +26,19 @@ int main() {
     static_assert(movies::movie_resource_kind==1);
     static_assert(movies::movie_resource_kind==1);
     assert((movies::movie_metadata(0x80BCA001)==std::array<std::uint32_t,4>{0x80BCA001,0x80BCA000,0x80B9EB33,0x80BCA032}));
     assert((movies::movie_metadata(0x80BCA001)==std::array<std::uint32_t,4>{0x80BCA001,0x80BCA000,0x80B9EB33,0x80BCA032}));
     assert((movies::movie_metadata(0x80BCA003)==std::array<std::uint32_t,4>{0x80BCA003,0x80BCA002,0x80B9EB34,0x80BCA032}));
     assert((movies::movie_metadata(0x80BCA003)==std::array<std::uint32_t,4>{0x80BCA003,0x80BCA002,0x80B9EB34,0x80BCA032}));
-    assert((movies::movie_metadata(0x80BCA034)==std::array<std::uint32_t,4>{})); // never pin raw video
+    assert((movies::movie_metadata(0x80BCA034)==std::array<std::uint32_t,4>{})); // media is not a wrapper
+    assert(movies::movie_stream(0x80BCA001)==0x80BCA034);
+    assert(movies::movie_stream(0x80BCA003)==0x80C7C000);
+    assert(movies::movie_stream(0x80BCA000)==0xFFFFFFFF);
+    // The 13f07ee capture: metadata was loaded, but the video stream still held a
+    // free-list entry (FEFE0035, 0, location 0). Native CRI entered error state 7.
+    assert(!movies::movie_stream_ready(0xFEFE0035,0,0));
+    assert(movies::movie_stream_ready(0xE80779A0,0x41363B,0x10000005));
+    assert(!movies::movie_stream_ready(0xC0000000,0x41363B,0x10000005));
+    assert(!movies::movie_stream_ready(0x280779A0,0x41363B,0x10000005));
+    assert(!movies::movie_stream_ready(0xE80779A0,0x40103B,0x10000005));
+    assert(!movies::movie_stream_ready(0xE80779A0,0x41363B,0));
+    assert(!movies::movie_stream_ready(0xE80779A0,0x41363B,0x100000005ULL));
     // Captured crash: registered movie tags contain free-list entries, not resident headers.
     // Captured crash: registered movie tags contain free-list entries, not resident headers.
     assert(!movies::movie_resources_ready(1,0x80BCA001,false,0,false,0xFFFFFFFF));
     assert(!movies::movie_resources_ready(1,0x80BCA001,false,0,false,0xFFFFFFFF));
     assert(!movies::movie_resources_ready(2,0x80BCA001,false,0x80BCA000,true,0x80BCA034));
     assert(!movies::movie_resources_ready(2,0x80BCA001,false,0x80BCA000,true,0x80BCA034));

+ 7 - 8
tests/mission_ember_routes_test.lua

@@ -213,7 +213,7 @@ timer('ember.apex.setup.')
 for _,name in ipairs({'SPECOPS_APEX_RING_LASER_OBJECT','SPECOPS_APEX_RING_RING_OBJECT'}) do
 for _,name in ipairs({'SPECOPS_APEX_RING_LASER_OBJECT','SPECOPS_APEX_RING_RING_OBJECT'}) do
     call(R.dispatch,'object',c,s,event(name,{generation=1,present=true,alive=true}))
     call(R.dispatch,'object',c,s,event(name,{generation=1,present=true,alive=true}))
 end
 end
-assert(transition('SPECOPS_APEX_RING_LASER_DEVICE',true).transition=='open')
+assert(transition('SPECOPS_APEX_RING_LASER_DEVICE',true).transition=='close')
 assert(transition('SPECOPS_APEX_RING_LASER_DEVICE',true).snap==false, 'startup must run effect events instead of seeking past them')
 assert(transition('SPECOPS_APEX_RING_LASER_DEVICE',true).snap==false, 'startup must run effect events instead of seeking past them')
 local primedCalls=#calls
 local primedCalls=#calls
 call(R.dispatch,'object',c,s,event('SPECOPS_APEX_RING_LASER_OBJECT',{generation=1,present=true,alive=true}))
 call(R.dispatch,'object',c,s,event('SPECOPS_APEX_RING_LASER_OBJECT',{generation=1,present=true,alive=true}))
@@ -279,7 +279,7 @@ local beamMoved=false
 for i=beforeAudio+1,#calls do
 for i=beforeAudio+1,#calls do
     local row=calls[i]
     local row=calls[i]
     if row[1]=='transition' and row[2]==slotDefs[m.Slot.SPECOPS_APEX_RING_LASER_DEVICE].name then
     if row[1]=='transition' and row[2]==slotDefs[m.Slot.SPECOPS_APEX_RING_LASER_DEVICE].name then
-        beamMoved=row[3].transition=='close'
+        beamMoved=row[3].transition=='open'
     end
     end
     if row[1]=='play_sequence' then
     if row[1]=='play_sequence' then
         assert(beamMoved,'alarm was requested before the surge state changed')
         assert(beamMoved,'alarm was requested before the surge state changed')
@@ -291,11 +291,10 @@ assert(vars['ember.apex.vent_step']=='warning' and timers['ember.apex.vents.1']=
 -- The weapon fires continuously through the fight; the exposure cycle must not blink it.
 -- The weapon fires continuously through the fight; the exposure cycle must not blink it.
 -- The surge is the authored device drive of a beam that stays present and powered.
 -- The surge is the authored device drive of a beam that stays present and powered.
 assert(vars['ember.apex.beam']==true,'the weapon must keep firing across the cycle')
 assert(vars['ember.apex.beam']==true,'the weapon must keep firing across the cycle')
--- Inverted pose, as on the landing and clamshell bridges: resting is open, the surge drives
--- to close. The opposite mapping rendered the surge as the weapon's normal state.
+-- Live-confirmed pose: close is normal; open is surge. Shutter timing stays independent.
 assert(vars['ember.apex.surge']==true,'the warning must drive the beam')
 assert(vars['ember.apex.surge']==true,'the warning must drive the beam')
-assert(transition('SPECOPS_APEX_RING_LASER_DEVICE', true).transition=='close')
-assert(transition('SPECOPS_APEX_RING_RING_DEVICE', true).transition=='close')
+assert(transition('SPECOPS_APEX_RING_LASER_DEVICE', true).transition=='open')
+assert(transition('SPECOPS_APEX_RING_RING_DEVICE', true).transition=='open')
 assert(transition('REACTOR_CLAMSHELL_EAST_DOOR_A_DEVICE').transition=='close')
 assert(transition('REACTOR_CLAMSHELL_EAST_DOOR_A_DEVICE').transition=='close')
 local coolingStart=#calls
 local coolingStart=#calls
 timer('ember.apex.vents.')
 timer('ember.apex.vents.')
@@ -307,7 +306,7 @@ for i=coolingStart+1,#calls do
     local row=calls[i]
     local row=calls[i]
     if row[1]=='transition' then
     if row[1]=='transition' then
         for _,name in ipairs({'SPECOPS_APEX_RING_LASER_DEVICE','SPECOPS_APEX_RING_RING_DEVICE'}) do
         for _,name in ipairs({'SPECOPS_APEX_RING_LASER_DEVICE','SPECOPS_APEX_RING_RING_DEVICE'}) do
-            if row[2]==slotDefs[m.Slot[name]].name and row[3].transition=='open' then restored[name]=true end
+            if row[2]==slotDefs[m.Slot[name]].name and row[3].transition=='close' then restored[name]=true end
         end
         end
         if row[2]==slotDefs[m.Slot.REACTOR_CLAMSHELL_EAST_DOOR_A_DEVICE].name and row[3].transition=='open' then
         if row[2]==slotDefs[m.Slot.REACTOR_CLAMSHELL_EAST_DOOR_A_DEVICE].name and row[3].transition=='open' then
             assert(restored.SPECOPS_APEX_RING_LASER_DEVICE and restored.SPECOPS_APEX_RING_RING_DEVICE,
             assert(restored.SPECOPS_APEX_RING_LASER_DEVICE and restored.SPECOPS_APEX_RING_RING_DEVICE,
@@ -428,7 +427,7 @@ assert(#calls==afterRail,'duplicate hazard callback must not reattach scorch')
 -- ring objects would take the beam and its surrounding structure out of the world entirely.
 -- ring objects would take the beam and its surrounding structure out of the world entirely.
 assert(vars['ember.apex.beam']==false,'the beam must stop firing after the deposit')
 assert(vars['ember.apex.beam']==false,'the beam must stop firing after the deposit')
 assert(vars['ember.apex.surge']==false,'the drive must return to its baseline with the power')
 assert(vars['ember.apex.surge']==false,'the drive must return to its baseline with the power')
-assert(transition('SPECOPS_APEX_RING_LASER_DEVICE', true).transition=='open',
+assert(transition('SPECOPS_APEX_RING_LASER_DEVICE', true).transition=='close',
     'a dark weapon rests in its normal pose, not the surge')
     'a dark weapon rests in its normal pose, not the surge')
 local poweredOff=false
 local poweredOff=false
 for _,row in ipairs(calls)do
 for _,row in ipairs(calls)do

+ 12 - 2
tests/verify_ember_movie_native.py

@@ -40,6 +40,13 @@ target(0x426920, 0x4F, 0x433050)
 # Kind 2 is routed to root+10; ordinary metadata belongs in root+20.
 # Kind 2 is routed to root+10; ordinary metadata belongs in root+20.
 assert data[0x4313CD:0x4313E2] == bytes.fromhex(
 assert data[0x4313CD:0x4313E2] == bytes.fromhex(
     '83 3f 02 b9 10 00 00 00 8b 57 04 41 b8 20 00 00 00 44 0f 44 c1')
     '83 3f 02 b9 10 00 00 00 8b 57 04 41 b8 20 00 00 00 44 0f 44 c1')
+# For stream type_info & 30000 == 10000, the load job maps offset|patch and
+# size|C0000000 directly. It bypasses the ordinary allocation/read branch.
+assert data[0x3592C6:0x3592EB] == bytes.fromhex(
+    '8b c3 c1 e8 10 83 e0 03 83 f8 01 75 2f 41 0f b7 4d 20 41 81 cf 00 00 00 c0 8b 55 50 45 8b c7 48 0b d1 8b 4d 48')
+target(0x3591B0, 0x13B, 0x351D00)
+target(0x41A160, 0x16, 0x3597C0)  # native video I/O opens this mapped package/patch
+target(0x41A160, 0x2C, 0x357DA0)  # then obtains offset and byte length
 movie = 'Sunrise/src/client/hooks/ember_movies/ember_movies.cpp'
 movie = 'Sunrise/src/client/hooks/ember_movies/ember_movies.cpp'
 start, stop, busy = (signature(movie, name) for name in ('startSig', 'stopSig', 'busySig'))
 start, stop, busy = (signature(movie, name) for name in ('startSig', 'stopSig', 'busySig'))
 for offset, expected in [(0x72, 0x41B040), (0x7A, 0x41A3C0), (0x8E, 0x41CD20)]:
 for offset, expected in [(0x72, 0x41B040), (0x7A, 0x41A3C0), (0x8E, 0x41CD20)]:
@@ -64,7 +71,8 @@ if len(sys.argv) > 2:
     for tag, expected in [(0x80BCA001, 0x80808495), (0x80BCA003, 0x80808495),
     for tag, expected in [(0x80BCA001, 0x80808495), (0x80BCA003, 0x80808495),
                           (0x80BCA000, 0x80808499), (0x80BCA002, 0x80808499),
                           (0x80BCA000, 0x80808499), (0x80BCA002, 0x80808499),
                           (0x80B9EB33, 0x80809A88), (0x80B9EB34, 0x80809A88),
                           (0x80B9EB33, 0x80809A88), (0x80B9EB34, 0x80809A88),
-                          (0x80BCA032, 0x80806B8F)]:
+                          (0x80BCA032, 0x80806B8F),
+                          (0x80BCA034, 0xFFFFFFFF), (0x80C7C000, 0xFFFFFFFF)]:
         # Tag package IDs include the bank: 80BCAxxx belongs to package 01E5.
         # Tag package IDs include the bank: 80BCAxxx belongs to package 01E5.
         package = (tag >> 13) & 0x3FF
         package = (tag >> 13) & 0x3FF
         _, path, header = latest[package]
         _, path, header = latest[package]
@@ -75,7 +83,9 @@ if len(sys.argv) > 2:
             reference, type_info, _ = struct.unpack('<IIQ', stream.read(16))
             reference, type_info, _ = struct.unpack('<IIQ', stream.read(16))
         assert reference == expected, (hex(tag), hex(reference), path)
         assert reference == expected, (hex(tag), hex(reference), path)
         assert type_info & 0xF000 != 0x2000, (hex(tag), hex(type_info))
         assert type_info & 0xF000 != 0x2000, (hex(tag), hex(type_info))
-    print('Installed movie metadata classes and ordinary (kind 1) package types verified.')
+        if expected == 0xFFFFFFFF:
+            assert (type_info & 0x30000) == 0x10000 and (type_info >> 6) & 0x3F == 24
+    print('Installed movie metadata, compact video streams and kind-1 package types verified.')
 attach = signature('Sunrise/src/client/hooks/bootflow/ember_sunburn.cpp', 'sig')
 attach = signature('Sunrise/src/client/hooks/bootflow/ember_sunburn.cpp', 'sig')
 assert attach == 0x9F2760
 assert attach == 0x9F2760
 # Native attach dereferences the runtime relative template, then passes it to the child factory.
 # Native attach dereferences the runtime relative template, then passes it to the child factory.