Host movie decoder: Fire Emblem Engage reaches its title screen
- hle_movie.c implements movie::Decoder (the SDK's MediaCodec wrapper) through hooks on every vtable method: DecoderEvents signalled with the SDK's SignalEvent, input / output index queues, Buffer and MediaData filled with the SDK's own setters, one NV12 frame per packet into the buffer Unity allocates (w x h x 1.5); packets wait while every output slot is taken. Frames are black for now (no VP9 decoding yet); the size comes from Configure or the VP9 header. - Engage on the Mac: loading screen, the 94 s opening movie, then the title screen and, left idle, the 3D attract demo. Not yet: input, a crash about 221 s into the demo, an unmapped texture format, iOS. - Docs: catalogue entries (movie protocol, shader keys, CPU-written textures, NVNprogram size), Engage graphics notes, README row.
This commit is contained in:
1 parent
830404a25e
commit
53a1273cd1
4 files changed
+297
-28
No files matched your search
@@ -12,7 +12,7 @@ The project is built from the generic parts of [d2r-mobile](../d2r-mobile) (MIT)
|
||||
| Game | Profile | State |
|
||||
|---|---|---|
|
||||
| Super Mario Bros. Wonder (base game v0, update 1.2.1) | `smbw` | **Playable on the iPhone with sound through several levels**: an 11-minute session at 60 fps with no crash and no audio dropouts. Scripted Mac runs reach the opening course. Notes are in the [Super Mario Bros. Wonder](#super-mario-bros-wonder-smbw) section |
|
||||
| Fire Emblem Engage | `fe_engage` | Bring-up started (Unity 2020.3 IL2CPP, SDK 12.3.7): on the Mac it boots through Unity's startup and runs to the opening movie, with nothing on screen yet. Next: its shaders, which sit in UnityFS bundles (27,152 of them) that `shader_split.py` cannot read yet, and the movies (VP9 WebM; the hardware decoder is stubbed) |
|
||||
| Fire Emblem Engage | `fe_engage` | Bring-up (Unity 2020.3 IL2CPP, SDK 12.3.7): **on the Mac it reaches the title screen** and, left idle, its 3D attract demo. `shader_split.py` reads the shaders out of the Unity bundles (9,268 of 9,269 compile). Movies play through a host decoder with black frames (no VP9 decoding yet). Not yet: input, some render targets and textures in the demo, iOS |
|
||||
|
||||
## Status
|
||||
|
||||
@@ -20,7 +20,7 @@ The project is built from the generic parts of [d2r-mobile](../d2r-mobile) (MIT)
|
||||
|---|---|
|
||||
| Rewriter (`dm/tools/patch_nso.py`, `DM_RULESET=native`) | Runs unchanged on Mario's `main` / `sdk`, v0 and 1.2.1 |
|
||||
| Mac host | Runs Mario through the menus into the opening course: the course, effects and HUD render. The title screen runs at 60 fps with flat memory (about 900 MB physical footprint). Menu and dialogue text currently render blank on the Mac (they are fine on the iPhone); this is not investigated yet |
|
||||
| Shaders | `shader_split.py` extracts the shaders from Nintendo's containers: bfsha archives, particle effects, `agl` packages, UI layouts, and shaders embedded in `main`. They are translated offline, and each vertex shader gets the outputs its paired pixel shader reads. For Mario, 16,266 of 16,268 shaders compile; 2 `agl` post-effect compute shaders make SPIRV-Cross throw. All 14,603 pairs link |
|
||||
| Shaders | `shader_split.py` extracts the shaders from Nintendo's containers (bfsha archives, particle effects, `agl` packages, UI layouts, shaders embedded in `main`) and from Unity's `Shader` objects. They are translated offline, and each vertex shader gets the outputs its paired pixel shader reads. For Mario, 16,266 of 16,268 shaders compile; 2 `agl` post-effect compute shaders make SPIRV-Cross throw. All 14,603 pairs link |
|
||||
| Audio | The game's own SDK renderer runs in-process in its CPU / manual-execution mode (`hle_audren.c`). The host renders one 5 ms frame per buffer the output hands back. It sends the device-sink mix (5.1 folded to stereo) through Core Audio (`DM_AUDIO=1` on Mac, default on iOS). This costs about 0.1 ms of CPU per frame on an M-series Mac |
|
||||
| Input | On iOS, a touch gamepad and physical controllers. On the Mac, scripted input (`DM_KEYS=<seconds>:<button>,…`) or `DM_WINDOW=1` for a window with keyboard input |
|
||||
| iOS bundle | `build_ipa.sh` builds one unsigned IPA for iPhone and iPad. The game code is embedded in the executable, and the iOS shader libraries and the RomFS are inside the app |
|
||||
|
||||
+273
-16
@@ -1,25 +1,282 @@
|
||||
// SDK 的影片库(subsdk0 = NintendoSDK_movie,Engage):Extractor 拆封装,Decoder 用 NVIDIA 的硬件解码器(NVOMX → NvRmChannelOpen 开 nvdrv 设备)。
|
||||
// 宿主没有 nvdrv:解码线程开设备时 SDK 中止。先让开片失败——Extractor::SetDataSource 回 0x1000(SDK 自己在没有实现对象时回的同一个码),
|
||||
// 播放器报错、游戏跳过影片,起机能往下走。Engage 的影片是 RomFS/Data/StreamingAssets/Movie 下的 103 个 WebM(2.7 GB);真要放,得在宿主用 VideoToolbox 解。
|
||||
// 宿主没有 nvdrv:解码线程开设备时 SDK 中止。Extractor::SetDataSource 回 0x1000(SDK 自己在没有实现对象时回的同一个码);Engage 的 Unity 自己拆 WebM,不走它。
|
||||
// Decoder 由宿主接管(下面):Engage 的影片是 RomFS/Data/StreamingAssets/Movie 下的 103 个 VP9 WebM(2.7 GB,无音轨),开场动画 S01F 不放完游戏不往下走。
|
||||
// The SDK's movie library (subsdk0 = NintendoSDK_movie, Engage): Extractor demuxes, Decoder uses NVIDIA's hardware decoder (NVOMX → NvRmChannelOpen opens the nvdrv device).
|
||||
// The host has no nvdrv: the SDK aborts when the decoder thread opens the device. For now opening a movie fails -- Extractor::SetDataSource returns 0x1000 (the same code the SDK returns when it has no implementation object),
|
||||
// the player reports an error, the game skips the movie, and bring-up can go on. Engage's movies are 103 WebM files under RomFS/Data/StreamingAssets/Movie (2.7 GB); actually playing them needs a VideoToolbox decoder in the host.
|
||||
// The host has no nvdrv: the SDK aborts when the decoder thread opens the device. Extractor::SetDataSource returns 0x1000 (the same code the SDK returns when it has no implementation object); Engage's Unity demuxes WebM itself and does not use it.
|
||||
// The host takes over Decoder (below): Engage's movies are 103 VP9 WebM files under RomFS/Data/StreamingAssets/Movie (2.7 GB, no audio track), and the game does not go on until the opening movie S01F has played.
|
||||
#include "hle.h"
|
||||
#include <pthread.h>
|
||||
|
||||
HLE_FORCE("_ZN5movie9Extractor13SetDataSourceEPKc", "影片拆封装:宿主还不放影片,开片失败(0x1000)", int, h_movie_SetSource, void *self, const char *path) {
|
||||
(void)self; static int red; if (!red++) LOG("【红】影片:宿主还不放影片,开片报失败(首个 %s)", path ? path : "(null)");
|
||||
(void)self; static int red; if (!red++) LOG("【红】影片:宿主不拆封装,开片报失败(首个 %s)", path ? path : "(null)");
|
||||
return 0x1000;
|
||||
}
|
||||
|
||||
// Decoder = { 虚表, DecoderImpl* };它的每个方法在 impl 为空时都回 0x100c(SDK 自己的失败码),析构也跳过空 impl。
|
||||
// 原版构造里 new 一个 DecoderImpl,起 NVOMX 调度线程开 NVIDIA 硬件解码器(宿主没有 nvdrv,中止或在它的错误路径上崩)。Unity 起机就建解码器(不先开片)。
|
||||
// 接管:只放虚表、impl 留空——解码器「建好了但什么都做不了」,游戏拿到的是 SDK 原有的失败码
|
||||
// Decoder = { vtable, DecoderImpl* }; each of its methods returns 0x100c (the SDK's own failure code) when impl is null, and the destructor skips a null impl.
|
||||
// The original constructor news a DecoderImpl, which starts an NVOMX scheduler thread and opens NVIDIA's hardware decoder (the host has no nvdrv: it aborts, or crashes on the decoder's error path). Unity creates a decoder at startup (before opening any movie).
|
||||
// Takeover: only the vtable is set and impl stays null -- the decoder is "constructed but can do nothing", and the game gets the SDK's existing failure code
|
||||
HLE_FORCE("_ZN5movie7DecoderC1ENS_11DecoderTypeENS_19DecoderOutputFormatENS_11DecoderModeEi", "影片解码器:宿主不建硬件解码器,impl 留空(各方法回 0x100c)",
|
||||
void, h_movie_Decoder, void **self, int type, int fmt, int mode, int flags) {
|
||||
static void *vt; if (!vt) { uint8_t *t = guest_sym("_ZTVN5movie7DecoderE"); if (!t) die("影片:缺 movie::Decoder 的虚表"); vt = t + 0x10; }
|
||||
self[0] = vt; self[1] = NULL;
|
||||
static int red; if (!red++) LOG("【红】影片:宿主不建硬件解码器(类型 %d 输出 %d 模式 %d 标志 %#x),解码器各方法回失败", type, fmt, mode, flags);
|
||||
// ---------- 解码器 | Decoder ----------
|
||||
// SDK 的 Decoder 是 Android MediaCodec 的薄封装(DecoderImpl 包 android::MediaCodec):
|
||||
// · 客户端注册 4 个 nn::os 事件 DecoderEvents {输入有空、输出就绪、格式变了、出错},有事就 SignalEvent;
|
||||
// · 输入:AcquireInputBufferIndex 取空输入号(没有回 0x1011)→ GetInputBuffer 把数据区交给客户端的 movie::Buffer → SendInputBufferForDecode(号, 偏移, 长, 时间 µs, 标志);
|
||||
// · 输出:AcquireOutputBufferIndex(号, 时间, 标志, 余数)(没有回 0x100e)→ GetOutputBufferFormat / GetOutputBuffer 把一帧 NV12 拷进客户端自己分配的缓冲 → ReleaseOutputBufferIndex。
|
||||
// Unity(mode 1、输出格式 4 = NV12)按 GetOutputBufferSize 分配输出缓冲,从 MediaData 读 width / height / crop / nv12-y-stride / nv12-y-offset / nv12-uv-offset / slice-height。
|
||||
// 宿主实现同一协议:每个输入包出一帧,时间与标志照抄(流结束标志随最后一帧传回)。输出槽都被客户端占着时,输入包先挂起、不还输入号(同 MediaCodec:解码器只在有空输出时消耗输入),
|
||||
// 客户端还一个输出号就处理一个挂起的包。画面暂为黑帧(宿主还没有 VP9 解码);尺寸取 Configure 的 MediaData,没有时取首个关键帧的 VP9 帧头。
|
||||
// Unity 按宽 × 高 × 1.5 自己分配输出缓冲(不调 GetOutputBufferSize),所以行距 = 宽、平面高 = 高,不对齐。
|
||||
// 对 Buffer / MediaData / 事件的操作都调 SDK 自己的函数(guest_sym),对象布局不用宿主知道。
|
||||
// The SDK's Decoder is a thin wrapper over Android's MediaCodec (DecoderImpl wraps android::MediaCodec):
|
||||
// · the client registers 4 nn::os events DecoderEvents {input available, output ready, format changed, error}, signalled with SignalEvent;
|
||||
// · input: AcquireInputBufferIndex takes a free input index (none: 0x1011) → GetInputBuffer hands its data area to the client's movie::Buffer → SendInputBufferForDecode(index, offset, size, time µs, flags);
|
||||
// · output: AcquireOutputBufferIndex(index, time, flags, remaining) (none: 0x100e) → GetOutputBufferFormat / GetOutputBuffer copy one NV12 frame into a buffer the client allocated → ReleaseOutputBufferIndex.
|
||||
// Unity (mode 1, output format 4 = NV12) allocates its output buffers by GetOutputBufferSize and reads width / height / crop / nv12-y-stride / nv12-y-offset / nv12-uv-offset / slice-height from the MediaData.
|
||||
// The host implements the same protocol: one frame per input packet, with its time and flags copied (the end-of-stream flag comes back with the last frame). While the client holds every output slot, input packets wait and their input indices are not returned (as with MediaCodec: a decoder only consumes input when it has a free output),
|
||||
// and each output index the client returns lets one waiting packet through. The picture is black for now (the host has no VP9 decoder yet); the size comes from Configure's MediaData, or else from the VP9 header of the first key frame.
|
||||
// Unity allocates its output buffers itself as width × height × 1.5 (it does not call GetOutputBufferSize), so the stride is the width and the plane height the height, unaligned.
|
||||
// Buffer / MediaData / event operations call the SDK's own functions (guest_sym), so the host needs no knowledge of their layouts.
|
||||
|
||||
#define MD_NIN 4 // 输入缓冲数 | input buffers
|
||||
#define MD_INCAP (4u << 20) // 每个输入缓冲 4 MB(1080p VP9 一包远小于此) | 4 MB per input buffer (a 1080p VP9 packet is far smaller)
|
||||
#define MD_NOUT 8 // 输出槽数 | output slots
|
||||
#define MD_NO_INPUT 0x1011 // SDK:没有空输入 | SDK: no free input
|
||||
#define MD_NO_OUTPUT 0x100e // SDK:没有就绪输出 | SDK: no output ready
|
||||
#define MD_FMT_NV12 4
|
||||
|
||||
typedef struct {
|
||||
pthread_mutex_t mu;
|
||||
int type, fmt, mode;
|
||||
void **ev; // DecoderEvents*:{输入, 输出, 格式, 出错} 的 EventType* | {input, output, format, error} EventType*
|
||||
int w, h, fmt_sent, started;
|
||||
uint8_t *in[MD_NIN]; int infree[MD_NIN], ninfree; // 空输入号队列 | queue of free input indices
|
||||
struct { int used; int64_t pts; uint32_t flags; } out[MD_NOUT];
|
||||
int outq[MD_NOUT], noutq; // 就绪输出号队列 | queue of ready output indices
|
||||
struct { int in; int64_t pts; uint32_t flags; } pend[MD_NIN]; int npend; // 等空输出槽的输入包 | input packets waiting for a free output slot
|
||||
unsigned long ncall[24];
|
||||
} mdec_t;
|
||||
|
||||
static struct {
|
||||
void (*signal)(void *ev);
|
||||
void (*set_data)(void *buf, void *data, size_t cap);
|
||||
void (*set_range)(void *buf, int off, int size);
|
||||
void *(*data)(const void *buf);
|
||||
size_t (*cap)(const void *buf);
|
||||
void (*set_i32)(void *md, const char *k, int v);
|
||||
void (*set_rect)(void *md, const char *k, int l, int t, int r, int b);
|
||||
int (*find_i32)(const void *md, const char *k, int *v);
|
||||
} G;
|
||||
|
||||
static void *msym(const char *n) { void *p = guest_sym(n); if (!p) die("影片:缺 SDK 符号 %s", n); return p; }
|
||||
static void md_syms(void) {
|
||||
if (G.signal) return;
|
||||
G.set_data = msym("_ZN5movie6Buffer18SetDataAndCapacityEPvm");
|
||||
G.set_range = msym("_ZN5movie6Buffer8SetRangeEii");
|
||||
G.data = msym("_ZNK5movie6Buffer4DataEv");
|
||||
G.cap = msym("_ZNK5movie6Buffer8CapacityEv");
|
||||
G.set_i32 = msym("_ZN5movie9MediaData8SetInt32EPKci");
|
||||
G.set_rect = msym("_ZN5movie9MediaData7SetRectEPKciiii");
|
||||
G.find_i32 = msym("_ZNK5movie9MediaData9FindInt32EPKcPi");
|
||||
G.signal = msym("_ZN2nn2os11SignalEventEPNS0_9EventTypeE");
|
||||
}
|
||||
static mdec_t *md_of(void **self) { return self ? (mdec_t *)self[1] : NULL; }
|
||||
static void md_event(mdec_t *d, int i) { if (d->ev && d->ev[i]) G.signal(d->ev[i]); } // 0 输入 1 输出 2 格式 3 出错 | 0 input 1 output 2 format 3 error
|
||||
// 前 12 次逐条记,之后每 2^k 次记一次 | log the first 12 calls one by one, then every 2^k-th
|
||||
#define MD_TRACE(d, slot, ...) do { unsigned long n_ = ++(d)->ncall[slot]; if (n_ <= 12 || !(n_ & (n_ - 1))) LOG("[影片] " __VA_ARGS__); } while (0)
|
||||
|
||||
// VP9 关键帧的未压缩帧头 → 宽高(VP9 规范 6.2:frame_marker、profile、show_existing_frame、frame_type、show_frame、error_resilient_mode、同步码、颜色配置、frame_size)
|
||||
// VP9 key frame uncompressed header → width and height (VP9 spec 6.2: frame_marker, profile, show_existing_frame, frame_type, show_frame, error_resilient_mode, sync code, color config, frame_size)
|
||||
static int vp9_size(const uint8_t *p, int n, int *w, int *h) {
|
||||
int bit = 0;
|
||||
#define RB(k) ({ unsigned v_ = 0; for (int i_ = 0; i_ < (k); i_++, bit++) { if (bit >> 3 >= n) return 0; v_ = v_ << 1 | (p[bit >> 3] >> (7 - (bit & 7)) & 1); } v_; })
|
||||
if (RB(2) != 2) return 0;
|
||||
unsigned lo = RB(1), hi = RB(1), prof = hi << 1 | lo;
|
||||
if (prof == 3) RB(1);
|
||||
if (RB(1)) return 0; // show_existing_frame
|
||||
if (RB(1) != 0) return 0; // 不是关键帧 | not a key frame
|
||||
RB(2); // show_frame, error_resilient_mode
|
||||
if (RB(24) != 0x498342) return 0;
|
||||
if (prof >= 2) RB(1);
|
||||
if (RB(3) != 7) { RB(1); if (prof == 1 || prof == 3) RB(3); }
|
||||
else if (prof == 1 || prof == 3) RB(1);
|
||||
*w = (int)RB(16) + 1; *h = (int)RB(16) + 1;
|
||||
#undef RB
|
||||
return 1;
|
||||
}
|
||||
static void md_dims(const mdec_t *d, int *w, int *h, int *stride, int *slice) {
|
||||
*w = d->w > 0 ? d->w : 1920; *h = d->h > 0 ? d->h : 1080;
|
||||
*stride = *w; *slice = *h;
|
||||
}
|
||||
|
||||
HLE_FORCE("_ZN5movie7DecoderC1ENS_11DecoderTypeENS_19DecoderOutputFormatENS_11DecoderModeEi", "影片解码器:宿主实现(不建 NVIDIA 硬件解码器)",
|
||||
void, h_movie_Decoder, void **self, int type, int fmt, int mode, int prio) {
|
||||
static void *vt; if (!vt) { uint8_t *t = guest_sym("_ZTVN5movie7DecoderE"); if (!t) die("影片:缺 movie::Decoder 的虚表"); vt = t + 0x10; }
|
||||
md_syms();
|
||||
self[0] = vt; self[1] = NULL;
|
||||
// 只做视频(AVC / VP8 / VP9 = 1..3)NV12 输出;别的(音频)impl 留空,各方法回 SDK 的失败码
|
||||
// Only video (AVC / VP8 / VP9 = 1..3) with NV12 output; anything else (audio) keeps a null impl and every method returns the SDK's failure code
|
||||
if (type < 1 || type > 3 || fmt != MD_FMT_NV12) { LOG("【红】影片:解码器类型 %d 输出 %d 模式 %d 宿主不支持,各方法回失败", type, fmt, mode); return; }
|
||||
mdec_t *d = calloc(1, sizeof *d); pthread_mutex_init(&d->mu, NULL);
|
||||
d->type = type; d->fmt = fmt; d->mode = mode;
|
||||
for (int i = 0; i < MD_NIN; i++) { d->in[i] = malloc(MD_INCAP); d->infree[i] = i; }
|
||||
d->ninfree = MD_NIN;
|
||||
self[1] = d;
|
||||
LOG("[影片] 解码器 %p:类型 %d 输出 %d 模式 %d 优先级 %d(宿主黑帧解码)", (void *)self, type, fmt, mode, prio);
|
||||
}
|
||||
static void md_free(void **self) {
|
||||
mdec_t *d = md_of(self); if (!d) return;
|
||||
self[1] = NULL;
|
||||
LOG("[影片] 解码器 %p 析构:送入 %lu 包,取出 %lu 帧", (void *)self, d->ncall[7], d->ncall[8]);
|
||||
for (int i = 0; i < MD_NIN; i++) free(d->in[i]);
|
||||
pthread_mutex_destroy(&d->mu); free(d);
|
||||
}
|
||||
HLE_FORCE("_ZN5movie7DecoderD2Ev", "影片解码器析构:释放宿主对象", void, h_movie_DecD2, void **self) { md_free(self); }
|
||||
// 删除式析构:Unity 用自己的分配器建对象、先调 D2 再自己释放,这里不释放对象本身 | deleting destructor: Unity allocates the object with its own allocator, calls D2 and frees it itself; the object itself is not freed here
|
||||
HLE_FORCE("_ZN5movie7DecoderD0Ev", "影片解码器删除式析构:释放宿主对象", void, h_movie_DecD0, void **self) { LOG("【红】影片:删除式析构(对象本身未释放)"); md_free(self); }
|
||||
|
||||
HLE_FORCE("_ZN5movie7Decoder9ConfigureEPNS_9MediaDataE", "影片解码器配置", int, h_movie_Configure, void **self, void *md) {
|
||||
mdec_t *d = md_of(self); if (!d) return 0x1000;
|
||||
int w = 0, h = 0; if (md) { G.find_i32(md, "width", &w); G.find_i32(md, "height", &h); }
|
||||
pthread_mutex_lock(&d->mu); if (w > 0 && h > 0) { d->w = w; d->h = h; } pthread_mutex_unlock(&d->mu);
|
||||
MD_TRACE(d, 0, "Configure:MediaData 宽 %d 高 %d", w, h);
|
||||
return 0;
|
||||
}
|
||||
HLE_FORCE("_ZN5movie7Decoder20RegisterClientEventsEPNS_13DecoderEventsE", "影片解码器:登记客户端事件", int, h_movie_RegEv, void **self, void **ev) {
|
||||
mdec_t *d = md_of(self); if (!d) return 0x1000;
|
||||
d->ev = ev; MD_TRACE(d, 1, "RegisterClientEvents:%p %p %p %p", ev ? ev[0] : 0, ev ? ev[1] : 0, ev ? ev[2] : 0, ev ? ev[3] : 0);
|
||||
return 0;
|
||||
}
|
||||
HLE_FORCE("_ZN5movie7Decoder5StartEv", "影片解码器开始:输入全空,通知客户端", int, h_movie_Start, void **self) {
|
||||
mdec_t *d = md_of(self); if (!d) return 0x2002;
|
||||
pthread_mutex_lock(&d->mu); d->started = 1; pthread_mutex_unlock(&d->mu);
|
||||
MD_TRACE(d, 2, "Start");
|
||||
md_event(d, 0);
|
||||
return 0;
|
||||
}
|
||||
static void md_reset(mdec_t *d) {
|
||||
pthread_mutex_lock(&d->mu);
|
||||
for (int i = 0; i < MD_NIN; i++) d->infree[i] = i;
|
||||
d->ninfree = MD_NIN; d->noutq = 0; d->npend = 0;
|
||||
for (int i = 0; i < MD_NOUT; i++) d->out[i].used = 0;
|
||||
pthread_mutex_unlock(&d->mu);
|
||||
}
|
||||
HLE_FORCE("_ZN5movie7Decoder4StopEv", "影片解码器停止", int, h_movie_Stop, void **self) {
|
||||
mdec_t *d = md_of(self); if (!d) return 0x2003;
|
||||
MD_TRACE(d, 3, "Stop"); md_reset(d); d->started = 0; return 0;
|
||||
}
|
||||
HLE_FORCE("_ZN5movie7Decoder5ResetEv", "影片解码器复位", int, h_movie_Reset, void **self) {
|
||||
mdec_t *d = md_of(self); if (!d) return 0x1000;
|
||||
MD_TRACE(d, 4, "Reset"); md_reset(d); d->fmt_sent = 0; return 0;
|
||||
}
|
||||
HLE_FORCE("_ZN5movie7Decoder5FlushEv", "影片解码器清空:输入输出都还回去", int, h_movie_Flush, void **self) {
|
||||
mdec_t *d = md_of(self); if (!d) return 0x1000;
|
||||
MD_TRACE(d, 5, "Flush"); md_reset(d); md_event(d, 0); return 0;
|
||||
}
|
||||
HLE_FORCE("_ZN5movie7Decoder23AcquireInputBufferIndexEPiS1_", "影片解码器:取空输入号", int, h_movie_AcqIn, void **self, int *idx, int *left) {
|
||||
mdec_t *d = md_of(self); if (!d) return 0x1000;
|
||||
if (!idx || !left) return 0x100b;
|
||||
pthread_mutex_lock(&d->mu);
|
||||
int r = MD_NO_INPUT; *idx = -1; *left = 0;
|
||||
if (d->ninfree) { *idx = d->infree[0]; memmove(d->infree, d->infree + 1, --d->ninfree * sizeof *d->infree); *left = d->ninfree; r = 0; }
|
||||
pthread_mutex_unlock(&d->mu);
|
||||
MD_TRACE(d, 6, "AcquireInputBufferIndex → %#x 号 %d 余 %d", r, *idx, *left);
|
||||
return r;
|
||||
}
|
||||
HLE_FORCE("_ZN5movie7Decoder14GetInputBufferEiPNS_6BufferE", "影片解码器:交出输入缓冲", int, h_movie_GetIn, void **self, int i, void *buf) {
|
||||
mdec_t *d = md_of(self); if (!d) return 0x1000;
|
||||
if (!buf || i < 0 || i >= MD_NIN) return 0x100b;
|
||||
G.set_data(buf, d->in[i], MD_INCAP); G.set_range(buf, 0, (int)MD_INCAP);
|
||||
return 0;
|
||||
}
|
||||
// 一包占一个空输出槽、输入号还回去;返回输出槽号,没有空槽回 −1(调用方持锁) | one packet takes a free output slot and its input index is returned; returns the slot, or -1 when none is free (caller holds the lock)
|
||||
static int md_emit(mdec_t *d, int in, int64_t pts, uint32_t flags) {
|
||||
int slot = -1;
|
||||
for (int k = 0; k < MD_NOUT; k++) if (!d->out[k].used) { slot = k; break; }
|
||||
if (slot < 0) return -1;
|
||||
d->out[slot].used = 1; d->out[slot].pts = pts; d->out[slot].flags = flags; d->outq[d->noutq++] = slot;
|
||||
d->infree[d->ninfree++] = in;
|
||||
return slot;
|
||||
}
|
||||
HLE_FORCE("_ZN5movie7Decoder24SendInputBufferForDecodeEiiilj", "影片解码器:送一包解码(宿主出黑帧)", int, h_movie_SendIn,
|
||||
void **self, int i, int off, int size, int64_t pts, uint32_t flags) {
|
||||
mdec_t *d = md_of(self); if (!d) return 0x1000;
|
||||
if (i < 0 || i >= MD_NIN || off < 0 || size < 0 || (uint32_t)off + (uint32_t)size > MD_INCAP) return 0x100b;
|
||||
pthread_mutex_lock(&d->mu);
|
||||
int fmt_now = 0, slot = -1;
|
||||
if (!d->w && size > 0 && vp9_size(d->in[i] + off, size, &d->w, &d->h)) LOG("[影片] VP9 关键帧:%d×%d", d->w, d->h);
|
||||
if (!d->fmt_sent) { d->fmt_sent = 1; fmt_now = 1; }
|
||||
if (!d->npend) slot = md_emit(d, i, pts, flags);
|
||||
if (slot < 0) { d->pend[d->npend].in = i; d->pend[d->npend].pts = pts; d->pend[d->npend].flags = flags; d->npend++; }
|
||||
pthread_mutex_unlock(&d->mu);
|
||||
MD_TRACE(d, 7, "SendInputBufferForDecode 号 %d 偏移 %d 长 %d 时间 %lld µs 标志 %#x → 输出槽 %d", i, off, size, (long long)pts, flags, slot);
|
||||
if (fmt_now) md_event(d, 2);
|
||||
if (slot >= 0) { md_event(d, 0); md_event(d, 1); }
|
||||
return 0;
|
||||
}
|
||||
HLE_FORCE("_ZN5movie7Decoder24AcquireOutputBufferIndexEPiPlPjS1_", "影片解码器:取就绪输出号", int, h_movie_AcqOut,
|
||||
void **self, int *idx, int64_t *pts, uint32_t *flags, int *left) {
|
||||
mdec_t *d = md_of(self); if (!d) return 0x1000;
|
||||
if (!idx || !pts || !flags || !left) return 0x100b;
|
||||
pthread_mutex_lock(&d->mu);
|
||||
int r = MD_NO_OUTPUT; *idx = -1; *left = 0;
|
||||
if (d->noutq) {
|
||||
int s = d->outq[0]; memmove(d->outq, d->outq + 1, --d->noutq * sizeof *d->outq);
|
||||
*idx = s; *pts = d->out[s].pts; *flags = d->out[s].flags; *left = d->noutq; r = 0;
|
||||
}
|
||||
pthread_mutex_unlock(&d->mu);
|
||||
if (!r) MD_TRACE(d, 8, "AcquireOutputBufferIndex → 号 %d 时间 %lld 标志 %#x 余 %d", *idx, (long long)*pts, *flags, *left);
|
||||
return r;
|
||||
}
|
||||
static void md_format(mdec_t *d, void *md) {
|
||||
int w, h, stride, slice; md_dims(d, &w, &h, &stride, &slice);
|
||||
G.set_i32(md, "width", w); G.set_i32(md, "height", h);
|
||||
G.set_rect(md, "crop", 0, 0, w - 1, h - 1);
|
||||
G.set_i32(md, "stride", stride); G.set_i32(md, "slice-height", slice);
|
||||
G.set_i32(md, "nv12-y-stride", stride); G.set_i32(md, "nv12-y-offset", 0); G.set_i32(md, "nv12-uv-offset", stride * slice);
|
||||
}
|
||||
HLE_FORCE("_ZN5movie7Decoder15GetOutputFormatEPNS_9MediaDataE", "影片解码器:输出格式(NV12)", int, h_movie_OutFmt, void **self, void *md) {
|
||||
mdec_t *d = md_of(self); if (!d) return 0x1000;
|
||||
if (!md) return 0x100b;
|
||||
md_format(d, md); MD_TRACE(d, 9, "GetOutputFormat");
|
||||
return 0;
|
||||
}
|
||||
HLE_FORCE("_ZN5movie7Decoder21GetOutputBufferFormatEiPNS_9MediaDataE", "影片解码器:输出帧格式(NV12)", int, h_movie_BufFmt, void **self, int i, void *md) {
|
||||
mdec_t *d = md_of(self); if (!d) return 0x1000;
|
||||
if (!md) return 0x100b;
|
||||
md_format(d, md); MD_TRACE(d, 10, "GetOutputBufferFormat 号 %d", i);
|
||||
return 0;
|
||||
}
|
||||
HLE_FORCE("_ZN5movie7Decoder19GetOutputBufferSizeEPiS1_", "影片解码器:输出缓冲大小(NV12 一帧)", int, h_movie_OutSize, void **self, int *size, int *mode) {
|
||||
mdec_t *d = md_of(self); if (!d) return 0x1000;
|
||||
if (!size || !mode) return 0x100b;
|
||||
int w, h, stride, slice; md_dims(d, &w, &h, &stride, &slice);
|
||||
*size = stride * slice * 3 / 2; *mode = d->mode; // SDK 第二个出参填解码器模式 | the SDK fills the second output with the decoder mode
|
||||
MD_TRACE(d, 11, "GetOutputBufferSize → %d(%d×%d)", *size, w, h);
|
||||
return 0;
|
||||
}
|
||||
// 把一帧写进客户端缓冲:现在是黑帧(视频范围 Y 16、UV 128) | write one frame into the client buffer: a black frame for now (video range Y 16, UV 128)
|
||||
HLE_FORCE("_ZN5movie7Decoder15GetOutputBufferEiPNS_6BufferE", "影片解码器:取一帧(宿主黑帧)", int, h_movie_GetOut, void **self, int i, void *buf) {
|
||||
mdec_t *d = md_of(self); if (!d) return 0x1000;
|
||||
if (!buf || i < 0 || i >= MD_NOUT) return 0x100b;
|
||||
int w, h, stride, slice; md_dims(d, &w, &h, &stride, &slice);
|
||||
size_t need = (size_t)stride * slice * 3 / 2, cap = G.cap(buf);
|
||||
uint8_t *p = G.data(buf);
|
||||
if (!p || cap < need) { LOG("【红】影片:客户端输出缓冲 %zu 字节,一帧要 %zu", cap, need); G.set_range(buf, 0, 0); return 0x100b; }
|
||||
memset(p, 16, (size_t)stride * slice); memset(p + (size_t)stride * slice, 128, need - (size_t)stride * slice);
|
||||
G.set_range(buf, 0, (int)need);
|
||||
MD_TRACE(d, 12, "GetOutputBuffer 号 %d → %zu 字节", i, need);
|
||||
return 0;
|
||||
}
|
||||
HLE_FORCE("_ZN5movie7Decoder15GetOutputBufferEiPNS_6BufferEPNS_9MediaDataE", "影片解码器:取一帧连格式", int, h_movie_GetOutFmt, void **self, int i, void *buf, void *md) {
|
||||
mdec_t *d = md_of(self); if (!d) return 0x1000;
|
||||
if (md) md_format(d, md);
|
||||
return h_movie_GetOut(self, i, buf);
|
||||
}
|
||||
HLE_FORCE("_ZN5movie7Decoder24ReleaseOutputBufferIndexEi", "影片解码器:还输出号", int, h_movie_RelOut, void **self, int i) {
|
||||
mdec_t *d = md_of(self); if (!d) return 0x1000;
|
||||
if (i < 0 || i >= MD_NOUT) return 0x100b;
|
||||
pthread_mutex_lock(&d->mu);
|
||||
d->out[i].used = 0;
|
||||
int slot = -1;
|
||||
if (d->npend && (slot = md_emit(d, d->pend[0].in, d->pend[0].pts, d->pend[0].flags)) >= 0) memmove(d->pend, d->pend + 1, --d->npend * sizeof *d->pend);
|
||||
pthread_mutex_unlock(&d->mu);
|
||||
MD_TRACE(d, 13, "ReleaseOutputBufferIndex 号 %d(挂起包进输出槽 %d)", i, slot);
|
||||
if (slot >= 0) { md_event(d, 0); md_event(d, 1); }
|
||||
return 0;
|
||||
}
|
||||
HLE_FORCE("_ZN5movie7Decoder12GetLastErrorEv", "影片解码器:最近错误(无)", int, h_movie_LastErr, void **self) { (void)self; return 0; }
|
||||
@@ -57,7 +57,8 @@ Each entry: symptom → root cause → fix (where the fix lives). Check these fi
|
||||
- **IL2CPP "Could not open …/global-metadata.dat" (then a crash in its logger)** → IL2CPP checks `nn::fs::IsMounted("rom")` before opening; the SDK's own `IsMounted` reads a mount table the host never fills → answer from the host's mount table (`hle_fs.c`).
|
||||
- **Crash in `nn::ro::InitializeWithPortName`** → `nn::ro` connects to `ldr:ro`; take over `Initialize` and answer `LookupSymbol` from the loaded modules (`hle_ro.c`).
|
||||
- **SDK 12 names** → older SDKs export other names for the same calls (`ListAudioDeviceName` / `GetActiveAudioDeviceName`, singular `hid::GetNpadState`, `OpenDefaultAudioOut`); hook both generations.
|
||||
- **Abort in NVIDIA's video path (`NvRmChannelOpen` → `NvOsDrvOpen`)** → the SDK movie library's hardware decoder opens nvdrv directly. `movie::Decoder` is `{vtable, impl}` and every method fails cleanly with `impl == NULL`, so the host constructs it with no `impl` (`hle_movie.c`); nvdrv opens fail (`hle_nvdrv.c`). Movies don't play yet (a VideoToolbox decoder is future work).
|
||||
- **Abort in NVIDIA's video path (`NvRmChannelOpen` → `NvOsDrvOpen`)** → the SDK movie library's hardware decoder opens nvdrv directly. The host takes over `movie::Decoder` (`hle_movie.c`, next entry); nvdrv opens fail (`hle_nvdrv.c`).
|
||||
- **The game waits forever at a movie (Engage's 94 s opening `S01F.webm`; frames keep presenting, nothing happens)** → Unity demuxes the WebM itself and feeds VP9 packets to `movie::Decoder`; with no working decoder the movie never ends. `movie::Decoder` is a thin wrapper over Android's MediaCodec, and its vtable entries are symbol relocations, so `HLE_FORCE` on each method also catches Unity's virtual calls. The host implements the protocol: four `nn::os` events (`DecoderEvents` = input available, output ready, format changed, error), input indices (`AcquireInputBufferIndex`, none = 0x1011; `GetInputBuffer` fills the client's `movie::Buffer`; `SendInputBufferForDecode(index, offset, size, µs, flags)`), output indices (`AcquireOutputBufferIndex`, none = 0x100e; one NV12 frame per packet copied into the buffer Unity allocated, w × h × 1.5; `ReleaseOutputBufferIndex`). While every output slot is taken, packets wait and their input indices are not returned, as with MediaCodec; dropping them instead lost frames. `Buffer`, `MediaData` and events are handled through the SDK's own exported functions (`SetDataAndCapacity`, `SetRange`, `SetInt32`, `SetRect`, `SignalEvent`). Unity reads `width`, `height`, `crop`, `nv12-y-stride`, `nv12-y-offset`, `nv12-uv-offset` and `slice-height`. The frames are black for now (no VP9 decoder in the host yet), and the game continues to its title screen when the movie ends.
|
||||
- **Abort inside `nn::sm::Initialize`** → an SDK path reached a system service the host doesn't replace → hook the public SDK entry point named in the backtrace (`HLE_FORCE` in the relevant `hle_*.c`). The log shows it as `SVC 0x1f` (ConnectToNamedPort, port name `sm:`) and `SVC 0x21` answered by placeholders, then `nn::diag` abort.
|
||||
- **A game update aborts at boot where the base game ran** → the update imports SDK functions the base game didn't. Diff the undefined dynamic symbols of the two `main` modules before the first run. Mario 1.2.1 added 28, among them `nn::nfp` (amiibo, initialized at boot), `pl::IsRunningOnOunce` (is this a Switch 2), `ec::ShowShopApplicationInformationForUpgrade`, `account::PopOpenUsers` and `aoc::CountAddOnContent`. Read the call sites before choosing answers: Mario only reads the count from `PopOpenUsers` and falls back to `TryOpenPreselectedUser` on 0, and only counts add-on content when running on a Switch 2. The host answers as a Switch 1 with no amiibo and no add-on content (`hle_sdk16.c`, `hle_account.c`, `hle_misc.c`).
|
||||
- **Code inherited from another title calls fixed addresses** → harmless on the build it was written for, silent memory corruption on any other. d2r-mobile's 3D render scale read a renderer pointer at `main+0x31e0848` and called `main+0x8582f0`; in Mario v0 that is the middle of a static initializer and only did nothing because the pointer happened to read 0. Such code checks `module_t.build_id` now (written into `fixups.tsv` by the rewriter) and stays off on other builds.
|
||||
@@ -75,6 +76,8 @@ Each entry: symptom → root cause → fix (where the fix lives). Check these fi
|
||||
- **`GetCommandMemoryUsed` must count everything the recording used**, including the parts that went into host segments (plus 16 bytes for the jump). Mario marks a recorded block as empty when the count is 0 and then skips calling it (see the next entry for how that happened).
|
||||
- **Frames stop, no crash, the main thread sits in `nvnSyncWait(sync, -1)`, and the watchdog's wait-state line shows the main fence one short (`已提交 N / 已完成 N-1`)** → a GPU command buffer never completes. Check GPU load from outside: `ioreg -r -d 1 -c IOAccelerator | grep 'Device Utilization'` at 100 % means a shader is looping forever. Find the shaders first used in the last frames (`DM_NVN_PASSDUMP`, compare the `K` keys) and look at their `.metal` for an empty `do {} while (true);` with no `return` (see [04](04-graphics.md) section 1: the compute program start).
|
||||
|
||||
- **Shaders reported missing at random, and their dumped control sections are garbage (Unity)** → NVN lets the caller free a program's control section once `nvnProgramSetShaders` returns; Unity does, and the host used to hash it at the first draw. The keys are now computed inside `SetShaders` (`hle_nvn.c` `prog_key`). Mario calls `SetShaders` for some programs with a code address that is not mapped yet, so a stage whose code is not in a registered memory pool at that moment is hashed at its first draw, as before.
|
||||
- **Video frames, or a game's built-in default textures, are all zero** → the texture is block-linear, has no packaged layout and is written from the CPU with `nvnTextureWriteTexels` (Engage's video frames: Y plane `R8` + UV plane `RG8`, every frame; Mario's and Unity's 4×4 fallback textures, which hold black, white, transparent and red texels). The host uploaded only packaged textures from guest memory and made everything else a GPU-only texture. `WriteTexels` now records the texture, and it resolves like a packaged one: uploaded from guest memory, re-uploaded when its pages are dirtied (`mtl_tex_cpu_written`). Mario's frames got slightly brighter where materials sample the default white texture.
|
||||
- **Menus and dialogue boxes draw but contain no text; images and pictogram digits are fine** → the game rasterizes glyphs on the CPU into a *linear* texture in a CPU-visible pool (Mario: 2048×8192 R8, flags IMAGE|LINEAR). A private Metal texture never sees those writes. Linear textures in CPU-visible pools are now created on the pool buffer (`tex_linear` in `nvn_mtl.m`). `DM_NVN_TEXCPU=1` lists non-packaged textures in CPU-visible pools.
|
||||
- **Characters (skinned models) invisible or stretched into streaks while the level renders** → integer vertex inputs (bone indices) translated as float; see [04](04-graphics.md) section 6. To find the draws: log model file ranges with `DM_PROBE=main:480f0` (x1 = file data, x2 = size), match the vertex/index buffer addresses in `DM_NVN_PASSDUMP_ALL` against them, then `DM_NVN_HILITE` their fragment shader.
|
||||
- **Sky black** → CPU-written linear colour ramps (see the linear-texture entry above).
|
||||
@@ -96,6 +99,7 @@ The game allocates NVN objects with the sizes of *its* SDK's headers, which shra
|
||||
|
||||
- `NVNtexture` is 0x80 bytes in SDK 16 (heap corruption: the old 0x88 record overwrote the next heap block's links).
|
||||
- `NVNtextureView` is 16 bytes in SDK 16 (stack smash: a 16-byte local next to the saved frame pointer; the function returned to `0x100000000`).
|
||||
- `NVNprogram` is smaller in Mario than the 0xC0 bytes in `nvn.h`: growing the host record from 112 to 168 bytes corrupted the next object (a crash in SHA-256 on the code address 0x16300). Keep `prog_t` at 112 bytes; per-program host data goes in a side table (`prog_key`).
|
||||
|
||||
Check every host object against the new SDK: `_Static_assert(sizeof(host) <= N)` next to the struct, and when a crash returns to a nonsense address, look at the 0x100 bytes below `sp` in the crash report for a zeroed frame.
|
||||
|
||||
|
||||
@@ -6,9 +6,10 @@ The graphics layer records NVN commands into its own command format (`dm/gfx/nvn
|
||||
|
||||
Do not stop at the obvious archive. In Mario the main `bfsha` held 4,888 of the 12,811 shaders the game actually uses.
|
||||
|
||||
`dm/gfx/tools/shader_split.py <RomFS> <ExeFS main> <pkg dir> <map out> <facts out> [<pairs out>]` scans **every** RomFS file (zstd-decompressed, SARC expanded recursively) and `main`, and recognizes the containers below. The pairs table lists the (vertex key, pixel key) of every BNSH variation (section 6). For a base game plus an update, give RomFS and `main` as comma-separated lists, base first: keys are content hashes, so the output is the union of both versions and one shader build serves both (Mario 1.2.1 recompiled 3,451 of its pixel shaders; v0 rows keep their package indices).
|
||||
`dm/gfx/tools/shader_split.py <RomFS> <ExeFS main> <pkg dir> <map out> <facts out> [<pairs out>]` scans **every** RomFS file (zstd-decompressed, SARC expanded recursively) and `main`, and recognizes the containers below. The pairs table lists the (vertex key, pixel key) of every BNSH variation or Unity program (section 6). Unity shaders need a second pass: files whose type trees are missing are scanned again once the bundles have supplied the `Shader` tree. For a base game plus an update, give RomFS and `main` as comma-separated lists, base first: keys are content hashes, so the output is the union of both versions and one shader build serves both (Mario 1.2.1 recompiled 3,451 of its pixel shaders; v0 rows keep their package indices).
|
||||
|
||||
- **BNSH** (`BNSH\0\0\0\0` + `grsc` block at +0x60): inside `bfsha`, particle effects (`esetb`, VFXB), UI layouts (`blarc`), and `main` itself. Variation table at +0x80 (count at +0x7c); binary program at variation +0x10; six stage pointers from program +0x08 (vs, hs, ds, gs, ps, cs); stage code descriptor: control ptr +0x08, code ptr +0x10, code size +0x18, control size +0x1c. All pointers relative to the BNSH start.
|
||||
- **Unity Shader objects** (Engage): Unity 2020.3 keeps each shader's compiled programs in its `Shader` object (class 48), in UnityFS bundles (`StreamingAssets/aa`) and in the player's own serialized files (`data.unity3d`, `Resources/unity default resources`). `dm/gfx/tools/unityfs.py` reads them: UnityFS blocks (LZ4 / LZMA), the serialized file, the object through its type tree, then `compressedBlob` per platform (LZ4 segments; each segment starts with a u32 count and 12-byte `{offset, length, segment}` entries). The player's files have no type trees; they borrow the bundles' `Shader` tree with the same type hash (same Unity build, same layout). Type-tree strings with the high bit set index Unity's common string table, which `unityfs.common_strings` reads from the game's own `main` (`AABB\0AnimationClip\0…`). On Switch every subprogram is one whole NVN program: its code is a 0x60-byte header of four `i32 ×6` arrays indexed by NVN stage (0 vertex, 1 fragment, 2 geometry, 3 tess control, 4 tess evaluation, 5 compute): an unused / scratch size, the block offset from +0x60 (−1 = no stage), the control size and the code size. Each block is the control section followed by the code area. The vertex and fragment stages of one program form a pair. Engage: 209 `Shader` objects in 188 bundles plus the engine files, 9,269 stage shaders (4,634 vertex / pixel pairs and one geometry shader), no compute shaders.
|
||||
- **BAHS** (agl `sharcb`, engine utility shaders): code region at header +0x1c (size +0x18). Binary entries in the table area are 16 bytes `{code offset, 0, control size (0x880/0x88c), signed offset from this entry to the control section}`. Control sections are shared between programs, so pair by entry, never by searching for code near a control.
|
||||
- **Code area**: graphics = 0x30-byte header (magic `0x12345678`) + 0x50 SPH + instructions (at +0x80); compute = 0x30-byte header (magic `0x12345679`) + **zero padding, instructions at +0x100** (all 71 of Mario's). **Control section**: magic `0x98761234`; +0x700 = c1 offset in the code area, +0x704 = code size, +0x748 = compute thread-group size (u32 ×3), +0x754 = compute shared memory bytes.
|
||||
- **The program start must be exact.** Ryujinx's decoder treats every 32-byte boundary *relative to the start* as a scheduling word. Mario's compute code was first fed from +0x30: the groups were misaligned by 0x10, instructions were dropped or misread, and 12 compute shaders (particles, post effects) translated into an endless loop that hung the GPU at the first gameplay scene. Check a new title: the first non-zero word after the header must be a control word, and the trailing `EXIT` (`e30000000007000f`) / `BRA`-to-self (`e2400fffff87000f`) must sit in instruction slots of 32-byte groups counted from your chosen start. `dmsh_build.py` now refuses compute code whose padding is not zero.
|
||||
@@ -23,15 +24,15 @@ The host key is `sha256(control[0:0x880] + code)[:16]` (`prog_get` in `nvn_mtl.m
|
||||
|
||||
The NVN driver puts binding tables in driver constant banks, and the layout changed between NVN versions. Don't guess: translate everything once (the translator alone takes seconds) and tabulate what the shaders reference.
|
||||
|
||||
| Binding | D2R (NVN 53) | Mario (NVN 55) | How it was measured |
|
||||
|---|---|---|---|
|
||||
| Bound texture handles (texture buffer) | `0x168 + 8 × slot`, 128 slots | **`0x20 + 8 × slot`**, 32 slots | Immediate handles of all TEX instructions: 0x20…0xF0 in steps of 8 |
|
||||
| Image handles | `0x120 + 8 × slot` | `0x120 + 8 × slot` | agl compute shaders: 0x120 / 0x128 / 0x130 |
|
||||
| Separate samplers | `0x568 + 8 × slot` | not used | — |
|
||||
| Uniform buffers | `c[3 + slot]` | same | |
|
||||
| Storage buffers | `c0[base + 16 × slot]`, base vs 0x110 / cs 0x310 / ps 0x510 | same | |
|
||||
| Binding | D2R (NVN 53) | Mario (NVN 55) | Engage (Unity, SDK 12) | How it was measured |
|
||||
|---|---|---|---|---|
|
||||
| Bound texture handles (texture buffer) | `0x168 + 8 × slot`, 128 slots | **`0x20 + 8 × slot`**, 32 slots | `0x168 + 8 × slot`, 128 slots | Mario: immediate handles of all TEX instructions, 0x20…0xF0 in steps of 8. Engage: the translator's texture names (`fp_t_cb<slots>_<handle>`, Ryujinx `TextureHandle` packing) give word 0x5A = 0x168 for the texture and 0x15A = 0x568 for the sampler |
|
||||
| Image handles | `0x120 + 8 × slot` | `0x120 + 8 × slot` | not measured (no compute shaders) | agl compute shaders: 0x120 / 0x128 / 0x130 |
|
||||
| Separate samplers | `0x568 + 8 × slot` | not used | `0x568 + 8 × slot` (Unity binds only separate textures and samplers) | — |
|
||||
| Uniform buffers | `c[3 + slot]` | same | same | |
|
||||
| Storage buffers | `c0[base + 16 × slot]`, base vs 0x110 / cs 0x310 / ps 0x510 | same | not measured | |
|
||||
|
||||
`dmsh_build.py` (`TEX_BASE`, `IMG_BASE`, `decode_tex`) and `Program.cs` (`TexBase`) hold these constants; the replay layer fills driver banks with the same layout (`NVN_RB_DRV2` in `encode_args`). After a full build, the summary must say `绑定没解出槽号的着色器 0` (no unresolved bindings).
|
||||
The profile's `nvn_*` keys hold these constants; `dmsh_build.py` decodes the bindings from them (`decode_tex`). `Program.cs` still has Mario's `TexBase` built in, but uses it only to look up per-slot texture / image types in the facts table, so a profile with other bases builds as long as its facts table has no such entries (Engage's has none; `dmsh_build.py` stops otherwise); the replay layer fills driver banks with the same layout: the texture, image and separate-sampler tables of `c2` (`NVN_RB_DRV2` in `encode_args`). Some shaders read `c2` rows as data at run time (Engage: 243, the texture and sampler handles of slots 0–11); the build lists reads outside those tables as `填不了` (unfillable). After a full build, the summary must say `绑定没解出槽号的着色器 0` (no unresolved bindings).
|
||||
|
||||
## 4. Bindless textures
|
||||
|
||||
@@ -64,6 +65,13 @@ Consequence: any optimization that infers "which textures a draw reads" from bou
|
||||
- **Every pixel shader input must be a vertex shader output.** Maxwell attributes are untyped slots: a pixel shader that reads a slot the vertex shader never wrote gets a default. Metal refuses to build the pipeline (`Fragment input(s) user(locnN) mismatching vertex shader output type(s) or not written by vertex shader`), and the draw is skipped. Translating one shader at a time cannot know what the partner reads, so `shader_split.py` writes the pairs of each BNSH variation (`shader_pairs.tsv`) and `dmsh_build.py` gives each vertex shader the outputs its partners read but it doesn't write, as `float4` (0, 0, 0, 1) (`vs_pad`; vertex shaders with partners are finished after the pixel shaders, and the partner list is part of the shader's cache rule). In Mario 331 of 9,043 pairs failed; the opening course used none of them, later levels did (about 11,500 failed builds in one 11-minute session). Check all pairs offline after a build (recipe in [05](05-debugging.md)). The host caches failed pipelines and reports each once with its vertex / fragment keys (`【红】建渲染管线失败(顶点 … 片元 …)`); before that, every draw retried the failing compile.
|
||||
- **iOS math mode**: the iOS libraries are compiled with `-fmetal-math-mode=relaxed` (`dm/ios/tools/ios_metal.py`). The translated shaders reproduce Maxwell's NaN behaviour with `isnan` selects (8,337 of 12,809 use them), and the default fast mode allows the GPU compiler to assume there is no NaN. Relaxed keeps fast math's reassociation but not that assumption.
|
||||
|
||||
### Seen in Engage (Unity)
|
||||
|
||||
- **Program control sections are freed after `nvnProgramSetShaders`**: shader keys are computed inside that call ([03](03-bringup.md), graphics entries).
|
||||
- **CPU-written block-linear textures** (`nvnTextureWriteTexels`: video frames, engine default textures) are uploaded and re-uploaded from guest memory like packaged textures ([03](03-bringup.md)).
|
||||
- **Video**: Unity's `Hidden/VideoDecode` samples luma as `.w` of an `R8` texture with the swizzle `(0, 0, 0, R)` (Unity's Alpha8) and chroma from an `RG8` texture at half size. To check the movie path without a decoder, fill the frames with a gradient in `hle_movie.c`; it should appear on screen.
|
||||
- **One geometry shader** (in `unity_builtin_extra`): Metal has no geometry stage, so it fails to compile (`unknown main0_out`); nothing in the opening scenes uses it.
|
||||
|
||||
## 7. Diagnostic switches (host)
|
||||
|
||||
| Switch | Output |
|
||||
|
||||
Reference in new issue
Block a user