DSP callers were charged the local SDK CPU server budget, whose command representation is larger than the Horizon service representation. For Age of Calamity the service requires 0x61d000 while the local SDK requires 0x760000, exhausting the fixed sound heap. Calculate the service requirement through renderer revision 15 and supply larger local backend storage separately, with cleanup on failed open and after renderer close. Remove the two game heap cuts and add sizing, nested-open, failure and lifetime regression coverage. Validation: 360 seeded cases match isolated Horizon machine-code execution across REV1-REV15; the original game heaps initialize all five players and produce audio; the host suite, documentation and patch checks, SDK 8/20 smoke checks and iOS C compilation pass. iOS runtime and long battles remain untested.
162 lines
7.8 KiB
C
162 lines
7.8 KiB
C
// Exercise the public size -> caller allocation -> both SDK open routes -> close boundary.
|
|
// The service requirement must not be inflated by the local backend's different command representation.
|
|
#include "../hle.h"
|
|
#include <assert.h>
|
|
#include <errno.h>
|
|
|
|
static void *backing;
|
|
static size_t backing_size;
|
|
static int allocations, frees, fail_alloc;
|
|
static int work_alloc(void **out, size_t align, size_t size) {
|
|
if (fail_alloc) return ENOMEM;
|
|
int r = posix_memalign(out, align, size);
|
|
if (!r) { assert(!backing); backing = *out; backing_size = size; allocations++; }
|
|
return r;
|
|
}
|
|
static void work_free(void *p) {
|
|
if (p) { assert(p == backing); backing = NULL; backing_size = 0; frees++; }
|
|
free(p);
|
|
}
|
|
#define posix_memalign work_alloc
|
|
#define free work_free
|
|
#include "../hle_audren.c"
|
|
#undef posix_memalign
|
|
#undef free
|
|
|
|
enum { SDK_WORK = 0x760000, SERVICE_WORK = 0x61d000 };
|
|
static int opens, starts, joins, closes, fail_open, nested;
|
|
static void *sdk_work;
|
|
static void *map_key, *map_value;
|
|
static size_t sdk_size(const uint8_t *param) {
|
|
assert(*(int32_t *)(param + P_DEVICE) == 1 && *(int32_t *)(param + P_MODE) == 1);
|
|
assert((*(uint32_t *)(param + 0x34) & 0xffffff) == 0x564552 || (*(uint32_t *)(param + 0x38) & 0xffffff) == 0x564552);
|
|
return SDK_WORK;
|
|
}
|
|
static Result sdk_open(void **handle, const uint8_t *param, void *work, size_t size) {
|
|
opens++;
|
|
size_t need = h_ar_WorkSize(param); // SDK opens can query the already converted parameter through the PLT
|
|
assert(need == SDK_WORK);
|
|
if (!work || ((uintptr_t)work & (WORK_PAGE - 1)) || size < need) return RES_FAIL;
|
|
if (fail_open) return RES_FAIL;
|
|
assert(!backing || (work == backing && size == backing_size));
|
|
sdk_work = work;
|
|
memset(work, 0x5a, need); // touch the entire area, not just the handle
|
|
handle[0] = work; handle[1] = (uint8_t *)work + 0x80;
|
|
return RES_OK;
|
|
}
|
|
static Result sdk_open_ev(void **handle, void *ev, const uint8_t *param, void *work, size_t size) {
|
|
assert(ev);
|
|
return nested ? h_ar_Open0(handle, param, work, size) : sdk_open(handle, param, work, size);
|
|
}
|
|
static void sdk_close(void *obj, void *state) {
|
|
assert(obj == sdk_work && state == (uint8_t *)sdk_work + 0x80);
|
|
assert(((uint8_t *)obj)[SDK_WORK - 1] == 0x5a); // storage is alive through SDK teardown
|
|
closes++;
|
|
}
|
|
void *guest_sym(const char *name) {
|
|
if (!strcmp(name, hle_force_h_ar_WorkSize.name)) return sdk_size;
|
|
if (!strcmp(name, hle_force_h_ar_Open.name)) return sdk_open_ev;
|
|
if (!strcmp(name, hle_force_h_ar_Open0.name)) return sdk_open;
|
|
if (!strcmp(name, hle_force_h_ar_Close.name)) return sdk_close;
|
|
return NULL;
|
|
}
|
|
void *objmap_get(const void *key) { return key == map_key ? map_value : NULL; }
|
|
void objmap_put(const void *key, void *value) { assert(!map_key); map_key = (void *)key; map_value = value; }
|
|
void *objmap_del(const void *key) {
|
|
if (key != map_key) return NULL;
|
|
void *value = map_value; map_key = map_value = NULL; return value;
|
|
}
|
|
void *dm_guest_thread_spawn(void (*fn)(void *), void *arg, int prio, size_t stack) {
|
|
(void)fn; (void)arg; (void)prio; (void)stack; starts++; return (void *)1;
|
|
}
|
|
void dm_guest_thread_join(void *tt) {
|
|
assert(tt == (void *)1 && !atomic_load(&g_open));
|
|
assert(((uint8_t *)sdk_work)[SDK_WORK - 1] == 0x5a); // storage also survives the render thread
|
|
joins++;
|
|
}
|
|
uint64_t now_ns(void) { return 0; }
|
|
void *dm_pcmout_open(int ch) { (void)ch; return NULL; }
|
|
void dm_pcmout_close(void *k) { (void)k; }
|
|
void dm_pcmout_start(void *k) { (void)k; }
|
|
int dm_pcmout_append(void *k, void *b) { (void)k; (void)b; return 0; }
|
|
void *dm_pcmout_released(void *k) { (void)k; return NULL; }
|
|
void dm_pcmout_wait(void *k, int ms) { (void)k; (void)ms; }
|
|
void hle_bind_system_event(void *ev, int ac) { (void)ev; (void)ac; }
|
|
|
|
int main(void) {
|
|
uint8_t param[P_SIZE] = {0}, saved[P_SIZE];
|
|
const uint32_t words[] = {48000, 240, 132, 19, 138, 1, 16, 0, 0, 85, 1800, 0, 0, 0x38564552};
|
|
memcpy(param, words, sizeof words);
|
|
memcpy(saved, param, sizeof param);
|
|
// Expected results were obtained by executing Horizon audio build 3a75b4a6's sizing routine,
|
|
// independently of this implementation. A changed count must change the service requirement.
|
|
const struct { int revision, word, value; size_t expected; } cases[] = {
|
|
{8, 4, 138, 0x61d000}, {8, 4, 137, 0x61b000}, {8, 3, 18, 0x5f7000},
|
|
{8, 10, 1799, 0x61c000}, {8, 7, 2, 0x622000}, {9, 4, 138, 0x61e000},
|
|
{12, 4, 138, 0x5f7000}, {14, 4, 138, 0x621000}, {15, 4, 138, 0x6a4000}
|
|
};
|
|
for (size_t i = 0; i < sizeof cases / sizeof *cases; i++) {
|
|
memcpy(param, saved, sizeof param);
|
|
*(uint32_t *)(param + 0x34) = 0x30564552 + ((uint32_t)cases[i].revision << 24);
|
|
((uint32_t *)param)[cases[i].word] = cases[i].value;
|
|
size_t got = h_ar_WorkSize(param);
|
|
if (got != cases[i].expected) { fprintf(stderr, "service size case %zu: expected %#zx got %#zx\n", i, cases[i].expected, got); return 1; }
|
|
}
|
|
memcpy(param, saved, sizeof param);
|
|
*(uint32_t *)(param + 0x34) = 0x34564552;
|
|
*(uint32_t *)(param + 0x24) = *(uint32_t *)(param + 0x28) = 0;
|
|
assert(h_ar_WorkSize(param) == 0x308000); // fixed command buffer and performance format 1
|
|
// The longer SDK 8 public layout, padding bytes, and unknown future revisions.
|
|
memcpy(param, saved, sizeof param);
|
|
*(uint32_t *)(param + 0x34) = 0;
|
|
*(uint32_t *)(param + 0x38) = 0x38564552;
|
|
param[0x21] = param[0x22] = param[0x23] = 0xff;
|
|
assert(h_ar_WorkSize(param) == SERVICE_WORK);
|
|
memcpy(param, saved, sizeof param);
|
|
*(uint32_t *)(param + 0x34) = 0x40564552;
|
|
assert(h_ar_WorkSize(param) == SDK_WORK);
|
|
memcpy(param, saved, sizeof param);
|
|
assert(h_ar_WorkSize(param) == SERVICE_WORK);
|
|
void *work; assert(!posix_memalign(&work, WORK_PAGE, SDK_WORK));
|
|
memset(work, 0xa5, SDK_WORK);
|
|
void *handle[2], *event[8];
|
|
for (int route = 0; route < 3; route++) {
|
|
nested = route == 2;
|
|
int a = allocations, s = starts, c = closes;
|
|
Result r = route ? h_ar_Open(handle, event, param, work, SERVICE_WORK) : h_ar_Open0(handle, param, work, SERVICE_WORK);
|
|
assert(!r && allocations == a + 1 && starts == s + 1);
|
|
assert(handle[0] != work && map_value == backing);
|
|
assert(((uint8_t *)work)[0] == 0xa5 && ((uint8_t *)work)[SERVICE_WORK - 1] == 0xa5);
|
|
assert(!memcmp(param, saved, sizeof param));
|
|
h_ar_Close(handle[0], handle[1]);
|
|
assert(!backing && !map_key && allocations == frees && closes == c + 1 && joins == starts);
|
|
}
|
|
fail_open = 1;
|
|
int s = starts;
|
|
assert(h_ar_Open0(handle, param, work, SERVICE_WORK));
|
|
assert(!backing && !map_key && allocations == frees && starts == s);
|
|
fail_open = 0; fail_alloc = 1;
|
|
int o = opens;
|
|
assert(h_ar_Open0(handle, param, work, SERVICE_WORK));
|
|
assert(opens == o && starts == s && !backing);
|
|
fail_alloc = 0;
|
|
// Invalid caller storage is still rejected, without giving it a host allocation.
|
|
int a = allocations;
|
|
assert(h_ar_Open0(handle, param, NULL, WORK_PAGE));
|
|
assert(h_ar_Open0(handle, param, (uint8_t *)work + 1, WORK_PAGE));
|
|
assert(h_ar_Open0(handle, param, work, WORK_PAGE - 1));
|
|
assert(h_ar_Open0(handle, param, work, SERVICE_WORK - WORK_PAGE));
|
|
assert(allocations == a && starts == s);
|
|
// An existing full-size buffer and an explicitly CPU renderer retain caller ownership.
|
|
for (int cpu = 0; cpu < 2; cpu++) {
|
|
*(int32_t *)(param + P_DEVICE) = cpu;
|
|
if (cpu) assert(h_ar_WorkSize(param) == SDK_WORK);
|
|
assert(!h_ar_Open0(handle, param, work, SDK_WORK));
|
|
assert(handle[0] == work && allocations == a && !map_key);
|
|
h_ar_Close(handle[0], handle[1]);
|
|
}
|
|
free(work);
|
|
puts("audren work buffer: Horizon sizing fixtures, both opens, nested open, failures and close lifetime passed");
|
|
return 0;
|
|
}
|