Repository navigation
Expand file tree
/
Copy pathboot_macos.cpp
More file actions
628 lines (569 loc) · 25.6 KB
/
Copy pathboot_macos.cpp
File metadata and controls
628 lines (569 loc) · 25.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
/*
* God of War II HD - macOS/arm64 boot host
*
* Phase F4 of ps3recomp/docs/MACOS_PORT_PLAN.md: the POSIX counterpart of the
* Windows boot_v2_new.exe launcher. Brings up guest memory, loads the
* decrypted EBOOT, wires the HLE registries and enters the guest.
*
* Mirrors ps3recomp/runtime/ppu/tests/boot_main.cpp, the reference host, which
* is the authority on the init contract. Diverging from it silently is how the
* first version of this file ended up hanging on stubbed timer syscalls: it
* skipped lv2_init_syscalls(), left ppu_vm_size at 0 (OOB guard off, so a
* stray guest pointer took the process down instead of being logged), and
* installed neither of the two hooks the runtime needs to re-enter guest code.
*
* Graphics backend is chosen with PS3_RSX_BACKEND (metal | sdl | vulkan).
* Default is metal (M10). PS3_NO_RSX=1 skips the window for headless CPU/SPURS.
*
* Build: ./build_macos.sh
*/
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cstdint>
#if defined(__APPLE__)
#include <TargetConditionals.h>
#endif
#include "gow2_boot.h"
#include "recomp_mid_v2/gow2_spu_register.h"
/* Weak: a build without lifted SPU images links no gow2_spu_register.o
* (build_macos.sh only compiles it when spu_lifted/ exists). */
extern "C" void gow2_register_spu_workloads(const gow2_spu_config* cfg) __attribute__((weak));
extern "C" void gow2_spu_config_from_env(gow2_spu_config* cfg) __attribute__((weak));
#if defined(__APPLE__) && TARGET_OS_IPHONE
#define GOW2_HOST_NAME "iOS"
#define GOW2_HAVE_VULKAN 0 /* no Vulkan on iOS; PS3_RSX_BACKEND=vulkan falls back to Metal */
#define GOW2_HAVE_METAL 1
#elif defined(__ANDROID__)
#define GOW2_HOST_NAME "Android"
#define GOW2_HAVE_VULKAN 1 /* the phone's native ICD; failure falls back to the SDL null backend */
#define GOW2_HAVE_METAL 0
#else
#define GOW2_HOST_NAME "macOS"
#define GOW2_HAVE_VULKAN 1
#define GOW2_HAVE_METAL 1
#endif
#include "ppu_recomp.h"
#include "ppu_coop.h" /* PS3_PPU_COOP: callback stack slots (OFF by default) */
#include "ps3emu/ps3_env.h" /* ps3_env_on: value-aware boolean gates */
extern "C" {
/*
* Guest address space. ppu_loader.cpp declares this and expects the host to
* own it; vm_init() in vm.h fills it in.
*
* Note for anyone porting further: the guest's window is mapped with
* mmap(NULL, ...) and every guest address is an offset from vm_base, so macOS
* reserving the low 4 GB as __PAGEZERO never comes into it. No -pagezero_size
* link flag is needed.
*/
uint8_t* vm_base = nullptr;
/* g_trampoline_fn (the cross-fragment branch trampoline) is defined by
* ppu_loader.cpp, which also drains it in its run loop. Do not define it here
* as well -- the lifted chunks only declare it extern. */
/* Guest address-space size. Setting this arms the runtime's bounds check, so a
* stray guest pointer is logged and reads back 0 rather than faulting the
* host: a first boot then reveals the next wall instead of dying at the first
* one. */
extern uint32_t ppu_vm_size;
uint32_t ppu_load_elf(const char* path);
void ppu_recomp_register(void);
int ppu_run(uint32_t entry_opd, uint32_t stack_top);
int ppu_opd_resolve(uint32_t opd, uint32_t* code, uint32_t* toc);
void ps3_indirect_call(ppu_context* ctx);
uint32_t ppu_function_count(void);
/* HLE registries. These have to be populated before ppu_resolve_imports runs,
* otherwise the guest's PRX import stubs have nothing to bind to and the first
* indirect call through one jumps into whatever the slot happened to hold. */
void ppu_hle_init(void);
void ppu_sysprx_register(void);
void ppu_fs_register(void);
void lv2_init_syscalls(void);
uint32_t ppu_resolve_imports(void);
extern thread_local void (*g_trampoline_fn)(void*);
extern const char* ppu_vfs_root;
typedef void (*ps3_guest_caller_fn)(uint32_t opd, uint64_t, uint64_t, uint64_t, uint64_t);
extern ps3_guest_caller_fn g_ps3_guest_caller;
typedef void (*ppu_thread_entry_fn)(ppu_context*);
extern ppu_thread_entry_fn g_ppu_thread_entry_trampoline;
int rsx_null_backend_init(uint32_t w, uint32_t h, const char* title);
void rsx_null_backend_shutdown(void);
int rsx_null_backend_pump_messages(void);
#if GOW2_HAVE_METAL
int rsx_metal_backend_init(uint32_t w, uint32_t h, const char* title);
void rsx_metal_backend_shutdown(void);
int rsx_metal_backend_pump_messages(void);
#endif
#if GOW2_HAVE_VULKAN
int rsx_vulkan_backend_init(uint32_t w, uint32_t h, const char* title);
void rsx_vulkan_backend_shutdown(void);
int rsx_vulkan_backend_pump_messages(void);
#endif
/*
* Mapa de commits do runtime (ppu_loader.cpp). O guard dos acessos vm_read e
* vm_write nao consulta as tabelas de paginas do SO: consulta esta lista, que o host
* tem de preencher, uma entrada por regiao que commita. Com a lista vazia o
* guard fica intencionalmente ABERTO (tudo "commitado"), que era o estado
* deste host -- ver commit_guest_regions().
*/
void ppu_register_committed_range(uint32_t lo, uint32_t hi);
int ppu_guest_range_committed(uint32_t addr, uint32_t n);
} /* extern "C" */
#include "movie_eos_arm.h" /* amostrador [MOVIEFSM], gated, so observa */
#include "gow2_overlay_provider.h" /* runtime overlay: WAD/movie/shader diagnostics */
#include "vm.h"
/*
* Guest memory layout, matching the reference host.
*
* VM_SIZE has to cover every region the address map uses, including the PPU
* thread-stack region at 0xD0000000 -- a smaller window leaves spawned thread
* stacks outside the VM, where the OOB guard silently drops their writes and
* the threads die.
*
* The main thread's stack goes in the free area below the image rather than in
* the 0xD0000000 band, which is reserved for threads the guest spawns.
*/
#define GUEST_VM_SIZE 0xE0000000u
#define GUEST_LOW_MB 0x51000000u /* image + heap + main stack + TLS + mmapper */
#define GUEST_STACK_TOP 0x0FF00000u
#define CB_STACK_TOP 0x0D000000u
#define RSX_LOCAL_BASE 0xC0000000u /* memoria local do RSX (cellGcmGetConfiguration) */
namespace {
enum class Backend { None, Sdl, Metal, Vulkan };
Backend g_backend = Backend::None;
char s_vfs_root[1024];
/*
* Re-enter guest code from an HLE bridge (callbacks: cellSysutil, cellGcm
* flip handlers, and so on). Each nested call gets its own stack slice so a
* callback issued from inside a callback cannot land on its caller's frame.
*/
void boot_guest_caller(uint32_t opd_addr, uint64_t a0, uint64_t a1,
uint64_t a2, uint64_t a3)
{
if (!opd_addr) {
return;
}
uint32_t code = 0, toc = 0;
ppu_opd_resolve(opd_addr, &code, &toc);
if (!code) {
return;
}
/* PS3_PPU_COOP: a slot held until this call returns (a callback can park on one fiber
* while another fiber enters its own); gate off: the depth counter, as before. */
static uint32_t cb_depth = 0;
const int cb_coop = ppu_coop_enabled();
const uint32_t cb_slot = cb_coop ? ppu_coop_cb_slot_acquire() : ++cb_depth;
/* PS3_PPU_COOP: the callback is guest code, not part of the HLE import that raised it
* (ps3_hle_call's no-preempt region): a busy loop in it must still let other fibers run.
* Audited: no caller (cellSysutil, cellGcmSys, cellSaveData, cellVdec, sys_ppu_thread_once,
* the callback fiber) holds a host mutex across this call. */
const int cb_np = cb_coop ? ppu_coop_nopreempt_suspend() : 0;
ppu_context ctx;
memset(&ctx, 0, sizeof(ctx));
ctx.gpr[1] = CB_STACK_TOP - 0x10000u * cb_slot;
ctx.gpr[2] = toc;
ctx.gpr[3] = a0;
ctx.gpr[4] = a1;
ctx.gpr[5] = a2;
ctx.gpr[6] = a3;
ctx.ctr = code;
ps3_indirect_call(&ctx);
while (g_trampoline_fn) { void (*tf)(void*) = g_trampoline_fn; g_trampoline_fn = 0; tf(&ctx); }
if (cb_coop) ppu_coop_nopreempt_resume(cb_np);
if (cb_coop) ppu_coop_cb_slot_release(cb_slot); else --cb_depth;
}
/* Entry thunk for guest-spawned PPU threads: resolve the OPD the guest handed
* sys_ppu_thread_create and run the lifted code behind it. */
void boot_thread_trampoline(ppu_context* ctx)
{
uint32_t code = 0, toc = 0;
if (ppu_opd_resolve((uint32_t)ctx->cia, &code, &toc) && code) {
ctx->ctr = code;
if (toc) {
ctx->gpr[2] = toc;
}
} else {
ctx->ctr = ctx->cia; /* already a raw code address */
}
/* PS3_TRACE_THRSTACK: r2 (TOC) is the base for every TOC-relative global
* access in lifted code; if it stays 0 the thread reads globals at small
* negative addresses and the failure only surfaces later as [vm] OOB. */
{ static int ts = -1; if (ts < 0) ts = getenv("PS3_TRACE_THRSTACK") ? 1 : 0;
if (ts) { fprintf(stderr, "[THRTOC] cia=0x%08X code=0x%08X toc=0x%08X -> r2=0x%08llX\n",
(unsigned)ctx->cia, code, toc,
(unsigned long long)ctx->gpr[2]); fflush(stderr); } }
ps3_indirect_call(ctx);
while (g_trampoline_fn) { void (*tf)(void*) = g_trampoline_fn; g_trampoline_fn = 0; tf(ctx); }
}
/* PS3 mount points resolve under this host directory. PS3_VFS_ROOT wins;
* otherwise strip EBOOT.ELF / USRDIR / PS3_GAME off the ELF path. */
void derive_vfs_root(const char* eboot)
{
const char* env = getenv("PS3_VFS_ROOT");
if (env && *env) {
ppu_vfs_root = env;
return;
}
strncpy(s_vfs_root, eboot, sizeof s_vfs_root - 1);
s_vfs_root[sizeof s_vfs_root - 1] = 0;
for (int i = 0; i < 3; i++) {
char* s = strrchr(s_vfs_root, '/');
if (s) *s = 0;
}
if (!s_vfs_root[0]) {
strcpy(s_vfs_root, ".");
}
ppu_vfs_root = s_vfs_root;
}
Backend pick_backend()
{
if (ps3_env_on("PS3_NO_RSX")) {
fprintf(stderr, "[boot] RSX backend=none (PS3_NO_RSX)\n");
return Backend::None;
}
const char* want = getenv("PS3_RSX_BACKEND");
if (!want || !*want) {
/* M10: native Metal is the windowed default on macOS; Vulkan on Android. */
want = GOW2_HAVE_METAL ? "metal" : "vulkan";
}
Backend b = Backend::Sdl;
if (strcmp(want, "metal") == 0) b = GOW2_HAVE_METAL ? Backend::Metal : Backend::Sdl;
else if (strcmp(want, "vulkan") == 0) b = GOW2_HAVE_VULKAN ? Backend::Vulkan : Backend::Metal;
else if (strcmp(want, "sdl") == 0) b = Backend::Sdl;
else {
fprintf(stderr, "[boot] unknown PS3_RSX_BACKEND '%s', falling back to %s\n", want,
GOW2_HAVE_METAL ? "metal" : "sdl");
b = GOW2_HAVE_METAL ? Backend::Metal : Backend::Sdl;
}
const char* name = "sdl";
if (b == Backend::Metal) name = "metal";
if (b == Backend::Vulkan) name = "vulkan";
fprintf(stderr, "[boot] RSX backend=%s\n", name);
fflush(stderr);
return b;
}
int backend_init(Backend b)
{
switch (b) {
case Backend::Sdl: return rsx_null_backend_init(1280, 720, "God of War II HD");
#if GOW2_HAVE_METAL
case Backend::Metal: return rsx_metal_backend_init(1280, 720, "God of War II HD");
#else
case Backend::Metal: return -1;
#endif
#if GOW2_HAVE_VULKAN
case Backend::Vulkan: return rsx_vulkan_backend_init(1280, 720, "God of War II HD");
#else
case Backend::Vulkan: return -1;
#endif
case Backend::None: return 0;
}
return -1;
}
void backend_shutdown(Backend b)
{
switch (b) {
case Backend::Sdl: rsx_null_backend_shutdown(); break;
#if GOW2_HAVE_METAL
case Backend::Metal: rsx_metal_backend_shutdown(); break;
#else
case Backend::Metal: break;
#endif
#if GOW2_HAVE_VULKAN
case Backend::Vulkan: rsx_vulkan_backend_shutdown(); break;
#else
case Backend::Vulkan: break;
#endif
case Backend::None: break;
}
}
/*
* Back the regions the boot actually touches.
*
* vm_init only makes main memory and the stack band accessible, but the
* reference host commits from guest address 0 upward -- which matters: the
* GoW2 CRT reads through a null pointer during early init, and with the low
* page unbacked that read is a SIGBUS rather than the zero the reference host
* returns. The gap between the low region and the thread-stack band stays
* reserved, as on Windows.
*
* Cada vm_commit e' seguido de ppu_register_committed_range com o MESMO span.
* Faltava: o boot_main.cpp do Windows regista as suas regioes e este host nao
* registava nenhuma, portanto g_committed_count ficava 0 e o guard do runtime
* ficava aberto -- todo o espaco de 3,5 GB contava como commitado. Consequencia
* pratica: ppu_guest_range_committed() devolvia 1 para QUALQUER endereco, e
* quem depende dele (o guard dos vm_read*, o DMA do SPU em spu_dma.h, e agora
* o amostrador do movie player) nao tinha como distinguir memoria com paginas
* por tras de espaco meramente reservado. As paginas fora destas regioes estao
* PROT_NONE, logo o que o guard passava a deixar passar acabava em SIGBUS.
*
* Medido (boot headless de 25 s, 2026-07-20): o guest aloca no maximo ate
* 0x4B100000 (sys_memory bump de 0x40000000 para cima) e a imagem TLS fica em
* 0x10F00000 -- tudo dentro de 0..GUEST_LOW_MB. Nota para quem vier a seguir:
* o sys_memory.c pode ir ate SYS_MEM_ALLOC_END (0x60000000) e faz vm_commit
* SEM registar; se um boot mais longo passar de 0x51000000 aparecem linhas
* "[vm] UNCOMMITTED" e o sitio certo de corrigir e' o registo na origem, em
* sys_memory.c -- nao alargar estas regioes a espaco que nao esta commitado.
*/
int commit_guest_regions()
{
if (vm_commit(0, GUEST_LOW_MB) != CELL_OK) {
fprintf(stderr, "[boot] FATAL: could not commit guest low memory\n");
return -1;
}
/* No prezero (spec 2026-09-24 iOS): the region is fresh MAP_ANONYMOUS
* memory, zero-filled by the kernel on first touch. Writing 1.26 GB of
* zeros only made it resident (a third of the iPhone's 4 GB jetsam limit). */
ppu_register_committed_range(0, GUEST_LOW_MB);
if (vm_commit(VM_STACK_BASE, VM_STACK_REGION) != CELL_OK) {
fprintf(stderr, "[boot] FATAL: could not commit guest thread-stack band\n");
return -1;
}
ppu_register_committed_range(VM_STACK_BASE, VM_STACK_BASE + VM_STACK_REGION);
/* Memoria local do RSX (0xC0000000, o localAddress/localSize de 256 MB que o
* cellGcmGetConfiguration anuncia). O alocador da GPU poe a sua arena aqui e
* o caminho de render escreve framebuffers e labels dentro dela. Faltava: o
* boot_main.cpp ja a commitava e este host nao, entao o titulo passava o
* wall SPURS, entrava no cellGcmSys e morria com SIGBUS no primeiro
* vm_write32 para 0xC0F40000 (guest), logo a seguir aos SetTile/
* SetDisplayBuffer de 1280x720. Regulavel por PS3_VM_RSX_MB. */
{
const char* e = getenv("PS3_VM_RSX_MB");
uint64_t rsx = e ? ((uint64_t)strtoul(e, nullptr, 10) << 20) : 0x10000000ull;
if (rsx && vm_commit(RSX_LOCAL_BASE, (uint32_t)rsx) != CELL_OK) {
fprintf(stderr, "[boot] FATAL: could not commit RSX local memory\n");
return -1;
}
if (rsx) {
ppu_register_committed_range(RSX_LOCAL_BASE, RSX_LOCAL_BASE + (uint32_t)rsx);
fprintf(stderr, "[boot] committed RSX local 0x%X..0x%llX\n",
RSX_LOCAL_BASE, (unsigned long long)RSX_LOCAL_BASE + rsx);
}
}
fprintf(stderr, "[boot] committed guest 0x0..0x%X and 0x%X..0x%X\n",
GUEST_LOW_MB, VM_STACK_BASE, VM_STACK_BASE + VM_STACK_REGION);
/*
* PS3_TRACE_COMMITMAP: prova de que o registo acima chegou de facto ao
* guard, e nao so ao log. Interroga o ppu_guest_range_committed com um
* endereco DENTRO e outro FORA de cada regiao -- se a lista nao tivesse
* sido preenchida, todas as respostas seriam "sim" (guard aberto), o que
* torna o probe capaz de distinguir os dois estados. Off por default.
*/
if (getenv("PS3_TRACE_COMMITMAP")) {
static const struct { const char* name; uint32_t ea; int expect; } probes[] = {
{ "baixa (imagem/heap)", 0x00540054u, 1 },
{ "baixa (limite-1)", GUEST_LOW_MB - 4u, 1 },
{ "buraco acima da baixa", GUEST_LOW_MB, 0 },
{ "buraco (0x80000000)", 0x80000000u, 0 },
{ "RSX local", RSX_LOCAL_BASE, 1 },
{ "banda de stacks", VM_STACK_BASE, 1 },
{ "acima da VM", 0xE0000000u, 0 },
};
int bad = 0;
for (unsigned i = 0; i < sizeof probes / sizeof probes[0]; i++) {
int got = ppu_guest_range_committed(probes[i].ea, 4) ? 1 : 0;
if (got != probes[i].expect) bad++;
fprintf(stderr, "[commitmap] 0x%08X %-24s commitado=%d esperado=%d%s\n",
probes[i].ea, probes[i].name, got, probes[i].expect,
got == probes[i].expect ? "" : " <-- DIVERGE");
}
fprintf(stderr, "[commitmap] divergencias=%d\n", bad);
fflush(stderr);
}
return 0;
}
} /* namespace */
static uint32_t s_boot_entry;
static int s_display_ready;
extern "C" int gow2_boot_prepare_guest(const char* elf_path)
{
/* SPU images register here, explicitly, after the host has its
* configuration (spec 2026-09-24 iOS, resolution 1): the old static
* constructor ran before main() and never saw what an iOS host exports
* with setenv(). Same point in the boot order as before (first). */
if (gow2_register_spu_workloads && gow2_spu_config_from_env) {
gow2_spu_config spu;
gow2_spu_config_from_env(&spu);
gow2_register_spu_workloads(&spu);
}
fprintf(stderr, "[boot] God of War II HD -- " GOW2_HOST_NAME "/arm64 host\n");
fprintf(stderr, "[boot] ELF: %s\n", elf_path);
if (vm_init() != CELL_OK) {
fprintf(stderr, "[boot] FATAL: vm_init failed\n");
return 1;
}
fprintf(stderr, "[boot] guest memory mapped at host %p\n", (void*)vm_base);
if (commit_guest_regions() != 0) {
vm_shutdown();
return 1;
}
ppu_vm_size = GUEST_VM_SIZE;
uint32_t entry = ppu_load_elf(elf_path);
if (!entry) {
fprintf(stderr, "[boot] FATAL: could not load %s\n", elf_path);
vm_shutdown();
return 1;
}
fprintf(stderr, "[boot] entry OPD 0x%08X\n", entry);
s_boot_entry = entry;
derive_vfs_root(elf_path);
fprintf(stderr, "[boot] VFS root: %s\n", ppu_vfs_root);
/* Without these two the runtime has no way back into guest code: HLE
* callbacks become no-ops and every thread the guest spawns runs nothing. */
g_ps3_guest_caller = boot_guest_caller;
g_ppu_thread_entry_trampoline = boot_thread_trampoline;
/* Order matters, and matches the reference host. */
ppu_recomp_register(); /* lifted function table -> address map */
ppu_hle_init(); /* firmware import NID -> HLE handlers */
ppu_sysprx_register(); /* boot-critical CRT (sys_initialize_tls, ...) */
ppu_fs_register(); /* cellFs VFS over the real game directory */
lv2_init_syscalls(); /* real lv2 table (timer/event/spu/mutex/fs/...) */
ppu_resolve_imports(); /* patch .lib.stub slots -> HLE bridge */
fprintf(stderr, "[boot] %u lifted functions registered, HLE wired\n",
ppu_function_count());
return 0;
}
extern "C" int gow2_boot_prepare_display(void)
{
if (s_display_ready)
return 0;
g_backend = pick_backend();
/* Runtime overlay: settings (window size, fullscreen, VSync, pad mapping)
* load BEFORE the backend reads them. The launcher hands its path over in
* PS3_OVERLAY_SETTINGS; started without the launcher, the per-user default
* path is used. The GoW2 provider only copies host-side snapshots (no guest
* pointer, no giant lock). The overlay stays closed until F1/Start.
* Only a backend with an overlay renderer draws it: Metal everywhere, and
* on Android also Vulkan (rsx_vk_overlay: the home screen and the touch
* controls). With the other backends (sdl/none, and vulkan off Android) it
* stays uninitialized, so Start and the pad reach the game exactly as
* before instead of toggling a menu nobody can see. */
auto overlay_init_for_ui = []() {
#if defined(__ANDROID__)
const bool draws = g_backend == Backend::Metal || g_backend == Backend::Vulkan; /* the Vulkan overlay renderer exists (rsx_vk_overlay) */
#else
const bool draws = g_backend == Backend::Metal; /* sdl/vulkan/none: nothing draws it, Start reaches the game */
#endif
if (!draws) return;
if (!rsx_overlay_init(rsx_overlay_settings_path_from_env())) {
fprintf(stderr, "[boot] runtime overlay unavailable\n");
#ifdef __ANDROID__
/* the home screen waits for a "Jogar" only while the core is live: disabled, it starts the game */
rsx_overlay_disable("overlay init failed");
#endif
}
rsx_overlay_set_title("God of War II HD");
gow2_overlay_provider_register();
};
overlay_init_for_ui();
int brc = backend_init(g_backend);
if (brc != 0 && g_backend == Backend::Vulkan) {
#if GOW2_HAVE_METAL
/* Spec (ps3recomp docs/superpowers/specs/2026-09-23-vulkan-backend-a-design.md §5):
* Vulkan unavailable -> platform default with one log line, never abort. The env is
* updated so cellGcmSys's rsx_bridge_mode keeps the Metal backend registered here.
* The overlay was skipped for Vulkan; it comes up now, before the Metal backend. */
fprintf(stderr, "[boot] vulkan init failed -> metal (platform default)\n");
setenv("PS3_RSX_BACKEND", "metal", 1);
g_backend = Backend::Metal;
overlay_init_for_ui();
#else
/* Android spec §4.3: no Metal -- fail loudly and keep booting on the SDL null
* backend (no pixels, the rest of the game runs), never abort. */
fprintf(stderr, "[BOOT] vulkan init failed -> sdl (null backend)\n");
/* The overlay core came up for Vulkan above; nothing draws it on the null backend:
* disabled, it withholds nothing from the guest and the Android home starts the game. */
rsx_overlay_disable("Vulkan unavailable: SDL null backend draws no overlay");
setenv("PS3_RSX_BACKEND", "sdl", 1);
g_backend = Backend::Sdl;
#endif
brc = backend_init(g_backend);
}
if (brc != 0) {
fprintf(stderr, "[boot] FATAL: RSX backend failed to initialise\n");
return 1;
}
s_display_ready = 1;
return 0;
}
extern "C" int gow2_boot_prepare(const char* elf_path)
{
/* GOW2_BOOT_DISPLAY_FIRST=1 (diagnostic, OFF by default): the iOS order
* (display, then guest) on the Mac, to catch an HLE init that needs the
* backend first before the phone does. */
const char* display_first = std::getenv("GOW2_BOOT_DISPLAY_FIRST");
if (display_first != nullptr && display_first[0] == '1') {
fprintf(stderr, "[boot] GOW2_BOOT_DISPLAY_FIRST=1: display before the guest (iOS order)\n");
if (gow2_boot_prepare_display() != 0)
return 1;
if (gow2_boot_prepare_guest(elf_path) != 0) {
/* Symmetric cleanup with the default path: the guest failed after
* display prep succeeded, so undo any partial guest vm state
* (idempotent: no-op if gow2_boot_prepare_guest already shut it
* down on its own failure path). */
vm_shutdown();
return 1;
}
} else {
/* The macOS order, unchanged: guest (SPU, vm, ELF, HLE), then display. */
if (gow2_boot_prepare_guest(elf_path) != 0)
return 1;
if (gow2_boot_prepare_display() != 0) {
vm_shutdown();
return 1;
}
}
return 0;
}
extern "C" int gow2_boot_run_guest(void)
{
/* Amostrador do objecto do movie player ([MOVIEFSM]/[MOVIEOBJ]). Gated por
* PS3_TRACE_MOVIEOBJ, no-op sem ela. SO OBSERVA: nao arma o read-hook de
* EOS -- isso e' a Task 3 e depende de um produtor real de "done", que no
* POSIX ainda nao existe. Arranca aqui, antes do guest, porque o objecto e'
* um inicializador estatico em BSS e ja esta presente desde o load. No
* macOS e' o mesmo ponto de antes (depois do backend, antes do ppu_run);
* no iOS corre na thread do guest, depois do gow2_boot_prepare_guest. */
movie_eos_sampler_start();
fprintf(stderr, "[boot] entering guest (stack top 0x%08X)\n", GUEST_STACK_TOP);
fflush(stderr);
const uint32_t entry = s_boot_entry;
int rc = ppu_run(entry, GUEST_STACK_TOP);
fprintf(stderr, "[boot] guest returned rc=%d\n", rc);
return rc;
}
extern "C" void gow2_boot_shutdown(void)
{
/* Orderly exit only: the overlay saves pending settings and frees its core
* before the backend (its only renderer) tears down. A no-op when it was
* never initialized (non-Metal backends). The window-close / Cmd+Q /
* shell "Sair" paths leave through metal_quit_now's _exit by design and
* never get here (see rsx_metal_backend.m). */
rsx_overlay_shutdown();
backend_shutdown(g_backend);
vm_shutdown();
}
#ifndef GOW2_BOOT_NO_MAIN
int main(int argc, char** argv)
{
const char* elf_path = (argc > 1) ? argv[1] : "EBOOT.ELF";
/*
* Line-buffer stdout.
*
* The runtime logs through both printf and fprintf(stderr): stderr is
* unbuffered, stdout is block-buffered as soon as it is redirected to a
* file. Every smoke and trace run here ends in kill, which discards
* whatever is still sitting in the stdout buffer -- so a captured log
* silently loses most of the boot and reads as if the guest stopped much
* earlier than it did. That cost real debugging time: the guest was
* reaching cellSpursInitializeWithAttribute while the log appeared to stop
* at the printf-server thread.
*/
setvbuf(stdout, nullptr, _IOLBF, 0);
if (gow2_boot_prepare(elf_path) != 0) {
return 1;
}
const int rc = gow2_boot_run_guest();
gow2_boot_shutdown();
return rc == 0 ? 0 : 1;
}
#endif /* GOW2_BOOT_NO_MAIN */