Merge pull request #50 from kuraeow/dev

UltraTears 0.3.0
This commit is contained in:
kuraeow authored and GitHub committed 2026-11-02 22:54:42 +03:00
commit c0d30eb53e
47 files changed
+2936 -243

No files matched your search

+2 -2
View File
@@ -4,8 +4,8 @@
# UltraTears
**UltraTears** builds a native PC version of *The Legend of Zelda: Tears of the Kingdom* (version 1.4.3) for Windows and
Linux / Steam Deck from your own copy of the game. It is not an emulator: the game's code is translated to native code
**UltraTears** builds a native version of *The Legend of Zelda: Tears of the Kingdom* (version 1.4.3) for Windows, Linux
and the Steam Deck from your own copy of the game. It is not an emulator: the game's code is translated to native code
ahead of time, and its shaders and textures are prepared when the game is built, not while you play.
> [!IMPORTANT]
+2 -2
View File
@@ -4,8 +4,8 @@
# UltraTears
**UltraTears** собирает нативную ПК-версию *The Legend of Zelda: Tears of the Kingdom* (версия 1.4.3) для Windows и
Linux / Steam Deck из вашей собственной копии игры. Это не эмулятор: код игры заранее переводится в нативный, а шейдеры и
**UltraTears** собирает нативную версию *The Legend of Zelda: Tears of the Kingdom* (версия 1.4.3) для Windows, Linux
и Steam Deck из вашей собственной копии игры. Это не эмулятор: код игры заранее переводится в нативный, а шейдеры и
текстуры готовятся при сборке игры, а не во время игры.
> [!IMPORTANT]
+91 -12
View File
@@ -57,6 +57,7 @@ typedef struct { uintptr_t lo, hi, base; VkBuffer buf; VkDeviceMemory mem; const
uint8_t *cmap; uint32_t ch; uint8_t cfilled; uint64_t cstamp; } pool_ent;
static pool_ent *g_pools;
static int g_npools, g_capools;
static _Atomic uint64_t g_pool_gen; // the table changed (a pool added, removed or made unshadowed): vkb_pool_use's cache
static vkb_rw g_pool_mu; // recording threads look pools up for every draw (read); changes write
static uint64_t g_pool_bytes;
@@ -522,6 +523,7 @@ void mtl_pool_add(const void *mem, uint64_t size) {
g_pools[i] = (pool_ent){lo, hi, a, buf, dm, mem, b - a};
g_pools[i].cmap = cmap; g_pools[i].ch = ch;
g_npools++;
atomic_fetch_add_explicit(&g_pool_gen, 1, memory_order_release);
for (int j = i - 4; j <= i + 4; j++) // overlapping pools: one copy each would diverge, so neither is copied
if (j >= 0 && j < g_npools && j != i && g_pools[j].lo < hi && lo < g_pools[j].hi) {
static int said;
@@ -549,6 +551,7 @@ void mtl_pool_del(const void *mem) {
e = g_pools[i];
memmove(&g_pools[i], &g_pools[i + 1], (size_t)(g_npools - i - 1) * sizeof *g_pools);
g_npools--;
atomic_fetch_add_explicit(&g_pool_gen, 1, memory_order_release);
g_pool_bytes -= e.hi - e.lo;
break;
}
@@ -976,8 +979,20 @@ static pool_ent *pool_find_mut(uintptr_t addr, uint64_t len);
// threads do not wait for each other's uniform updates and stores (the write lock is for bringing a copy up to date).
static pthread_mutex_t g_wb_mu = PTHREAD_MUTEX_INITIALIZER;
static _Atomic uint64_t g_pool_use_n[3]; // the per-frame log line: write lock to update a copy / for a write / writes under the read lock
// Per recording thread, the pool of its last read that needed nothing more in this submission: a draw's buffers mostly
// lie in a few pools, and each look took the table's lock, searched it and wrote the pool's used_sub, one cache line
// written by every recording thread (vkb_pool_use was ~6 % of each recording thread at Lookout Landing, 2026-11-02).
static __thread struct { uint64_t lo, hi, sub, gen; } t_pool_hit;
static void pool_hit_note(const pool_ent *p, uint64_t addr, uint64_t len, uint64_t sub, uint64_t gen) {
if (addr >= p->lo && addr + len <= p->hi) { t_pool_hit.lo = p->lo; t_pool_hit.hi = p->hi; t_pool_hit.sub = sub; t_pool_hit.gen = gen; }
}
static void used_mark(pool_ent *p, uint64_t sub) { // read first: a store only when it changes
if (__atomic_load_n(&p->used_sub, __ATOMIC_RELAXED) != sub) __atomic_store_n(&p->used_sub, sub, __ATOMIC_RELAXED);
}
void vkb_pool_use(uint64_t addr, uint64_t len, int write) {
if (g_ok != 1 || !addr || !len) return;
uint64_t sub = g_fence + 1, gen = atomic_load_explicit(&g_pool_gen, memory_order_acquire);
if (!write && t_pool_hit.sub == sub && t_pool_hit.gen == gen && addr >= t_pool_hit.lo && addr + len <= t_pool_hit.hi) return;
if (vkb_pool_copies()) { // the pool is synced before the submission (pc_sync); a GPU write goes back after it
vkb_rw_read(&g_pool_mu, LK_POOLS);
// a range running on past its pool's end (GPU addresses, vkb_span_addr): the pool it starts in, up to its end
@@ -985,16 +1000,17 @@ void vkb_pool_use(uint64_t addr, uint64_t len, int write) {
if (!p) p = pool_find_mut((uintptr_t)addr, 1);
VkBuffer b = p ? p->buf : VK_NULL_HANDLE;
uint64_t n = p && addr + len > p->hi ? p->hi - addr : len;
if (p) __atomic_store_n(&p->used_sub, g_fence + 1, __ATOMIC_RELAXED);
if (p) used_mark(p, sub);
if (p && !write && !vkb_shadow_on()) pool_hit_note(p, addr, len, sub, gen);
vkb_rw_read_end(&g_pool_mu);
if (p && write) pc_pend_add(b, addr, n);
}
if (!vkb_shadow_on()) return;
uint64_t sub = g_fence + 1;
vkb_rw_read(&g_pool_mu, LK_POOLS); // the common cases: a pool already current in this submission, or not copied
pool_ent *p = pool_find_mut((uintptr_t)addr, len);
int done = !p || p->no_shadow || (p->sync_sub == sub && !write);
if (p) __atomic_store_n(&p->used_sub, sub, __ATOMIC_RELAXED);
if (p) used_mark(p, sub);
if (p && done && !write) pool_hit_note(p, addr, len, sub, gen);
if (!done && write && p->sync_sub == sub) {
pthread_mutex_lock(&g_wb_mu);
wb_add(p, addr, len);
@@ -1768,6 +1784,15 @@ static __thread uint64_t g_pass_serial; // passes begun on this thread (vkb_pa
uint64_t vkb_pass_serial(void) { return g_pass_serial; }
static void shadow_presync(void);
static void span_presync(void);
// A pass's targets (2026-11-02): an image whose layout changes gets its barrier with the move (vkb_image_use); the others
// none of their own, only one barrier for the whole pass from the target writes before (an earlier pass's) to this
// pass's tests, output and sampling. Before, every target of every pass had its own barrier from ALL_COMMANDS: ~1700
// full GPU drains a frame on the RTX 3060 (587 passes, 3600 barriers), costlier on AMD. TOTK_VK_PASS_BARRIER=1: as before.
static int pass_barrier_old(void) {
static int v = -1;
if (v < 0) { const char *e = getenv("TOTK_VK_PASS_BARRIER"); v = e && *e == '1'; }
return v;
}
static int pass_begin(VkExtent2D bare, uint32_t fb) {
if (g_in_pass) return 1;
shadow_presync();
@@ -1831,7 +1856,8 @@ static int pass_begin(VkExtent2D bare, uint32_t fb) {
}
int loop = (fb >> i) & 1;
VkImageLayout layout = loop ? fb_layout : VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
vkb_image_use(cb, im, layout, VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT | (loop ? VK_PIPELINE_STAGE_2_ALL_GRAPHICS_BIT : 0),
if (pass_barrier_old() || !vkb_layout_is(im, layout))
vkb_image_use(cb, im, layout, VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT | (loop ? VK_PIPELINE_STAGE_2_ALL_GRAPHICS_BIT : 0),
VK_ACCESS_2_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT | (loop ? VK_ACCESS_2_SHADER_SAMPLED_READ_BIT : 0));
color[i].imageView = vkb_view(im, v, 1, -1);
vkb_image_written(im);
@@ -1849,7 +1875,8 @@ static int pass_begin(VkExtent2D bare, uint32_t fb) {
const view_t *v = g_rt.has_dview ? &g_rt.dview : NULL;
int loop = (fb & VKB_FB_DEPTH) != 0;
VkImageLayout layout = loop ? fb_layout : VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
vkb_image_use(cb, dim, layout,
if (pass_barrier_old() || !vkb_layout_is(dim, layout))
vkb_image_use(cb, dim, layout,
VK_PIPELINE_STAGE_2_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_2_LATE_FRAGMENT_TESTS_BIT | (loop ? VK_PIPELINE_STAGE_2_ALL_GRAPHICS_BIT : 0),
VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | (loop ? VK_ACCESS_2_SHADER_SAMPLED_READ_BIT : 0));
VkRenderingAttachmentInfo a = {VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO, .imageView = vkb_view(dim, v, 1, -1),
@@ -1872,6 +1899,18 @@ static int pass_begin(VkExtent2D bare, uint32_t fb) {
VkRenderingInfo ri = {VK_STRUCTURE_TYPE_RENDERING_INFO, .renderArea = {{0, 0}, {w, h}}, .layerCount = layers,
.colorAttachmentCount = (uint32_t)ncolor, .pColorAttachments = color,
.pDepthAttachment = depth.imageView ? &depth : NULL, .pStencilAttachment = stencil.imageView ? &stencil : NULL};
if (any && !pass_barrier_old()) { // the target writes of earlier passes before this pass's (see pass_barrier_old)
const VkPipelineStageFlags2 att = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_2_EARLY_FRAGMENT_TESTS_BIT |
VK_PIPELINE_STAGE_2_LATE_FRAGMENT_TESTS_BIT;
VkMemoryBarrier2 mb = {VK_STRUCTURE_TYPE_MEMORY_BARRIER_2, .srcStageMask = att,
.srcAccessMask = VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT,
.dstStageMask = att | VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT,
.dstAccessMask = VK_ACCESS_2_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT |
VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT |
VK_ACCESS_2_SHADER_SAMPLED_READ_BIT};
VkDependencyInfo dep = {VK_STRUCTURE_TYPE_DEPENDENCY_INFO, .memoryBarrierCount = 1, .pMemoryBarriers = &mb};
vk.vkCmdPipelineBarrier2(cb, &dep);
}
vkb_checkpoint("begin pass", NULL, NULL);
pp_begin(cb, g_pass_extent);
vk.vkCmdBeginRendering(cb, &ri);
@@ -1943,6 +1982,7 @@ static void trace_rt(void) {
int vkb_pass_begin(VkExtent2D bare, uint32_t fb) { return pass_begin(bare, fb); }
uint32_t vkb_pass_fb(void) { return g_in_pass ? g_pass_fb : 0; }
vkb_cnt vkb_pass_end_n[PE_COUNT]; // passes ended, by reason (the per-frame log line)
static int rt_keep(void) { static int on = -1; if (on < 0) { const char *e = getenv("TOTK_VK_RT_KEEP"); on = !e || *e != '0'; } return on; }
static void pass_end_why(int why) { if (g_in_pass) vkb_cnt_add(vkb_pass_end_n[why], 1); pass_end(); }
void vkb_pass_end_r(int why) { pass_end_why(why); }
void vkb_pass_end(void) { pass_end_why(PE_OTHER); }
@@ -2483,7 +2523,14 @@ static int exec_ops(const uint8_t *p, int depth, int mode, int hidx, const mt_ch
mtl_sub_copy(s, d, n);
break;
}
case OP_SET_RT: pass_end_why(PE_SET_RT); if (mode == EX_ONE) split_if_due(); memcpy(&g_rt, a, sizeof g_rt); trace_rt(); break;
case OP_SET_RT:
// The same targets again (TOTK sets them before many draws): the pass goes on. Ending it stored and loaded
// every attachment and drained the GPU between draws into the same targets (totk-pc-recomp vk_exec.c set_rt
// compares the same way). TOTK_VK_RT_KEEP=0: a new pass every time, as before.
if (!(g_in_pass && rt_keep() && !memcmp(&g_rt, a, sizeof g_rt))) {
pass_end_why(PE_SET_RT); if (mode == EX_ONE) split_if_due(); memcpy(&g_rt, a, sizeof g_rt);
}
trace_rt(); break;
case OP_CLEAR_COLOR: if (g_render_off) vkb_cnt_add(g_cond_skipped, 1); else if (g_ok == 1) clear_color((const p_clear_color *)a); break;
case OP_CLEAR_DS: if (g_render_off) vkb_cnt_add(g_cond_skipped, 1); else if (g_ok == 1) clear_ds((const p_clear_ds *)a); break;
case OP_CLEAR_TEX: if (g_ok == 1) clear_tex((const p_clear_tex *)a); break;
@@ -2599,6 +2646,12 @@ uint64_t mtl_sub_commit(void) {
// chunk waits for all earlier work and moves its images there. The submission is then one vkQueueSubmit2 of: the slot's
// command buffer (GPU start time, captured uniforms, the move into the prologue's layouts), the prologue, and for each
// chunk its move and itself, then the end (copies back).
// Between chunks (2026-11-02): no wait for all earlier work. The game's own barriers are replayed inside the chunks
// (OP_BARRIER), so a chunk boundary needs only what the cut itself adds: after the prologue everything (its uploads and
// pool copies), into a pass cut inside its targets' writes (it begins again with LOAD), and after a chunk whose shaders
// stored the storage writes (that flag is its recording thread's: the next chunk may be recorded on another). Before,
// a full ALL_COMMANDS barrier drained the GPU before each of the 16-32 chunks of a frame (totk-pc-recomp splits with none,
// vk_exec.c). TOTK_VK_CHUNK_BARRIER=1: the full barrier between all chunks, as before.
// [temporary] Mirrors of written storage buffers are not made (one thread only); TOTK_VK_RECORD_THREADS=1 records as before.
// A pass longer than 5/4 of the chunk size (the Great Sky Island's G-buffer pass: ~2100 draws, one chunk of 12 of the
// submission's 14 ms while the other threads waited) is cut inside: the chunk ends its pass and the next one begins it
@@ -2618,6 +2671,7 @@ struct mt_chunk {
VkCommandBuffer cb;
vkb_trk trk;
uint64_t draws, ns; // the log line: its draws and dispatches, its recording time
int split, stores; // it begins inside a cut pass; its shaders left storage writes pending
};
static mt_chunk g_ch[MT_MAXCH];
static int g_nch;
@@ -2740,6 +2794,7 @@ static void mt_record(mt_chunk *c) {
if (chunk_end_at(c, i, p) || exec_ops(p, 0, EX_CHUNK, i, c)) break;
}
pass_end_why(PE_SUBMIT);
c->stores = vkb_stores_take();
vk.vkEndCommandBuffer(c->cb);
vkb_trk_set(NULL);
t_recording = 0;
@@ -2785,6 +2840,7 @@ static void chunk_open(int hidx, const uint8_t *p) { // a chunk starts at p, i
c->rt = g_rt;
c->render_off = g_render_off;
c->stride = vkb_copy_stride_get();
c->split = 0;
g_mt_draws = 0;
}
static void chunk_close(int hidx, const uint8_t *p) { // the open chunk ends before p: recording threads may take it
@@ -2803,6 +2859,7 @@ static int walk_op(const uint8_t *p, int hidx) { // before a command at depth
if (((h.op == OP_SET_RT && g_mt_draws >= (uint64_t)g_chunk_draws) || split) && g_nch < MT_MAXCH - 1) {
chunk_close(hidx, p);
chunk_open(hidx, p);
g_ch[g_nch].split = split && h.op != OP_SET_RT;
}
return 0;
}
@@ -2840,13 +2897,35 @@ static void walk_command(uint32_t op, const uint8_t *a) {
}
}
// Before a chunk: everything earlier is done, and its images move into the layouts it needs first.
static void glue(VkCommandBuffer cb, vkb_trk *t) {
// Before a chunk: its images move into the layouts it needs first, with what the cut needs (see above). full: everything
// earlier is done (after the prologue); attach: the targets' writes before a pass cut inside; stores: storage writes.
enum { GLUE_FULL = 1, GLUE_ATTACH = 2, GLUE_STORES = 4 };
static void glue(VkCommandBuffer cb, vkb_trk *t, int need) {
static int all = -1;
if (all < 0) { const char *e = getenv("TOTK_VK_CHUNK_BARRIER"); all = e && *e == '1'; }
if (all) need = GLUE_FULL;
VkMemoryBarrier2 mb = {VK_STRUCTURE_TYPE_MEMORY_BARRIER_2, .srcStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
.srcAccessMask = VK_ACCESS_2_MEMORY_WRITE_BIT, .dstStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
.dstAccessMask = VK_ACCESS_2_MEMORY_READ_BIT | VK_ACCESS_2_MEMORY_WRITE_BIT};
if (!(need & GLUE_FULL)) {
mb.srcStageMask = mb.dstStageMask = 0; mb.srcAccessMask = mb.dstAccessMask = 0;
if (need & GLUE_ATTACH) {
const VkPipelineStageFlags2 att = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_2_EARLY_FRAGMENT_TESTS_BIT |
VK_PIPELINE_STAGE_2_LATE_FRAGMENT_TESTS_BIT;
mb.srcStageMask |= att; mb.dstStageMask |= att;
mb.srcAccessMask |= VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
mb.dstAccessMask |= VK_ACCESS_2_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT |
VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
}
if (need & GLUE_STORES) { // as pass_begin's after storage writes
mb.srcStageMask |= VK_PIPELINE_STAGE_2_VERTEX_SHADER_BIT | VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT;
mb.srcAccessMask |= VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT;
mb.dstStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT;
mb.dstAccessMask |= VK_ACCESS_2_MEMORY_READ_BIT | VK_ACCESS_2_MEMORY_WRITE_BIT;
}
}
VkImageMemoryBarrier2 ib[64];
int ni = 0, first = 1;
int ni = 0, first = mb.srcStageMask != 0;
for (int k = 0; k <= t->n; k++) {
if (k < t->n) {
vkb_trk_e *e = &t->e[k];
@@ -2858,7 +2937,7 @@ static void glue(VkCommandBuffer cb, vkb_trk *t) {
.subresourceRange = {e->im->aspect, 0, VK_REMAINING_MIP_LEVELS, 0, VK_REMAINING_ARRAY_LAYERS}};
if (ni < 64) continue;
}
if (ni || first) {
if (ni || first) { // first: the memory barrier, with the first batch of moves or alone
VkDependencyInfo dep = {VK_STRUCTURE_TYPE_DEPENDENCY_INFO, .memoryBarrierCount = first ? 1u : 0u, .pMemoryBarriers = &mb,
.imageMemoryBarrierCount = (uint32_t)ni, .pImageMemoryBarriers = ib};
vk.vkCmdPipelineBarrier2(cb, &dep);
@@ -2922,13 +3001,13 @@ void vkb_mt_submit(const uint64_t *h, int n) { // after mtl_sub_begin and the
rec_ns += tt - tr;
// in order: the slot's command buffer moves images into the prologue's layouts, then each chunk's move and the chunk
vk.vkEndCommandBuffer(g_pro_cb);
glue(g_slots[g_cur].cb, &g_pro_trk);
glue(g_slots[g_cur].cb, &g_pro_trk, GLUE_FULL);
g_mt_nlist = 0;
g_mt_list[g_mt_nlist++] = g_pro_cb;
for (int k = 0; k < g_nch; k++) {
VkCommandBuffer m = wp_get(g_cur, VKB_MAXREC);
glue_images += (uint64_t)g_ch[k].trk.n;
glue(m, &g_ch[k].trk);
glue(m, &g_ch[k].trk, k == 0 ? GLUE_FULL : (g_ch[k].split ? GLUE_ATTACH : 0) | (g_ch[k - 1].stores ? GLUE_STORES : 0));
vk.vkEndCommandBuffer(m);
g_mt_list[g_mt_nlist++] = m;
g_mt_list[g_mt_nlist++] = g_ch[k].cb;
+30 -17
View File
@@ -323,6 +323,7 @@ NVN(nvnMemoryPoolBuilderSetFlags, void, NVNmemoryPoolBuilder *b, int f) { ((pool
// TOTK: logging switches checked on per-call paths (command memory, recording, submits) are read once.
static int cmdlog_env(void) { static int on = -1; if (on < 0) on = getenv("TOTK_CMD_LOG") != NULL; return on; }
static int poollog_env(void) { static int on = -1; if (on < 0) on = getenv("TOTK_POOL_LOG") != NULL; return on; }
static int chainlog_env(void) { static int on = -1; if (on < 0) on = getenv("TOTK_CHAIN_LOG") != NULL; return on; } // read once: EndRecording and Submit run hundreds of times a frame
NVN(nvnMemoryPoolInitialize, NVNboolean, NVNmemoryPool *p, const NVNmemoryPoolBuilder *b) {
const pool_builder_t *s = (const pool_builder_t *)b; pool_t *o = (pool_t *)p;
memset(o, 0, sizeof *o);
@@ -925,11 +926,13 @@ static pthread_mutex_t g_copy_mu = PTHREAD_MUTEX_INITIALIZER;
typedef struct { uint64_t magic; _Atomic uint64_t used_at; uint64_t made_at; uint32_t body_bytes, last_size; } copy_prefix;
static _Atomic uint64_t g_copy_bytes; /* live recording-copy bytes (logged with the copy count) */
static copy_prefix *copy_of(NVNcommandHandle h) { copy_prefix *c = (copy_prefix *)(uintptr_t)h - 1; return c; }
static uint8_t *flatten_recording(const uint8_t *p) {
size_t n = sizeof(copy_prefix), cap = 4096; uint8_t *out = malloc(cap);
int bulk_ok = 1; uint32_t last_size = 0;
if (!out) die("command copy allocation");
for (;;) {
// Two passes (as totk-pc-recomp's flatten_recording): the first walks the ops and measures, the second copies each run
// between jumps with one memcpy into one allocation of the exact size. Copying op by op into a buffer that doubled
// and was trimmed at the end cost the game thread a realloc chain per recording.
static uint8_t *flatten_recording(const uint8_t *start) {
size_t n = sizeof(copy_prefix);
int bulk_ok = 1; uint32_t last_size = 0, end_size = 0;
for (const uint8_t *p = start;;) {
op_hdr h; memcpy(&h, p, sizeof h);
if (h.size < 8 || (h.size & 7)) die("corrupt recording at %p (op %u size %u)", (const void *)p, h.op, h.size);
if (h.op == OP_JUMP) { memcpy(&p, p + 8, 8); continue; }
@@ -937,18 +940,28 @@ static uint8_t *flatten_recording(const uint8_t *p) {
if (n == sizeof(copy_prefix)) { if (h.op != OP_BEGIN || h.size != 8) bulk_ok = 0; }
else if (h.op == OP_BEGIN) bulk_ok = 0;
else if (h.op != OP_END) last_size = h.size;
if (n + h.size > cap) { while (n + h.size > cap) cap *= 2; out = realloc(out, cap); if (!out) die("command copy allocation"); }
memcpy(out + n, p, h.size); n += h.size;
if (h.op == OP_END) {
uint8_t *trimmed = realloc(out, n); if (trimmed) out = trimmed;
copy_prefix *c = (copy_prefix *)out; c->magic = COPY_MAGIC; c->used_at = 0; c->made_at = g_submits;
size_t body = n >= sizeof(copy_prefix) + 16 ? n - sizeof(copy_prefix) - 16 : 0;
c->body_bytes = bulk_ok && h.size == 8 && body <= UINT32_MAX ? (uint32_t)body : 0;
c->last_size = c->body_bytes ? last_size : 0;
g_copy_bytes += malloc_size(out); return out + sizeof(copy_prefix);
}
n += h.size;
if (h.op == OP_END) { end_size = h.size; break; }
p += h.size;
}
uint8_t *out = malloc(n);
if (!out) die("command copy allocation");
size_t at = sizeof(copy_prefix);
for (const uint8_t *p = start, *run = start;;) { // run: the start of the ops not yet copied
op_hdr h; memcpy(&h, p, sizeof h);
if (h.op == OP_JUMP) {
memcpy(out + at, run, (size_t)(p - run)); at += (size_t)(p - run);
memcpy(&p, p + 8, 8); run = p; continue;
}
p += h.size;
if (h.op == OP_END) { memcpy(out + at, run, (size_t)(p - run)); at += (size_t)(p - run); break; }
}
if (at != n) die("command copy: %zu bytes copied of %zu measured", at, n);
copy_prefix *c = (copy_prefix *)out; c->magic = COPY_MAGIC; c->used_at = 0; c->made_at = g_submits;
size_t body = n >= sizeof(copy_prefix) + 16 ? n - sizeof(copy_prefix) - 16 : 0;
c->body_bytes = bulk_ok && end_size == 8 && body <= UINT32_MAX ? (uint32_t)body : 0;
c->last_size = c->body_bytes ? last_size : 0;
g_copy_bytes += malloc_size(out); return out + sizeof(copy_prefix);
}
/* Track handle use for diagnostics; lifetime depends on control-memory reuse, never elapsed frames. */
static void copy_touch(NVNcommandHandle h, uint64_t now) {
@@ -1057,7 +1070,7 @@ NVN(nvnCommandBufferEndRecording, NVNcommandHandle, NVNcommandBuffer *c) {
pthread_mutex_unlock(&g_copy_mu);
if (cmdlog_env()) LOG("[cmd] EndRecording cb=%p recorded=%p copy=%p ctl=%p", (void *)c, (void *)o->seg, (void *)copy, o->ctl);
if (totk_life_on() && totk_life_cb_take(c)) { totk_life_log("end cb=%p handle=%p", (void *)c, (void *)copy); totk_life_handle_add((uint64_t)(uintptr_t)copy); }
if (getenv("TOTK_CHAIN_LOG")) { static int n; if (n++ < 200) guest_chain("EndRecording"); }
if (chainlog_env()) { static int n; if (n++ < 200) guest_chain("EndRecording"); }
return (NVNcommandHandle)copy;
}
NVN(nvnCommandBufferIsRecording, NVNboolean, const NVNcommandBuffer *c) { return ((const cmd_t *)c)->recording != 0; }
@@ -2288,7 +2301,7 @@ static void submit_exec(int n, const uint64_t *h, void *ucap) {
LOG("[shape] calls %u: draws in calls %u (outside %u), largest %u, calls with 100+: %u; per call:%s", ncall, inc, tot - inc, cmx, cbig, line);
}
} }
if (getenv("TOTK_CHAIN_LOG")) { static int k; if (k++ < 20) guest_chain("Submit"); }
if (chainlog_env()) { static int k; if (k++ < 20) guest_chain("Submit"); }
uint64_t t0 = now_ns(), c0 = clock_gettime_nsec_np(CLOCK_THREAD_CPUTIME_ID), i0, y0, s0 = thr_sys_ns(); thr_counts(&i0, &y0);
/* TOTK diagnostic (TOTK_WATCH_ADDR): submissions in progress, and their start / end times, to line up guest writes with replay */
atomic_fetch_add(&dm_submit_busy, 1);
+1
View File
@@ -65,6 +65,7 @@ typedef struct vkb_image {
VkImageUsageFlags usage;
int compressed; // block-compressed format: no attachment / storage use
int no_block_views; // a 3D block-compressed image, made without block-texel views (vkb_tex.c make_image)
int cube; // made cube compatible (vkb_tex.c cube_compatible): only such an image gets cube views
int gpu_written; // rendered / cleared / copied to by the GPU: never overwritten from game memory ([temporary])
uint64_t upload_stamp; // nvn_dirty generation of the last upload from game memory (0 = never)
uint64_t dirty_checked; // the submission in which the game memory was last checked for CPU writes
+41 -16
View File
@@ -299,7 +299,8 @@ void vkb_state(uint32_t op, const uint8_t *a) {
// ---------------- texture and sampler pools ----------------
typedef struct { tex_t t; view_t v; uint8_t has_view, valid; } treg_t;
typedef struct { samp_par p; VkSampler s; uint8_t valid, custom; } sreg_t; // custom: s has its own border colour
// custom: s has its own border colour; sp: its point-sampled form (sampler_of), made on first use
typedef struct { samp_par p; VkSampler s, sp; uint8_t valid, custom, custom_p; } sreg_t;
static treg_t *g_treg;
static sreg_t *g_sreg;
static int g_ntreg, g_nsreg;
@@ -332,16 +333,20 @@ void mtl_reg_samp(int id, const samp_par *p) {
vkb_rw_write(&g_reg_mu, LK_TEXREG);
g_sreg = grow(g_sreg, &g_nsreg, id, sizeof *g_sreg);
sreg_t *r = &g_sreg[id];
VkSampler old = VK_NULL_HANDLE;
VkSampler old = VK_NULL_HANDLE, old_p = VK_NULL_HANDLE;
int old_custom = 0;
if (r->s && (!p || memcmp(&r->p, p, sizeof *p))) { old = r->s; old_custom = r->custom; r->s = VK_NULL_HANDLE; r->custom = 0; }
if ((r->s || r->sp) && (!p || memcmp(&r->p, p, sizeof *p))) {
old = r->s; old_p = r->sp; old_custom = r->custom + r->custom_p;
r->s = r->sp = VK_NULL_HANDLE; r->custom = r->custom_p = 0;
}
r->valid = p != NULL;
if (p) r->p = *p;
vkb_rw_write_end(&g_reg_mu);
if (old) {
if (old || old_p) {
mtl_finish(); // [temporary] waits for the GPU
vk.vkDestroySampler(vkc.device, old, NULL);
if (old_custom) atomic_fetch_sub(&g_custom_borders, 1);
if (old) vk.vkDestroySampler(vkc.device, old, NULL);
if (old_p) vk.vkDestroySampler(vkc.device, old_p, NULL);
if (old_custom) atomic_fetch_sub(&g_custom_borders, old_custom);
}
}
void mtl_set_tex_pool_n(int n) { (void)n; }
@@ -355,11 +360,15 @@ static VkSamplerAddressMode wrap(uint8_t w) {
default: return VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; // CLAMP (GL legacy), CLAMP_TO_EDGE
}
}
static VkSampler sampler(int id) {
// point: the sampler with nearest filtering and no anisotropy. A depth texture read without a compare lands on one
// texel on the Switch whatever the sampler's filter, and TOTK binds linear samplers to depth it reads as data:
// blending neighbouring depths put reconstructed positions in mid-air at object edges (MacNativeGames 6283599,
// totk-pc-recomp 6029719, both MIT). TOTK_VK_DEPTH_POINT=0: the sampler as the game sets it.
static VkSampler sampler_of(int id, int point) {
static VkSampler fallback; // made before threads record (vkb_draw_mt_init)
vkb_rw_read(&g_reg_mu, LK_TEXREG);
int valid = id >= 0 && id < g_nsreg && g_sreg[id].valid;
VkSampler have = valid ? g_sreg[id].s : VK_NULL_HANDLE;
VkSampler have = valid ? (point ? g_sreg[id].sp : g_sreg[id].s) : VK_NULL_HANDLE;
samp_par par;
if (valid) par = g_sreg[id].p;
vkb_rw_read_end(&g_reg_mu);
@@ -372,6 +381,12 @@ static VkSampler sampler(int id) {
}
return fallback;
}
samp_par reg = par; // as registered: what a slot made meanwhile is compared with
if (point) {
par.magf = 0;
par.minf = par.minf == 1 ? 0 : par.minf >= 2 ? 2 : par.minf;
par.aniso = 1.0f;
}
const samp_par *p = &par;
int mip = p->minf >= 2;
VkSamplerReductionModeCreateInfo red = {VK_STRUCTURE_TYPE_SAMPLER_REDUCTION_MODE_CREATE_INFO,
@@ -412,13 +427,15 @@ static VkSampler sampler(int id) {
VkSampler s;
if (!vk_check(vk.vkCreateSampler(vkc.device, &ci, NULL, &s), "sampler")) {
if (custom) atomic_fetch_sub(&g_custom_borders, 1);
return sampler(-1);
return sampler_of(-1, 0);
}
vkb_rw_write(&g_reg_mu, LK_TEXREG);
int kept = id < g_nsreg && g_sreg[id].valid && !memcmp(&g_sreg[id].p, &par, sizeof par) && !g_sreg[id].s;
if (kept) { g_sreg[id].s = s; g_sreg[id].custom = (uint8_t)custom; }
else if (id < g_nsreg && g_sreg[id].valid && !memcmp(&g_sreg[id].p, &par, sizeof par) && g_sreg[id].s) { // another thread made one meanwhile
VkSampler other = g_sreg[id].s;
int same = id < g_nsreg && g_sreg[id].valid && !memcmp(&g_sreg[id].p, &reg, sizeof reg);
VkSampler *slot = same ? (point ? &g_sreg[id].sp : &g_sreg[id].s) : NULL;
int kept = slot && !*slot;
if (kept) { *slot = s; if (point) g_sreg[id].custom_p = (uint8_t)custom; else g_sreg[id].custom = (uint8_t)custom; }
else if (slot) { // another thread made one meanwhile
VkSampler other = *slot;
vkb_rw_write_end(&g_reg_mu);
vk.vkDestroySampler(vkc.device, s, NULL);
if (custom) atomic_fetch_sub(&g_custom_borders, 1);
@@ -736,8 +753,13 @@ static int stand_in_first(const char *key, int kind) {
}
// A buffer range for a guest address: the imported pool (offset aligned as the descriptor type needs) or a copy now.
// copy_ok: a uniform buffer (a misaligned one is bound from a copy; at most 64 KiB, the uniform range limit), else a
// storage buffer. The kind is said, not taken from the alignment: where the uniform and storage alignments are the same
// (AMD, Mesa: 4 and 4, against 64 and 16 on NVIDIA) storage buffers were cut to 64 KiB too, and TOTK's Depths terrain
// and cave walls, whose patch tables lie 0x64000-0xf0000 into a range of ~200 MB, read zeros past it: their vertices
// collapsed, the ground and walls were missing and Link was unlit (L20 on the Steam Deck and on a Linux RX 570).
static VkDescriptorBufferInfo guest_range(uint64_t addr, uint64_t size, VkDeviceSize align, int copy_ok) {
if (size > 65536 && align == vkc.props.limits.minUniformBufferOffsetAlignment) size = 65536;
if (size > 65536 && copy_ok) size = 65536;
VkBuffer buf;
VkDeviceSize off;
if (addr && size && vkb_ucap_range(addr, size, &buf, &off) && !(off % align)) // contents as of the submit call (stores dropped it)
@@ -1199,7 +1221,10 @@ static void write_stage(writes_t *W, VkDescriptorSet set, const vkb_shader *sh,
else if (!im) vkb_audit(A_TEX_NO_IMAGE, sh->key);
else if (!view) vkb_audit(A_TEX_NOT_READY, sh->key);
else if (im->not_uploadable) vkb_audit(A_TEX_NOT_UPLOADED, sh->key);
VkDescriptorImageInfo info = {sampler((int)(h >> 20) & 0xFFF), view ? view : dummy_view(vt),
static int depth_point = -1;
if (depth_point < 0) { const char *e = getenv("TOTK_VK_DEPTH_POINT"); depth_point = !e || *e != '0'; }
int point = depth_point && view && (im->aspect & VK_IMAGE_ASPECT_DEPTH_BIT) && !(sh->tex[i].type & 0x400); // sampler_of
VkDescriptorImageInfo info = {sampler_of((int)(h >> 20) & 0xFFF, point), view ? view : dummy_view(vt),
view ? vkb_layout(im) : VK_IMAGE_LAYOUT_GENERAL};
if (view && stage == 5 && (sh->tex[i].type & 0x400)) { // a compare sampler in a dispatch: the depth twin
VkImageView tv = twin_view(vkb_cmd(), im, vt);
@@ -1899,7 +1924,7 @@ void vkb_draw_mt_init(void) {
if (!g_dummy) dummy_init();
static const VkImageViewType types[] = {VK_IMAGE_VIEW_TYPE_2D, VK_IMAGE_VIEW_TYPE_2D_ARRAY, VK_IMAGE_VIEW_TYPE_CUBE, VK_IMAGE_VIEW_TYPE_CUBE_ARRAY};
for (unsigned t = 0; t < sizeof types / sizeof *types; t++) dummy_view(types[t]); // (1D ones: as before, on first use)
sampler(-1);
sampler_of(-1, 0);
}
void vkb_draw(const uint8_t *a, int indexed) {
+40 -8
View File
@@ -196,17 +196,37 @@ uint32_t vkb_shadow_alloc(VkDeviceSize size, VkBuffer *buf, VkDeviceSize *off) {
void vkb_shadow_free(uint32_t h) { piece_free(&g_dev, h); }
uint64_t vkb_shadow_bytes(void) { return g_dev.bytes; }
// Host-visible memory for buf (a pool's own buffer), bound to it; *map: where the CPU sees it. Cached memory where the
// device has it (the copies back of GPU writes read it on the CPU). 0: no memory.
// Host-visible memory for buf (a pool's own buffer), bound to it; *map: where the CPU sees it. 0: no memory.
// Which memory (TOTK_POOL_MEM, an A/B for the Steam Deck, where the GPU reads every vertex, index and uniform from these
// copies): cached (default) - cached memory where the device has it, the copies back of GPU writes read it on the CPU;
// on an AMD APU it is snooped system memory, which the GPU reads more slowly. uncached - host-visible memory without
// CPU caching (write-combined), read faster by the GPU, slowly by the CPU's copies back. device - device-local and
// host-visible (the APU's carve-out, a discrete GPU's BAR). A type that runs out falls back to the cached one.
static int pool_mem_mode(void) {
static int m = -1;
if (m < 0) {
const char *e = getenv("TOTK_POOL_MEM");
m = e && !strcmp(e, "uncached") ? 1 : e && !strcmp(e, "device") ? 2 : 0;
VLOG("pool copies in %s memory (TOTK_POOL_MEM)", m == 1 ? "uncached host" : m == 2 ? "device-local host-visible" : "cached host");
}
return m;
}
static uint32_t pool_mem_type(uint32_t bits, int mode) {
const VkMemoryPropertyFlags want = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
for (int pass = 0; pass < 2; pass++) // the mode's kind first, then any host-visible coherent memory
for (uint32_t i = 0; i < vkc.memory.memoryTypeCount; i++) {
VkMemoryPropertyFlags f = vkc.memory.memoryTypes[i].propertyFlags;
if (!(bits & (1u << i)) || (f & want) != want) continue;
int cached = !!(f & VK_MEMORY_PROPERTY_HOST_CACHED_BIT), dev = !!(f & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
if (pass || (mode == 0 ? cached : mode == 1 ? !cached && !dev : dev)) return i;
}
return UINT32_MAX;
}
uint32_t vkb_poolmem_alloc(VkBuffer buf, uint8_t **map) {
VkMemoryRequirements req;
vk.vkGetBufferMemoryRequirements(vkc.device, buf, &req);
uint32_t type = UINT32_MAX, want = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
for (int pass = 0; pass < 2 && type == UINT32_MAX; pass++) // cached first
for (uint32_t i = 0; i < vkc.memory.memoryTypeCount && type == UINT32_MAX; i++) {
VkMemoryPropertyFlags f = vkc.memory.memoryTypes[i].propertyFlags;
if ((req.memoryTypeBits & (1u << i)) && (f & want) == want && (pass || (f & VK_MEMORY_PROPERTY_HOST_CACHED_BIT))) type = i;
}
int mode = pool_mem_mode();
uint32_t type = pool_mem_type(req.memoryTypeBits, mode);
if (type == UINT32_MAX) { VLOG("pool copies: no host-visible memory type for a pool"); return 0; }
VkDeviceSize size = (req.size + HOST_ALIGN - 1) & ~(VkDeviceSize)(HOST_ALIGN - 1);
int own = req.alignment > HOST_ALIGN || size > BLOCK; // a block of its own (its start meets any alignment)
@@ -216,10 +236,22 @@ uint32_t vkb_poolmem_alloc(VkBuffer buf, uint8_t **map) {
int bi = own ? -1 : fit(&g_host, size, type, &at);
if (bi < 0) {
bi = host_block_new(own ? size : BLOCK, type);
if (bi < 0 && mode) { // that memory ran out: the cached kind from here on
static int said;
uint32_t t0 = pool_mem_type(req.memoryTypeBits, 0);
if (!said++) VLOG("pool copies: TOTK_POOL_MEM memory ran out at %llu MiB, the rest in cached memory", (unsigned long long)(g_host.bytes >> 20));
if (t0 != UINT32_MAX && t0 != type) {
type = t0;
bi = own ? -1 : fit(&g_host, size, type, &at);
if (bi < 0) bi = host_block_new(own ? size : BLOCK, type);
else goto found;
}
}
if (bi < 0) { pthread_mutex_unlock(&g_mu); return 0; }
at = 0;
take_first(&g_host.blocks[bi], size);
}
found:;
uint32_t h = piece_new(&g_host, bi, at, size);
block_t *b = &g_host.blocks[bi];
VkResult r = vk.vkBindBufferMemory(vkc.device, buf, b->mem, at);
+28 -7
View File
@@ -226,6 +226,17 @@ static void entry_drop(entry *e) { // g_mu held: out of the indexes, destroyed
static uint32_t memory_type(uint32_t bits, VkMemoryPropertyFlags want); // below
static _Atomic uint64_t g_fb_copies; // copies made (vkb_tex_feedback_copies)
uint64_t vkb_tex_feedback_copies(void) { return g_fb_copies; }
// Cube compatible only where a cube view can come: the game's cube textures, and square arrays of 6 layers or more it
// renders to (the faces drawn as layers, sampled as a cube). AMD's drivers lay a cube compatible image out otherwise, and
// every square array made so - TOTK's ground textures, 1024x1024 with 121 layers - showed blocks that moved with the mip
// level on AMD (totk-pc-recomp 4d7fa27, MIT; Savage Wind Recomp issue #59). TOTK_VK_CUBE_ALL=1: every square array,
// as before.
static int cube_compatible(int target, uint32_t w, uint32_t h, uint32_t layers, VkImageUsageFlags usage) {
static int all = -1;
if (all < 0) { const char *e = getenv("TOTK_VK_CUBE_ALL"); all = e && *e == '1'; }
if (target == NVN_TEXTURE_TARGET_CUBEMAP || target == NVN_TEXTURE_TARGET_CUBEMAP_ARRAY) return 1;
return layers >= 6 && w == h && (all || (usage & (VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT)));
}
static int twin_make(entry *src) {
entry *t = calloc(1, sizeof *t);
if (!t) return 0;
@@ -234,7 +245,7 @@ static int twin_make(entry *src) {
s->d.target == NVN_TEXTURE_TARGET_1D_ARRAY) ? VK_IMAGE_TYPE_1D : VK_IMAGE_TYPE_2D;
int ms = s->d.target == NVN_TEXTURE_TARGET_2D_MULTISAMPLE || s->d.target == NVN_TEXTURE_TARGET_2D_MULTISAMPLE_ARRAY;
VkImageCreateFlags flags = VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT | VK_IMAGE_CREATE_EXTENDED_USAGE_BIT; // as create()
if (type == VK_IMAGE_TYPE_2D && !ms && s->layers >= 6 && s->width == s->height) flags |= VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT;
if (type == VK_IMAGE_TYPE_2D && !ms && s->cube) flags |= VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT; // as its source
if (type == VK_IMAGE_TYPE_3D) flags |= VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT;
VkImageCreateInfo ci = {VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, .flags = flags, .imageType = type, .format = s->format,
.extent = {s->width, s->height, s->depth}, .mipLevels = s->levels, .arrayLayers = s->layers,
@@ -448,7 +459,8 @@ static int create(vkb_image *im, const tex_t *t) {
(vkb_tex_block_target), which needs neither. */
int bc3d = compressed && type == VK_IMAGE_TYPE_3D;
if (compressed && !bc3d) flags |= VK_IMAGE_CREATE_BLOCK_TEXEL_VIEW_COMPATIBLE_BIT; // read a block as RGBA16UI / RG32UI (TOTK's GPU BC encoder)
if (type == VK_IMAGE_TYPE_2D && !ms && layers >= 6 && w == h) flags |= VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT;
int cube = type == VK_IMAGE_TYPE_2D && !ms && cube_compatible(d->target, w, h, layers, usage);
if (cube) flags |= VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT;
if (type == VK_IMAGE_TYPE_3D && !bc3d) flags |= VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT;
VkImageCreateInfo ci = {VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, .flags = flags, .imageType = type, .format = f,
.extent = {w, h, depth}, .mipLevels = levels, .arrayLayers = layers,
@@ -477,6 +489,7 @@ static int create(vkb_image *im, const tex_t *t) {
im->width = w; im->height = h; im->depth = depth; im->layers = layers; im->levels = levels; im->astc = astc;
im->storage = t->mem;
im->usage = usage;
im->cube = cube;
im->compressed = compressed;
im->no_block_views = bc3d;
return 1;
@@ -683,7 +696,9 @@ vkb_image *vkb_image_of(const tex_t *t) {
g_all = e;
g_images_made++;
}
if (t->mem && getenv("TOTK_VK_SIZE_REFRESH")) refresh_mark(e); // [experiment, off] picks transient aliases (see above)
static int size_refresh = -1; // read once: this runs for every image a draw binds
if (size_refresh < 0) size_refresh = getenv("TOTK_VK_SIZE_REFRESH") != NULL;
if (t->mem && size_refresh) refresh_mark(e); // [experiment, off] picks transient aliases (see above)
e->refs++;
if (s->serial == t->serial) g_tomb--; else s->serial = t->serial;
s->e = e;
@@ -835,8 +850,8 @@ VkImageView vkb_view(vkb_image *im, const view_t *v, int attachment, int view_ty
int target = v && v->set_target ? v->target : im->d.target;
VkImageViewType type = im->depth > 1 ? VK_IMAGE_VIEW_TYPE_3D
: target == NVN_TEXTURE_TARGET_1D ? VK_IMAGE_VIEW_TYPE_1D : target == NVN_TEXTURE_TARGET_1D_ARRAY ? VK_IMAGE_VIEW_TYPE_1D_ARRAY
: target == NVN_TEXTURE_TARGET_CUBEMAP && !attachment ? VK_IMAGE_VIEW_TYPE_CUBE
: target == NVN_TEXTURE_TARGET_CUBEMAP_ARRAY && !attachment ? VK_IMAGE_VIEW_TYPE_CUBE_ARRAY
: target == NVN_TEXTURE_TARGET_CUBEMAP && !attachment && im->cube ? VK_IMAGE_VIEW_TYPE_CUBE
: target == NVN_TEXTURE_TARGET_CUBEMAP_ARRAY && !attachment && im->cube ? VK_IMAGE_VIEW_TYPE_CUBE_ARRAY
: nlay > 1 || target == NVN_TEXTURE_TARGET_2D_ARRAY || target == NVN_TEXTURE_TARGET_2D_MULTISAMPLE_ARRAY ? VK_IMAGE_VIEW_TYPE_2D_ARRAY
: VK_IMAGE_VIEW_TYPE_2D;
if (type == VK_IMAGE_VIEW_TYPE_3D && attachment) { type = VK_IMAGE_VIEW_TYPE_2D_ARRAY; nlay = im->depth; }
@@ -844,6 +859,12 @@ VkImageView vkb_view(vkb_image *im, const view_t *v, int attachment, int view_ty
type = (VkImageViewType)view_type;
if (((type == VK_IMAGE_VIEW_TYPE_CUBE || type == VK_IMAGE_VIEW_TYPE_CUBE_ARRAY) && nlay < 6) || type == VK_IMAGE_VIEW_TYPE_3D)
vkb_audit(A_TEX_VIEW_ADJUSTED, NULL);
if ((type == VK_IMAGE_VIEW_TYPE_CUBE || type == VK_IMAGE_VIEW_TYPE_CUBE_ARRAY) && !im->cube) { // not made cube compatible
static int said;
if (!said++) VLOG("texture %llu (target %d, %ux%u, %u layers) sampled as a cube but not made cube compatible: a 2D array view",
(unsigned long long)im->serial, im->d.target, im->width, im->height, im->layers);
type = nlay > 1 ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D;
}
if ((type == VK_IMAGE_VIEW_TYPE_CUBE && nlay < 6) || (type == VK_IMAGE_VIEW_TYPE_CUBE_ARRAY && nlay < 6))
type = nlay > 1 ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D;
if (type == VK_IMAGE_VIEW_TYPE_CUBE) nlay = 6;
@@ -1159,7 +1180,7 @@ static int to_bc7(vkb_image *im, int srgb, int fmt) {
if (!(fp.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT)) return 0;
VkImageUsageFlags usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
VkImageCreateInfo ci = {VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO,
.flags = im->layers >= 6 && im->width == im->height ? VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT : 0,
.flags = cube_compatible(im->d.target, im->width, im->height, im->layers, usage) ? VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT : 0,
.imageType = VK_IMAGE_TYPE_2D, .format = f, .extent = {im->width, im->height, 1}, .mipLevels = im->levels,
.arrayLayers = im->layers, .samples = VK_SAMPLE_COUNT_1_BIT, .tiling = VK_IMAGE_TILING_OPTIMAL, .usage = usage,
.sharingMode = VK_SHARING_MODE_EXCLUSIVE, .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED};
@@ -1174,7 +1195,7 @@ static int to_bc7(vkb_image *im, int srgb, int fmt) {
if (ai.memoryTypeIndex == UINT32_MAX || vk.vkAllocateMemory(vkc.device, &ai, NULL, &mem) != VK_SUCCESS) { vk.vkDestroyImage(vkc.device, img, NULL); return 0; }
vk.vkBindImageMemory(vkc.device, img, mem, 0);
vkb_destroy_later(VK_NULL_HANDLE, im->img, im->mem); // the RGBA8 image no command has used
im->img = img; im->mem = mem; im->format = f; im->usage = usage; im->compressed = 1; im->bc7 = fmt == VKB_BC_1 ? 2 : 1;
im->img = img; im->mem = mem; im->format = f; im->usage = usage; im->cube = (int)(ci.flags != 0); im->compressed = 1; im->bc7 = fmt == VKB_BC_1 ? 2 : 1;
im->layout = VK_IMAGE_LAYOUT_UNDEFINED;
return 1;
}
+10 -2
View File
@@ -169,10 +169,18 @@ void mtl_sub_snapshot_ubos_cap(const uint64_t *h, int n, void *capv) { // repl
for (int i = 0; i < c->n; i++) {
at = ((at + 255) & ~(VkDeviceSize)255) + (c->r[i].lo & 255);
g_u[i] = (urng){c->r[i].lo, c->r[i].hi, at, UINT64_MAX};
for (int j = 0; j < g_npend && g_u[i].drop_at; j++) // a GPU write the capture predates: read memory
if (g_pend[j].fence > c->done && g_pend[j].lo < c->r[i].hi && c->r[i].lo < g_pend[j].hi) { g_u[i].drop_at = 0; g_drops++; }
at += c->r[i].hi - c->r[i].lo;
}
// A GPU write the capture predates: the ranges it overlaps read memory. The ranges are sorted and disjoint, so each
// write finds its first one by binary search (each range against every pending write was ~27 % of the translation
// thread at Lookout Landing, 2026-11-02: thousands of ranges times thousands of writes a submission).
for (int j = 0; j < g_npend; j++) {
if (g_pend[j].fence <= c->done) continue;
int lo = 0, hi = c->n; // the first range ending after the write's start
while (lo < hi) { int m = (lo + hi) / 2; if (c->r[m].hi <= g_pend[j].lo) lo = m + 1; else hi = m; }
for (int i = lo; i < c->n && c->r[i].lo < g_pend[j].hi; i++)
if (g_u[i].drop_at) { g_u[i].drop_at = 0; g_drops++; }
}
int slot = vkb_slot();
if (!arena_get(slot, at)) return;
VkBuffer rb;
+130
View File
@@ -0,0 +1,130 @@
// Steam Deck: the gyro and accelerometer of the Deck's own controller, read from its HID device. In game mode Steam
// Input hands the game a virtual gamepad, which has buttons and sticks but no motion sensors, so motion aiming did
// nothing on the Deck; turning Steam Input off for the game would lose the player's Steam layout. The controller's HID
// device (Valve 28de:1205) stays readable beside Steam (Steam's udev rules give the logged-in user access to it): this
// thread reads its state reports and feeds their motion to motion_fusion.c, while buttons and sticks keep coming
// through Steam Input. Started only when the gamepad SDL opened has no motion sensors of its own (window_sdl.c).
// Report layout and scales as SDL's Steam Deck driver reads them (SDL3 src/joystick/hidapi/SDL_hidapi_steamdeck.c,
// zlib; no code taken): a 64-byte report, bytes 0-1 the version 0x0001, byte 2 the type (0x09: the controller's state),
// the state from byte 4: packet number (4), buttons (8), the two pads (8), then accelerometer x, y, z and gyro x, y, z
// as int16, little-endian; 32768 = 2 G and 2000 deg/s. Steam keeps the controller's motion off unless the player's
// Steam Input layout uses it (the reports carried zeros, 2026-11-02): as SDL's driver does, the controller is told to
// send raw gyro and accelerometer data (feature report 0x87 "set settings", register SETTING_IMU_MODE), and told again
// every few seconds in case Steam sets it back. Only that register is written; Steam's layout stays as it is.
// TOTK_DECK_GYRO=0: off.
#include <stdint.h>
int deck_imu_start(void);
#ifndef __linux__
int deck_imu_start(void) { return 0; }
#else
#include <errno.h>
#include <fcntl.h>
#include <linux/hidraw.h>
#include <sys/ioctl.h>
#include <math.h>
#include <poll.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "host.h"
enum { REPORT = 64, STATE = 0x09, SET_SETTINGS = 0x87, SETTING_IMU_MODE = 48, IMU_RAW_ACCEL = 0x08, IMU_RAW_GYRO = 0x10 };
static int g_writable;
static int imu_on(int fd) { // 1: the controller took the setting
unsigned char f[REPORT + 1] = {0}; // f[0]: the report number, 0 (the Deck's reports are not numbered)
f[1] = SET_SETTINGS;
f[2] = 3; // one setting: register, value low, value high
f[3] = SETTING_IMU_MODE;
f[4] = IMU_RAW_ACCEL | IMU_RAW_GYRO;
return ioctl(fd, HIDIOCSFEATURE(sizeof f), f) >= 0;
}
static int g_fd = -1, g_started;
static int le16(const unsigned char *p) { return (int16_t)(p[0] | p[1] << 8); }
static int is_state(const unsigned char *b, ssize_t n) { return n >= 36 && b[0] == 0x01 && b[1] == 0x00 && b[2] == STATE; }
// The hidraw device of the Deck's controller interface: the one of 28de:1205 that sends state reports.
static int open_deck(void) {
int denied = 0;
for (int i = 0; i < 64; i++) {
char path[96], line[256];
snprintf(path, sizeof path, "/sys/class/hidraw/hidraw%d/device/uevent", i);
FILE *f = fopen(path, "r");
if (!f) continue;
int match = 0;
while (fgets(line, sizeof line, f)) if (!strncmp(line, "HID_ID=", 7) && strstr(line, "000028DE:00001205")) match = 1;
fclose(f);
if (!match) continue;
snprintf(path, sizeof path, "/dev/hidraw%d", i);
int fd = open(path, O_RDWR | O_NONBLOCK | O_CLOEXEC);
g_writable = fd >= 0;
if (fd < 0) fd = open(path, O_RDONLY | O_NONBLOCK | O_CLOEXEC);
if (fd < 0) { if (errno == EACCES) denied = 1; continue; }
unsigned char b[REPORT];
for (int tries = 0; tries < 10; tries++) { // up to ~0.2 s for a report
struct pollfd pf = {fd, POLLIN, 0};
if (poll(&pf, 1, 20) <= 0) continue;
ssize_t n = read(fd, b, sizeof b);
if (is_state(b, n)) {
fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) & ~O_NONBLOCK);
int on = g_writable && imu_on(fd);
fprintf(stderr, "[totk] Steam Deck controller motion read from %s (beside Steam Input); raw motion %s\n", path,
on ? "turned on" : g_writable ? "could not be turned on" : "not writable: as Steam has it");
return fd;
}
}
close(fd);
}
if (denied) fprintf(stderr, "[totk] the Steam Deck controller's HID device is not readable (permissions): no gyro aiming\n");
return -1;
}
static void *deck_main(void *arg) {
(void)arg;
unsigned char b[REPORT];
uint64_t quiet = 0, said_quiet = 0, last_on = runtime_now_ns(), said_live = 0;
for (;;) {
uint64_t now = runtime_now_ns();
if (g_writable && now - last_on > 3000000000ull) { imu_on(g_fd); last_on = now; } // Steam may set it back
ssize_t n = read(g_fd, b, sizeof b);
if (n < 0) {
if (errno == EINTR) continue;
fprintf(stderr, "[totk] Steam Deck controller motion: read failed (%s); stopped\n", strerror(errno));
return NULL;
}
if (!is_state(b, n)) continue;
const unsigned char *s = b + 4;
int ax = le16(s + 20), ay = le16(s + 22), az = le16(s + 24), gx = le16(s + 26), gy = le16(s + 28), gz = le16(s + 30);
if (!(ax | ay | az | gx | gy | gz)) { // Steam has the IMU off: nothing to feed
if (++quiet == 1000 && !said_quiet++) fprintf(stderr, "[totk] Steam Deck controller: its motion sensors report nothing\n");
continue;
}
quiet = 0;
if (!said_live++) fprintf(stderr, "[totk] Steam Deck controller: motion data arriving\n");
// SDL's gamepad frame from the report (as its Steam Deck driver), then the Switch frame (as window_sdl.c's
// pad_sensor): the gyro reads in the report's own axes, the acceleration negated (gravity, not the push).
const double k = 2000.0 * M_PI / 180.0 / 32768.0, a = 2.0 / 32768.0;
double acc[3] = {-ax * a, -ay * a, -az * a}, gyro[3] = {gx * k, gy * k, gz * k};
runtime_motion_feed(acc, gyro, runtime_now_ns(), 0);
}
}
// 1: the Deck's motion is fed (or already was).
int deck_imu_start(void) {
if (g_started) return g_fd >= 0;
g_started = 1;
const char *e = getenv("TOTK_DECK_GYRO");
if (e && *e == '0') return 0;
if ((g_fd = open_deck()) < 0) return 0;
pthread_t t;
if (pthread_create(&t, NULL, deck_main, NULL) != 0) { close(g_fd); g_fd = -1; return 0; }
pthread_detach(t);
return 1;
}
#endif
+33 -2
View File
@@ -379,8 +379,9 @@ static pthread_once_t once = PTHREAD_ONCE_INIT;
static _Atomic int save_writers, save_dirty;
static _Atomic uint64_t save_changed_ns;
static void save_touched(void) { atomic_store(&save_changed_ns, runtime_now_ns()); atomic_store(&save_dirty, 1); }
static _Atomic int sync_pending; /* commits still being written to the disk (sync_later) */
int runtime_fs_save_busy(void) {
if (atomic_load(&save_writers) > 0) return 1;
if (atomic_load(&save_writers) > 0 || atomic_load(&sync_pending) > 0) return 1;
return atomic_load(&save_dirty) && runtime_now_ns() - atomic_load(&save_changed_ns) < 3000000000ull;
}
static void make_dir(const char *p) { if (make_dir_p(p) && errno != EEXIST) runtime_fail("create profile storage directory"); }
@@ -543,6 +544,36 @@ static void unmount(const char *name) {
pthread_mutex_unlock(&lock);
if (!romfs_owns(name) && strcmp(name, mount_name)) runtime_fail("unmount of unknown fs mount");
}
/* Commit writes the files to the disk on a thread of its own: fsync of every open file on the game's thread stopped it
for ~100 ms at each save (totk-pc-recomp measured 95-135 ms; its 6fca957 does it in the background, MIT). Each file
goes as a duplicate of its descriptor, so the game may close or reuse it meanwhile; until they are written the save
counts as busy (runtime_fs_save_busy), so leaving the game waits for them. */
static pthread_mutex_t sync_mu = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t sync_cv = PTHREAD_COND_INITIALIZER;
static int sync_q[64], sync_n, sync_started;
static void *sync_main(void *p) {
(void)p;
for (;;) {
pthread_mutex_lock(&sync_mu);
while (!sync_n) pthread_cond_wait(&sync_cv, &sync_mu);
int fd = sync_q[--sync_n];
pthread_mutex_unlock(&sync_mu);
fsync(fd); close(fd);
atomic_fetch_sub(&sync_pending, 1);
}
return NULL;
}
static void sync_later(int fd) {
int d = dup(fd);
if (d < 0) { fsync(fd); return; }
pthread_mutex_lock(&sync_mu);
if (!sync_started) { pthread_t t; sync_started = !pthread_create(&t, NULL, sync_main, NULL); if (sync_started) pthread_detach(t); }
if (!sync_started || sync_n == (int)(sizeof sync_q / sizeof *sync_q)) { pthread_mutex_unlock(&sync_mu); fsync(d); close(d); return; }
sync_q[sync_n++] = d;
atomic_fetch_add(&sync_pending, 1);
pthread_cond_signal(&sync_cv);
pthread_mutex_unlock(&sync_mu);
}
static unsigned commit(const char *name) {
pthread_mutex_lock(&lock);
int found = 0, save = 0;
@@ -553,7 +584,7 @@ static unsigned commit(const char *name) {
pthread_mutex_lock(&lock);
int fd = files[i].fd;
pthread_mutex_unlock(&lock);
if (fd > 0) fsync(fd);
if (fd > 0) sync_later(fd);
}
if (save) atomic_store(&save_dirty, 0);
return 0;
+2 -1
View File
@@ -55,7 +55,8 @@ uint64_t guest_call_args(uint64_t fn, int n, const uint64_t *args) {
}
// Every `svc #imm` of the recompiled code lands here (cpu.h).
void rt_svc(Cpu *c, uint32_t imm, uint64_t pc) { runtime_svc(imm, c->x, pc); }
void runtime_core_checkpoint(void); // threads.c: give the core up to a better waiting thread
void rt_svc(Cpu *c, uint32_t imm, uint64_t pc) { runtime_core_checkpoint(); runtime_svc(imm, c->x, pc); }
_Noreturn void runtime_fail(const char *message) {
fprintf(stderr, "[totk] FATAL: %s\n", message);
+1 -1
View File
@@ -104,7 +104,7 @@ static uint32_t six_start(const uint32_t *h) {
six_axis_check(h); static _Atomic int once; note_once(&once, "the game started the six-axis sensor", *h);
runtime_motion_start(); return 0;
}
static uint32_t six_stop(const uint32_t *h) { six_axis_check(h); return 0; }
static uint32_t six_stop(const uint32_t *h) { six_axis_check(h); runtime_motion_stop(); return 0; }
/* Newest first, like the hid service's ring. No motion (no controller with sensors, or muted): one state of a controller
lying still, not marked connected. */
static uint32_t six_states(int *out, SixAxisState *s, int max, const uint32_t *h) {
+2 -1
View File
@@ -65,9 +65,10 @@ typedef struct { uint64_t sampling, delta_ns; float acc[3], gyro[3], angle[3], d
void runtime_rumble_set(unsigned side, float amp_low, float freq_low, float amp_high, float freq_high); // side 0 left, 1 right
int runtime_motion_read(RuntimeMotionState *out, int max); // newest first; 0 = no motion now
void runtime_motion_start(void); // the game (re)started its sensor: angle and direction start over
void runtime_motion_stop(void); // the game stopped its sensor (the zero-rate bias may follow the controller again)
// One sample in the Switch six-axis frame (x right, y toward the top edge, z out of the face): acceleration in G with
// gravity (face up reads z = -1), rotation in rad/s.
void runtime_motion_feed(const double acc_g[3], const double gyro_rad_s[3], uint64_t now_ns);
void runtime_motion_feed(const double acc_g[3], const double gyro_rad_s[3], uint64_t now_ns, uint64_t sample_ns); // sample_ns: the sensor's time, 0 = none
int runtime_motion_axes(const char *spec); // TOTK_GYRO_AXES ("-x,y,-z"): 1 set, 0 unset, -1 malformed
void runtime_motion_deadzone_default(double deg_s);
void runtime_motion_live(int on); // a controller with motion attached (1) / detached (0)
+66 -24
View File
@@ -27,6 +27,7 @@ enum { MOTION_RING = 64 };
static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
static RuntimeMotionState ring[MOTION_RING];
static uint64_t produced, last_ns, logged, log_at, reads_logged;
static uint64_t first; /* produced at the last StartSixAxisSensor: older samples are not read */
static double R[3][3]; /* controller -> reference; row i = reference axis i in controller coordinates */
static double angle[3];
static int restart = 1, live;
@@ -34,15 +35,27 @@ static int from[3] = {0, 1, 2};
static double sign[3] = {1, 1, 1}, det = 1;
static double deadzone = -1; /* rad/s; -1 until read from TOTK_GYRO_DEADZONE */
static double deadzone_default; /* deg/s, when TOTK_GYRO_DEADZONE is unset */
/* [port] Zero-rate calibration (L12, GitHub #39, 2026-10-31): with a Pro Controller the aim crept to one side and then the
other with the controller at rest - its gyro's bias, which nothing took off (yaw has no reference). While the
controller is still (rotation under STILL_RAD and acceleration within 5 % of 1 G for STILL_S) the bias follows the
gyro's reading, and it is subtracted from every sample, as the Switch, Steam Input and SDL games do. Moving, the bias
stays as it was. TOTK_GYRO_CALIBRATE=0 turns it off. */
#define STILL_RAD (4.0 * M_PI / 180)
#define STILL_S 0.4
static double bias[3], still_s;
static int calibrate = -1, bias_set;
/* [port] Zero-rate calibration (L12, GitHub #39): with a Pro Controller the aim crept to one side and then the other with
the controller at rest - its gyro's bias, which nothing took off (yaw has no reference). The bias is subtracted from
every sample, as the Switch, Steam Input and SDL games do. Still is told by steadiness, not by size: the player's Pro
Controller read a constant 7.9 deg/s at rest and 0.76 G of gravity (2026-11-02), so "rotation under 4 deg/s and
acceleration within 5 % of 1 G" (the rule before) never held and no bias was ever taken. Now the gyro's running mean
and spread over about half a second (STILL_TAU) are kept; when the spread stays under STILL_SPREAD, the acceleration's
size stays within 3 % of its own mean and the mean is a plausible bias (under BIAS_MAX), for STILL_S, the controller
is still: the first time the bias becomes that mean, later it follows it over BIAS_TAU, and only while the mean stays
within FOLLOW_MAX of the bias, so a slow steady turn (aiming) is not taken for drift and the aim does not creep back
the other way when it stops. (Freezing it from StartSixAxisSensor to StopSixAxisSensor did not do: TOTK starts the
sensor at boot and never stops it, so no bias was ever taken, 2026-11-02.) The acceleration at rest also gives the
controller's own reading of gravity (rest_g: that Pro Controller reads ~0.8 G), which the tilt correction below
compares the acceleration with instead of 1 G; at 0.8 G it never corrected. TOTK_GYRO_CALIBRATE=0 turns it off. */
#define STILL_TAU 0.5
#define STILL_SPREAD (3.0 * M_PI / 180)
#define BIAS_MAX (20.0 * M_PI / 180)
#define FOLLOW_MAX (3.0 * M_PI / 180)
#define STILL_S 0.6
#define BIAS_TAU 4.0
static double bias[3], still_s, mean[3], var[3], amean, spread_now, rest_g = 1;
static int calibrate = -1, bias_set, stats_set;
/* TOTK_GYRO_AXES: reference axis i = sign * controller axis from[i]. 0 if unset, -1 if malformed. */
int runtime_motion_axes(const char *spec) {
@@ -82,7 +95,7 @@ static void align_to(const double acc[3]) {
}
static void integrate(const double gyro[3], const double acc[3], double dt) {
double w[3] = {gyro[0], gyro[1], gyro[2]}, n = norm3(acc);
if (fabs(n - 1) < 0.15) { /* not being accelerated: turn the estimated down toward the measured one */
if (fabs(n / rest_g - 1) < 0.15) { /* not being accelerated: turn the estimated down toward the measured one */
const double kp = 0.5;
double measured[3] = {acc[0] / n, acc[1] / n, acc[2] / n}, estimated[3] = {-R[2][0], -R[2][1], -R[2][2]}, e[3];
cross3(measured, estimated, e);
@@ -103,7 +116,8 @@ static void integrate(const double gyro[3], const double acc[3], double dt) {
}
memcpy(R, out, sizeof R); orthonormalize();
}
void runtime_motion_feed(const double raw_acc[3], const double raw_gyro[3], uint64_t now) {
void runtime_motion_feed(const double raw_acc[3], const double raw_gyro[3], uint64_t now, uint64_t sample_ns) {
static uint64_t last_sample;
double acc[3], gyro[3];
pthread_mutex_lock(&lock);
for (int i = 0; i < 3; i++) { acc[i] = sign[i] * raw_acc[from[i]]; gyro[i] = det * sign[i] * raw_gyro[from[i]]; }
@@ -113,19 +127,36 @@ void runtime_motion_feed(const double raw_acc[3], const double raw_gyro[3], uint
deadzone = v * M_PI / 180;
if (e) fprintf(stderr, "[gyro] dead zone %.2f deg/s (TOTK_GYRO_DEADZONE)\n", v);
}
double dt = last_ns && now > last_ns ? (now - last_ns) / 1e9 : 0.004;
/* dt from the sensor's own clock (SDL's sensor_timestamp): SDL hands several samples of one report over at once, so
the time they were handled gave the first one the whole gap and the others none; else from now */
double dt = sample_ns && last_sample && sample_ns > last_sample ? (sample_ns - last_sample) / 1e9
: last_ns && now > last_ns ? (now - last_ns) / 1e9 : 0.004;
if (dt > 0.05) dt = 0.004; /* a gap (sensors were off, the app was in the background): no integration across it */
last_sample = sample_ns;
if (calibrate < 0) { const char *e = getenv("TOTK_GYRO_CALIBRATE"); calibrate = !(e && !strcmp(e, "0")); }
if (calibrate) {
double g[3] = {gyro[0] - bias[0], gyro[1] - bias[1], gyro[2] - bias[2]}, a = norm3(acc);
if (norm3(g) < STILL_RAD && fabs(a - 1) < 0.05) still_s += dt; else still_s = 0;
if (still_s >= STILL_S) { /* still: the bias follows the reading (about a second; at once the first time) */
double k = bias_set ? dt / 1.0 : 1;
for (int i = 0; i < 3; i++) bias[i] += (gyro[i] - bias[i]) * k;
double a = norm3(acc), k = dt / STILL_TAU, spread = 0;
if (!stats_set) { memcpy(mean, gyro, sizeof mean); memset(var, 0, sizeof var); amean = a; stats_set = 1; }
for (int i = 0; i < 3; i++) {
double d = gyro[i] - mean[i];
mean[i] += d * k;
var[i] += (d * d - var[i]) * k;
spread += var[i];
}
amean += (a - amean) * k;
spread_now = sqrt(spread);
double off[3] = {mean[0] - bias[0], mean[1] - bias[1], mean[2] - bias[2]};
if (spread_now < STILL_SPREAD && fabs(a - amean) < 0.03 * amean && norm3(mean) < BIAS_MAX &&
(!bias_set || norm3(off) < FOLLOW_MAX)) still_s += dt;
else still_s = 0;
if (still_s >= STILL_S) { /* still: the bias is the mean (at once the first time, then slowly) */
double f = bias_set ? dt / BIAS_TAU : 1;
for (int i = 0; i < 3; i++) bias[i] += (mean[i] - bias[i]) * f;
if (amean > 0.5 && amean < 1.5) rest_g = amean;
if (!bias_set) {
bias_set = 1;
fprintf(stderr, "[gyro] at rest: zero-rate bias %.2f %.2f %.2f deg/s taken off (TOTK_GYRO_CALIBRATE=0: off)\n",
bias[0] * 180 / M_PI, bias[1] * 180 / M_PI, bias[2] * 180 / M_PI);
fprintf(stderr, "[gyro] at rest: zero-rate bias %.2f %.2f %.2f deg/s taken off, gravity read as %.2f G "
"(TOTK_GYRO_CALIBRATE=0: off)\n", bias[0] * 180 / M_PI, bias[1] * 180 / M_PI, bias[2] * 180 / M_PI, amean);
}
}
for (int i = 0; i < 3; i++) gyro[i] -= bias[i];
@@ -146,9 +177,11 @@ void runtime_motion_feed(const double raw_acc[3], const double raw_gyro[3], uint
if (!log_at) log_at = now;
if (now - log_at >= 30000000000ull) {
uint64_t values, reads; runtime_hid_feedback_stats(&values, &reads);
fprintf(stderr, "[gyro] last 30 s: %llu samples (%.0f a second), the game read the states %llu times; now acc (%+.2f %+.2f %+.2f) G, gyro (%+.3f %+.3f %+.3f) rev/s\n",
fprintf(stderr, "[gyro] last 30 s: %llu samples (%.0f a second), the game read the states %llu times; now acc (%+.2f %+.2f %+.2f) G, gyro (%+.3f %+.3f %+.3f) rev/s; bias (%.2f %.2f %.2f) deg/s\n",
(unsigned long long)(produced - logged), (produced - logged) / ((now - log_at) / 1e9), (unsigned long long)(reads - reads_logged),
s->acc[0], s->acc[1], s->acc[2], s->gyro[0], s->gyro[1], s->gyro[2]);
s->acc[0], s->acc[1], s->acc[2], s->gyro[0], s->gyro[1], s->gyro[2], bias[0] * 180 / M_PI, bias[1] * 180 / M_PI, bias[2] * 180 / M_PI);
fprintf(stderr, "[gyro] at rest: gravity read as %.2f G; now: spread %.2f deg/s, |a| %.2f G (still %.1f s)\n",
rest_g, spread_now * 180 / M_PI, norm3(acc), still_s);
logged = produced; reads_logged = reads; log_at = now;
}
pthread_mutex_unlock(&lock);
@@ -157,15 +190,24 @@ int runtime_motion_read(RuntimeMotionState *out, int max) {
if (max <= 0) return 0;
int n = 0;
pthread_mutex_lock(&lock);
if (live && runtime_now_ns() - last_ns < 200000000ull) { /* no sample for 200 ms: no motion */
uint64_t have = produced < MOTION_RING ? produced : MOTION_RING;
if (live && produced > first && runtime_now_ns() - last_ns < 200000000ull) { /* no sample for 200 ms: no motion */
uint64_t have = produced - first < MOTION_RING ? produced - first : MOTION_RING;
n = (int)(have < (uint64_t)max ? have : (uint64_t)max);
for (int i = 0; i < n; i++) out[i] = ring[(produced - 1 - (uint64_t)i) % MOTION_RING];
}
pthread_mutex_unlock(&lock);
return n;
}
void runtime_motion_start(void) { pthread_mutex_lock(&lock); restart = 1; pthread_mutex_unlock(&lock); }
/* StartSixAxisSensor (the bow drawn): the samples from before it are dropped, so a read in the same frame returned the
previous session's states as live and motion aim was dead, then lagged ~60 ms (totk-pc-recomp 7d89fde, MIT). Reads
return nothing until the next sample, which starts a fresh frame and angle; sampling numbers go on. */
void runtime_motion_start(void) {
pthread_mutex_lock(&lock);
restart = 1; first = produced; last_ns = 0; memset(angle, 0, sizeof angle);
pthread_mutex_unlock(&lock);
}
/* StopSixAxisSensor: nothing to do (the calibration follows the controller at rest whatever the game's sensor does). */
void runtime_motion_stop(void) {}
/* The dead zone in deg/s when TOTK_GYRO_DEADZONE is unset: the HID reader sets 1.2, a GameController controller 0. */
void runtime_motion_deadzone_default(double deg) { pthread_mutex_lock(&lock); deadzone_default = deg; deadzone = -1; pthread_mutex_unlock(&lock); }
void runtime_motion_live(int on) { pthread_mutex_lock(&lock); live = on; if (on) restart = 1; pthread_mutex_unlock(&lock); }
+167 -69
View File
@@ -26,6 +26,9 @@
// Events 0x45 CreateEvent 0x11 SignalEvent 0x12 ClearEvent 0x17 ResetSignal 0x18 WaitSynchronization 0x16 CloseHandle
// Semantics follow the public Horizon kernel behavior (KConditionVariable / KAddressArbiter / KSynchronization of Atmosphère mesosphère).
// All waits hang on one global lock G, with one condition variable per thread; correctness first, performance to be measured later.
#if !defined(_WIN32) && !defined(_GNU_SOURCE)
#define _GNU_SOURCE // glibc's adaptive mutex (G, below)
#endif
#include "host.h"
#include "ipc.h"
#include <errno.h>
@@ -38,6 +41,47 @@
#define LOG(...) do {fprintf(stderr,"[totk-thread] " __VA_ARGS__);fputc('\n',stderr);}while(0)
#define die(...) do {LOG(__VA_ARGS__);runtime_fail("thread runtime failure");}while(0)
#define now_ns runtime_now_ns
// The global lock G and the per-thread condition variables. Windows: a slim reader/writer lock and condition variables
// (user-space fast paths, a spin before sleeping), not winpthreads' mutex (no spin) and condition variable (built on
// semaphores and locks of its own: several kernel transitions per wait or signal), which ~50 guest threads convoyed on
// (totk-pc-recomp uses the same pair). Linux: glibc's adaptive mutex, which spins briefly before the futex.
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
typedef CONDITION_VARIABLE gcond_t;
static SRWLOCK G = SRWLOCK_INIT;
static inline void g_lock(void) { AcquireSRWLockExclusive(&G); }
static inline void g_unlock(void) { ReleaseSRWLockExclusive(&G); }
static inline void gc_init(gcond_t *c) { InitializeConditionVariable(c); }
static inline void gc_destroy(gcond_t *c) { (void)c; }
static inline void gc_signal(gcond_t *c) { WakeConditionVariable(c); }
static inline int gc_wait(gcond_t *c) { SleepConditionVariableSRW(c, &G, INFINITE, 0); return 0; }
static inline int gc_timedwait(gcond_t *c, const struct timespec *dl) { // dl: CLOCK_REALTIME, as pthread_cond_timedwait
struct timespec now;
clock_gettime(CLOCK_REALTIME, &now);
int64_t left = (int64_t)(dl->tv_sec - now.tv_sec) * 1000000000 + (dl->tv_nsec - now.tv_nsec);
if (left <= 0) return ETIMEDOUT;
SleepConditionVariableSRW(c, &G, (DWORD)((left + 999999) / 1000000), 0); // whole ms, rounded up
clock_gettime(CLOCK_REALTIME, &now);
left = (int64_t)(dl->tv_sec - now.tv_sec) * 1000000000 + (dl->tv_nsec - now.tv_nsec);
return left <= 0 ? ETIMEDOUT : 0; // early (woken, or spurious): the caller looks again
}
#else
typedef pthread_cond_t gcond_t;
#ifdef PTHREAD_ADAPTIVE_MUTEX_INITIALIZER_NP // glibc with _GNU_SOURCE
static pthread_mutex_t G = PTHREAD_ADAPTIVE_MUTEX_INITIALIZER_NP;
#else
static pthread_mutex_t G = PTHREAD_MUTEX_INITIALIZER;
#endif
static inline void g_lock(void) { pthread_mutex_lock(&G); }
static inline void g_unlock(void) { pthread_mutex_unlock(&G); }
static inline void gc_init(gcond_t *c) { pthread_cond_init(c, NULL); }
static inline void gc_destroy(gcond_t *c) { pthread_cond_destroy(c); }
static inline void gc_signal(gcond_t *c) { pthread_cond_signal(c); }
static inline int gc_wait(gcond_t *c) { return pthread_cond_wait(c, &G); }
static inline int gc_timedwait(gcond_t *c, const struct timespec *dl) { return pthread_cond_timedwait(c, &G, dl); }
#endif
#define guest_ns runtime_now_ns
static inline int dm_detclock_enabled(void) {return 0;}
@@ -56,7 +100,7 @@ typedef struct thr {
uint64_t entry, arg, stack_top; uint64_t host_context[2]; int prio, core; uint64_t mask;
uint64_t born_ns, exit_ns; uint32_t born_tick, exit_tick; // 建立 / 退出的时刻与当时的游戏拍(现场导出用:作业工人是不是在卡住前退的) | Time of creation / exit and the game tick at that time (for the state dump: did the job worker exit before the stall)
int started, exited, cancel;
pthread_t pt; pthread_cond_t cv;
pthread_t pt; gcond_t cv;
// 当前等待:kind 0=无 1=锁 2=条件 3=地址 4=同步对象
// Current wait: kind 0=none 1=lock 2=condition 3=address 4=synchronization object
int wkind, woke; uint64_t waddr, wkey; uint32_t wtag; int64_t wres;
@@ -76,7 +120,6 @@ typedef struct thr {
} thr_t;
static thr_t *g_all;
static pthread_mutex_t G = PTHREAD_MUTEX_INITIALIZER;
#define MAX_H 4096
static struct { int type; void *obj; } g_h[MAX_H];
/* [port] the calling guest thread: a thread-local instead of a pthread key (winpthreads hands out key 0, which
@@ -123,7 +166,7 @@ static thr_t *pick(int kind, uint64_t a) {
}
return b;
}
static void wake(thr_t *t, int64_t res) { wait_unlink(t); t->wkind = 0; t->woke = 1; t->wres = res; pthread_cond_signal(&t->cv); }
static void wake(thr_t *t, int64_t res) { wait_unlink(t); t->wkind = 0; t->woke = 1; t->wres = res; gc_signal(&t->cv); }
// 挂起直到被唤醒或超时(ns<0 无限、0 立即超时);返回 wres 或 R_TIMEOUT
// Suspend until woken or timed out (ns<0 infinite, 0 immediate timeout); returns wres or R_TIMEOUT
static int64_t block_inner(thr_t *t, int kind, int64_t ns);
@@ -144,7 +187,7 @@ static void core_grant(int c) { // holding G: give free core c to the best wai
for (thr_t **p = &g_core_wait; *p; p = &(*p)->cnext)
if (core_allowed(*p, c) && (!b || (*p)->prio < b->prio || ((*p)->prio == b->prio && (*p)->cseq < b->cseq))) { b = *p; bp = p; }
if (!b) return;
*bp = b->cnext; b->cnext = NULL; b->cur_core = c; b->core_since = now_ns(); g_core_owner[c] = b; pthread_cond_signal(&b->cv);
*bp = b->cnext; b->cnext = NULL; b->cur_core = c; b->core_since = now_ns(); g_core_owner[c] = b; gc_signal(&b->cv);
}
static void core_put(thr_t *t) { // holding G
if (t->cur_core < 0) return;
@@ -158,24 +201,67 @@ static void core_take(thr_t *t) { // holding G; waits until a core in t's mask
if (c >= 0) { t->cur_core = c; t->core_since = now_ns(); g_core_owner[c] = t; return; }
t->cseq = ++g_wseq; t->cnext = g_core_wait; g_core_wait = t;
for (int i = 0; i < NCORE; i++) if (core_allowed(t, i) && g_core_owner[i] && g_core_owner[i]->prio > t->prio) g_core_owner[i]->preempt = 1;
while (t->cur_core < 0) pthread_cond_wait(&t->cv, &G);
while (t->cur_core < 0) gc_wait(&t->cv);
}
static thr_t *me_or_null(void) { return t_self; }
static int core_better_waiter(const thr_t *t, int c) { // holding G: a waiter for core c better than t
for (const thr_t *w = g_core_wait; w; w = w->cnext) if (core_allowed(w, c) && w->prio < t->prio) return 1;
return 0;
}
void runtime_thread_note_hle(unsigned i) { thr_t *t = me_or_null(); if (t) t->last_hle = (int)i + 1; } // hle_stack.c, while tracing
uint32_t runtime_thread_current_handle(void) { thr_t *t = me_or_null(); return t ? t->handle : 0; }
// Host services that wait for other threads or the GPU give the core up around the wait (no host locks held).
int runtime_core_away(void) {
thr_t *t = me_or_null(); if (!t || t->cur_core < 0) return 0;
pthread_mutex_lock(&G); core_put(t); pthread_mutex_unlock(&G); return 1;
g_lock(); core_put(t); g_unlock(); return 1;
}
void runtime_core_back(int *away) {
thr_t *t = me_or_null(); if (!*away || !t) return;
pthread_mutex_lock(&G); core_take(t); pthread_mutex_unlock(&G);
g_lock(); core_take(t); g_unlock();
}
// Preemption point at SVC entry: a higher-priority thread waits for this core.
// Called at every SVC (guest.c rt_svc) and indirect call (runtime/dispatch.c rt_dispatch). The request may be stale:
// the waiter got another core meanwhile, and giving this one up would hand it to a worse thread.
void runtime_core_checkpoint(void) {
thr_t *t = me_or_null(); if (!t || !t->preempt) return;
pthread_mutex_lock(&G); t->preempt = 0; core_put(t); core_take(t); pthread_mutex_unlock(&G);
g_lock();
if (t->cur_core >= 0 && core_better_waiter(t, t->cur_core)) { core_put(t); core_take(t); } // core_put clears the request
else t->preempt = 0;
g_unlock();
}
extern __thread volatile int *rt_preempt_flag; // runtime/dispatch.c
extern void (*rt_preempt_fn)(void);
static void preempt_point_set(thr_t *t) {
rt_preempt_fn = runtime_core_checkpoint;
rt_preempt_flag = t && cores_enabled() && !getenv("TOTK_NO_PREEMPT") ? &t->preempt : NULL; // TOTK_NO_PREEMPT=1: as before
}
// A pass through a spin wait: a guest loop that only another thread can end (tools/lifter/spin_waits_v143.txt; the
// lifter calls this on its back-edge). The loop holds a virtual core while it spins, and the thread that would end it
// may be waiting for that core: once one wait has lasted 50 us (TOTK_SPIN_HANDOFF_US) and any thread waits for a core,
// the core goes over for a yield and comes back. Passes of one loop at most 20 us apart are one wait. As totk-pc-recomp
// runtime/kernel.c rt_spin_gate (MIT). TOTK_SPIN=0: the loop keeps its core, as before.
static __thread uint64_t sw_head, sw_t0, sw_last;
void rt_spin_gate(Cpu *c, uint64_t head) {
(void)c;
__builtin_ia32_pause();
thr_t *t = t_self;
if (!t) return;
static int on = -1;
static uint64_t handoff_ns;
if (on < 0) {
const char *e = getenv("TOTK_SPIN_HANDOFF_US");
handoff_ns = (uint64_t)(e && *e ? atoll(e) : 50) * 1000;
e = getenv("TOTK_SPIN");
on = !(e && *e == '0');
}
uint64_t now = now_ns();
if (head != sw_head || now - sw_last > 20000) { sw_head = head; sw_t0 = now; } // a new wait
sw_last = now;
if (!on || t->cur_core < 0 || now - sw_t0 < handoff_ns || !__atomic_load_n(&g_core_wait, __ATOMIC_RELAXED)) return;
int away = runtime_core_away();
sched_yield();
runtime_core_back(&away);
sw_last = now_ns();
}
int runtime_current_core(void) { thr_t *t = me_or_null(); return t ? (t->cur_core >= 0 ? t->cur_core : t->core) : -1; }
@@ -197,8 +283,8 @@ static int64_t block_inner(thr_t *t, int kind, int64_t ns) {
uint64_t now = guest_ns();
if (now >= guest_deadline) { wait_unlink(t); t->wkind = 0; return R_TIMEOUT; }
(void)(guest_deadline - now);
rc = pthread_cond_wait(&t->cv, &G);
} else rc = ns > 0 ? pthread_cond_timedwait(&t->cv, &G, &dl) : pthread_cond_wait(&t->cv, &G);
rc = gc_wait(&t->cv);
} else rc = ns > 0 ? gc_timedwait(&t->cv, &dl) : gc_wait(&t->cv);
if (rc == ETIMEDOUT && !t->woke && (!det_wait || guest_ns() >= guest_deadline)) { wait_unlink(t); t->wkind = 0; return R_TIMEOUT; }
}
return t->wres;
@@ -237,17 +323,18 @@ uint32_t (*dm_thread_tick_fn)(void); // the current game tick, when diagnostic
static void *thread_main(void *p) {
thr_t *t = p;
t_self = t;
preempt_point_set(t);
plat_thread_start();
Cpu *c = guest_cpu_new(t->stack_top, guest_tls_alloc());
guest_cpu_set(c);
pthread_mutex_lock(&G); while (!t->started) pthread_cond_wait(&t->cv, &G); core_take(t); pthread_mutex_unlock(&G);
g_lock(); while (!t->started) gc_wait(&t->cv); core_take(t); g_unlock();
c->x[0] = t->arg;
guest_call(c, t->entry); // does not return: the guest leaves through ExitThread (svc 0x0A)
die("guest thread entry returned (Horizon requires ExitThread)");
}
static void thr_free(thr_t *t) { // 持 G;从全部线程链摘下并释放 | Holding G; unlink from the all-threads chain and free
for (thr_t **p = &g_all; *p; p = &(*p)->all) if (*p == t) { *p = t->all; break; }
pthread_cond_destroy(&t->cv); free(t);
gc_destroy(&t->cv); free(t);
}
static _Noreturn void thread_exit_on_host(void *argument) {
@@ -255,13 +342,14 @@ static _Noreturn void thread_exit_on_host(void *argument) {
t->exit_ns = now_ns(); t->exit_tick = dm_thread_tick_fn ? dm_thread_tick_fn() : 0;
// 这里不打日志:退出发生在客体线程自己的栈上(有的只有十几 KiB),带浮点的 printf 会把栈撑爆(真机冒烟:LibcurlResolver 退出时 SIGBUS,回溯 fprintf → __dtoa)。时刻只记下来,现场导出时再打
// No logging here: the exit happens on the guest thread's own stack (some are only a dozen or so KiB), and a printf with floating point would blow the stack (real-device smoke test: SIGBUS when LibcurlResolver exits, backtrace fprintf → __dtoa). Only the time is recorded, and it is printed later in the state dump
pthread_mutex_lock(&G);
g_lock();
core_put(t);
t->exited = 1;
for (thr_t *w = g_wait; w; ) { thr_t *n = w->next; if (w->wkind == 4) wake(w, -1); w = n; } // 同步等待者重查 | Synchronization waiters re-check
t_self = NULL;
rt_preempt_flag = NULL;
if (t->closed) thr_free(t);
pthread_mutex_unlock(&G);
g_unlock();
pthread_exit(NULL);
__builtin_unreachable(); // winpthreads does not declare pthread_exit noreturn
}
@@ -271,11 +359,12 @@ static _Noreturn void thread_exit(thr_t *t) { thread_exit_on_host(t); } // alr
// Register the main thread and return its handle (passed to nnosInitialize as the main thread handle)
uint32_t svc_thread_main_init(void) {
thr_t *t = calloc(1, sizeof *t);
pthread_cond_init(&t->cv, NULL);
gc_init(&t->cv);
t->started = 1; t->prio = 44; t->core = 0; t->mask = 7; t->pt = pthread_self();
t->cur_core = -1;
pthread_mutex_lock(&G); t->handle = h_new(O_THREAD, t); t->id = g_next_id++; t->all = g_all; g_all = t; core_take(t); pthread_mutex_unlock(&G);
g_lock(); t->handle = h_new(O_THREAD, t); t->id = g_next_id++; t->all = g_all; g_all = t; core_take(t); g_unlock();
t_self = t;
preempt_point_set(t);
return t->handle;
}
@@ -301,9 +390,9 @@ static void wake_sync_waiters(void) {
static int64_t now_mono_ns(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return ts.tv_sec * 1000000000ll + ts.tv_nsec; }
void dm_guest_clock_advanced(void) {
if (!dm_detclock_enabled()) return;
pthread_mutex_lock(&G);
for (thr_t *t = g_wait; t; t = t->next) if (t->wkind) pthread_cond_signal(&t->cv);
pthread_mutex_unlock(&G);
g_lock();
for (thr_t *t = g_wait; t; t = t->next) if (t->wkind) gc_signal(&t->cv);
g_unlock();
}
// 处理线程 / 同步类 SVC;返回 1 = 已处理
@@ -340,11 +429,11 @@ int svc_thread(int num, uint64_t *regs) {
thr_t *t = calloc(1, sizeof *t);
if(!t)die("thread allocation");
t->cur_core = -1;
pthread_cond_init(&t->cv, NULL);
gc_init(&t->cv);
t->entry = regs[1]; t->arg = regs[2]; t->stack_top = regs[3] & ~0xFull;
t->prio = (int)regs[4]; t->core = (int)regs[5]; t->mask = t->core >= 0 ? 1ull << t->core : 7;
t->born_ns = now_ns(); t->born_tick = dm_thread_tick_fn ? dm_thread_tick_fn() : 0;
pthread_mutex_lock(&G); t->handle = h_new(O_THREAD, t); t->id = g_next_id++; t->all = g_all; g_all = t; pthread_mutex_unlock(&G);
g_lock(); t->handle = h_new(O_THREAD, t); t->id = g_next_id++; t->all = g_all; g_all = t; g_unlock();
pthread_attr_t a; pthread_attr_init(&a); pthread_attr_setstacksize(&a, 8 << 20); // also the host-service stack of this guest thread
pthread_attr_setdetachstate(&a, PTHREAD_CREATE_DETACHED); // 退出即回收宿主资源(客体经句柄等待,不 join) | Host resources are reclaimed on exit (the guest waits through the handle, no join)
if (pthread_create(&t->pt, &a, thread_main, t)) die("pthread_create");
@@ -353,10 +442,10 @@ int svc_thread(int num, uint64_t *regs) {
regs[0] = 0; regs[1] = t->handle; return 1;
}
case 0x09: { // StartThread(w0)
pthread_mutex_lock(&G);
g_lock();
thr_t *t = h_get((uint32_t)regs[0], O_THREAD);
if (t) { t->started = 1; pthread_cond_signal(&t->cv); }
pthread_mutex_unlock(&G);
if (t) { t->started = 1; gc_signal(&t->cv); }
g_unlock();
regs[0] = t ? 0 : R_BAD_HANDLE; return 1;
}
case 0x0A: thread_exit(cur()); return 1;
@@ -364,57 +453,66 @@ int svc_thread(int num, uint64_t *regs) {
int64_t ns = (int64_t)regs[0];
thr_t *self = cur();
if (ns > 0) { pthread_mutex_lock(&G); core_put(self); pthread_mutex_unlock(&G); }
if (ns > 0) { g_lock(); core_put(self); g_unlock(); }
if (ns > 0 && dm_detclock_enabled()) {
uint64_t deadline = guest_ns() + (uint64_t)ns;
while (guest_ns() < deadline) { struct timespec ts = {0, 1000000}; nanosleep(&ts, 0); }
} else if (ns > 0) { struct timespec ts = {ns / 1000000000, ns % 1000000000}; nanosleep(&ts, 0); }
} else if (ns > 0) {
#ifdef _WIN32 // winpthreads' nanosleep counts whole ms: the SDK's 20 us sleeps did not sleep at all (two takes of G
// each), and 1 ms or more came up to 1 ms late. A high-resolution timer, spinning the last 0.2 ms,
// with the core already given up (as totk-pc-recomp's sleep_ns).
uint64_t plat_now_ns(void); void plat_sleep_until_ns(uint64_t t);
plat_sleep_until_ns(plat_now_ns() + (uint64_t)ns);
#else // exact on Linux (timer slack 1 us, platform/linux.c)
struct timespec ts = {ns / 1000000000, ns % 1000000000}; nanosleep(&ts, 0);
#endif
}
else { // 让出(0 / -1 / -2)。主线程的让出次数与耗时记账:游戏渲染任务图「等全部做完」是纯轮询 + 让出(main+0x15ee898),量它占一帧多少 | Yield (0 / -1 / -2). Count and time of the main thread's yields are recorded: the game's render task graph "wait for everything to finish" is pure polling + yield (main+0x15ee898); measure how much of a frame it takes
thr_t *me = t_self;
uint64_t y0 = now_ns();
if (me && me->nsvc != me->yrun_svc + 1) me->yrun_ns = y0; // another SVC since the last yield: a new run
if (me) me->yrun_svc = me->nsvc;
pthread_mutex_lock(&G); // Horizon yield: hand the core to a waiting thread of equal or higher priority
g_lock(); // Horizon yield: hand the core to a waiting thread of equal or higher priority
int give = 0; for (thr_t *w = g_core_wait; w && !give; w = w->cnext) give = me->cur_core >= 0 && core_allowed(w, me->cur_core) && w->prio <= me->prio;
if (give) { core_put(me); core_take(me); }
pthread_mutex_unlock(&G);
g_unlock();
uint64_t backoff = !give && me ? yield_backoff_ns(y0 - me->yrun_ns) : 0;
if (backoff) { // a long poll: sleep with the core given up, as SleepThread(ns) does
pthread_mutex_lock(&G); core_put(me); pthread_mutex_unlock(&G);
g_lock(); core_put(me); g_unlock();
struct timespec ts = {0, (long)backoff}; nanosleep(&ts, 0);
pthread_mutex_lock(&G); core_take(me); pthread_mutex_unlock(&G);
g_lock(); core_take(me); g_unlock();
} else if (!give) sched_yield();
if (me && me->handle == 0x1001) { g_main_yield_ns += now_ns() - y0; g_main_yield_n++; }
}
if (ns > 0) { pthread_mutex_lock(&G); core_take(self); pthread_mutex_unlock(&G); }
if (ns > 0) { g_lock(); core_take(self); g_unlock(); }
return 1;
}
case 0x0C: { pthread_mutex_lock(&G);thr_t *t = h_get((uint32_t)regs[1], O_THREAD); regs[0] = t ? 0 : R_BAD_HANDLE; regs[1] = t ? (uint64_t)t->prio : 0; pthread_mutex_unlock(&G);return 1; }
case 0x0D: { if(regs[1]<28||regs[1]>59){regs[0]=R_BAD_STATE;return 1;}pthread_mutex_lock(&G);thr_t *t = h_get((uint32_t)regs[0], O_THREAD); if (t) t->prio = (int)regs[1]; regs[0] = t ? 0 : R_BAD_HANDLE;pthread_mutex_unlock(&G); return 1; }
case 0x0E: { pthread_mutex_lock(&G);thr_t *t = h_get((uint32_t)regs[2], O_THREAD);
regs[0] = t ? 0 : R_BAD_HANDLE; regs[1] = t ? (uint64_t)(uint32_t)t->core : 0; regs[2] = t ? t->mask : 0;pthread_mutex_unlock(&G); return 1; }
case 0x0C: { g_lock();thr_t *t = h_get((uint32_t)regs[1], O_THREAD); regs[0] = t ? 0 : R_BAD_HANDLE; regs[1] = t ? (uint64_t)t->prio : 0; g_unlock();return 1; }
case 0x0D: { if(regs[1]<28||regs[1]>59){regs[0]=R_BAD_STATE;return 1;}g_lock();thr_t *t = h_get((uint32_t)regs[0], O_THREAD); if (t) t->prio = (int)regs[1]; regs[0] = t ? 0 : R_BAD_HANDLE;g_unlock(); return 1; }
case 0x0E: { g_lock();thr_t *t = h_get((uint32_t)regs[2], O_THREAD);
regs[0] = t ? 0 : R_BAD_HANDLE; regs[1] = t ? (uint64_t)(uint32_t)t->core : 0; regs[2] = t ? t->mask : 0;g_unlock(); return 1; }
case 0x0F: { // SetThreadCoreMask(w0, w1 理想核, x2 掩码):-1 = 用进程缺省、-2/-3 = 不改理想核;宿主只记账 | SetThreadCoreMask(w0, w1 ideal core, x2 mask): -1 = use the process default, -2/-3 = leave the ideal core unchanged; the host only keeps the bookkeeping
pthread_mutex_lock(&G);thr_t *t = h_get((uint32_t)regs[0], O_THREAD);
g_lock();thr_t *t = h_get((uint32_t)regs[0], O_THREAD);
if (t) { int c = (int)regs[1]; if (c >= 0) t->core = c; else if (c == -1) t->core = 0; if (regs[2]) t->mask = regs[2];
if (t == cur() && t->cur_core >= 0 && !core_allowed(t, t->cur_core)) { core_put(t); core_take(t); } } // migrate off a core no longer allowed
regs[0] = t ? 0 : R_BAD_HANDLE;pthread_mutex_unlock(&G); return 1;
regs[0] = t ? 0 : R_BAD_HANDLE;g_unlock(); return 1;
}
case 0x10: { thr_t *t = cur(); regs[0] = (uint64_t)(uint32_t)(t->cur_core >= 0 ? t->cur_core : t->core); return 1; } // GetCurrentProcessorNumber
case 0x25: { pthread_mutex_lock(&G);thr_t *t = h_get((uint32_t)regs[1], O_THREAD); regs[0] = t ? 0 : R_BAD_HANDLE; regs[1] = t ? t->id : 0;pthread_mutex_unlock(&G); return 1; }
case 0x25: { g_lock();thr_t *t = h_get((uint32_t)regs[1], O_THREAD); regs[0] = t ? 0 : R_BAD_HANDLE; regs[1] = t ? t->id : 0;g_unlock(); return 1; }
case 0x1A: { // ArbitrateLock(w0 持有者句柄, x1 地址, w2 本线程标签) | ArbitrateLock(w0 owner handle, x1 address, w2 this thread's tag)
thr_t *t = cur(); uint64_t addr = regs[1];
pthread_mutex_lock(&G);
g_lock();
int64_t r = 0;
if (ld32(addr) == ((uint32_t)regs[0] | WAITERS_BIT)) { t->waddr = addr; t->wtag = (uint32_t)regs[2]; r = block(t, 1, -1); }
pthread_mutex_unlock(&G);
g_unlock();
regs[0] = (uint64_t)r; return 1;
}
case 0x1B: pthread_mutex_lock(&G); lock_handoff(regs[0]); pthread_mutex_unlock(&G); regs[0] = 0; return 1;
case 0x1B: g_lock(); lock_handoff(regs[0]); g_unlock(); regs[0] = 0; return 1;
case 0x1C: { // WaitProcessWideKeyAtomic(x0 锁地址, x1 条件键, w2 标签, x3 超时) | WaitProcessWideKeyAtomic(x0 lock address, x1 condition key, w2 tag, x3 timeout)
thr_t *t = cur(); uint64_t addr = regs[0], key = regs[1];
pthread_mutex_lock(&G);
g_lock();
// 顺序要紧:先置条件键 = 1,再放互斥锁。SDK 的 InternalConditionVariable::Signal 在用户态先读条件键、为 0 就不发 svc 0x1D;
// 用户态抢互斥锁也不经过 G。原先先 lock_handoff 后置键:放锁后、置键前,别的线程可在用户态抢到锁、改完条件、读到键 0 就不唤醒,
// 本线程随后挂上去永远等不到(并行负载下资源表读写锁 main+0x320e5e0 卡死:读者计数 0、锁空闲、写者条件键 1、写者仍在等)。
@@ -438,33 +536,33 @@ int svc_thread(int num, uint64_t *regs) {
t->wkey = key; t->waddr = addr; t->wtag = (uint32_t)regs[2];
#endif
int64_t r = block(t, 2, (int64_t)regs[3]); // 超时:不重新拿锁(用户态自己拿) | Timeout: the lock is not re-acquired (user mode takes it itself)
pthread_mutex_unlock(&G);
g_unlock();
regs[0] = (uint64_t)r; return 1;
}
case 0x1D: { // SignalProcessWideKey(x0 条件键, w1 个数,<=0 全部) | SignalProcessWideKey(x0 condition key, w1 count, <=0 all)
uint64_t key = regs[0]; int cnt = (int)regs[1], n = 0;
pthread_mutex_lock(&G);
g_lock();
for (thr_t *w; (cnt <= 0 || n < cnt) && (w = pick(2, key)); n++) lock_acquire_for(w, w->waddr, w->wtag); // 按内核顺序;转为锁等待或被唤醒后不再是 kind 2 | In kernel order; once turned into a lock wait or woken it is no longer kind 2
int left = 0; for (thr_t *w = g_wait; w; w = w->next) if (w->wkind == 2 && w->wkey == key) { left = 1; break; }
if (!left) st32(key, 0);
pthread_mutex_unlock(&G);
g_unlock();
regs[0] = 0; return 1;
}
case 0x34: { // WaitForAddress(x0 地址, w1 类型, w2 值, x3 超时):0 小于则等、1 小于则减一再等、2 相等则等 | WaitForAddress(x0 address, w1 type, w2 value, x3 timeout): 0 wait if less than, 1 decrement if less than then wait, 2 wait if equal
thr_t *t = cur(); uint64_t addr = regs[0]; int type = (int)regs[1]; int32_t val = (int32_t)regs[2];
pthread_mutex_lock(&G);
g_lock();
int64_t r; uint32_t v = ld32(addr);
int ok = type == 2 ? (int32_t)v == val : (int32_t)v < val;
if (ok && type == 1) { while (!cas32(addr, &v, v - 1)) if (!((int32_t)v < val)) { ok = 0; break; } }
if (!ok) r = R_BAD_STATE;
else { t->waddr = addr; r = block(t, 3, (int64_t)regs[3]); }
pthread_mutex_unlock(&G);
g_unlock();
regs[0] = (uint64_t)r; return 1;
}
case 0x35: { // SignalToAddress(x0 地址, w1 类型, w2 值, w3 个数):0 唤醒、1 相等则加一并唤醒、2 相等则按等待数改值并唤醒 | SignalToAddress(x0 address, w1 type, w2 value, w3 count): 0 wake, 1 increment if equal and wake, 2 if equal modify the value by waiter count and wake
uint64_t addr = regs[0]; int type = (int)regs[1]; int32_t val = (int32_t)regs[2]; int cnt = (int)regs[3];
pthread_mutex_lock(&G);
g_lock();
int nw = 0; for (thr_t *w = g_wait; w; w = w->next) if (w->wkind == 3 && w->waddr == addr) nw++;
int64_t r = 0;
if (type != 0) {
@@ -478,17 +576,17 @@ int svc_thread(int num, uint64_t *regs) {
}
if (!r) { int n = 0;
for (thr_t *w; (cnt <= 0 || n < cnt) && (w = pick(3, addr)); n++) wake(w, 0); }
pthread_mutex_unlock(&G);
g_unlock();
regs[0] = (uint64_t)r; return 1;
}
case 0x45: { // CreateEvent(→w1 写端, w2 读端) | CreateEvent(→w1 write end, w2 read end)
event_t *e = calloc(1, sizeof *e); e->refs = 2;
pthread_mutex_lock(&G); uint32_t hw = h_new(O_EVENT_W, e), hr = h_new(O_EVENT_R, e); pthread_mutex_unlock(&G);
g_lock(); uint32_t hw = h_new(O_EVENT_W, e), hr = h_new(O_EVENT_R, e); g_unlock();
regs[0] = 0; regs[1] = hw; regs[2] = hr; return 1;
}
case 0x11: case 0x12: case 0x17: { // SignalEvent(写端) / ClearEvent(任一端) / ResetSignal(读端,未触发报错) | SignalEvent(write end) / ClearEvent(either end) / ResetSignal(read end, error if not signaled)
pthread_mutex_lock(&G);
g_lock();
// SignalEvent 只认写端;ClearEvent 两端都认;ResetSignal 只认读端
// SignalEvent accepts only the write end; ClearEvent accepts both ends; ResetSignal accepts only the read end
uint32_t h = (uint32_t)regs[0];
@@ -500,14 +598,14 @@ int svc_thread(int num, uint64_t *regs) {
else if (num == 0x11) { e->signaled = 1; wake_sync_waiters(); }
else if (num == 0x17 && !e->signaled) r = R_BAD_STATE;
else e->signaled = 0;
pthread_mutex_unlock(&G);
g_unlock();
regs[0] = (uint64_t)r; return 1;
}
case 0x18: { // WaitSynchronization(→w1 下标; x1 句柄数组, w2 个数, x3 超时) | WaitSynchronization(→w1 index; x1 handle array, w2 count, x3 timeout)
thr_t *t = cur(); const uint32_t *hs = (const uint32_t *)regs[1]; int n = (int)regs[2]; int64_t ns = (int64_t)regs[3];
if(n<0||n>64||(!hs&&n)){regs[0]=R_BAD_STATE;return 1;}
int64_t deadline = ns > 0 ? (dm_detclock_enabled() ? (int64_t)guest_ns() : now_mono_ns()) + ns : 0;
pthread_mutex_lock(&G);
g_lock();
t->nwh = n; for (int i = 0; i < n && i < 4; i++) t->wh[i] = hs[i];
int64_t r; int idx = -1;
for (;;) {
@@ -520,18 +618,18 @@ int svc_thread(int num, uint64_t *regs) {
if (ns > 0 && left <= 0) { r = R_TIMEOUT; break; }
if (block(t, 4, left) == R_TIMEOUT) { r = R_TIMEOUT; break; }
}
pthread_mutex_unlock(&G);
g_unlock();
regs[0] = (uint64_t)r; regs[1] = idx >= 0 ? (uint64_t)idx : 0; return 1;
}
case 0x19: { // CancelSynchronization(w0 线程) | CancelSynchronization(w0 thread)
pthread_mutex_lock(&G);
g_lock();
thr_t *t = h_get((uint32_t)regs[0], O_THREAD);
if (t) { t->cancel = 1; if (t->wkind == 4) wake(t, -1); }
pthread_mutex_unlock(&G);
g_unlock();
regs[0] = t ? 0 : R_BAD_HANDLE; return 1;
}
case 0x16: { // CloseHandle(w0):事件两端都关才释放;线程句柄关闭后槽位回收,对象在线程也退出后释放 | CloseHandle(w0): an event is freed only when both ends are closed; after a thread handle is closed the slot is recycled, and the object is freed once the thread has also exited
pthread_mutex_lock(&G);
g_lock();
unsigned i = (uint32_t)regs[0] - 0x1000;int valid=0;
if (i > 0 && i < MAX_H && (g_h[i].type == O_EVENT_R || g_h[i].type == O_EVENT_W)) {
valid=1;event_t *e = g_h[i].obj; if (--e->refs == 0) free(e);
@@ -539,16 +637,16 @@ int svc_thread(int num, uint64_t *regs) {
} else if (i > 0 && i < MAX_H && (g_h[i].type == O_SESSION || g_h[i].type == O_TMEM)) {
valid=1;void *o = g_h[i].obj; int session = g_h[i].type == O_SESSION;
g_h[i].type = O_FREE; g_h[i].obj = NULL;
pthread_mutex_unlock(&G);
g_unlock();
if (session) runtime_ipc_session_closed(o); else runtime_ipc_tmem_closed(o);
pthread_mutex_lock(&G);
g_lock();
} else if (i > 0 && i < MAX_H && g_h[i].type == O_THREAD) {
valid=1;thr_t *t = g_h[i].obj;
g_h[i].type = O_FREE; g_h[i].obj = NULL;
t->closed = 1;
if (t->exited) thr_free(t);
}
pthread_mutex_unlock(&G);
g_unlock();
regs[0] = valid?0:R_BAD_HANDLE; return 1;
}
}
@@ -586,32 +684,32 @@ void hle_bind_system_event(void *ev, int autoclear) { // 必须在客体线
void hle_signal_system_event(void *ev) { // 未绑定的按手动清除即时绑定(只能在客体线程上发生) | An unbound one is bound on the spot as manual-clear (can only happen on a guest thread)
pthread_mutex_lock(&g_se_lock); uint32_t wh = se_find(ev); pthread_mutex_unlock(&g_se_lock);
if (!wh) { hle_bind_system_event(ev, 0); pthread_mutex_lock(&g_se_lock); wh = se_find(ev); pthread_mutex_unlock(&g_se_lock); }
pthread_mutex_lock(&G);
g_lock();
event_t *e = h_get(wh, O_EVENT_W);
if (!e) die("SystemEvent 写端句柄失效 %#x", wh);
e->signaled = 1; wake_sync_waiters();
pthread_mutex_unlock(&G);
g_unlock();
}
// ---------- IPC support (ipc.c): sessions, transfer memory and service-owned events ----------
// Weak fallbacks keep standalone thread tests linkable; the runtime links the real ipc.c versions.
__attribute__((weak)) void runtime_ipc_session_closed(void *s) { (void)s; die("IPC session closed without ipc.c"); }
__attribute__((weak)) void runtime_ipc_tmem_closed(void *t) { (void)t; die("transfer memory closed without ipc.c"); }
uint32_t runtime_handle_new(int session, void *obj) { pthread_mutex_lock(&G); uint32_t h = h_new(session ? O_SESSION : O_TMEM, obj); pthread_mutex_unlock(&G); return h; }
void *runtime_handle_get(uint32_t h, int session) { pthread_mutex_lock(&G); void *o = h_peek(h, session ? O_SESSION : O_TMEM); pthread_mutex_unlock(&G); return o; }
uint32_t runtime_handle_new(int session, void *obj) { g_lock(); uint32_t h = h_new(session ? O_SESSION : O_TMEM, obj); g_unlock(); return h; }
void *runtime_handle_get(uint32_t h, int session) { g_lock(); void *o = h_peek(h, session ? O_SESSION : O_TMEM); g_unlock(); return o; }
// Service-side event: returns the writable handle kept by the service.
uint32_t runtime_event_new(void) {
event_t *e = calloc(1, sizeof *e); if (!e) die("event allocation"); e->refs = 1;
pthread_mutex_lock(&G); uint32_t hw = h_new(O_EVENT_W, e); pthread_mutex_unlock(&G); return hw;
g_lock(); uint32_t hw = h_new(O_EVENT_W, e); g_unlock(); return hw;
}
// Copy-handle transfer of the readable end: each call gives the client its own handle to the same event.
uint32_t runtime_event_readable(uint32_t hw) {
pthread_mutex_lock(&G); event_t *e = h_get(hw, O_EVENT_W); if (!e) die("service event handle %#x", hw);
e->refs++; uint32_t hr = h_new(O_EVENT_R, e); pthread_mutex_unlock(&G); return hr;
g_lock(); event_t *e = h_get(hw, O_EVENT_W); if (!e) die("service event handle %#x", hw);
e->refs++; uint32_t hr = h_new(O_EVENT_R, e); g_unlock(); return hr;
}
void runtime_event_signal(uint32_t hw) {
pthread_mutex_lock(&G); event_t *e = h_get(hw, O_EVENT_W); if (!e) die("service event handle %#x", hw);
e->signaled = 1; wake_sync_waiters(); pthread_mutex_unlock(&G);
g_lock(); event_t *e = h_get(hw, O_EVENT_W); if (!e) die("service event handle %#x", hw);
e->signaled = 1; wake_sync_waiters(); g_unlock();
}
// ---------- Diagnostics ----------
+11 -1
View File
@@ -142,15 +142,25 @@ int window_init(void) {
}
// Motion: the controller's accelerometer and gyro (SDL_EVENT_GAMEPAD_SENSOR_UPDATE) go to motion_fusion.c.
int deck_imu_start(void); // deck_imu.c
static void pad_attached(void) { // pad_lock held; pad may be NULL
if (!pad) { runtime_motion_live(0); return; }
fprintf(stderr, "[totk] game controller: %s\n", SDL_GetGamepadName(pad));
{ // which device it is: a Steam Input virtual gamepad (Steam handle) gives no motion; the Deck's own controller,
// opened by SDL's HIDAPI driver (Steam Input off for the game), has a gyro
const char *path = SDL_GetGamepadPath(pad);
fprintf(stderr, "[totk] type %d, vendor %04x product %04x, %s, path %s\n", (int)SDL_GetGamepadType(pad),
SDL_GetGamepadVendor(pad), SDL_GetGamepadProduct(pad),
SDL_GetGamepadSteamHandle(pad) ? "a Steam Input virtual gamepad" : "not through Steam Input", path ? path : "-");
}
if (SDL_GamepadHasSensor(pad, SDL_SENSOR_GYRO) && SDL_GamepadHasSensor(pad, SDL_SENSOR_ACCEL) &&
SDL_SetGamepadSensorEnabled(pad, SDL_SENSOR_GYRO, true) && SDL_SetGamepadSensorEnabled(pad, SDL_SENSOR_ACCEL, true)) {
const char *axes = getenv("TOTK_GYRO_AXES");
if (runtime_motion_axes(axes) < 0) fprintf(stderr, "[totk] TOTK_GYRO_AXES=%s is not like \"-x,y,-z\": ignored\n", axes);
runtime_motion_live(1);
fprintf(stderr, "[totk] controller motion sensors on (%.0f Hz gyro)\n", SDL_GetGamepadSensorDataRate(pad, SDL_SENSOR_GYRO));
} else if (deck_imu_start()) { // a Steam Input gamepad on the Steam Deck: the Deck's own motion (deck_imu.c)
runtime_motion_live(1);
} else {
runtime_motion_live(0);
fprintf(stderr, "[totk] controller has no motion sensors: no gyro aiming\n");
@@ -169,7 +179,7 @@ static void pad_sensor(const SDL_GamepadSensorEvent *e) {
acc[2] = -d[1] / SDL_STANDARD_GRAVITY;
} else if (e->sensor == SDL_SENSOR_GYRO) {
double gyro[3] = {d[0], -d[2], d[1]};
runtime_motion_feed(acc, gyro, runtime_now_ns());
runtime_motion_feed(acc, gyro, runtime_now_ns(), e->sensor_timestamp);
}
}
+146
View File
@@ -0,0 +1,146 @@
// The game's zstd decoder on the host's libzstd. Most RomFS files of TotK 1.4.3 are zstd frames, many with a dictionary
// from Pack/ZsDic.pack.zs; the decoder is BSD zstd linked into main. Seven call sites of ZSTD_decompressMultiFrame
// (main+0x3a1f0) and ZSTD_decompress_usingDDict (main+0x3a1e0) come here instead (tools/lifter/patches.py, zstd):
// lifted, the decoder ran its literal and match copies as chains of small guest functions; here the same frames decode
// in native code, several times faster (loading stalls). After totk-pc-recomp runtime/ov_zstd.c (MIT, Copyright (c)
// 2026 Valiantsin Dzerakh and contributors), which found the sites and checked the results against the guest decoder.
//
// - Results match the guest's: libzstd decodes the same format, and every call it cannot take or answers with an error
// goes to the guest decoder (which writes its own error code: a frame libzstd rejects may be one the game accepts).
// - Input inside the output buffer (decoding in place) stays in the guest: how the writes overtake the reads there is
// not something libzstd promises to do as the game's version does.
// - A guest ZSTD_DDict (zstd's struct: dictBuffer, dictContent, dictSize first) becomes a host ZSTD_DDict made from a
// copy of its content, kept by guest address, content address, size and a fingerprint of the content.
// - Inputs of 64 KiB or more give the virtual core up while they decode, so other guest threads run on it.
// TOTK_ZSTD_NATIVE=0: the guest decoder everywhere.
#include <pthread.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <zstd.h>
#include "host.h"
int runtime_core_away(void);
void runtime_core_back(int *away);
#define GUEST_MULTIFRAME 0x80003a1f0ull // 1.4.3 main: ZSTD_decompressMultiFrame
#define GUEST_USING_DDICT 0x80003a1e0ull // ZSTD_decompress_usingDDict
#define AWAY_BYTES (64u << 10)
#define MAX_DICT (64u << 20)
#define ADDRESS_LIMIT (1ull << 40) // above any guest mapping: a field that does not look like an address
#define NDICT 32
static void *gp(uint64_t a) { return (void *)(uintptr_t)a; }
int totk_zstd_native(void) {
static int on = -1;
if (on < 0) {
const char *e = getenv("TOTK_ZSTD_NATIVE");
on = !(e && *e == '0');
fprintf(stderr, "[zstd] the game's zstd frames decode %s (TOTK_ZSTD_NATIVE)\n", on ? "on the host" : "in the guest");
}
return on;
}
typedef struct { uint64_t guest, content, size, print; ZSTD_DDict *dd; } Dict;
static Dict dicts[NDICT];
static int ndicts;
static pthread_rwlock_t dict_lock = PTHREAD_RWLOCK_INITIALIZER;
static _Atomic uint64_t s_calls, s_in, s_out, s_guest, s_inplace;
// Size, both ends and a sample of the middle: what tells a new dictionary at a reused address from the old one.
static uint64_t fingerprint(const uint8_t *p, uint64_t n) {
uint64_t h = n * 0x9E3779B97F4A7C15ull;
uint64_t at[3] = {0, n / 2, n > 64 ? n - 64 : 0};
for (int k = 0; k < 3; k++)
for (uint64_t i = at[k]; i < at[k] + 64 && i < n; i += 8) {
uint64_t v = 0;
memcpy(&v, p + i, n - i < 8 ? n - i : 8);
h = (h ^ v) * 0xC2B2AE3D27D4EB4Full;
h ^= h >> 29;
}
return h;
}
// The host dictionary for a guest ZSTD_DDict or raw dictionary bytes (guest == 0); NULL when its fields look wrong.
// Kept for good (another thread may be decoding with one); a full table sends further dictionaries to the guest.
static ZSTD_DDict *dict_for(uint64_t guest, uint64_t content, uint64_t size) {
if (!content || !size || size > MAX_DICT || content >= ADDRESS_LIMIT || content + size > ADDRESS_LIMIT) return NULL;
uint64_t print = fingerprint(gp(content), size);
ZSTD_DDict *dd = NULL;
pthread_rwlock_rdlock(&dict_lock);
for (int i = 0; i < ndicts && !dd; i++)
if (dicts[i].guest == guest && dicts[i].content == content && dicts[i].size == size && dicts[i].print == print) dd = dicts[i].dd;
pthread_rwlock_unlock(&dict_lock);
if (dd) return dd;
ZSTD_DDict *made = ZSTD_createDDict(gp(content), (size_t)size); // a copy: the guest may free or reuse its buffer
if (!made) return NULL;
int slot = -1;
pthread_rwlock_wrlock(&dict_lock);
for (int i = 0; i < ndicts && !dd; i++) // another thread may have made it meanwhile
if (dicts[i].guest == guest && dicts[i].content == content && dicts[i].size == size && dicts[i].print == print) dd = dicts[i].dd;
if (!dd && ndicts < NDICT) slot = ndicts, dicts[ndicts++] = (Dict){guest, content, size, print, made}, dd = made;
pthread_rwlock_unlock(&dict_lock);
if (dd != made) {
ZSTD_freeDDict(made);
if (!dd) fprintf(stderr, "[zstd] more than %d dictionaries: further ones decode in the guest\n", NDICT);
return dd;
}
fprintf(stderr, "[zstd] host dictionary %d for %s %#llx: %llu bytes, id %u\n", slot, guest ? "DDict" : "raw dictionary",
(unsigned long long)(guest ? guest : content), (unsigned long long)size, ZSTD_getDictID_fromDDict(dd));
return dd;
}
// libzstd's result for the call; *ok 0 when the inputs cannot be taken.
static size_t host_decode(uint64_t dst, uint64_t cap, uint64_t src, uint64_t size, uint64_t dict, uint64_t dict_size,
uint64_t ddict, int *ok) {
static __thread ZSTD_DCtx *dctx;
*ok = 0;
if ((cap && !dst) || dst + cap > ADDRESS_LIMIT || !src || src + size > ADDRESS_LIMIT) return 0;
ZSTD_DDict *dd = NULL;
if (ddict) {
if (ddict >= ADDRESS_LIMIT - 24) return 0;
uint64_t f[3]; // ZSTD_DDict: dictBuffer, dictContent, dictSize
memcpy(f, gp(ddict), sizeof f);
if (!(dd = dict_for(ddict, f[1], f[2]))) return 0;
} else if (dict && dict_size) {
if (!(dd = dict_for(0, dict, dict_size))) return 0;
}
if (!dctx && !(dctx = ZSTD_createDCtx())) return 0;
*ok = 1;
int away = size >= AWAY_BYTES ? runtime_core_away() : 0;
size_t r = dd ? ZSTD_decompress_usingDDict(dctx, gp(dst), (size_t)cap, gp(src), (size_t)size, dd)
: ZSTD_decompressDCtx(dctx, gp(dst), (size_t)cap, gp(src), (size_t)size);
runtime_core_back(&away);
return r;
}
// At the call: x0 dctx, x1 dst, x2 capacity, x3 src, x4 size, then (dict, dict size, ddict) as the entry has them; x30
// the return address. guest: the guest decoder for what the host does not take.
static void decode(Cpu *c, uint64_t dict, uint64_t dict_size, uint64_t ddict, uint64_t guest) {
uint64_t dst = c->x[1], cap = c->x[2], src = c->x[3], size = c->x[4];
int ok = 0;
size_t r = 0;
if (cap && src < dst + cap && dst < src + size) s_inplace++; // in place: the guest's
else r = host_decode(dst, cap, src, size, dict, dict_size, ddict, &ok);
if (!ok || ZSTD_isError(r)) {
uint64_t n = ++s_guest;
if (ok && n <= 8) fprintf(stderr, "[zstd] %llu bytes at %#llx go to the guest decoder: %s\n", (unsigned long long)size,
(unsigned long long)src, ZSTD_getErrorName(r));
GuestFn g = dispatch_lookup(guest);
if (!g) runtime_fail("zstd: no guest decoder");
g(c); // the registers are still the call's
return;
}
c->x[0] = r;
uint64_t n = ++s_calls;
s_in += size; s_out += r;
if (n == 1 || !(n & 0x3fff))
fprintf(stderr, "[zstd] %llu calls on the host, %.1f MiB in, %.1f MiB out; %llu to the guest decoder (%llu in place)\n",
(unsigned long long)n, s_in / 1048576.0, s_out / 1048576.0, (unsigned long long)s_guest, (unsigned long long)s_inplace);
}
void totk_zstd_multiframe(Cpu *c) { decode(c, c->x[5], c->x[6], c->x[7], GUEST_MULTIFRAME); }
void totk_zstd_using_ddict(Cpu *c) { decode(c, 0, 0, c->x[5], GUEST_USING_DDICT); }
+1 -1
View File
@@ -55,7 +55,7 @@ void window_dark_title(SDL_Window *w);
Lang system_lang();
int logical_cpus();
// ultratears-launcher.ini: lang ("" = the system's), dump (the folder with exefs and romfs), install (another folder
// than the launcher's), updates ("off": no questions to GitHub), update_checked (unix time), update_* (a release
// than the launcher's), updates ("off": no questions to GitHub), update_* (a release
// found). Which package the game was built by is not here but with the build (data/setup.json "package").
struct Settings {
std::vector<std::pair<std::string, std::string>> kv;
+1 -1
View File
@@ -158,7 +158,7 @@ static const Text TEXTS[] = {
{"row_counter", {"In-game FPS counter", "Счётчик FPS в игре"}},
{"counter_key", {"F10 turns it on or off", "F10 включение или отключение"}},
{"row_text", {"Text and subtitles", "Текст и субтитры"}},
{"lang_system", {"As in Windows", "Как в Windows"}},
{"lang_system", {"As in the system", "Как в системе"}},
{"lang_ja", {"Japanese", "Японский"}},
{"lang_ko", {"Korean", "Корейский"}},
{"lang_zh", {"Chinese (Simplified)", "Китайский (упрощённый)"}},
+11 -12
View File
@@ -289,12 +289,18 @@ static Json setup_state() { // data/setup.json of the install folder: setup.py
// no builder, the AppImage is it, so a newer AppImage put in the folder makes the build stale. A folder of an older
// layout (a builder copied in) is stale too once the AppImage that runs is a newer release than that copy.
static bool has_package(const std::string &folder);
// the package in `from` replaces the one in `to`: another build (the whole VERSION.txt line, so a rebuild with the same
// number, 0.2.0-41-g... then 0.2.0, counts too), only never an older release over a newer one
static bool package_replaces(const std::string &from, const std::string &to) {
std::string a = package_version_line(from), b = package_version_line(to);
return !a.empty() && a != b && !version_newer(package_version(to), package_version(from));
}
static bool stale() {
std::string line = package_version_line(U.root);
#ifdef _WIN32
bool newer = false; // the PC's launcher is in its package: an update replaces it with the package (apply_update.py)
#else
bool newer = has_package(U.home) && has_package(U.install) && version_newer(package_version(U.home), package_version(U.install));
bool newer = has_package(U.home) && has_package(U.install) && package_replaces(U.home, U.install);
#endif
return installed() && ((!line.empty() && setup_state()["package"].str() != "UltraTears " + line) || newer);
}
@@ -324,7 +330,7 @@ static bool has_package(const std::string &folder) { return path_is_file(path_jo
static bool needs_copy() {
if (same_path(U.install, U.home))
return false;
return !has_package(U.install) || version_newer(package_version(U.home), package_version(U.install));
return !has_package(U.install) || package_replaces(U.home, U.install);
}
// GB a build needs free: the game, the package copy when it goes elsewhere, and for the Steam Deck its game, objects,
@@ -1909,7 +1915,8 @@ void ui_init(SDL_Window *w) {
U.platform = win && lin ? "both" : lin ? "linux" : "windows";
}
update_cleanup(U.root);
// updates: one question to GitHub a day at most; a release found stays offered until it is installed
// updates: one question to GitHub at every start, in the background (no answer: nothing shown); a release found
// stays offered until it is installed
U.upd.version = U.set.get("update_version");
if (!U.upd.version.empty() && U.upd.version != U.version && !U.version.empty()) {
U.upd.name = U.set.get("update_name"), U.upd.url = U.set.get("update_url");
@@ -1917,16 +1924,8 @@ void ui_init(SDL_Window *w) {
U.upd.size = SDL_strtoull(U.set.get("update_size").c_str(), nullptr, 10);
U.has_upd = !U.upd.url.empty() && !U.upd.sha256.empty();
}
long long checked = SDL_strtoll(U.set.get("update_checked").c_str(), nullptr, 10);
SDL_Time now = 0;
SDL_GetCurrentTime(&now);
long long secs = (long long)(now / SDL_NS_PER_SECOND);
if (U.set.get("updates") != "off" && !U.version.empty() && !python_path(U.root).empty() &&
(secs - checked > 24 * 3600 || secs < checked)) {
U.set.set("update_checked", std::to_string(secs));
settings_save(U.set);
if (U.set.get("updates") != "off" && !U.version.empty() && !python_path(U.root).empty())
update_check_start(U.version);
}
ImGuiStyle &st = ImGui::GetStyle();
st.WindowPadding = ImVec2(0, 0);
st.WindowBorderSize = 0;
+4 -4
View File
@@ -1,5 +1,5 @@
// Updates of the builder package from GitHub Releases: at most one question a day (ultratears-launcher.ini, updates=off
// turns it off), the newest release's builder zip (Windows) or AppImage (Linux) downloaded into update\ of the package,
// Updates of the builder package from GitHub Releases: one question at every start (ultratears-launcher.ini, updates=off
// turns it off; no answer, no network: nothing is shown), the newest release's builder zip (Windows) or AppImage (Linux) downloaded into update\ of the package,
// its size and SHA-256 (the digest GitHub keeps for every release file) checked before tools/apply_update.py puts the
// zip in place, or the AppImage takes UltraTears.AppImage's place (platform.cpp appimage_update).
// HTTP: WinHTTP on Windows, the system's libcurl on Linux; SHA-256: BCrypt on Windows, its own on Linux.
@@ -235,9 +235,9 @@ static int check_thread(void *) {
Json list;
UpdateInfo best;
if (ok && json_parse(body, list)) {
for (size_t i = 0; i < list.size(); i++) { // newest first; pre-releases count (the port is in beta)
for (size_t i = 0; i < list.size(); i++) { // newest first; drafts and pre-releases are not offered
const Json &r = list[i];
if (r["draft"].truth())
if (r["draft"].truth() || r["prerelease"].truth())
continue;
std::string tag = r["tag_name"].str();
if (!newer(tag, G.current) || (best.version.size() && !newer(tag, best.version)))
+103
View File
@@ -35,6 +35,7 @@ void rt_svc(Cpu *c, uint32_t imm, uint64_t pc);
void rt_brk(Cpu *c, uint32_t imm, uint64_t pc) __attribute__((noreturn));
void rt_udf(Cpu *c, uint64_t pc) __attribute__((noreturn));
uint64_t rt_ticks(void);
void rt_spin_gate(Cpu *c, uint64_t head); // host/threads.c: a pass of a spin wait (tools/lifter/spin_waits_v143.txt)
#if defined(__aarch64__)
static inline uint64_t rt_get_fpcr(void) { uint64_t v; __asm__ volatile("mrs %0, fpcr" : "=r"(v)); return v; }
@@ -161,6 +162,108 @@ static inline uint32_t logic32(Cpu *c, uint32_t r) { c->nzcv = MKFLAGS(r >> 31,
ADC_IMPL(64, )
ADC_IMPL(32, )
// ---- function-local flags (tools/lifter/flags.py). From totk-pc-recomp runtime/cpu.h @ v0.3.1 (MIT, Copyright (c)
// 2026 Valiantsin Dzerakh and contributors), unchanged below.
// Generated functions keep NZCV in a local Flags F, never in c->nzcv. A definition of a known kind stores its
// operands (a - b, a + b, or a logic result in a); fc_<kind>(&F, CC_xx) computes a condition from them, so a compare
// and its branch become one host compare. F.nz holds the packed form for definitions only known packed (CCMP, FCMP,
// MSR, ADCS) and for readers that several kinds of definition reach. Every helper is always inlined with a constant
// condition, so F stays in registers and only the condition's own arithmetic remains.
typedef struct { uint64_t a, b, nz; } Flags;
enum { CC_EQ, CC_NE, CC_CS, CC_CC, CC_MI, CC_PL, CC_VS, CC_VC, CC_HI, CC_LS, CC_GE, CC_LT, CC_GT, CC_LE };
#define CPU_FL static inline __attribute__((always_inline))
CPU_FL int fl_cond(int cc, int n, int z, int cy, int v) {
switch (cc) {
case CC_EQ: return z;
case CC_NE: return !z;
case CC_CS: return cy;
case CC_CC: return !cy;
case CC_MI: return n;
case CC_PL: return !n;
case CC_VS: return v;
case CC_VC: return !v;
case CC_HI: return cy && !z;
case CC_LS: return !cy || z;
case CC_GE: return n == v;
case CC_LT: return n != v;
case CC_GT: return !z && n == v;
default: return z || n != v;
}
}
// Subtract (SUBS, CMP): conditions as host compares of the operands.
#define FL_SUB(N, U, S) \
CPU_FL U fsub##N(Flags *f, U a, U b) { f->a = a; f->b = b; return (U)(a - b); } \
CPU_FL int fc_sub##N(const Flags *f, int cc) { \
U a = (U)f->a, b = (U)f->b, r = (U)(a - b); \
switch (cc) { \
case CC_EQ: return a == b; \
case CC_NE: return a != b; \
case CC_CS: return a >= b; \
case CC_CC: return a < b; \
case CC_MI: return (S)r < 0; \
case CC_PL: return (S)r >= 0; \
case CC_VS: return (S)((a ^ b) & (a ^ r)) < 0; \
case CC_VC: return (S)((a ^ b) & (a ^ r)) >= 0; \
case CC_HI: return a > b; \
case CC_LS: return a <= b; \
case CC_GE: return (S)a >= (S)b; \
case CC_LT: return (S)a < (S)b; \
case CC_GT: return (S)a > (S)b; \
default: return (S)a <= (S)b; \
} \
} \
CPU_FL uint64_t nz_sub##N(U a, U b) { \
U r = (U)(a - b); \
return MKFLAGS((S)r < 0, r == 0, a >= b, (S)((a ^ b) & (a ^ r)) < 0); \
} \
CPU_FL uint64_t fnz_sub##N(const Flags *f) { return nz_sub##N((U)f->a, (U)f->b); }
FL_SUB(64, uint64_t, int64_t)
FL_SUB(32, uint32_t, int32_t)
// Add (ADDS, CMN) and logic results (ANDS, TST, BICS): conditions from N, Z, C, V.
#define FL_ADD(N, U, S) \
CPU_FL U fadd##N(Flags *f, U a, U b) { f->a = a; f->b = b; return (U)(a + b); } \
CPU_FL uint64_t nz_add##N(U a, U b) { \
U r = (U)(a + b); \
return MKFLAGS((S)r < 0, r == 0, r < a, (S)((a ^ r) & (b ^ r)) < 0); \
} \
CPU_FL int fc_add##N(const Flags *f, int cc) { \
U a = (U)f->a, b = (U)f->b, r = (U)(a + b); \
return fl_cond(cc, (S)r < 0, r == 0, r < a, (S)((a ^ r) & (b ^ r)) < 0); \
} \
CPU_FL uint64_t fnz_add##N(const Flags *f) { return nz_add##N((U)f->a, (U)f->b); } \
CPU_FL U flogic##N(Flags *f, U r) { f->a = r; return r; } \
CPU_FL int fc_logic##N(const Flags *f, int cc) { \
U r = (U)f->a; \
return fl_cond(cc, (S)r < 0, r == 0, 0, 0); \
} \
CPU_FL uint64_t fnz_logic##N(const Flags *f) { return MKFLAGS((S)f->a < 0, (U)f->a == 0, 0, 0); }
FL_ADD(64, uint64_t, int64_t)
FL_ADD(32, uint32_t, int32_t)
// Packed flags.
CPU_FL int fc_nz(const Flags *f, int cc) {
return fl_cond(cc, f->nz >> 31 & 1, f->nz >> 30 & 1, f->nz >> 29 & 1, f->nz >> 28 & 1);
}
CPU_FL uint64_t fnz_nz(const Flags *f) { return f->nz; }
// ADC, SBC, ADCS, SBCS with the carry in from F.
#define FL_ADC(N, U) \
CPU_FL U adc##N##f(int cin, U a, U b) { return (U)(a + b + (U)cin); } \
CPU_FL U sbc##N##f(int cin, U a, U b) { return (U)(a - b - 1 + (U)cin); } \
CPU_FL U adcs##N##f(Flags *f, int cin, U a, U b) { \
U r = (U)(a + b + (U)cin); \
int carry = cin ? r <= a : r < a; \
f->nz = MKFLAGS(r >> (N - 1), r == 0, carry, ((a ^ r) & (b ^ r)) >> (N - 1)); \
return r; \
} \
CPU_FL U sbcs##N##f(Flags *f, int cin, U a, U b) { return adcs##N##f(f, cin, a, (U)~b); }
FL_ADC(64, uint64_t)
FL_ADC(32, uint32_t)
#undef CPU_FL
// ---- bit ops
#define ROR64(v, r) ((uint64_t)(v) >> ((r) & 63) | (uint64_t)(v) << ((64 - ((r) & 63)) & 63))
#define ROR32(v, r) ((uint32_t)(v) >> ((r) & 31) | (uint32_t)(v) << ((32 - ((r) & 31)) & 31))
+11 -1
View File
@@ -163,8 +163,16 @@ static GuestFn dispatch_slow(Cpu *c) {
return f;
}
// Preemption point (host/threads.c runtime_core_checkpoint): a guest thread whose core a better thread waits for gives
// it up at its next indirect call or SVC, as Horizon would run the better thread at once. Without it the waiter got
// the core only when the holder blocked or slept (totk-pc-recomp measured waits up to 26 ms; its rt_dispatch checks
// the same way). rt_preempt_flag: the running thread's request flag, set by threads.c; one load and branch here.
__thread volatile int *rt_preempt_flag;
void (*rt_preempt_fn)(void);
// Always tail-calls, so chains of indirect jumps don't grow the host stack.
void rt_dispatch(Cpu *c) {
if (__builtin_expect(rt_preempt_flag && *rt_preempt_flag, 0)) rt_preempt_fn();
unsigned h = (unsigned)(c->pc >> 2) & (DCACHE - 1);
GuestFn f = dcache[h].addr == c->pc ? dcache[h].fn : dispatch_slow(c);
__attribute__((musttail)) return f(c);
@@ -252,4 +260,6 @@ volatile int g_fnprof;
void fnprof_hit(uint64_t addr, uint64_t lr, uint64_t x0) { (void)addr; (void)lr; (void)x0; }
// ---- CNTPCT_EL0: the Switch's 19.2 MHz system counter
uint64_t rt_ticks(void) { return (uint64_t)((unsigned __int128)plat_now_ns() * 12 / 625); }
// The Switch's 19.2 MHz counter: ns * 12 / 625, as a multiply by that ratio in 16.48 fixed point (a 128-bit division
// per call before; the game reads the counter often).
uint64_t rt_ticks(void) { return (uint64_t)(((unsigned __int128)plat_now_ns() * (((12ull << 48) + 312) / 625)) >> 48); }
+32 -9
View File
@@ -5,7 +5,22 @@
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#ifdef _WIN32
#include <windows.h>
static SRWLOCK g_lock = SRWLOCK_INIT;
#define RD_LOCK() AcquireSRWLockShared(&g_lock)
#define RD_UNLOCK() ReleaseSRWLockShared(&g_lock)
#define WR_LOCK() AcquireSRWLockExclusive(&g_lock)
#define WR_UNLOCK() ReleaseSRWLockExclusive(&g_lock)
#define strdup _strdup
#else // Linux: glibc walks the whole environment on every getenv, and Steam's is large
#include <pthread.h>
static pthread_rwlock_t g_lock = PTHREAD_RWLOCK_INITIALIZER;
#define RD_LOCK() pthread_rwlock_rdlock(&g_lock)
#define RD_UNLOCK() pthread_rwlock_unlock(&g_lock)
#define WR_LOCK() pthread_rwlock_wrlock(&g_lock)
#define WR_UNLOCK() pthread_rwlock_unlock(&g_lock)
#endif
char *__real_getenv(const char *name);
@@ -13,42 +28,50 @@ enum { SLOTS = 1024 };
typedef struct { char *name; char *value; } slot;
static slot g_tab[SLOTS];
static int g_used;
static SRWLOCK g_lock = SRWLOCK_INIT;
static uint32_t hash(const char *s) { uint32_t h = 2166136261u; while (*s) h = (h ^ (uint8_t)*s++) * 16777619u; return h; }
char *__wrap_getenv(const char *name) {
if (!name) return __real_getenv(name);
uint32_t h = hash(name);
AcquireSRWLockShared(&g_lock);
RD_LOCK();
for (uint32_t i = 0; i < SLOTS; i++) {
slot *s = &g_tab[(h + i) & (SLOTS - 1)];
if (!s->name) break;
if (!strcmp(s->name, name)) { char *v = s->value; ReleaseSRWLockShared(&g_lock); return v; }
if (!strcmp(s->name, name)) { char *v = s->value; RD_UNLOCK(); return v; }
}
ReleaseSRWLockShared(&g_lock);
AcquireSRWLockExclusive(&g_lock);
RD_UNLOCK();
WR_LOCK();
char *v = __real_getenv(name);
if (g_used < SLOTS / 2) { // a full table just asks the C runtime
for (uint32_t i = 0; i < SLOTS; i++) {
slot *s = &g_tab[(h + i) & (SLOTS - 1)];
if (s->name && !strcmp(s->name, name)) break;
if (!s->name) {
char *n = _strdup(name);
char *n = strdup(name);
if (n) { s->value = v; s->name = n; g_used++; }
break;
}
}
}
ReleaseSRWLockExclusive(&g_lock);
WR_UNLOCK();
return v;
}
// Any change of the environment (platform/win_compat.h setenv / unsetenv, platform/windows.c): the values kept point into
// the environment as it was. (_putenv_s is called through the DLL import, which the link's --wrap does not reach.)
void runtime_env_changed(void) {
AcquireSRWLockExclusive(&g_lock);
WR_LOCK();
for (int i = 0; i < SLOTS; i++) { free(g_tab[i].name); g_tab[i].name = NULL; }
g_used = 0;
ReleaseSRWLockExclusive(&g_lock);
WR_UNLOCK();
}
#ifndef _WIN32
// Linux: the link wraps the environment's changes too (tools/build_game.py linux_libs), so the table follows them.
int __real_setenv(const char *name, const char *value, int overwrite);
int __real_unsetenv(const char *name);
int __real_putenv(char *s);
int __wrap_setenv(const char *name, const char *value, int overwrite) { int r = __real_setenv(name, value, overwrite); runtime_env_changed(); return r; }
int __wrap_unsetenv(const char *name) { int r = __real_unsetenv(name); runtime_env_changed(); return r; }
int __wrap_putenv(char *s) { int r = __real_putenv(s); runtime_env_changed(); return r; }
#endif
+6 -2
View File
@@ -171,12 +171,16 @@ int plat_protect(uint64_t addr, uint64_t size, int prot) {
// ---- time
static LARGE_INTEGER qpf;
static uint64_t g_ns_mul; // ns per counter tick in 32.32 fixed point: a multiply, not a 128-bit division per call
uint64_t plat_now_ns(void) {
if (!qpf.QuadPart) QueryPerformanceFrequency(&qpf);
if (!g_ns_mul) {
QueryPerformanceFrequency(&qpf);
g_ns_mul = (uint64_t)(((unsigned __int128)1000000000u << 32) / (uint64_t)qpf.QuadPart);
}
LARGE_INTEGER t;
QueryPerformanceCounter(&t);
return (uint64_t)((unsigned __int128)t.QuadPart * 1000000000u / (uint64_t)qpf.QuadPart);
return (uint64_t)(((unsigned __int128)(uint64_t)t.QuadPart * g_ns_mul) >> 32);
}
#ifndef CREATE_WAITABLE_TIMER_HIGH_RESOLUTION
+15 -3
View File
@@ -217,6 +217,7 @@ static VkPhysicalDeviceDepthClipControlFeaturesEXT fdcc = {VK_STRUCTURE_TYPE_PHY
static VkPhysicalDeviceDepthClipEnableFeaturesEXT fdce = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DEPTH_CLIP_ENABLE_FEATURES_EXT};
static VkPhysicalDeviceCustomBorderColorFeaturesEXT fcbc = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_CUSTOM_BORDER_COLOR_FEATURES_EXT};
static VkPhysicalDeviceFaultFeaturesEXT ffault = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FAULT_FEATURES_EXT};
static VkPhysicalDeviceRobustness2FeaturesEXT frb2 = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ROBUSTNESS_2_FEATURES_EXT};
static VkPhysicalDeviceProvokingVertexFeaturesEXT fpv = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROVOKING_VERTEX_FEATURES_EXT};
static VkPhysicalDeviceConditionalRenderingFeaturesEXT fcr = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_CONDITIONAL_RENDERING_FEATURES_EXT};
static VkPhysicalDeviceIndexTypeUint8FeaturesEXT fu8 = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_INDEX_TYPE_UINT8_FEATURES_EXT};
@@ -268,9 +269,13 @@ static int create_device(void) {
{VK_AMD_BUFFER_MARKER_EXTENSION_NAME, NULL, &vkc.has_buffer_marker},
// the driver's own report of a lost device: what failed and where (AMD, Mesa; as totk-pc-recomp 112fb2d)
{VK_EXT_DEVICE_FAULT_EXTENSION_NAME, &ffault, &vkc.has_device_fault},
// development (TOTK_VK_ROBUST=2): out-of-range buffer reads give 0, as AMD's buffer descriptors always do
{VK_EXT_ROBUSTNESS_2_EXTENSION_NAME, &frb2, NULL},
};
const char *robust = getenv("TOTK_VK_ROBUST");
for (unsigned i = 0; i < sizeof want / sizeof *want; i++) {
if (!strcmp(want[i].name, VK_KHR_SWAPCHAIN_EXTENSION_NAME) && !vkc.surface) continue;
if (want[i].features == &frb2 && !(robust && !strcmp(robust, "2"))) continue;
if ((want[i].flag == &vkc.has_checkpoints || want[i].flag == &vkc.has_buffer_marker) && !getenv("TOTK_VK_CHECKPOINTS")) continue;
if (!ext_in(&ex, want[i].name)) { LOG("extension %s not available", want[i].name); continue; }
names[nnames++] = want[i].name;
@@ -324,9 +329,16 @@ static int create_device(void) {
fcbc.customBorderColors = (VkBool32)vkc.has_custom_border; // the features enabled are the ones used
fcbc.customBorderColorWithoutFormat = (VkBool32)vkc.has_custom_border;
// TOTK_VK_ROBUST=1 (development): bounds-checked buffer access, to tell a shader's out-of-range access from other
// causes of a lost device
if (!getenv("TOTK_VK_ROBUST")) f2.features.robustBufferAccess = VK_FALSE;
else LOG("robust buffer access %s (TOTK_VK_ROBUST)", f2.features.robustBufferAccess ? "on" : "not supported");
// causes of a lost device; =2 with robustBufferAccess2: a read past the bound range gives 0 (vertex fetch: 0 or
// (0,0,0,1)), as on AMD, whose buffer descriptors check the range in hardware whatever the device enables
frb2.robustImageAccess2 = frb2.nullDescriptor = VK_FALSE;
if (!robust) f2.features.robustBufferAccess = VK_FALSE;
else LOG("robust buffer access %s%s (TOTK_VK_ROBUST)", f2.features.robustBufferAccess ? "on" : "not supported",
frb2.robustBufferAccess2 ? ", reads past the range give 0 (robustBufferAccess2)" : "");
if (f2.features.robustBufferAccess && vkc.has_descriptor_buffer) { // robust buffer descriptors may be larger
vkc.db.uniformBufferDescriptorSize = vkc.db.robustUniformBufferDescriptorSize;
vkc.db.storageBufferDescriptorSize = vkc.db.robustStorageBufferDescriptorSize;
}
vkc.has_descriptor_buffer = vkc.has_descriptor_buffer && fdb.descriptorBuffer;
if (vkc.has_descriptor_buffer) fdb.descriptorBufferCaptureReplay = VK_FALSE; // tools only, costs driver work
if (vkc.has_descriptor_buffer)
@@ -33,8 +33,13 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
{
foreach (AstOperand local in function.Locals)
{
SpvInstruction localPointerType = context.TypePointer(StorageClass.Function, context.GetType(local.VarType));
SpvInstruction spvLocal = context.Variable(localPointerType, StorageClass.Function);
SpvInstruction localType = context.GetType(local.VarType);
SpvInstruction localPointerType = context.TypePointer(StorageClass.Function, localType);
// UltraTears: a register the program reads on a path where it was not written is 0, not undefined. The
// NVIDIA driver gives 0 there; Mesa (RADV) takes an undefined value as any value and folds what it
// reaches, so such a shader computed something else on AMD.
SpvInstruction spvLocal = context.Variable(localPointerType, StorageClass.Function, context.ConstantNull(localType));
context.AddLocalVariable(spvLocal);
context.DeclareLocal(local, spvLocal);
@@ -473,10 +478,9 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
context.Decorate(perVertexStructType, Decoration.Block);
if (context.HostCapabilities.ReducedPrecision)
{
context.MemberDecorate(perVertexStructType, 0, Decoration.Invariant);
}
// TOTK: Maxwell computes a position identically in every shader that runs the same code (a depth pre-pass drawn
// again with EQUAL), so the position is always invariant, not only with the ReducedPrecision job flag.
context.MemberDecorate(perVertexStructType, 0, Decoration.Invariant);
context.MemberDecorate(perVertexStructType, 0, Decoration.BuiltIn, (LiteralInteger)BuiltIn.Position);
context.MemberDecorate(perVertexStructType, 1, Decoration.BuiltIn, (LiteralInteger)BuiltIn.PointSize);
@@ -574,7 +578,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
context.Decorate(spvVar, Decoration.Patch);
}
if (context.HostCapabilities.ReducedPrecision && ioVariable == IoVariable.Position)
if (ioVariable == IoVariable.Position)
{
context.Decorate(spvVar, Decoration.Invariant);
}
@@ -1283,6 +1283,18 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
if (isShadow)
{
dRef = Src(AggregateType.FP32);
// TOTK: the shadow cascades are D16 array textures. Maxwell rounds the compare reference to the texture's
// 16-bit precision before the compare; Vulkan drivers may compare the full float (AMD does), so a lit
// surface fails its own compare by a fraction of a step and the whole face reads as shadowed. Array
// compares only: in TotK those are the D16 cascades (MacNativeGames 6283599 rounds the same ones).
if (isArray)
{
SpvInstruction d16Fp32 = context.TypeFP32();
SpvInstruction d16Clamped = context.GlslFClamp(d16Fp32, dRef, context.Constant(d16Fp32, 0f), context.Constant(d16Fp32, 1f));
SpvInstruction d16Scaled = context.FAdd(d16Fp32, context.FMul(d16Fp32, d16Clamped, context.Constant(d16Fp32, 65535f)), context.Constant(d16Fp32, 0.5f));
dRef = context.FMul(d16Fp32, context.GlslFloor(d16Fp32, d16Scaled), context.Constant(d16Fp32, 1f / 65535f));
}
}
SpvInstruction[] derivatives = null;
@@ -187,6 +187,17 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
{
context.AddEntryPoint(context.Definitions.Stage.Convert(), spvFunc, "main", context.GetMainInterface());
// UltraTears: 32-bit denormals as the translation is told (ULTRS_DENORM=preserve|ftz; unset: the driver's
// default). Maxwell keeps them unless an instruction says .FTZ; Mesa and AMD flush them by default.
string denorm = System.Environment.GetEnvironmentVariable("ULTRS_DENORM");
if (denorm == "preserve" || denorm == "ftz")
{
bool preserve = denorm == "preserve";
context.AddExtension("SPV_KHR_float_controls");
context.AddCapability(preserve ? Capability.DenormPreserve : Capability.DenormFlushToZero);
context.AddExecutionMode(spvFunc, preserve ? ExecutionMode.DenormPreserve : ExecutionMode.DenormFlushToZero, (SpvLiteralInteger)32);
}
if (context.Definitions.Stage == ShaderStage.TessellationControl)
{
context.AddExecutionMode(spvFunc, ExecutionMode.OutputVertices, (SpvLiteralInteger)context.Definitions.ThreadsPerInputPrimitive);
@@ -142,13 +142,26 @@ namespace Ryujinx.Graphics.Shader.Instructions
Operand size = context.BitfieldExtractU32(srcB, Const(8), Const(8));
// UltraTears: a field that runs past bit 31 is cut there, and one that starts past it is empty (Maxwell, as
// PTX bfe: 0, or for a signed field bit 31 repeated). SPIR-V leaves both undefined: AMD's driver takes the
// position modulo 32 and reads other bits. TOTK's Depths terrain vertex shaders read a 5-bit field at
// (2y + x) * 8 of a patch word, up to 96 for the vertices on a patch's far edges (0 there on the Switch):
// on AMD those vertices moved, and walls and ground of the Depths were missing (L20 on the Steam Deck).
Operand clampedPosition = context.IMinimumU32(position, Const(32));
Operand clampedSize = context.IMinimumU32(size, context.ISubtract(Const(32), clampedPosition));
Operand res = isSigned
? context.BitfieldExtractS32(srcA, position, size)
: context.BitfieldExtractU32(srcA, position, size);
? context.BitfieldExtractS32(srcA, clampedPosition, clampedSize)
: context.BitfieldExtractU32(srcA, clampedPosition, clampedSize);
Operand startsPast = context.ICompareGreaterOrEqualUnsigned(position, Const(32));
res = context.ConditionalSelect(startsPast, isSigned ? context.ShiftRightS32(srcA, Const(31)) : Const(0), res);
res = context.ConditionalSelect(context.ICompareEqual(size, Const(0)), Const(0), res);
context.Copy(GetDest(rd), res);
// TODO: CC, X, corner cases.
// TODO: CC, X.
}
private static void EmitBfi(EmitterContext context, Operand srcA, Operand srcB, Operand srcC, int rd)
@@ -157,7 +170,13 @@ namespace Ryujinx.Graphics.Shader.Instructions
Operand size = context.BitfieldExtractU32(srcB, Const(8), Const(8));
Operand res = context.BitfieldInsert(srcC, srcA, position, size);
// UltraTears: as in EmitBfe, the field is cut at bit 31 (none past it: C unchanged), defined in SPIR-V too.
Operand clampedPosition = context.IMinimumU32(position, Const(32));
Operand clampedSize = context.IMinimumU32(size, context.ISubtract(Const(32), clampedPosition));
Operand res = context.BitfieldInsert(srcC, srcA, clampedPosition, clampedSize);
res = context.ConditionalSelect(context.ICompareGreaterOrEqualUnsigned(position, Const(32)), srcC, res);
context.Copy(GetDest(rd), res);
}
@@ -19,6 +19,7 @@ namespace Ryujinx.Graphics.Shader.Translation
private readonly IGpuAccessor _gpuAccessor;
public UInt128 NextInputAttributesComponents { get; private set; }
public bool NextStageKnown { get; private set; }
public UInt128 ThisInputAttributesComponents { get; private set; }
public AttributeUsage(IGpuAccessor gpuAccessor)
@@ -55,6 +56,7 @@ namespace Ryujinx.Graphics.Shader.Translation
public void MergeFromtNextStage(bool gpPassthrough, bool nextUsesFixedFunctionAttributes, AttributeUsage nextStage)
{
NextStageKnown = true;
NextInputAttributesComponents = nextStage.ThisInputAttributesComponents;
NextUsedInputAttributesPerPatch = nextStage.UsedInputAttributesPerPatch;
NextUsesFixedFuncAttributes = nextUsesFixedFunctionAttributes;
@@ -210,6 +210,19 @@ namespace Ryujinx.Graphics.Shader.Translation
}
UInt128 usedAttributes = context.TranslatorContext.AttributeUsage.NextInputAttributesComponents;
// TOTK (2026-10-22): the host translates each stage on its own (no SetNextStage), so the components the next
// stage reads are unknown. Maxwell reads an unwritten output component as (0, 0, 0, 1) (a VS writing xyz of a
// varying the FS divides by w); start every component of every written output there, as a linked pipeline would
// for the components the next stage reads. The host adds outputs the VS never writes (vk_pipeline.c).
if (!context.TranslatorContext.AttributeUsage.NextStageKnown)
{
for (int mask = context.TranslatorContext.AttributeUsage.UsedOutputAttributes; mask != 0; mask &= mask - 1)
{
usedAttributes |= (UInt128)0xf << (int.TrailingZeroCount(mask) * 4);
}
}
while (usedAttributes != UInt128.Zero)
{
int index = (int)UInt128.TrailingZeroCount(usedAttributes);
@@ -0,0 +1,47 @@
--- a/src/Ryujinx.Graphics.Shader/Instructions/InstEmitBitfield.cs
+++ b/src/Ryujinx.Graphics.Shader/Instructions/InstEmitBitfield.cs
@@ -142,13 +142,26 @@ namespace Ryujinx.Graphics.Shader.Instructions
Operand size = context.BitfieldExtractU32(srcB, Const(8), Const(8));
+ // UltraTears: a field that runs past bit 31 is cut there, and one that starts past it is empty (Maxwell, as
+ // PTX bfe: 0, or for a signed field bit 31 repeated). SPIR-V leaves both undefined: AMD's driver takes the
+ // position modulo 32 and reads other bits. TOTK's Depths terrain vertex shaders read a 5-bit field at
+ // (2y + x) * 8 of a patch word, up to 96 for the vertices on a patch's far edges (0 there on the Switch):
+ // on AMD those vertices moved, and walls and ground of the Depths were missing (L20 on the Steam Deck).
+ Operand clampedPosition = context.IMinimumU32(position, Const(32));
+ Operand clampedSize = context.IMinimumU32(size, context.ISubtract(Const(32), clampedPosition));
+
Operand res = isSigned
- ? context.BitfieldExtractS32(srcA, position, size)
- : context.BitfieldExtractU32(srcA, position, size);
+ ? context.BitfieldExtractS32(srcA, clampedPosition, clampedSize)
+ : context.BitfieldExtractU32(srcA, clampedPosition, clampedSize);
+
+ Operand startsPast = context.ICompareGreaterOrEqualUnsigned(position, Const(32));
+
+ res = context.ConditionalSelect(startsPast, isSigned ? context.ShiftRightS32(srcA, Const(31)) : Const(0), res);
+ res = context.ConditionalSelect(context.ICompareEqual(size, Const(0)), Const(0), res);
context.Copy(GetDest(rd), res);
- // TODO: CC, X, corner cases.
+ // TODO: CC, X.
}
private static void EmitBfi(EmitterContext context, Operand srcA, Operand srcB, Operand srcC, int rd)
@@ -157,7 +170,13 @@ namespace Ryujinx.Graphics.Shader.Instructions
Operand size = context.BitfieldExtractU32(srcB, Const(8), Const(8));
- Operand res = context.BitfieldInsert(srcC, srcA, position, size);
+ // UltraTears: as in EmitBfe, the field is cut at bit 31 (none past it: C unchanged), defined in SPIR-V too.
+ Operand clampedPosition = context.IMinimumU32(position, Const(32));
+ Operand clampedSize = context.IMinimumU32(size, context.ISubtract(Const(32), clampedPosition));
+
+ Operand res = context.BitfieldInsert(srcC, srcA, clampedPosition, clampedSize);
+
+ res = context.ConditionalSelect(context.ICompareGreaterOrEqualUnsigned(position, Const(32)), srcC, res);
context.Copy(GetDest(rd), res);
}
@@ -0,0 +1,40 @@
--- a/src/Ryujinx.Graphics.Shader/Translation/AttributeUsage.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/AttributeUsage.cs
@@ -19,6 +19,7 @@
private readonly IGpuAccessor _gpuAccessor;
public UInt128 NextInputAttributesComponents { get; private set; }
+ public bool NextStageKnown { get; private set; }
public UInt128 ThisInputAttributesComponents { get; private set; }
public AttributeUsage(IGpuAccessor gpuAccessor)
@@ -55,6 +56,7 @@
public void MergeFromtNextStage(bool gpPassthrough, bool nextUsesFixedFunctionAttributes, AttributeUsage nextStage)
{
+ NextStageKnown = true;
NextInputAttributesComponents = nextStage.ThisInputAttributesComponents;
NextUsedInputAttributesPerPatch = nextStage.UsedInputAttributesPerPatch;
NextUsesFixedFuncAttributes = nextUsesFixedFunctionAttributes;
--- a/src/Ryujinx.Graphics.Shader/Translation/Translator.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/Translator.cs
@@ -210,6 +210,19 @@
}
UInt128 usedAttributes = context.TranslatorContext.AttributeUsage.NextInputAttributesComponents;
+
+ // TOTK (2026-10-22): the host translates each stage on its own (no SetNextStage), so the components the next
+ // stage reads are unknown. Maxwell reads an unwritten output component as (0, 0, 0, 1) (a VS writing xyz of a
+ // varying the FS divides by w); start every component of every written output there, as a linked pipeline would
+ // for the components the next stage reads. The host adds outputs the VS never writes (vk_pipeline.c).
+ if (!context.TranslatorContext.AttributeUsage.NextStageKnown)
+ {
+ for (int mask = context.TranslatorContext.AttributeUsage.UsedOutputAttributes; mask != 0; mask &= mask - 1)
+ {
+ usedAttributes |= (UInt128)0xf << (int.TrailingZeroCount(mask) * 4);
+ }
+ }
+
while (usedAttributes != UInt128.Zero)
{
int index = (int)UInt128.TrailingZeroCount(usedAttributes);
@@ -0,0 +1,25 @@
--- a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Declarations.cs
+++ b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Declarations.cs
@@ -465,10 +465,9 @@
context.Decorate(perVertexStructType, Decoration.Block);
- if (context.HostCapabilities.ReducedPrecision)
- {
- context.MemberDecorate(perVertexStructType, 0, Decoration.Invariant);
- }
+ // TOTK: Maxwell computes a position identically in every shader that runs the same code (a depth pre-pass drawn
+ // again with EQUAL), so the position is always invariant, not only with the ReducedPrecision job flag.
+ context.MemberDecorate(perVertexStructType, 0, Decoration.Invariant);
context.MemberDecorate(perVertexStructType, 0, Decoration.BuiltIn, (LiteralInteger)BuiltIn.Position);
context.MemberDecorate(perVertexStructType, 1, Decoration.BuiltIn, (LiteralInteger)BuiltIn.PointSize);
@@ -566,7 +565,7 @@
context.Decorate(spvVar, Decoration.Patch);
}
- if (context.HostCapabilities.ReducedPrecision && ioVariable == IoVariable.Position)
+ if (ioVariable == IoVariable.Position)
{
context.Decorate(spvVar, Decoration.Invariant);
}
@@ -0,0 +1,21 @@
--- a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Instructions.cs
+++ b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Instructions.cs
@@ -1283,6 +1283,18 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
if (isShadow)
{
dRef = Src(AggregateType.FP32);
+
+ // TOTK: the shadow cascades are D16 array textures. Maxwell rounds the compare reference to the texture's
+ // 16-bit precision before the compare; Vulkan drivers may compare the full float (AMD does), so a lit
+ // surface fails its own compare by a fraction of a step and the whole face reads as shadowed. Array
+ // compares only: in TotK those are the D16 cascades (MacNativeGames 6283599 rounds the same ones).
+ if (isArray)
+ {
+ SpvInstruction d16Fp32 = context.TypeFP32();
+ SpvInstruction d16Clamped = context.GlslFClamp(d16Fp32, dRef, context.Constant(d16Fp32, 0f), context.Constant(d16Fp32, 1f));
+ SpvInstruction d16Scaled = context.FAdd(d16Fp32, context.FMul(d16Fp32, d16Clamped, context.Constant(d16Fp32, 65535f)), context.Constant(d16Fp32, 0.5f));
+ dRef = context.FMul(d16Fp32, context.GlslFloor(d16Fp32, d16Scaled), context.Constant(d16Fp32, 1f / 65535f));
+ }
}
SpvInstruction[] derivatives = null;
@@ -0,0 +1,18 @@
--- a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Declarations.cs
+++ b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Declarations.cs
@@ -33,8 +33,13 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
{
foreach (AstOperand local in function.Locals)
{
- SpvInstruction localPointerType = context.TypePointer(StorageClass.Function, context.GetType(local.VarType));
- SpvInstruction spvLocal = context.Variable(localPointerType, StorageClass.Function);
+ SpvInstruction localType = context.GetType(local.VarType);
+ SpvInstruction localPointerType = context.TypePointer(StorageClass.Function, localType);
+
+ // UltraTears: a register the program reads on a path where it was not written is 0, not undefined. The
+ // NVIDIA driver gives 0 there; Mesa (RADV) takes an undefined value as any value and folds what it
+ // reaches, so such a shader computed something else on AMD.
+ SpvInstruction spvLocal = context.Variable(localPointerType, StorageClass.Function, context.ConstantNull(localType));
context.AddLocalVariable(spvLocal);
context.DeclareLocal(local, spvLocal);
+9 -4
View File
@@ -32,8 +32,9 @@ TARGETS = {'probe_cpu': ['src/probe/probe_cpu.c'],
'src/host/audio_reverb.c', 'src/host/opus_codec.c', 'src/host/movie_host.c', 'src/host/hid_feedback.c',
'src/host/shared_font.c',
'src/host/window_sdl.c', 'src/vk/vk.c', 'src/vk/present.c', 'src/vk/fsr.c', 'src/vk/overlay.c', 'src/host/motion_fusion.c',
'src/host/deck_imu.c',
'src/host/profiler.c', 'src/host/watch.c', 'src/host/frame_rate.c', 'src/host/render_res.c', 'src/host/ui_capture.c',
'src/host/ui_aspect.c']}
'src/host/ui_aspect.c', 'src/host/zstd_host.c']}
# The graphics front end (MacNativeGames' NVN layer) with its macOS stand-ins: own include paths, own rule
GFX = ['src/gfx/nvn/hle_nvn.c', 'src/gfx/nvn/nvn_tex.c', 'src/gfx/nvn/totk_nvn_ext.c', 'src/gfx/nvn/nvn_registry.c',
'src/gfx/nvn/nvn_thunks.gen.c', 'src/gfx/nvn/nvn_dirty_win.c', 'src/gfx/nvn/totk_timeline_win.c',
@@ -51,7 +52,8 @@ GAME_CFLAGS = CFLAGS + ' -w -Isrc/runtime'
# Linux (--platform linux): the kit is deps/linux in a builder, else dev-local/linux-cross (ULTRS_LINUX_KIT overrides).
LINUX_KIT = pathlib.Path(os.environ.get('ULTRS_LINUX_KIT') or next(
(p for p in (ROOT / 'deps/linux', ROOT / 'dev-local/linux-cross') if (p / 'sysroot').is_dir()), ROOT / 'deps/linux'))
LINUX_RUNTIME = ['src/runtime/dispatch.c', 'src/runtime/loader.c', 'src/runtime/platform/linux.c', 'src/runtime/logcost.c', 'src/runtime/fma.c']
LINUX_RUNTIME = ['src/runtime/dispatch.c', 'src/runtime/loader.c', 'src/runtime/platform/linux.c', 'src/runtime/logcost.c', 'src/runtime/fma.c',
'src/runtime/envcache.c']
# The rpath of the Linux programs, as ninja writes it into a command: on Linux ninja runs commands through sh, which
@@ -68,7 +70,8 @@ def linux_flags():
def linux_libs():
# SDL3 shared (beside the game: the sysroot's static one is not position-independent); Opus, libvpx, zstd and
# libstdc++ static, so the game needs only glibc, the Vulkan loader and that SDL3.
return (f'-L{LINUX_KIT.as_posix()}/lib -Wl,--wrap=fprintf,--wrap=vfprintf,--wrap=fputs,--wrap=fputc -lSDL3 '
return (f'-L{LINUX_KIT.as_posix()}/lib -Wl,--wrap=fprintf,--wrap=vfprintf,--wrap=fputs,--wrap=fputc '
'-Wl,--wrap=getenv,--wrap=setenv,--wrap=unsetenv,--wrap=putenv -lSDL3 ' # src/runtime/envcache.c
'-l:libopus.a -l:libvpx.a -l:libzstd.a -l:libstdc++.a -lm -lpthread -ldl')
@@ -218,7 +221,9 @@ def main():
# [temporary] a function with an unresolved computed branch is one switch over every
# instruction; with thousands of labels clang -O1/-O2 takes from minutes to hours per file (main_376:
# 20+ min at -O1), -O0 15 s. Such functions run unoptimized until their tables resolve.
if 'L_dispatch:' in text and text.count(chr(10) + 'L_') > 2000:
# Counted per function: a file's total once sent whole files of small dispatching functions to -O0
# (main_000 and sdk_130 of 1.4.3: ~630 functions unoptimized for dispatches of 133 and 385 labels).
if any('L_dispatch:' in f and f.count(chr(10) + 'L_') > 2000 for f in text.split(chr(10) + 'void f_')):
lines.append(' gameflags = ' + game_cflags.replace('-O2', '-O0'))
game_objs.append(obj)
if not game_objs:
+145
View File
@@ -0,0 +1,145 @@
"""A/B check of the lifter's C: guest functions as gen.py emits them with registers and flags in the Cpu, against the
same functions with them in C locals (tools/lifter/regs.py, flags.py), run from the same random states by
tools/lifter/ab_harness.c (which says how). Windows, development: reads the module from your dump's ExeFS and writes
the generated C, the image and the harness into --out (game-derived: dev-local/, never committed).
Picks functions with 6 to 600 instructions that the locals path emits differently, no SVC, exclusive or system-register
access, and no patched site; a quarter of them without calls. Prints, per function, the cases that agreed, differed
and faulted on both sides, and the registers that differed in the first few.
Usage: python tools/lifter/ab_check.py --exefs ../release-test/data/exefs [--module main] [--functions 300]
[--cases 48] [--seed 1] [--out dev-local/tmp/ab]
"""
import argparse
import os
import random
import re
import struct
import subprocess
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
import gen # noqa: E402
import regs # noqa: E402
from nso import R_AARCH64_RELATIVE # noqa: E402
from patches import PATCHES, PRE_PATCHES # noqa: E402
ROOT = Path(__file__).resolve().parents[2]
BASES = {'main': 0x800000000, 'subsdk0': 0x804000000, 'sdk': 0x805000000} # tools/gen_game.py MODULES
FN = re.compile(r'\bf_([0-9a-f]+)\b')
FORBID = re.compile(r'rt_svc|ldx\d|stx\d|ldxp|stxp|rt_fpcr|tpidr|cntv|cntp|rt_ticks|excl_')
def image(m, base):
img = bytearray(m.nso.image)
for off, typ, _sym, add in m.nso.relocs:
if typ == R_AARCH64_RELATIVE:
struct.pack_into('<Q', img, off, base + add)
return bytes(img)
def main():
ap = argparse.ArgumentParser()
ap.add_argument('--exefs', required=True)
ap.add_argument('--module', default='main')
ap.add_argument('--functions', type=int, default=300)
ap.add_argument('--cases', type=int, default=48)
ap.add_argument('--seed', type=int, default=1)
ap.add_argument('--out', default=str(ROOT / 'dev-local/tmp/ab'))
ap.add_argument('--clang', default='C:/msys64/clang64/bin/clang')
a = ap.parse_args()
base = BASES[a.module]
out = Path(a.out)
out.mkdir(parents=True, exist_ok=True)
path = str(Path(a.exefs) / a.module)
old = gen.Module(a.module, path, base, 'c')
new = gen.Module(a.module, path, base, 'c')
new.reg_locals, new.local_flags = regs.LOCALS, True
entries, _ = gen.partition(old, old.boundaries())
rnd = random.Random(a.seed)
order = sorted(entries)
rnd.shuffle(order)
picked, nocall = [], 0
for e in order:
if len(picked) >= a.functions:
break
end, labels, tables = entries[e]
n = (end - e) // 4
if not 6 <= n <= 600 or any(v == 'all' for v in tables.values()):
continue
if any(x in PATCHES or x in PRE_PATCHES for x in range(e, end, 4)):
continue
try:
o = old.emit_function(e, end, labels, tables, {})
nl = new.emit_function(e, end, labels, tables, {})
except Exception:
continue
to, tn = '\n'.join(o), '\n'.join(nl)
if to == tn or FORBID.search(to):
continue
calls = 'rt_dispatch(c);' in to or re.search(r' f_[0-9a-f]+\(c\);', to)
if not calls and nocall >= a.functions // 4:
continue
nocall += not calls
picked.append((e, to, tn))
print(f'{len(picked)} functions picked ({nocall} without calls)', flush=True)
refs = set()
for e, to, tn in picked:
refs |= set(FN.findall(to)) | set(FN.findall(tn))
tus = {}
for tag, k in (('o', 1), ('n', 2)):
text = ['#include "cpu.h"\n#include "simd.h"\n']
text += [f'void f_{r}(Cpu *c);\n' for r in sorted(refs)]
for p in picked:
t = p[k].replace(f'void f_{p[0]:x}(Cpu *c) {{', f'void {tag}_{p[0]:x}(Cpu *c) {{', 1)
text.append(t + '\n')
tus[tag] = ''.join(text)
(out / 'ab_old.c').write_text(tus['o'])
(out / 'ab_new.c').write_text(tus['n'])
stubs = ['#include "cpu.h"\nvoid ab_stub(Cpu *c);\n'] + [f'void f_{r}(Cpu *c) {{ ab_stub(c); }}\n' for r in sorted(refs)]
stubs.append('typedef struct { uint64_t addr; GuestFn old_fn, new_fn; } ab_fn;\n')
stubs += [f'void o_{e:x}(Cpu *c); void n_{e:x}(Cpu *c);\n' for e, _, _ in picked]
stubs.append('const ab_fn ab_fns[] = {\n' + ''.join(f' {{{e:#x}ull, o_{e:x}, n_{e:x}}},\n' for e, _, _ in picked) + '};\n')
stubs.append(f'const int ab_nfns = {len(picked)};\n')
(out / 'ab_table.c').write_text(''.join(stubs))
(out / f'{a.module}.img').write_bytes(image(old, base))
exe = out / 'ab.exe'
cmd = [a.clang, '-O2', '-march=x86-64-v3', '-fno-strict-aliasing', '-w', f'-I{ROOT / "src/runtime"}', '-o', str(exe),
str(ROOT / 'tools/lifter/ab_harness.c'), str(out / 'ab_old.c'), str(out / 'ab_new.c'), str(out / 'ab_table.c'), '-lm']
print('compiling', flush=True)
r = subprocess.run(cmd, capture_output=True, text=True)
if r.returncode:
print(r.stderr[-4000:])
sys.exit(1)
start, bad_fns, hangs, totals = 0, [], [], [0, 0, 0]
while start < len(picked):
p = subprocess.run([str(exe), str(out / f'{a.module}.img'), hex(base), str(start), str(a.cases), str(a.seed)],
capture_output=True, text=True)
cur = start
for line in p.stdout.splitlines():
if line.startswith('FN '):
cur = int(line.split()[1])
elif line.startswith('RESULT'):
f = line.split()
ok, bad, faults = int(f[3]), int(f[5]), int(f[7])
totals[0] += ok; totals[1] += bad; totals[2] += faults
if bad:
bad_fns.append(picked[cur][0])
print(f'{picked[cur][0]:#x}: {bad} of {ok + bad} differ', flush=True)
elif line.startswith(' case'):
print(line, flush=True)
if 'DONE' in p.stdout:
break
if 'HANG' in p.stdout:
hangs.append(picked[cur][0])
else:
print(f'harness stopped at function {cur} (exit {p.returncode}): {p.stderr[-500:]}', flush=True)
start = cur + 1
print(f'cases: {totals[0]} agreed, {totals[1]} differed, {totals[2]} faulted on both; functions that differed: '
f'{len(bad_fns)}, hung: {len(hangs)}')
sys.exit(1 if bad_fns else 0)
if __name__ == '__main__':
main()
+209
View File
@@ -0,0 +1,209 @@
// A/B check of the lifter (tools/lifter/ab_check.py): every picked guest function is compiled twice, as gen.py emitted
// it with registers and flags in the Cpu (o_<addr>) and with them in C locals (n_<addr>), and both run from the same
// random states. Development tool: the module image it maps is made from the player's own dump at run time.
//
// The module image is mapped at its base, writable, with a write watch: after each run the pages it wrote are hashed
// and put back. x0-x7 point into a scratch buffer (or hold small integers), the buffer holds pointers into itself and
// random data, SP sits near its top. Every call and indirect branch goes to one stub (ab_stub) on both sides instead of
// its callee: it folds the guest state at the call into a digest and sets the caller-saved registers from it, so a
// register the locals version did not store before the call, or did not load after it, shows. A fault or trap ends a
// run (vectored handler, longjmp); a run that does not finish in 2 s ends the process (the caller skips the function).
#include "cpu.h"
#include <setjmp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <windows.h>
typedef struct { uint64_t addr; GuestFn old_fn, new_fn; } ab_fn;
extern const ab_fn ab_fns[];
extern const int ab_nfns;
volatile int g_fnprof;
void fnprof_hit(uint64_t a, uint64_t lr, uint64_t x0) { (void)a; (void)lr; (void)x0; }
enum { BUF_SIZE = 0x40000 };
#define BUF 0x7200000000ull
#define PTR_LO (BUF + 0x8000)
#define PTR_HI (BUF + 0x28000)
#define STACK_TOP (BUF + 0x3F000)
static uint64_t mix(uint64_t z) {
z += 0x9E3779B97F4A7C15ull;
z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ull;
z = (z ^ (z >> 27)) * 0x94D049BB133111EBull;
return z ^ (z >> 31);
}
static uint64_t fnv(uint64_t h, const void *p, size_t n) {
const uint8_t *b = p;
for (size_t i = 0; i < n; i++) h = (h ^ b[i]) * 0x100000001B3ull;
return h;
}
static jmp_buf *g_jb;
static volatile int g_status; // 0 finished, 1 fault, 2 trap or SVC, 3 a loop around calls
static uint64_t g_digest;
static int g_calls;
void ab_stub(Cpu *c) {
uint64_t h = g_digest ^ 0x5bd1e995u;
for (int i = 0; i < 31; i++) h = mix(h ^ c->x[i]);
h = mix(h ^ c->sp);
for (int i = 0; i < 8; i++) h = mix(h ^ (uint64_t)c->v[i]);
g_digest = h;
if (++g_calls > 100000) { g_status = 3; longjmp(*g_jb, 1); }
for (int i = 0; i <= 18; i++) c->x[i] = mix(h + (uint64_t)i);
c->x[30] = mix(h + 30);
for (int i = 0; i < 8; i++) c->v[i] = mix(h + 100 + (uint64_t)i);
c->nzcv = h & 0xF0000000u;
}
void rt_dispatch(Cpu *c) { ab_stub(c); }
static volatile int g_in_run;
static volatile uint64_t g_fault_at;
void rt_udf(Cpu *c, uint64_t pc) { (void)c; g_status = 2; g_fault_at = pc; longjmp(*g_jb, 1); }
void rt_brk(Cpu *c, uint32_t imm, uint64_t pc) { (void)c; (void)imm; g_status = 2; g_fault_at = pc; longjmp(*g_jb, 1); }
void rt_svc(Cpu *c, uint32_t imm, uint64_t pc) { (void)c; (void)imm; g_status = 2; g_fault_at = pc; longjmp(*g_jb, 1); }
uint64_t rt_ticks(void) { return 0; }
void rt_spin_gate(Cpu *c, uint64_t head) { (void)c; (void)head; }
static LONG CALLBACK veh(EXCEPTION_POINTERS *e) {
if (!g_in_run) return EXCEPTION_CONTINUE_SEARCH;
DWORD code = e->ExceptionRecord->ExceptionCode;
if (code == EXCEPTION_ACCESS_VIOLATION || code == EXCEPTION_INT_DIVIDE_BY_ZERO || code == EXCEPTION_ILLEGAL_INSTRUCTION ||
code == EXCEPTION_STACK_OVERFLOW || code == EXCEPTION_DATATYPE_MISALIGNMENT) {
g_status = 1;
g_fault_at = code == EXCEPTION_ACCESS_VIOLATION ? (uint64_t)e->ExceptionRecord->ExceptionInformation[1] : code;
g_in_run = 0;
longjmp(*g_jb, 1);
}
return EXCEPTION_CONTINUE_SEARCH;
}
static uint8_t *g_img, *g_pristine;
static uint64_t g_base, g_size;
static volatile LONG64 g_run_started;
static DWORD WINAPI watchdog(void *p) {
(void)p;
for (;;) {
Sleep(200);
LONG64 t = g_run_started;
if (t && GetTickCount64() - (ULONGLONG)t > 2000) { fprintf(stdout, "HANG\n"); fflush(stdout); ExitProcess(3); }
}
}
typedef struct { uint64_t x[31], sp, v[32][2], digest, mem, buf; int calls, status; } result;
static void state(Cpu *c, uint64_t seed) {
memset(c, 0, sizeof *c);
uint8_t *b = (uint8_t *)(uintptr_t)BUF;
uint64_t s = seed;
for (size_t i = 0; i < BUF_SIZE; i += 8) {
s = mix(s);
uint64_t v = (s & 3) == 0 ? PTR_LO + ((s >> 8) % (PTR_HI - PTR_LO) & ~7ull) : (s & 3) == 1 ? (s >> 40) & 0xff : s;
memcpy(b + i, &v, 8);
}
for (int i = 0; i < 31; i++) {
s = mix(s);
int k = (int)(s & 3);
c->x[i] = k == 0 || (i < 8 && k == 1) ? PTR_LO + ((s >> 8) % (PTR_HI - PTR_LO) & ~7ull) : k == 2 ? (s >> 32) & 0xff : s;
}
c->x[30] = 0x7210000000ull;
c->sp = STACK_TOP;
for (int i = 0; i < 32; i++) { s = mix(s); c->v[i] = ((__uint128_t)mix(s) << 64) | s; }
s = mix(s);
c->nzcv = s & 0xF0000000u;
}
static void run(GuestFn f, uint64_t seed, result *r) {
static Cpu cpu __attribute__((aligned(64)));
Cpu *c = &cpu;
state(c, seed);
g_digest = 0; g_calls = 0; g_status = 0; g_fault_at = 0;
jmp_buf jb;
g_jb = &jb;
g_run_started = (LONG64)GetTickCount64();
if (!setjmp(jb)) {
g_in_run = 1;
f(c);
g_in_run = 0;
}
g_in_run = 0;
g_run_started = 0;
memset(r, 0, sizeof *r);
r->status = g_status;
if (r->status) return;
memcpy(r->x, c->x, sizeof r->x);
r->sp = c->sp;
memcpy(r->v, c->v, sizeof r->v);
r->digest = g_digest; r->calls = g_calls;
r->buf = fnv(0xcbf29ce484222325ull, (void *)(uintptr_t)BUF, BUF_SIZE);
// module pages written: hashed, then put back
static void *pages[1 << 16];
ULONG_PTR n = sizeof pages / sizeof *pages; DWORD gran;
uint64_t h = 0xcbf29ce484222325ull;
if (GetWriteWatch(WRITE_WATCH_FLAG_RESET, g_img, g_size, pages, &n, &gran) == 0)
for (ULONG_PTR i = 0; i < n; i++) {
uint8_t *p = pages[i];
h = fnv(h, &p, sizeof p); h = fnv(h, p, gran);
memcpy(p, g_pristine + (p - g_img), gran);
}
r->mem = h;
ResetWriteWatch(g_img, g_size);
}
static void restore_after_fault(void) { // a run that faulted may have written module pages too
static void *pages[1 << 16];
ULONG_PTR n = sizeof pages / sizeof *pages; DWORD gran;
if (GetWriteWatch(WRITE_WATCH_FLAG_RESET, g_img, g_size, pages, &n, &gran) == 0)
for (ULONG_PTR i = 0; i < n; i++) memcpy(pages[i], g_pristine + ((uint8_t *)pages[i] - g_img), gran);
ResetWriteWatch(g_img, g_size); // the copies back are writes too
}
int main(int argc, char **argv) {
if (argc < 6) { fprintf(stderr, "usage: %s <image> <base> <from> <cases> <seed>\n", argv[0]); return 2; }
g_base = strtoull(argv[2], NULL, 0);
int from = atoi(argv[3]), cases = atoi(argv[4]);
uint64_t seed = strtoull(argv[5], NULL, 0);
FILE *f = fopen(argv[1], "rb");
if (!f) { perror(argv[1]); return 2; }
fseek(f, 0, SEEK_END); g_size = (uint64_t)ftell(f); fseek(f, 0, SEEK_SET);
g_img = VirtualAlloc((void *)(uintptr_t)g_base, g_size, MEM_RESERVE | MEM_COMMIT | MEM_WRITE_WATCH, PAGE_READWRITE);
g_pristine = malloc(g_size);
void *buf = VirtualAlloc((void *)(uintptr_t)BUF, BUF_SIZE, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
if (!g_img || !g_pristine || !buf || fread(g_pristine, 1, g_size, f) != g_size) { fprintf(stderr, "cannot map\n"); return 2; }
fclose(f);
memcpy(g_img, g_pristine, g_size);
ResetWriteWatch(g_img, g_size);
AddVectoredExceptionHandler(1, veh);
CreateThread(NULL, 0, watchdog, NULL, 0, NULL);
int self_check = getenv("AB_SELF") != NULL; // the Cpu version against itself: what the harness alone gives
for (int i = from; i < ab_nfns; i++) {
printf("FN %d %llx\n", i, (unsigned long long)ab_fns[i].addr);
fflush(stdout);
int ok = 0, bad = 0, faults = 0;
for (int k = 0; k < cases; k++) {
uint64_t s = mix(seed ^ ab_fns[i].addr ^ ((uint64_t)k << 48));
result a, b;
run(ab_fns[i].old_fn, s, &a);
if (a.status) restore_after_fault();
run(self_check ? ab_fns[i].old_fn : ab_fns[i].new_fn, s, &b);
if (b.status) restore_after_fault();
if (a.status && b.status) { faults++; continue; }
if (a.status != b.status || memcmp(&a, &b, sizeof a)) {
bad++;
if (bad <= 2) {
printf(" case %d: status %d/%d calls %d/%d digest %d mem %d buf %d sp %d", k, a.status, b.status, a.calls, b.calls,
a.digest != b.digest, a.mem != b.mem, a.buf != b.buf, a.sp != b.sp);
for (int r = 0; r < 31; r++) if (a.x[r] != b.x[r]) printf(" x%d", r);
for (int r = 0; r < 32; r++) if (memcmp(a.v[r], b.v[r], 16)) printf(" v%d", r);
printf("\n");
}
} else ok++;
}
printf("RESULT %d ok %d bad %d faults %d\n", i, ok, bad, faults);
fflush(stdout);
}
printf("DONE\n");
return 0;
}
+146
View File
@@ -0,0 +1,146 @@
# From totk-pc-recomp recomp/flags.py @ v0.3.1 (MIT, Copyright (c) 2026 Valiantsin Dzerakh and contributors); used by
# tools/lifter/gen.py Module.emit_function. Unchanged below but for this header.
"""Function-local NZCV for tools/lifter/gen.py.
The lifter writes flags the way the guest does: every flag-setting instruction packs N, Z, C and V into c->nzcv
(subs64() and friends in src/runtime/cpu.h) and every reader unpacks them (COND_xx(c)). Compilers keep NZCV inside a
function, and AAPCS64 neither passes it to a callee nor preserves it across a call. So a generated function keeps its
flags in a local `Flags F` (src/runtime/cpu.h), which clang holds in registers, and writes c->nzcv only where it leaves
for another function other than by a call or a return (see plan()).
A flag-setting instruction of a known kind (subtract, add or logic result; 32 or 64 bits) stores its operands in F.a
and F.b. A reader all of whose reaching definitions are of one kind computes its condition from those operands, so
`cmp x1, x2; b.lt` becomes a signed compare in C. Instructions whose result is only known packed (CCMP, FCMP, MSR
NZCV, ADCS) write F.nz, the packed form; a reader whose definitions are of several kinds reads F.nz, and those
definitions then also write it.
A read that can see flags a callee left reads the caller's flags from before the call instead. AAPCS64 code never
reads a callee's flags, so such a read only sits on a path that does not run: in TotK it follows calls to noreturn
functions (asserts) that the compiler placed before another branch's target. Plan.after_call counts them.
"""
import re
KIND_DEF = re.compile(r'\b(subs|adds|logic)(32|64)\(c, ')
CARRY_DEF = re.compile(r'\b(adcs|sbcs)(32|64)\(c, ')
CARRY_USE = re.compile(r'\b(adc|sbc)(32|64)\(c, ')
COND = re.compile(r'COND_([A-Z]{2})\(c\)')
NZ_DEF = 'c->nzcv = '
NZ_USE = '(c->nzcv)'
KIND_NAME = {'subs': 'sub', 'adds': 'add', 'logic': 'logic'}
ENTRY, CALL = 'entry', 'call'
class FlagError(Exception):
pass
def effect(code):
"""(reads flags, kind of the flags written or None) of one instruction's C."""
reads = bool(COND.search(code) or CARRY_USE.search(code) or CARRY_DEF.search(code) or NZ_USE in code)
if NZ_DEF in code or CARRY_DEF.search(code):
return reads, 'nz'
defs = KIND_DEF.findall(code)
if len(defs) > 1:
raise FlagError(f'several flag definitions in one instruction: {code}')
if defs:
op, size = defs[0]
return reads, KIND_NAME[op] + size
return reads, None
class Plan:
"""Per instruction: the kind its flag reads use, whether its flag definition also writes F.nz, and what it writes
to c->nzcv before it leaves for another function."""
def __init__(self, n):
self.reader = [None] * n
self.pack = [False] * n
self.exit = [None] * n # kind of the flags to publish in c->nzcv at a tail exit, or None
self.entry = False # F.nz starts as c->nzcv: flags from the entry reach a read or an exit
self.entry_read = False # they reach a read, not only an exit
self.after_call = 0 # reads that can see flags a callee left (see the module docstring)
self.used = False
def decl(self):
"""Declaration of F at the top of the function."""
return 'Flags F = {.nz = c->nzcv};' if self.entry else 'Flags F = {0};'
def publish(self, i):
"""Statement writing the flags to c->nzcv at instruction i's tail exit ('' when it need not)."""
k = self.exit[i]
return f'c->nzcv = fnz_{k}(&F); ' if k else ''
def rewrite(self, i, code):
"""code of instruction i with its flag reads and writes on F."""
rk = self.reader[i]
_, dk = effect(code)
if rk:
code = COND.sub(lambda m: f'fc_{rk}(&F, CC_{m.group(1)})', code)
code = CARRY_DEF.sub(lambda m: f'{m.group(1)}{m.group(2)}f(&F, fc_{rk}(&F, CC_CS), ', code)
code = CARRY_USE.sub(lambda m: f'{m.group(1)}{m.group(2)}f(fc_{rk}(&F, CC_CS), ', code)
code = code.replace(NZ_USE, f'(fnz_{rk}(&F))')
if dk == 'nz':
code = code.replace(NZ_DEF, 'F.nz = ')
code = re.sub(r'\(void\)(subs|adds)(32|64)\(c, ', lambda m: f'F.nz = nz_{KIND_NAME[m.group(1)]}{m.group(2)}(',
code)
elif dk:
code = KIND_DEF.sub(lambda m: f'f{KIND_NAME[m.group(1)]}{m.group(2)}(&F, ', code)
if self.pack[i]:
code += f' F.nz = fnz_{dk}(&F);'
if 'c->nzcv' in code or COND.search(code) or KIND_DEF.search(code) or CARRY_USE.search(code):
raise FlagError(f'flag access left after the rewrite: {code}')
return code
def plan(effects, succ, calls, exits):
"""effects[i]: (reads, kind or None); succ[i]: successor indices inside the function; calls: indices of calls,
whose callee may leave any flags; exits: indices that can leave for another function other than by a call or a
return (a tail branch, falling into the next function, a br through rt_dispatch).
Discovery can start a function inside another one (a branch target, a materialised address), and the code
before it then falls or branches into it with live flags. So a tail exit publishes the flags in c->nzcv, and a
function whose reads can see its entry flags starts F.nz from c->nzcv. Calls and returns publish nothing: AAPCS64
code neither passes flags to a callee nor reads a callee's, so a call's flags count as no definition."""
n = len(effects)
p = Plan(n)
if not any(r or k for r, k in effects):
return p
p.used = True
reach = [set() for _ in range(n)]
reach[0].add(ENTRY)
work = [0]
while work:
i = work.pop()
out = {i} if effects[i][1] else ({CALL} if i in calls else reach[i])
for s in succ[i]:
if not out <= reach[s]:
reach[s] |= out
work.append(s)
def kind(d):
return 'nz' if d == ENTRY else effects[d][1]
def use(r):
"""Kind a use of the definitions r reads; several kinds read the packed form, which they then all write."""
kinds = {kind(d) for d in r}
if ENTRY in r:
p.entry = True
if len(kinds) == 1:
return kinds.pop()
for d in r:
if d != ENTRY:
p.pack[d] = True
return 'nz' # several kinds, or none (unreachable)
for i, (reads, _) in enumerate(effects):
if reads:
p.after_call += CALL in reach[i]
p.reader[i] = use(reach[i] - {CALL})
p.entry_read |= ENTRY in reach[i]
for i in exits:
# Entry flags alone are still in c->nzcv.
r = reach[i] - {CALL}
if r and r != {ENTRY}:
p.exit[i] = use(r)
return p
+190 -30
View File
@@ -24,6 +24,8 @@ from unicorn import arm64_const as A
from analyze import eh_frame_functions
from lifter import Insn, Lifter, Unsupported, bits, emit_asm_batch, hx, sext
from nso import Nso, R_AARCH64_RELATIVE
import flags
import regs
SCRATCH_MEM = 0x7300000000
PROBE_DELTA = 0x12345674 # Module.lift: changes the page and the low bits of an address
@@ -624,12 +626,150 @@ class Module:
return ['return;']
raise Unsupported(k)
# Functions lifted without locals: their hand-written C (tools/lifter/patches.py) reads and writes the Cpu, and the
# frame-rate controller (main+0x2db174) gets X21 changed by nvnWindowSetPresentInterval under it (hle_nvn.c).
NO_LOCALS_AT = (0x8002db2ec,)
reg_locals = 0 # set by main(): registers per function in C locals (tools/lifter/regs.py), 0: none
local_flags = False # and NZCV in a local (tools/lifter/flags.py)
def emit_local(self, entry, end, labels, tables):
"""The function [entry, end) with guest registers and flags in C locals (tools/lifter/regs.py, flags.py, the
scheme of totk-pc-recomp recomp/gen.py Emitter.function, MIT), or None where it keeps them in the Cpu: an
unresolved computed branch (any instruction is an entry), a patched site, an asm word, or flags the plan
cannot place (flags.FlagError)."""
if any(v == 'all' for v in tables.values()):
return None
if any(a in PATCHES or a in PRE_PATCHES for a in range(entry, end, 4)) or \
any(entry <= a < end for a in self.NO_LOCALS_AT):
return None
insns = [self.lift(a) for a in range(entry, end, 4)]
if any(ins.kind == 'asm' for ins in insns):
return None
n = len(insns)
def idx(t):
return [(t - entry) // 4] if entry <= t < end else []
succ = []
for i, ins in enumerate(insns):
k = ins.kind
if k in ('op', 'bl', 'blr'):
succ.append([i + 1])
elif k == 'b':
succ.append(idx(ins.target))
elif k == 'bcond':
succ.append(idx(ins.target) + [i + 1])
elif k == 'br':
jt = tables.get(entry + 4 * i)
succ.append([t for x in jt for t in idx(x)] if jt else [])
else:
succ.append([]) # ret, trap
succ.append([]) # n: falling into the next function
fp = None
if self.local_flags:
calls, exits, effects = set(), {n}, []
for i, ins in enumerate(insns):
k = ins.kind
if k in ('bl', 'blr'):
calls.add(i)
elif (k in ('b', 'bcond') and not entry <= ins.target < end) or k == 'br':
exits.add(i)
code = ins.cond if k == 'bcond' else ins.code if k in ('op', 'trap') else ''
effects.append(flags.effect(code or ''))
effects.append((False, None))
fp = flags.plan(effects, succ, calls, exits)
if not fp.used:
fp = None
locs = bool(self.reg_locals)
spill, reload, reload_call = (regs.SPILL, regs.RELOAD, regs.RELOAD_CALL) if locs else ('', '', '')
refs = set()
def fx(i, code):
return fp.rewrite(i, code) if fp else code
def publish(i):
return fp.publish(i) if fp else ''
def jump(t, i):
if entry <= t < end:
return f'goto L_{t:x};'
if not self.in_text(t):
return f'{publish(i)}{spill}c->pc = {hx(t)}; __attribute__((musttail)) return rt_dispatch(c);'
refs.add(t)
return f'{publish(i)}{spill}__attribute__((musttail)) return {fname(t)}(c);'
body = [] # (instruction index, or None for a label; line)
k = None
try:
for i, ins in enumerate(insns):
a = entry + 4 * i
if a in labels:
body.append((None, f'L_{a:x}:;'))
k = ins.kind
if k == 'op':
code = fx(i, ins.code)
body.append((i, ' ' + (f'{spill}{code}{reload}' if regs.ESCAPE.search(code) else code)))
elif k == 'trap':
body.append((i, f' {spill}{fx(i, ins.code)}'))
elif k == 'b':
body.append((i, ' ' + spin_gate(a, ins.target) + jump(ins.target, i)))
elif k == 'bcond':
body.append((i, f' if ({fx(i, ins.cond)}) {{ {spin_gate(a, ins.target)}{jump(ins.target, i)} }}'))
elif k == 'bl':
t = ins.target
if self.in_text(t):
refs.add(t)
body.append((i, f' c->x[30] = {hx(a + 4)}; {spill}{fname(t)}(c);{reload_call}'))
else:
body.append((i, f' c->x[30] = {hx(a + 4)}; {spill}c->pc = {hx(t)}; rt_dispatch(c);{reload_call}'))
elif k == 'blr':
body.append((i, f' c->pc = {ins.target}; c->x[30] = {hx(a + 4)}; {spill}rt_dispatch(c);{reload_call}'))
elif k == 'br':
jt = tables.get(a)
if jt:
cases = ' '.join(f'case {hx(t)}: goto L_{t:x};' for t in jt)
body.append((i, f' switch ({ins.target}) {{ {cases} default: break; }}'))
body.append((i, f' {publish(i)}{spill}c->pc = {ins.target}; __attribute__((musttail)) return rt_dispatch(c);'))
elif k == 'ret':
body.append((i, f' {spill}return;'))
else:
return None
except flags.FlagError:
self.stats['local_flag_errors'] += 1
return None
if k not in ('b', 'br', 'ret', 'trap'): # falls through into the next function
body.append((n, ' ' + jump(end, n) if end < self.hi else f' {spill}rt_udf(c, {hx(end)});'))
out = [f'void {fname(entry)}(Cpu *c) {{', f' FN_ENTER({hx(entry)});']
if fp:
out.append(' ' + fp.decl())
if locs:
texts = [''] * (n + 1)
for i, line in body:
if i is not None:
texts[i] += line
clean = {i for i, ins in enumerate(insns)
if ins.kind in ('bl', 'blr') or (ins.kind == 'op' and regs.RELOAD in texts[i])}
rp = regs.plan(texts, succ, clean, self.reg_locals)
if rp.regs:
out.append(' ' + rp.decl())
out += [line if i is None else rp.resolve(i, line) for i, line in body]
else:
out += [line for _, line in body]
out.append('}')
self.stats['local_functions'] += 1
self._refs = refs
return out
def emit_function(self, entry, end, labels, tables, hle):
out = [f'void {fname(entry)}(Cpu *c) {{']
if entry in hle:
out.append(f' {sanitize(hle[entry])}(c);')
out.append('}')
return out
if self.reg_locals or self.local_flags:
local = self.emit_local(entry, end, labels, tables)
if local:
return local
out.append(f' FN_ENTER({hx(entry)});')
batch = []
refs = set()
@@ -685,9 +825,9 @@ class Module:
if k == 'op' or k == 'trap':
out.append(' ' + ins.code)
elif k == 'b':
out.append(' ' + jump(ins.target))
out.append(' ' + spin_gate(a, ins.target) + jump(ins.target))
elif k == 'bcond':
out.append(f' if ({ins.cond}) {{ {jump(ins.target)} }}')
out.append(f' if ({ins.cond}) {{ {spin_gate(a, ins.target)}{jump(ins.target)} }}')
elif k == 'bl':
t = ins.target
if self.in_text(t):
@@ -727,7 +867,7 @@ class Module:
return out
from patches import PATCHES, PRE_PATCHES
from patches import PATCHES, PRE_PATCHES, SPINS
def table_base(m, i, reg, window=40):
@@ -748,6 +888,11 @@ def table_base(m, i, reg, window=40):
return False
def spin_gate(a, target):
"""rt_spin_gate before the back-edge of a spin wait (tools/lifter/spin_waits_v143.txt), else nothing."""
return f'rt_spin_gate(c, {hx(target)}); ' if SPINS.get(a) == target else ''
def write_if_changed(path, text):
"""Leaves unchanged chunks untouched so make only recompiles what a patch affected. Lines end in LF on every system:
the C lifted on Windows for a Steam Deck is the same file a lift on the Deck writes."""
@@ -761,31 +906,9 @@ def write_if_changed(path, text):
f.write(text)
def main():
ap = argparse.ArgumentParser()
ap.add_argument('name')
ap.add_argument('nso')
ap.add_argument('base', type=lambda s: int(s, 0))
ap.add_argument('outdir')
ap.add_argument('--hle')
ap.add_argument('--chunk', type=int, default=1500) # functions per file at most
ap.add_argument('--chunk-bytes', type=int, default=1500000) # and about this much C (even compile times)
ap.add_argument('--big-fn', type=int, default=100000) # bytes of C from which a function gets its own file
ap.add_argument('--limit', type=int, default=0)
ap.add_argument('--simd', choices=('asm', 'c'),
default='asm' if platform.machine().lower() in ('arm64', 'aarch64') else 'c')
args = ap.parse_args()
t0 = time.time()
m = Module(args.name, args.nso, args.base, args.simd)
starts = m.boundaries()
hle = {}
if args.hle and os.path.exists(args.hle):
want = {l.strip() for l in open(args.hle) if l.strip() and not l.startswith('#')}
for name, info, shndx, value, size in m.nso.syms:
if shndx and name in want:
hle[args.base + value] = name
def partition(m, starts):
"""{entry: (end, labels, tables)} of every function of the module, and the rounds it took (main(); the A/B check
of tools/lifter/ab_check.py)."""
# Partition text into functions; any branch/call target outside its function becomes a new
# boundary (splitting the containing function) until a fixpoint is reached.
start_set = set(starts)
@@ -838,6 +961,39 @@ def main():
end = order[i + 1] if i + 1 < len(order) else m.hi
labels, ext, tables = scanned[(a, end)]
entries[a] = (end, labels, tables)
return entries, rounds
def main():
ap = argparse.ArgumentParser()
ap.add_argument('name')
ap.add_argument('nso')
ap.add_argument('base', type=lambda s: int(s, 0))
ap.add_argument('outdir')
ap.add_argument('--hle')
ap.add_argument('--chunk', type=int, default=1500) # functions per file at most
ap.add_argument('--chunk-bytes', type=int, default=1500000) # and about this much C (even compile times)
ap.add_argument('--big-fn', type=int, default=100000) # bytes of C from which a function gets its own file
ap.add_argument('--limit', type=int, default=0)
ap.add_argument('--reg-locals', type=int, default=regs.LOCALS,
help=f'guest registers per function kept in C locals (tools/lifter/regs.py; default {regs.LOCALS}, 0: none)')
ap.add_argument('--no-local-flags', action='store_true', help='keep NZCV in the Cpu (tools/lifter/flags.py off)')
ap.add_argument('--simd', choices=('asm', 'c'),
default='asm' if platform.machine().lower() in ('arm64', 'aarch64') else 'c')
args = ap.parse_args()
t0 = time.time()
m = Module(args.name, args.nso, args.base, args.simd)
m.reg_locals, m.local_flags = args.reg_locals, not args.no_local_flags
starts = m.boundaries()
hle = {}
if args.hle and os.path.exists(args.hle):
want = {l.strip() for l in open(args.hle) if l.strip() and not l.startswith('#')}
for name, info, shndx, value, size in m.nso.syms:
if shndx and name in want:
hle[args.base + value] = name
entries, rounds = partition(m, starts)
print(f'{args.name}: {len(starts)} boundaries, {len(entries)} functions, {rounds} rounds, {dict(m.stats)}, '
f'{time.time() - t0:.0f}s', file=sys.stderr)
@@ -862,14 +1018,18 @@ def main():
r = getattr(m, '_refs', set())
m._refs = set()
n = sum(len(l) + 1 for l in lines)
if n > args.big_fn and fns: # (a big function gets a file of its own: they compile in parallel)
# A big function gets a file of its own (they compile in parallel), and so does one with a dispatch over
# thousands of labels: build_game.py compiles such a file at -O0, and every function sharing it ran
# unoptimized too (in 1.4.3 about 640 functions, 459 of them the SDK's, in 2 files).
alone = n > args.big_fn or ('L_dispatch:' in lines[-4:] and sum(l.startswith('L_') for l in lines) > 2000)
if alone and fns:
pending = (a, lines, r)
break
body += lines
size += n
fns.append(a)
refs |= r
if n > args.big_fn:
if alone:
break
path = os.path.join(args.outdir, f'{args.name}_{nchunks:03d}.c')
text = ['#include "cpu.h"\n' + ('#include "simd.h"\n' if m.lifter.sc else '')]
+7
View File
@@ -324,6 +324,13 @@ class Lifter:
if crn == 3 and op1 == 3 and rt == 31: # barriers
if op2 == 2:
return Insn('op', 'c->excl_addr = ~0ull;')
# DMB ...LD orders earlier loads before later loads and stores: an acquire fence. DMB ...ST orders
# stores before stores: a release fence is that and more. On x86 (TSO) both are compiler barriers, not
# an mfence; the full DMB and DSB keep the sequentially consistent fence (store-load order).
if op2 == 5 and crm & 3 == 1:
return Insn('op', '__atomic_thread_fence(__ATOMIC_ACQUIRE);')
if op2 == 5 and crm & 3 == 2:
return Insn('op', '__atomic_thread_fence(__ATOMIC_RELEASE);')
return Insn('op', '__atomic_thread_fence(__ATOMIC_SEQ_CST);')
raise Unsupported(f'pstate/system {w:08x}')
if op0 == 1: # SYS
+47
View File
@@ -188,7 +188,54 @@ def shadow_size(gen, addr):
return '{ extern int totk_shadow_size(uint32_t *n); uint32_t n_; if (totk_shadow_size(&n_)) c->x[11] = n_; }'
# Equipment-switch cooldown (from totk-pc-recomp recomp/patches-v143.txt, MIT; their issue #18, found by JieML1100):
# the player update (main+0x2b0368) steps the countdown at player state +0x109c only while it is above FLT_EPSILON, but
# the equipment gate (main+0x31cc7c) blocks weapon, shield and bow changes while it is above 0. Whole 30 Hz steps reach
# exactly 0; a measured step leaves a rounding remainder, and one in (0, FLT_EPSILON] is never stepped again and keeps
# equipment locked until a load. With realtime the threshold is 0: the remainder goes below 0 like any last step.
def cooldown(gen, addr): # movi v1.2s, #0x34, lsl #24 (FLT_EPSILON) -> movi v1.2s, #0
return word_or(gen, addr, 0x0f016681, 0x0f000401, fr('totk_fr_realtime'), 'equipment cooldown')
# The game's zstd decoder through the host's libzstd (src/host/zstd_host.c; sites from totk-pc-recomp
# recomp/patches-v143.txt, MIT): each call of ZSTD_decompressMultiFrame (main+0x3a1f0) and ZSTD_decompress_usingDDict
# (main+0x3a1e0) below goes to a host function, which hands what it does not take to the same guest function. Not
# main+0x547a04: that caller (main+0x547964) resets a static DCtx of its own with which the game's zstd rejects some
# valid frames, and the game relies on that result. TOTK_ZSTD_NATIVE=0: the guest decoder.
def zstd_call(word, target, host):
def f(gen, addr):
expect(gen, addr, word, 'zstd call')
return (f'{{ extern int totk_zstd_native(void); extern void {host}(Cpu *c); extern void f_{target:x}(Cpu *c); '
f'c->x[30] = {addr + 4:#x}ull; if (totk_zstd_native()) {host}(c); else f_{target:x}(c); }}')
return f
# Spin waits (tools/lifter/spin_waits_v143.txt): {back-edge: loop head}, absolute. gen.py puts rt_spin_gate on the
# back-edge; the bases are tools/gen_game.py MODULES.
def _spins():
import os
bases = {'main': 0x800000000, 'subsdk0': 0x804000000, 'sdk': 0x805000000}
out = {}
with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'spin_waits_v143.txt')) as f:
for line in f:
w = line.split('#')[0].split()
if len(w) == 4 and w[0] == 'spin':
b = bases[w[1]]
out[b + int(w[3], 16)] = b + int(w[2], 16)
return out
SPINS = _spins()
ZSTD_MULTI, ZSTD_DDICT = MAIN + 0x3a1f0, MAIN + 0x3a1e0
ZSTD_SITES = {0x5f5358: (0x97e913a6, ZSTD_MULTI), 0x6d7788: (0x97e58a9a, ZSTD_MULTI), 0xbdc8e8: (0x97d17642, ZSTD_MULTI),
0x27ce4dc: (0x9761af45, ZSTD_MULTI), 0x5f574c: (0x97e912a5, ZSTD_DDICT), 0xbdc910: (0x97d17634, ZSTD_DDICT),
0x1852314: (0x979f9fb3, ZSTD_DDICT)}
PATCHES = {
MAIN + 0x2b036c: cooldown,
**{MAIN + a: zstd_call(w, t, 'totk_zstd_multiframe' if t == ZSTD_MULTI else 'totk_zstd_using_ddict')
for a, (w, t) in ZSTD_SITES.items()},
0x8005843e0: drs_off,
MAIN + 0x585e50: scene_size(0),
MAIN + 0x585e60: scene_size(1),
+112
View File
@@ -0,0 +1,112 @@
# From totk-pc-recomp recomp/regs.py @ v0.3.1 (MIT, Copyright (c) 2026 Valiantsin Dzerakh and contributors); used by
# tools/lifter/gen.py Module.emit_function. Unchanged below but for this header and the paragraph on what
# could change callee-saved registers, rewritten for this runtime.
"""Function-local general registers for tools/lifter/gen.py.
The lifter reads and writes the guest's X0-X30 and SP in the Cpu (c->x[n], c->sp), so every guest instruction loads
and stores memory, and the C compiler cannot keep a register in a host register across a guest store: guest stores go
through pointers that may alias the Cpu. A generated function instead copies the LOCALS registers it uses most into
locals at its entry (xN, sp_), works on those, and syncs with the Cpu only where something else reads or writes them:
before a call, an SVC, a trap, a return or a tail exit it stores the registers it changed since the last sync (spill),
and after an SVC it loads them all again (reload). The other registers stay in the Cpu.
After a call it reloads only X0-X18 and X30: AAPCS64 has the callee preserve X19-X29 and SP, so their locals still
hold what the Cpu holds. What would break that does not occur in TotK 1.4.3: main and subsdk0 import neither longjmp
nor the C++ unwinder, and sdk's longjmp, __cxa_throw and _Unwind_* have no direct callers (totk-pc-recomp's finding,
same binaries). In this runtime host code that calls the guest (src/host/guest.c guest_call_args) restores X0-X30 and
SP after it, and the one host function that writes a register of its caller above X18, nvnWindowSetPresentInterval
(X21 of the frame-rate controller, src/gfx/nvn/hle_nvn.c), returns into a function gen.py lifts without locals
(it has patches of its own).
Only plain assignments write registers in the lifted C (`c->x[n] = ...`, `c->sp = ...`), and no code takes a
register's address, so the rewrite is textual. A register counts as dirty from a write until the next spill on some
path: plan() runs that forward over the function's control flow, so a call spills only what changed since the
previous call or the entry. Spilling a register that is not dirty is harmless (the local equals the Cpu), so where a
case is unclear the plan errs on spilling more.
"""
import re
from collections import Counter
REG = re.compile(r'c->x\[(\d+)\]|c->sp\b')
WRITE = re.compile(r'c->(?:x\[(\d+)\]|(sp))\s*=(?!=)')
# Lifted instructions that hand the Cpu to the runtime: SVCs return, BRK and undefined words do not.
ESCAPE = re.compile(r'\brt_(?:svc|brk|udf)\(c\b')
SP = 31 # index of SP in the register sets
# Markers gen.py puts where the Cpu must be current, resolved by Plan.resolve(): spill before; reload all, or reload
# what a call may change, after.
SPILL, RELOAD, RELOAD_CALL = '\x00S', '\x00R', '\x00C'
CALLEE_SAVED = frozenset(range(19, 30)) | {SP}
# Locals per function: the most used registers. More cost more than they save, as clang then spills them to the stack
# and the syncs stay (300 hot TotK functions: fewest instructions and memory operands at 4-6).
LOCALS = 6
def name(r):
return 'sp_' if r == SP else f'x{r}'
def cpu(r):
return 'c->sp' if r == SP else f'c->x[{r}]'
def refs(text):
"""Registers text reads or writes, once per occurrence."""
return [SP if m.group(0) == 'c->sp' else int(m.group(1)) for m in REG.finditer(text)]
def written(text):
return {SP if m.group(2) else int(m.group(1)) for m in WRITE.finditer(text)}
class Plan:
def __init__(self, regs, spill, kept):
self.regs = regs # sorted registers the function keeps in locals
self.local = set(regs)
self.spill = spill # per instruction: registers to store at its sync points
self.kept = kept # registers a call leaves as they were (CALLEE_SAVED, or none)
def decl(self):
"""Locals at the top of the function, loaded from the Cpu."""
return 'uint64_t ' + ', '.join(f'{name(r)} = {cpu(r)}' for r in self.regs) + ';' if self.regs else ''
def resolve(self, i, text):
"""text of instruction i with its registers on the locals and its sync markers expanded."""
def local(m):
r = SP if m.group(0) == 'c->sp' else int(m.group(1))
return name(r) if r in self.local else m.group(0)
text = REG.sub(local, text)
spill = ''.join(f'{cpu(r)} = {name(r)}; ' for r in sorted(self.spill[i]))
reload = ''.join(f' {name(r)} = {cpu(r)};' for r in self.regs)
reload_call = ''.join(f' {name(r)} = {cpu(r)};' for r in self.regs if r not in self.kept)
return text.replace(SPILL, spill).replace(RELOAD, reload).replace(RELOAD_CALL, reload_call)
def plan(texts, succ, clean, n_locals=LOCALS, kept=CALLEE_SAVED):
"""texts[i]: instruction i's C with markers; succ[i]: successor indices inside the function; clean: instructions
after which every local equals the Cpu (a call or an SVC that spills and reloads on every path through it);
n_locals: how many registers to keep in locals; kept: registers a call does not reload."""
n = len(texts)
uses = Counter(r for t in texts for r in refs(t))
regs = {r for r, _ in sorted(uses.items(), key=lambda kv: (-kv[1], kv[0]))[:n_locals]}
writes = [written(t) & regs for t in texts]
dirty = [set() for _ in range(n)]
seen = [False] * n
work = [0] if n else []
if n:
seen[0] = True
while work:
i = work.pop()
out = set() if i in clean else dirty[i] | writes[i]
for s in succ[i]:
if not seen[s] or not out <= dirty[s]:
seen[s] = True
dirty[s] |= out
work.append(s)
every = set().union(*writes) if writes else set()
for i in range(n):
if not seen[i]: # not reached from the entry in the plan's flow: anything may be dirty
dirty[i] = every
# A sync point stores what was dirty on entry to its instruction and what the instruction wrote before it
# (c->x[30] of a call): all the instruction's writes, which is at most a little more.
return Plan(sorted(regs), [dirty[i] | writes[i] if SPILL in texts[i] else set() for i in range(n)], kept)
+861
View File
@@ -0,0 +1,861 @@
# Spin waits of TotK 1.4.3: loops that only another thread can end (module, loop head, back-edge, module offsets).
# Their back-edges pass rt_spin_gate (src/host/threads.c); tools/lifter/patches.py SPINS reads this file.
# From totk-pc-recomp recomp/patches-v143.txt @ v0.3.1 (MIT, Copyright (c) 2026 Valiantsin Dzerakh and contributors),
# made there by its tools/spin_waits.py; the entries are unchanged.
spin main 231dc 231ec # 5 instructions; loads
spin main 24780 24790 # 5 instructions; loads
spin main 5748c 574a0 # 6 instructions; loads
spin main 574b8 574cc # 6 instructions; loads
spin main 5775c 57770 # 6 instructions; loads
spin main 577c8 577dc # 6 instructions; loads
spin main 59ac0 59ad4 # 6 instructions; loads
spin main 95298 952b0 # 7 instructions; loads
spin main 952c4 952dc # 7 instructions; loads
spin main 952ec 95304 # 7 instructions; loads
spin main aeeb0 aefa8 # 8 instructions; stc
spin main ee90c ee93c # 13 instructions; loads
spin main ee950 ee96c # 8 instructions; loads
spin main 11982c 11985c # 13 instructions; loads
spin main 11986c 119888 # 8 instructions; loads
spin main 1391f8 139208 # 5 instructions; loads
spin main 13abc8 13abfc # 14 instructions; loads
spin main 13ac0c 13ac28 # 8 instructions; loads
spin main 15c004 15c018 # 6 instructions; loads
spin main 15cb4c 15cb60 # 6 instructions; loads
spin main 15cc80 15cc94 # 6 instructions; loads
spin main 15d0c0 15d0d4 # 6 instructions; loads
spin main 176ec8 176edc # 6 instructions; loads
spin main 191be4 191bf8 # 6 instructions; loads
spin main 1946a4 1946b8 # 6 instructions; loads
spin main 1ac9b0 1ac9b8 # 3 instructions; loads
spin main 1f9720 1f9734 # 6 instructions; loads
spin main 212a28 212a3c # 6 instructions; loads
spin main 212afc 212b10 # 6 instructions; loads
spin main 213944 213958 # 6 instructions; loads
spin main 216378 21638c # 6 instructions; loads
spin main 2163d4 2163e8 # 6 instructions; loads
spin main 21bb64 21bb78 # 6 instructions; loads
spin main 21c0a0 21c0b4 # 6 instructions; loads
spin main 2368a8 2368bc # 6 instructions; loads
spin main 236a5c 236a70 # 6 instructions; loads
spin main 236c08 236c1c # 6 instructions; loads
spin main 236ebc 236ed0 # 6 instructions; loads
spin main 236fc4 236fd8 # 6 instructions; loads
spin main 23822c 238240 # 6 instructions; loads
spin main 239f08 239f1c # 6 instructions; loads
spin main 24f3ec 24f400 # 6 instructions; loads
spin main 24f500 24f514 # 6 instructions; loads
spin main 24f52c 24f540 # 6 instructions; loads
spin main 24f590 24f5a4 # 6 instructions; loads
spin main 24fa54 24fa68 # 6 instructions; loads
spin main 24fe38 24fe4c # 6 instructions; loads
spin main 25b364 25b378 # 6 instructions; loads
spin main 25b614 25b628 # 6 instructions; loads
spin main 25c1c8 25c1dc # 6 instructions; loads
spin main 270cd0 270ce4 # 6 instructions; loads
spin main 2836ac 2836c0 # 6 instructions; loads
spin main 28ae98 28aeac # 6 instructions; loads
spin main 29f890 29f8a4 # 6 instructions; loads
spin main 29f968 29f97c # 6 instructions; loads
spin main 2a8e74 2a8e88 # 6 instructions; loads
spin main 2dd3e8 2dd3fc # 6 instructions; loads
spin main 2e03c4 2e03d8 # 6 instructions; loads
spin main 2f9758 2f976c # 6 instructions; loads
spin main 2fddb8 2fddcc # 6 instructions; loads
spin main 2ffb20 2ffb34 # 6 instructions; loads
spin main 30e230 30e244 # 6 instructions; loads
spin main 3100e8 3100fc # 6 instructions; loads
spin main 317abc 317ad0 # 6 instructions; loads
spin main 319664 319678 # 6 instructions; loads
spin main 319f24 319f38 # 6 instructions; loads
spin main 31aeac 31aec0 # 6 instructions; loads
spin main 31b558 31b56c # 6 instructions; loads
spin main 33538c 3353a0 # 6 instructions; loads
spin main 33e898 33e8ac # 6 instructions; loads
spin main 34e07c 34e090 # 6 instructions; loads
spin main 350e7c 350e90 # 6 instructions; loads
spin main 350f28 350f3c # 6 instructions; loads
spin main 3510fc 351110 # 6 instructions; loads
spin main 3511b8 3511cc # 6 instructions; loads
spin main 352840 352854 # 6 instructions; loads
spin main 3528b0 3528c4 # 6 instructions; loads
spin main 36cec4 36ced8 # 6 instructions; loads
spin main 3a341c 3a3430 # 6 instructions; loads
spin main 3a3444 3a3458 # 6 instructions; loads
spin main 3de1cc 3de1e8 # 8 instructions; loads
spin main 3de2a4 3de2f0 # 7 instructions; loads
spin main 3de2fc 3de348 # 7 instructions; loads
spin main 3e8418 3e8428 # 5 instructions; loads
spin main 3e855c 3e856c # 5 instructions; loads
spin main 4096c4 4096d8 # 6 instructions; loads
spin main 409748 40975c # 6 instructions; loads
spin main 41e068 41e070 # 3 instructions; loads
spin main 42d138 42d14c # 6 instructions; loads
spin main 43b260 43b274 # 6 instructions; loads
spin main 43e01c 43e030 # 6 instructions; loads
spin main 446bac 446bc0 # 6 instructions; loads
spin main 4503f0 450404 # 6 instructions; loads
spin main 457274 457288 # 6 instructions; loads
spin main 4609dc 4609f0 # 6 instructions; loads
spin main 46313c 463150 # 6 instructions; loads
spin main 467844 467858 # 6 instructions; loads
spin main 467f68 467f7c # 6 instructions; loads
spin main 46ca90 46caa4 # 6 instructions; loads
spin main 46ef8c 46efa0 # 6 instructions; loads
spin main 472154 472168 # 6 instructions; loads
spin main 473b88 473b9c # 6 instructions; loads
spin main 47c85c 47c870 # 6 instructions; loads
spin main 47cb00 47cb14 # 6 instructions; loads
spin main 47da50 47da64 # 6 instructions; loads
spin main 47fcb4 47fcc8 # 6 instructions; loads
spin main 480b44 480b58 # 6 instructions; loads
spin main 480c18 480c2c # 6 instructions; loads
spin main 482c3c 482c50 # 6 instructions; loads
spin main 482d18 482d2c # 6 instructions; loads
spin main 482e78 482e8c # 6 instructions; loads
spin main 4902bc 4902d0 # 6 instructions; loads
spin main 490394 4903a8 # 6 instructions; loads
spin main 4904cc 4904e0 # 6 instructions; loads
spin main 49061c 490630 # 6 instructions; loads
spin main 49114c 491160 # 6 instructions; loads
spin main 491418 49142c # 6 instructions; loads
spin main 492234 492248 # 6 instructions; loads
spin main 4991bc 4991d0 # 6 instructions; loads
spin main 49de50 49de64 # 6 instructions; loads
spin main 49def8 49df0c # 6 instructions; loads
spin main 49f714 49f728 # 6 instructions; loads
spin main 4a0bf4 4a0c08 # 6 instructions; loads
spin main 4a2834 4a2848 # 6 instructions; loads
spin main 4a36a4 4a36b8 # 6 instructions; loads
spin main 4b99b8 4b99cc # 6 instructions; loads
spin main 4c05e8 4c05fc # 6 instructions; loads
spin main 4c0950 4c0964 # 6 instructions; loads
spin main 4c0bf4 4c0c08 # 6 instructions; loads
spin main 4d3088 4d309c # 6 instructions; loads
spin main 4d4bf4 4d4c08 # 6 instructions; loads
spin main 4e70dc 4e70f0 # 6 instructions; loads
spin main 4ec724 4ec738 # 6 instructions; loads
spin main 4f6068 4f607c # 6 instructions; loads
spin main 4f6264 4f6278 # 6 instructions; loads
spin main 4f94fc 4f9510 # 6 instructions; loads
spin main 4fcacc 4fcae0 # 6 instructions; loads
spin main 4fd858 4fd86c # 6 instructions; loads
spin main 4fedc0 4fedd4 # 6 instructions; loads
spin main 4feed4 4feee8 # 6 instructions; loads
spin main 4ff408 4ff41c # 6 instructions; loads
spin main 500960 500974 # 6 instructions; loads
spin main 50437c 504390 # 6 instructions; loads
spin main 504608 50461c # 6 instructions; loads
spin main 511b60 511b74 # 6 instructions; loads
spin main 587f98 587fac # 6 instructions; loads
spin main 5a9c04 5a9c18 # 6 instructions; loads
spin main 5b15a8 5b15bc # 6 instructions; loads
spin main 5b1600 5b1614 # 6 instructions; loads
spin main 5b1690 5b16a4 # 6 instructions; loads
spin main 5b16f0 5b1704 # 6 instructions; loads
spin main 5b7080 5b7094 # 6 instructions; loads
spin main 5bfc2c 5bfc40 # 6 instructions; loads
spin main 5bfd00 5bfd14 # 6 instructions; loads
spin main 5fe024 5fe038 # 6 instructions; loads
spin main 601814 601828 # 6 instructions; loads
spin main 602398 6023ac # 6 instructions; loads
spin main 602448 60245c # 6 instructions; loads
spin main 602914 602928 # 6 instructions; loads
spin main 602d8c 602da0 # 6 instructions; loads
spin main 6034ac 6034c0 # 6 instructions; loads
spin main 603560 603574 # 6 instructions; loads
spin main 603a44 603a58 # 6 instructions; loads
spin main 61ac14 61ac28 # 6 instructions; loads
spin main 628694 6286a8 # 6 instructions; loads
spin main 6289f8 628a0c # 6 instructions; loads
spin main 6294b4 6294c8 # 6 instructions; loads
spin main 62abc4 62abd8 # 6 instructions; loads
spin main 62d6a0 62d6b4 # 6 instructions; loads
spin main 62d9d8 62d9ec # 6 instructions; loads
spin main 631bdc 631bf0 # 6 instructions; loads
spin main 6575a4 6575b8 # 6 instructions; loads
spin main 65b450 65b464 # 6 instructions; loads
spin main 65bb7c 65bb90 # 6 instructions; loads
spin main 662d34 662d48 # 6 instructions; loads
spin main 662e38 662e4c # 6 instructions; loads
spin main 675a30 675a44 # 6 instructions; loads
spin main 68bb10 68bb24 # 6 instructions; loads
spin main 68bb60 68bb74 # 6 instructions; loads
spin main 68bd04 68bd18 # 6 instructions; loads
spin main 6b5038 6b504c # 6 instructions; loads
spin main 6b51c4 6b51d8 # 6 instructions; loads
spin main 6cf050 6cf064 # 6 instructions; loads
spin main 6cf0c8 6cf0dc # 6 instructions; loads
spin main 6cf160 6cf174 # 6 instructions; loads
spin main 6cf1b4 6cf1c8 # 6 instructions; loads
spin main 6cfc34 6cfc48 # 6 instructions; loads
spin main 6cfc5c 6cfc70 # 6 instructions; loads
spin main 6e41e4 6e41f8 # 6 instructions; loads
spin main 712ab8 712acc # 6 instructions; loads
spin main 715894 7158a8 # 6 instructions; loads
spin main 715aac 715ac0 # 6 instructions; loads
spin main 715e84 715e98 # 6 instructions; loads
spin main 715f30 715f44 # 6 instructions; loads
spin main 718a40 718a54 # 6 instructions; loads
spin main 718ae4 718af8 # 6 instructions; loads
spin main 718bd8 718bec # 6 instructions; loads
spin main 718c64 718c78 # 6 instructions; loads
spin main 72caa0 72cab4 # 6 instructions; loads
spin main 72e5c8 72e5dc # 6 instructions; loads
spin main 72ebfc 72ec10 # 6 instructions; loads
spin main 747f84 747f98 # 6 instructions; loads
spin main 748270 748284 # 6 instructions; loads
spin main 7487b4 7487c8 # 6 instructions; loads
spin main 748b10 748b24 # 6 instructions; loads
spin main 7522ac 7522c0 # 6 instructions; loads
spin main 75259c 7525b0 # 6 instructions; loads
spin main 7b5118 7b512c # 6 instructions; loads
spin main 7bb674 7bb688 # 6 instructions; loads
spin main 7bb724 7bb738 # 6 instructions; loads
spin main 7bb7d0 7bb7e4 # 6 instructions; loads
spin main 7bb87c 7bb890 # 6 instructions; loads
spin main 7cd938 7cd94c # 6 instructions; loads
spin main 7ce440 7ce454 # 6 instructions; loads
spin main 7ce7c0 7ce7d4 # 6 instructions; loads
spin main 7cea30 7cea44 # 6 instructions; loads
spin main 7ced60 7ced74 # 6 instructions; loads
spin main 7cf108 7cf11c # 6 instructions; loads
spin main 7cf374 7cf388 # 6 instructions; loads
spin main 7cf3ec 7cf400 # 6 instructions; loads
spin main 7cf8a8 7cf8bc # 6 instructions; loads
spin main 7cfbd0 7cfbe4 # 6 instructions; loads
spin main 7d2a34 7d2a48 # 6 instructions; loads
spin main 7d2c54 7d2c68 # 6 instructions; loads
spin main 7d4100 7d411c # 8 instructions; loads
spin main 7d41b0 7d41cc # 8 instructions; loads
spin main 7d4260 7d427c # 8 instructions; loads
spin main 7d46d4 7d46e8 # 6 instructions; loads
spin main 7d5128 7d513c # 6 instructions; loads
spin main 7d5760 7d5774 # 6 instructions; loads
spin main 7d57a8 7d57bc # 6 instructions; loads
spin main 7d664c 7d6660 # 6 instructions; loads
spin main 7d6748 7d675c # 6 instructions; loads
spin main 7d67f4 7d6808 # 6 instructions; loads
spin main 7d6b48 7d6b5c # 6 instructions; loads
spin main 7d6f18 7d6f2c # 6 instructions; loads
spin main 7d7168 7d717c # 6 instructions; loads
spin main 7d752c 7d7540 # 6 instructions; loads
spin main 7d79b8 7d79cc # 6 instructions; loads
spin main 7d7bf4 7d7c08 # 6 instructions; loads
spin main 7d8794 7d87a8 # 6 instructions; loads
spin main 7d8918 7d892c # 6 instructions; loads
spin main 7d8ab0 7d8ac4 # 6 instructions; loads
spin main 7d94e4 7d94f8 # 6 instructions; loads
spin main 7dd1cc 7dd1e0 # 6 instructions; loads
spin main 7ddd3c 7ddd50 # 6 instructions; loads
spin main 7de684 7de698 # 6 instructions; loads
spin main 7dead4 7deae8 # 6 instructions; loads
spin main 7deb48 7deb5c # 6 instructions; loads
spin main 7ded58 7ded6c # 6 instructions; loads
spin main 7e049c 7e04b0 # 6 instructions; loads
spin main 7e4570 7e4584 # 6 instructions; loads
spin main 7e462c 7e4640 # 6 instructions; loads
spin main 7e46dc 7e46f0 # 6 instructions; loads
spin main 7e478c 7e47a0 # 6 instructions; loads
spin main 7e4868 7e487c # 6 instructions; loads
spin main 7e4930 7e4944 # 6 instructions; loads
spin main 7e4a00 7e4a14 # 6 instructions; loads
spin main 7e4ac4 7e4ad8 # 6 instructions; loads
spin main 7e4b84 7e4b98 # 6 instructions; loads
spin main 7e4c44 7e4c58 # 6 instructions; loads
spin main 7e4d04 7e4d18 # 6 instructions; loads
spin main 7e4dd0 7e4de4 # 6 instructions; loads
spin main 7e4e94 7e4ea8 # 6 instructions; loads
spin main 7e528c 7e52a0 # 6 instructions; loads
spin main 7e53cc 7e53e0 # 6 instructions; loads
spin main 7e53f4 7e5408 # 6 instructions; loads
spin main 7e54a4 7e54b8 # 6 instructions; loads
spin main 7e5564 7e5578 # 6 instructions; loads
spin main 7e5664 7e5678 # 6 instructions; loads
spin main 7e572c 7e5740 # 6 instructions; loads
spin main 7e5e90 7e5ea4 # 6 instructions; loads
spin main 7e5f8c 7e5fa0 # 6 instructions; loads
spin main 7e64fc 7e6510 # 6 instructions; loads
spin main 7e65b8 7e65cc # 6 instructions; loads
spin main 7ead90 7eada4 # 6 instructions; loads
spin main 7eae34 7eae48 # 6 instructions; loads
spin main 7ed8ac 7ed8c0 # 6 instructions; loads
spin main 7ee8a8 7ee8bc # 6 instructions; loads
spin main 7eea7c 7eea90 # 6 instructions; loads
spin main 7eef80 7eef94 # 6 instructions; loads
spin main 7ef3a8 7ef3bc # 6 instructions; loads
spin main 7ef494 7ef4a8 # 6 instructions; loads
spin main 7ef584 7ef598 # 6 instructions; loads
spin main 7f00ac 7f00c0 # 6 instructions; loads
spin main 7f0470 7f0484 # 6 instructions; loads
spin main 7f0520 7f0534 # 6 instructions; loads
spin main 7f7be4 7f7bf8 # 6 instructions; loads
spin main 7f8df0 7f8e04 # 6 instructions; loads
spin main 7fcd74 7fcd88 # 6 instructions; loads
spin main 7fd738 7fd74c # 6 instructions; loads
spin main 7fd9d8 7fd9ec # 6 instructions; loads
spin main 7fda18 7fda2c # 6 instructions; loads
spin main 7ff020 7ff034 # 6 instructions; loads
spin main 7ff064 7ff078 # 6 instructions; loads
spin main 7ff624 7ff638 # 6 instructions; loads
spin main 7ff678 7ff68c # 6 instructions; loads
spin main 7ff6c8 7ff6dc # 6 instructions; loads
spin main 7ffff4 800010 # 8 instructions; loads
spin main 800504 800520 # 8 instructions; loads
spin main 800a3c 800a50 # 6 instructions; loads
spin main 807424 807438 # 6 instructions; loads
spin main 808910 808924 # 6 instructions; loads
spin main 8209cc 8209e0 # 6 instructions; loads
spin main 822a60 822a74 # 6 instructions; loads
spin main 83bf5c 83bf70 # 6 instructions; loads
spin main 83d5f0 83d604 # 6 instructions; loads
spin main 84643c 846450 # 6 instructions; loads
spin main 847d80 847d94 # 6 instructions; loads
spin main 849410 849424 # 6 instructions; loads
spin main 849b14 849b28 # 6 instructions; loads
spin main 8534dc 8534f0 # 6 instructions; loads
spin main 853d44 853d58 # 6 instructions; loads
spin main 86bd68 86bde0 # 9 instructions; loads
spin main 86be14 86be8c # 9 instructions; loads
spin main 871d20 871d34 # 6 instructions; loads
spin main 874a64 874a78 # 6 instructions; loads
spin main 8753a8 8753bc # 6 instructions; loads
spin main 885dc8 885ddc # 6 instructions; loads
spin main 88c4cc 88c4e0 # 6 instructions; loads
spin main 88cb8c 88cc14 # 6 instructions; loads
spin main 88dcf4 88dd08 # 6 instructions; loads
spin main 88de9c 88deb0 # 6 instructions; loads
spin main 88ecc0 88ecd4 # 6 instructions; loads
spin main 88fa2c 88fa40 # 6 instructions; loads
spin main 88fd00 88fd14 # 6 instructions; loads
spin main 8907b8 8907cc # 6 instructions; loads
spin main 890834 890848 # 6 instructions; loads
spin main 893a28 893a3c # 6 instructions; loads
spin main 893b18 893b2c # 6 instructions; loads
spin main 894278 89428c # 6 instructions; loads
spin main 8a5964 8a5978 # 6 instructions; loads
spin main 8ac4fc 8ac510 # 6 instructions; loads
spin main 8b1164 8b1178 # 6 instructions; loads
spin main 8c04c8 8c04dc # 6 instructions; loads
spin main 8c0614 8c0628 # 6 instructions; loads
spin main 8c6b04 8c6b0c # 3 instructions; loads
spin main 8c6b14 8c6b18 # 2 instructions; loads
spin main 8c6b60 8c6b68 # 3 instructions; loads
spin main 8db878 8db88c # 6 instructions; loads
spin main 8dbd38 8dbd4c # 6 instructions; loads
spin main 938098 9380ac # 6 instructions; loads
spin main 93a2f0 93a304 # 6 instructions; loads
spin main 93a508 93a51c # 6 instructions; loads
spin main 93a684 93a698 # 6 instructions; loads
spin main 93b870 93b884 # 6 instructions; loads
spin main 93bab8 93bacc # 6 instructions; loads
spin main 93cab8 93cacc # 6 instructions; loads
spin main 93cd18 93cd2c # 6 instructions; loads
spin main 942d9c 942db0 # 6 instructions; loads
spin main 9436c8 9436dc # 6 instructions; loads
spin main 94400c 944020 # 6 instructions; loads
spin main 94421c 94438c # 6 instructions; loads
spin main 947ce8 947cfc # 6 instructions; loads
spin main 948124 948140 # 8 instructions; loads
spin main 94c13c 94c150 # 6 instructions; loads
spin main 9518d4 9518e8 # 6 instructions; loads
spin main 972e48 972e5c # 6 instructions; loads
spin main 972f44 972f58 # 6 instructions; loads
spin main 973038 97304c # 6 instructions; loads
spin main 97312c 973140 # 6 instructions; loads
spin main 9733fc 973410 # 6 instructions; loads
spin main 974e2c 974e34 # 3 instructions; loads
spin main 974e3c 974e40 # 2 instructions; loads
spin main 974e68 974e70 # 3 instructions; loads
spin main 98810c 988128 # 8 instructions; loads
spin main 996614 996628 # 6 instructions; loads
spin main 999330 999344 # 6 instructions; loads
spin main 9995f0 999604 # 6 instructions; loads
spin main 99b4c4 99b4d8 # 6 instructions; loads
spin main 99b918 99b92c # 6 instructions; loads
spin main 99bd6c 99bd80 # 6 instructions; loads
spin main 99c0d4 99c0e8 # 6 instructions; loads
spin main 99c320 99c334 # 6 instructions; loads
spin main 99d994 99d9a8 # 6 instructions; loads
spin main 9a0318 9a032c # 6 instructions; loads
spin main 9a0490 9a04a4 # 6 instructions; loads
spin main 9a0608 9a061c # 6 instructions; loads
spin main 9a0784 9a0798 # 6 instructions; loads
spin main 9a0900 9a0914 # 6 instructions; loads
spin main 9a0a8c 9a0aa0 # 6 instructions; loads
spin main 9aa844 9aa858 # 6 instructions; loads
spin main 9c7e10 9c7e24 # 6 instructions; loads
spin main 9c7fc8 9c7fdc # 6 instructions; loads
spin main 9c8188 9c819c # 6 instructions; loads
spin main 9d2550 9d2564 # 6 instructions; loads
spin main 9d2ab8 9d2acc # 6 instructions; loads
spin main 9d2ae0 9d2af4 # 6 instructions; loads
spin main 9dd078 9dd08c # 6 instructions; loads
spin main 9deb00 9deb14 # 6 instructions; loads
spin main 9ec4ec 9ec500 # 6 instructions; loads
spin main 9ecbac 9ecbc0 # 6 instructions; loads
spin main a0139c a013b0 # 6 instructions; loads
spin main a099a8 a099bc # 6 instructions; loads
spin main a10f08 a10f1c # 6 instructions; loads
spin main a10f58 a10f6c # 6 instructions; loads
spin main a11284 a11298 # 6 instructions; loads
spin main a16678 a1668c # 6 instructions; loads
spin main a1683c a16850 # 6 instructions; loads
spin main a168f4 a16908 # 6 instructions; loads
spin main a16a14 a16a28 # 6 instructions; loads
spin main a16b00 a16b14 # 6 instructions; loads
spin main a17d70 a17d84 # 6 instructions; loads
spin main a18570 a18584 # 6 instructions; loads
spin main a1db34 a1db48 # 6 instructions; loads
spin main a1ddec a1de00 # 6 instructions; loads
spin main a1e1f8 a1e20c # 6 instructions; loads
spin main a1e464 a1e478 # 6 instructions; loads
spin main a1e5b8 a1e6bc # 6 instructions; loads
spin main a1eff4 a1f008 # 6 instructions; loads
spin main a1f25c a1f270 # 6 instructions; loads
spin main a20084 a20098 # 6 instructions; loads
spin main a200bc a200d0 # 6 instructions; loads
spin main a3aea0 a3aeb4 # 6 instructions; loads
spin main a3b1a8 a3b1bc # 6 instructions; loads
spin main a3b450 a3b464 # 6 instructions; loads
spin main a3b6f0 a3b704 # 6 instructions; loads
spin main a3bfcc a3bfe0 # 6 instructions; loads
spin main a3c2d0 a3c2e4 # 6 instructions; loads
spin main a3c584 a3c598 # 6 instructions; loads
spin main a3c5f0 a3c604 # 6 instructions; loads
spin main a3d8e4 a3d8f8 # 6 instructions; loads
spin main a3db78 a3db8c # 6 instructions; loads
spin main a3dd14 a3dd28 # 6 instructions; loads
spin main a3de28 a3de3c # 6 instructions; loads
spin main a3dfdc a3dff0 # 6 instructions; loads
spin main a3e0cc a3e0e0 # 6 instructions; loads
spin main a3e244 a3e258 # 6 instructions; loads
spin main a3e2fc a3e310 # 6 instructions; loads
spin main a3e3b4 a3e3c8 # 6 instructions; loads
spin main a3e46c a3e480 # 6 instructions; loads
spin main a3e530 a3e544 # 6 instructions; loads
spin main a3e644 a3e658 # 6 instructions; loads
spin main a3e750 a3e764 # 6 instructions; loads
spin main a3e924 a3e938 # 6 instructions; loads
spin main a3ea70 a3eb04 # 6 instructions; loads
spin main a40848 a4085c # 6 instructions; loads
spin main a40874 a40888 # 6 instructions; loads
spin main a41834 a41848 # 6 instructions; loads
spin main a42444 a42458 # 6 instructions; loads
spin main a43398 a433ac # 6 instructions; loads
spin main a4470c a44720 # 6 instructions; loads
spin main a44a04 a44a18 # 6 instructions; loads
spin main a44cc4 a44cd8 # 6 instructions; loads
spin main a44f9c a44fb0 # 6 instructions; loads
spin main a45264 a45278 # 6 instructions; loads
spin main a4552c a45540 # 6 instructions; loads
spin main a4559c a455b0 # 6 instructions; loads
spin main a4b33c a4b350 # 6 instructions; loads
spin main a4b5a8 a4b5bc # 6 instructions; loads
spin main a4b734 a4b748 # 6 instructions; loads
spin main a4b8bc a4b8d0 # 6 instructions; loads
spin main a4bb90 a4bba4 # 6 instructions; loads
spin main a4c260 a4c274 # 6 instructions; loads
spin main a4c2c4 a4c2d8 # 6 instructions; loads
spin main a4c888 a4c89c # 6 instructions; loads
spin main a4ce64 a4ce78 # 6 instructions; loads
spin main a4ce90 a4cea4 # 6 instructions; loads
spin main a4dd1c a4dd30 # 6 instructions; loads
spin main a4e07c a4e090 # 6 instructions; loads
spin main a4e0f0 a4e104 # 6 instructions; loads
spin main a500a0 a500b4 # 6 instructions; loads
spin main a50194 a501a8 # 6 instructions; loads
spin main a501d8 a501ec # 6 instructions; loads
spin main a503e8 a503fc # 6 instructions; loads
spin main a508a0 a508b4 # 6 instructions; loads
spin main a508f0 a50904 # 6 instructions; loads
spin main a52a6c a52a80 # 6 instructions; loads
spin main a52b88 a52b9c # 6 instructions; loads
spin main a52bb4 a52bc8 # 6 instructions; loads
spin main a52c04 a52c18 # 6 instructions; loads
spin main a52cf0 a52d04 # 6 instructions; loads
spin main a53020 a53034 # 6 instructions; loads
spin main a54554 a54568 # 6 instructions; loads
spin main a54f0c a54f20 # 6 instructions; loads
spin main a551bc a551d0 # 6 instructions; loads
spin main a5561c a55630 # 6 instructions; loads
spin main a55640 a55654 # 6 instructions; loads
spin main a58374 a58388 # 6 instructions; loads
spin main a5cd1c a5cd30 # 6 instructions; loads
spin main a5d71c a5d730 # 6 instructions; loads
spin main a5d754 a5d768 # 6 instructions; loads
spin main a5f61c a5f630 # 6 instructions; loads
spin main a61eb0 a61ec4 # 6 instructions; loads
spin main a666fc a66710 # 6 instructions; loads
spin main a6b0f4 a6b108 # 6 instructions; loads
spin main a6d59c a6d5b0 # 6 instructions; loads
spin main a86458 a8646c # 6 instructions; loads
spin main a865dc a865f0 # 6 instructions; loads
spin main a9a618 a9a62c # 6 instructions; loads
spin main a9e5f4 a9e608 # 6 instructions; loads
spin main a9e978 a9e98c # 6 instructions; loads
spin main a9fb24 a9fb38 # 6 instructions; loads
spin main aa93ec aa9400 # 6 instructions; loads
spin main aaa450 aaa464 # 6 instructions; loads
spin main aaa91c aaa930 # 6 instructions; loads
spin main aace80 aace94 # 6 instructions; loads
spin main aad104 aad118 # 6 instructions; loads
spin main aad1d0 aad1e4 # 6 instructions; loads
spin main ab16d4 ab16e8 # 6 instructions; loads
spin main ab18b0 ab18c4 # 6 instructions; loads
spin main ab1ab4 ab1ac8 # 6 instructions; loads
spin main ab1ae8 ab1afc # 6 instructions; loads
spin main ab20d0 ab20e4 # 6 instructions; loads
spin main abb028 abb03c # 6 instructions; loads
spin main abf730 abf744 # 6 instructions; loads
spin main abfbdc abfbf0 # 6 instructions; loads
spin main ac5d0c ac5d20 # 6 instructions; loads
spin main ac5f14 ac5f28 # 6 instructions; loads
spin main ac67a8 ac67bc # 6 instructions; loads
spin main ac8b48 ac8b5c # 6 instructions; loads
spin main ac8e2c ac8e40 # 6 instructions; loads
spin main acd9b0 acd9c4 # 6 instructions; loads
spin main aceedc aceef0 # 6 instructions; loads
spin main ad0654 ad0668 # 6 instructions; loads
spin main ad843c ad8450 # 6 instructions; loads
spin main ae3c90 ae3ca4 # 6 instructions; loads
spin main ae4510 ae4524 # 6 instructions; loads
spin main ae51e8 ae51fc # 6 instructions; loads
spin main ae521c ae5230 # 6 instructions; loads
spin main ae8bc4 ae8bd8 # 6 instructions; loads
spin main aee5cc aee5e0 # 6 instructions; loads
spin main aee8d8 aee8ec # 6 instructions; loads
spin main af4dd0 af4de4 # 6 instructions; loads
spin main af4df8 af4e0c # 6 instructions; loads
spin main af88fc af8910 # 6 instructions; loads
spin main aff3dc aff3f0 # 6 instructions; loads
spin main affbd0 affbe4 # 6 instructions; loads
spin main b09474 b09488 # 6 instructions; loads
spin main b0de70 b0de84 # 6 instructions; loads
spin main b14958 b1496c # 6 instructions; loads
spin main b17ecc b17ee0 # 6 instructions; loads
spin main b1b8b0 b1b8c4 # 6 instructions; loads
spin main b1c15c b1c170 # 6 instructions; loads
spin main b1c4a8 b1c4bc # 6 instructions; loads
spin main b1c6e0 b1c6f4 # 6 instructions; loads
spin main b1c7d4 b1c7e8 # 6 instructions; loads
spin main b1eb20 b1eb34 # 6 instructions; loads
spin main b1fac4 b1fad8 # 6 instructions; loads
spin main b213cc b213e0 # 6 instructions; loads
spin main b21730 b21744 # 6 instructions; loads
spin main b219c8 b219dc # 6 instructions; loads
spin main b21c24 b21c38 # 6 instructions; loads
spin main b23b6c b23b80 # 6 instructions; loads
spin main b26a14 b26a28 # 6 instructions; loads
spin main b30f80 b30f94 # 6 instructions; loads
spin main b3199c b319b0 # 6 instructions; loads
spin main b31e9c b31eb0 # 6 instructions; loads
spin main b324fc b32510 # 6 instructions; loads
spin main b33bd8 b33bec # 6 instructions; loads
spin main b4084c b40860 # 6 instructions; loads
spin main b434fc b43510 # 6 instructions; loads
spin main b43b3c b43b50 # 6 instructions; loads
spin main b43f90 b43fa4 # 6 instructions; loads
spin main b4c470 b4c484 # 6 instructions; loads
spin main b55f24 b55f38 # 6 instructions; loads
spin main b56270 b56284 # 6 instructions; loads
spin main b5c624 b5c638 # 6 instructions; loads
spin main b5cb38 b5cb4c # 6 instructions; loads
spin main b5d0f8 b5d10c # 6 instructions; loads
spin main b5d81c b5d830 # 6 instructions; loads
spin main b6d590 b6d5a4 # 6 instructions; loads
spin main bad0dc bad0f0 # 6 instructions; loads
spin main bd06f0 bd0704 # 6 instructions; loads
spin main c009d4 c009e8 # 6 instructions; loads
spin main c0d404 c0d4f0 # 8 instructions; loads
spin main c14b3c c14b50 # 6 instructions; loads
spin main c242f4 c24308 # 6 instructions; loads
spin main c2a370 c2a384 # 6 instructions; loads
spin main c31338 c3134c # 6 instructions; loads
spin main c31ecc c31ee0 # 6 instructions; loads
spin main c4495c c44970 # 6 instructions; loads
spin main c44a8c c44aa0 # 6 instructions; loads
spin main c45e30 c45e44 # 6 instructions; loads
spin main c52710 c52724 # 6 instructions; loads
spin main c684a8 c684bc # 6 instructions; loads
spin main c6862c c68640 # 6 instructions; loads
spin main c68658 c6866c # 6 instructions; loads
spin main c855e4 c855f8 # 6 instructions; loads
spin main c8724c c87260 # 6 instructions; loads
spin main c87278 c8728c # 6 instructions; loads
spin main c9af88 c9af9c # 6 instructions; loads
spin main c9b218 c9b22c # 6 instructions; loads
spin main ca174c ca1760 # 6 instructions; loads
spin main cb609c cb6144 # 6 instructions; loads
spin main cd0568 cd057c # 6 instructions; loads
spin main ce095c ce0970 # 6 instructions; loads
spin main ce9f88 ce9f9c # 6 instructions; loads
spin main cea378 cea38c # 6 instructions; loads
spin main cea4a8 cea4bc # 6 instructions; loads
spin main cea6dc cea6f0 # 6 instructions; loads
spin main cea7a4 cea7b8 # 6 instructions; loads
spin main cea868 cea8f8 # 6 instructions; loads
spin main cea908 cea998 # 6 instructions; loads
spin main cea9a8 cea9c0 # 7 instructions; loads
spin main cebb5c cebb70 # 6 instructions; loads
spin main cebc40 cebc54 # 6 instructions; loads
spin main cebec4 cebed8 # 6 instructions; loads
spin main cec920 cec934 # 6 instructions; loads
spin main cecdc0 cecdd4 # 6 instructions; loads
spin main cecf38 cecfc0 # 6 instructions; loads
spin main cecfd4 ced05c # 6 instructions; loads
spin main ced078 ced104 # 6 instructions; loads
spin main ced114 ced1a0 # 6 instructions; loads
spin main ced1ac ced234 # 6 instructions; loads
spin main ced240 ced2c8 # 6 instructions; loads
spin main ceeef8 ceef0c # 6 instructions; loads
spin main cefa48 cefa5c # 6 instructions; loads
spin main d0adc8 d0addc # 6 instructions; loads
spin main d14f78 d14f8c # 6 instructions; loads
spin main d15240 d15254 # 6 instructions; loads
spin main d186dc d186f0 # 6 instructions; loads
spin main d1b400 d1b414 # 6 instructions; loads
spin main d1c4bc d1c4d0 # 6 instructions; loads
spin main d204e4 d204f8 # 6 instructions; loads
spin main d21a70 d21a84 # 6 instructions; loads
spin main d22ba8 d22bbc # 6 instructions; loads
spin main d33ae0 d33af4 # 6 instructions; loads
spin main d34268 d3427c # 6 instructions; loads
spin main d40fb4 d40fc8 # 6 instructions; loads
spin main d448d0 d448e4 # 6 instructions; loads
spin main d4504c d45060 # 6 instructions; loads
spin main d460ec d46100 # 6 instructions; loads
spin main d478b8 d478cc # 6 instructions; loads
spin main d480b0 d480c4 # 6 instructions; loads
spin main d4b6c4 d4b6d8 # 6 instructions; loads
spin main d54af0 d54b04 # 6 instructions; loads
spin main d577dc d577f0 # 6 instructions; loads
spin main d7449c d744b0 # 6 instructions; loads
spin main d74bc0 d74bd4 # 6 instructions; loads
spin main d74c70 d74c84 # 6 instructions; loads
spin main d74d48 d74d5c # 6 instructions; loads
spin main d775c0 d775d4 # 6 instructions; loads
spin main d7846c d78480 # 6 instructions; loads
spin main d792b8 d7938c # 6 instructions; loads
spin main d79898 d798ac # 6 instructions; loads
spin main d82cfc d82d10 # 6 instructions; loads
spin main d86904 d86918 # 6 instructions; loads
spin main d88eec d88f00 # 6 instructions; loads
spin main d8a8c0 d8a8d4 # 6 instructions; loads
spin main d8bc68 d8bc7c # 6 instructions; loads
spin main d8d144 d8d158 # 6 instructions; loads
spin main d8d50c d8d520 # 6 instructions; loads
spin main da05c4 da05d8 # 6 instructions; loads
spin main da1410 da1424 # 6 instructions; loads
spin main da46c0 da46d4 # 6 instructions; loads
spin main da471c da4730 # 6 instructions; loads
spin main da4898 da48ac # 6 instructions; loads
spin main da6f94 da6fa8 # 6 instructions; loads
spin main da7040 da7054 # 6 instructions; loads
spin main da70d4 da70e8 # 6 instructions; loads
spin main dab860 dab874 # 6 instructions; loads
spin main dab9c4 dab9d8 # 6 instructions; loads
spin main daf8f0 daf904 # 6 instructions; loads
spin main db014c db0160 # 6 instructions; loads
spin main db04c8 db04dc # 6 instructions; loads
spin main db071c db0730 # 6 instructions; loads
spin main db1e1c db1e30 # 6 instructions; loads
spin main e19994 e199a8 # 6 instructions; loads
spin main e19e54 e19e68 # 6 instructions; loads
spin main e1ae08 e1ae1c # 6 instructions; loads
spin main e1d7fc e1d810 # 6 instructions; loads
spin main e1ea1c e1ea30 # 6 instructions; loads
spin main e1f2d0 e1f2e4 # 6 instructions; loads
spin main e1f5b4 e1f5c8 # 6 instructions; loads
spin main e1f5fc e1f610 # 6 instructions; loads
spin main e1ffe4 e1fff8 # 6 instructions; loads
spin main e22330 e22344 # 6 instructions; loads
spin main e22704 e22718 # 6 instructions; loads
spin main e229bc e229d0 # 6 instructions; loads
spin main e22f2c e22f40 # 6 instructions; loads
spin main e23374 e23388 # 6 instructions; loads
spin main e23668 e2367c # 6 instructions; loads
spin main e26f44 e26f58 # 6 instructions; loads
spin main e27080 e27094 # 6 instructions; loads
spin main e27430 e27444 # 6 instructions; loads
spin main e280ac e280c0 # 6 instructions; loads
spin main e281b8 e281cc # 6 instructions; loads
spin main e28e78 e28e8c # 6 instructions; loads
spin main e29bc8 e29bdc # 6 instructions; loads
spin main e2a3a4 e2a3b8 # 6 instructions; loads
spin main e2a960 e2a974 # 6 instructions; loads
spin main e2d3b8 e2d3cc # 6 instructions; loads
spin main e2defc e2df10 # 6 instructions; loads
spin main e2eb5c e2eb70 # 6 instructions; loads
spin main e2ed08 e2ed1c # 6 instructions; loads
spin main e309b0 e309c4 # 6 instructions; loads
spin main e30c60 e30c74 # 6 instructions; loads
spin main e31a1c e31a30 # 6 instructions; loads
spin main e33398 e333ac # 6 instructions; loads
spin main e34d10 e34d24 # 6 instructions; loads
spin main e35060 e35074 # 6 instructions; loads
spin main e35af8 e35b0c # 6 instructions; loads
spin main ec09cc ec09e0 # 6 instructions; loads
spin main ecd874 ecd888 # 6 instructions; loads
spin main ee3648 ee365c # 6 instructions; loads
spin main ee3fa4 ee3fb8 # 6 instructions; loads
spin main f39bc0 f39bd4 # 6 instructions; loads
spin main f39cd8 f39cec # 6 instructions; loads
spin main 10997cc 10997e0 # 6 instructions; loads
spin main 1099b9c 1099bb0 # 6 instructions; loads
spin main 109ce5c 109ce70 # 6 instructions; loads
spin main 109d20c 109d220 # 6 instructions; loads
spin main 109dc8c 109dca0 # 6 instructions; loads
spin main 109e4d4 109e4e8 # 6 instructions; loads
spin main 109efd8 109efec # 6 instructions; loads
spin main 10a52e4 10a52f8 # 6 instructions; loads
spin main 111b2c4 111b2cc # 3 instructions; loads
spin main 1144cc0 1144ce0 1144cf0 # 13 instructions; loads
spin main 1144d40 1144d68 1144d78 # 15 instructions; loads
spin main 1144db0 1144dd8 1144de8 # 15 instructions; loads
spin main 12c2d00 12c2d14 # 6 instructions; loads
spin main 1861cb8 1861ccc # 6 instructions; loads
spin main 187ce28 187ce3c # 6 instructions; loads
spin main 195af84 195af98 # 6 instructions; loads
spin main 197a160 197a174 # 6 instructions; loads
spin main 1995314 1995328 # 6 instructions; loads
spin main 1b079c4 1b079d8 # 6 instructions; loads
spin main 1b09ac8 1b09adc # 6 instructions; loads
spin main 1b19108 1b1911c # 6 instructions; loads
spin main 1b1b8d8 1b1b8ec # 6 instructions; loads
spin main 1b4ad40 1b4ad54 # 6 instructions; loads
spin main 1b4d308 1b4d31c # 6 instructions; loads
spin main 1b8016c 1b80180 # 6 instructions; loads
spin main 1b87780 1b87794 # 6 instructions; loads
spin main 1ba10c0 1ba10d4 # 6 instructions; loads
spin main 1bb1b98 1bb1bac # 6 instructions; loads
spin main 1bc4578 1bc458c # 6 instructions; loads
spin main 1bc4678 1bc468c # 6 instructions; loads
spin main 1bc57d8 1bc57ec # 6 instructions; loads
spin main 1bc5938 1bc594c # 6 instructions; loads
spin main 1bf0a54 1bf0a68 # 6 instructions; loads
spin main 1bf41bc 1bf41d0 # 6 instructions; loads
spin main 1bf7cb8 1bf7ccc # 6 instructions; loads
spin main 1c1df28 1c1df3c # 6 instructions; loads
spin main 1c4cf40 1c4cf54 # 6 instructions; loads
spin main 1d21008 1d2101c # 6 instructions; loads
spin main 1d96ea4 1d96eb8 # 6 instructions; loads
spin main 1edc4d8 1edc4e8 # 5 instructions; loads
spin main 1f55a94 1f55aa8 # 6 instructions; loads
spin main 1f57098 1f570ac # 6 instructions; loads
spin main 1f8dcb4 1f8dcbc # 3 instructions; loads
spin main 1f8dcc4 1f8dcc8 # 2 instructions; loads
spin main 1f8dd64 1f8dd6c # 3 instructions; loads
spin main 1f8de34 1f8de3c # 3 instructions; loads
spin main 1f8de44 1f8de48 # 2 instructions; loads
spin main 1f8ded8 1f8dee0 # 3 instructions; loads
spin main 1fa0dd4 1fa0ddc # 3 instructions; loads
spin main 1fa0de4 1fa0de8 # 2 instructions; loads
spin main 1fa0e0c 1fa0e14 # 3 instructions; loads
spin main 1fa0e44 1fa0e4c # 3 instructions; loads
spin main 1fa0e54 1fa0e58 # 2 instructions; loads
spin main 1fa0e80 1fa0e88 # 3 instructions; loads
spin main 1fcd71c 1fcd724 # 3 instructions; loads
spin main 1fcd72c 1fcd730 # 2 instructions; loads
spin main 1fcd754 1fcd75c # 3 instructions; loads
spin main 224cdbc 224cdd0 # 6 instructions; loads
spin main 224ce44 224ce58 # 6 instructions; loads
spin main 2437524 2437570 # 20 instructions; loads
spin main 24799dc 24799e8 # 4 instructions; loads
spin main 281d710 281d718 # 3 instructions; loads
spin main 281d720 281d724 # 2 instructions; loads
spin main 281d758 281d760 # 3 instructions; loads
spin main 281d808 281d810 # 3 instructions; loads
spin main 281d818 281d81c # 2 instructions; loads
spin main 2857518 2857540 # 3 instructions; loads
spin main 28604d0 28604f8 # 11 instructions; loads
spin main 2860868 2860894 # 12 instructions; loads
spin main 28608a8 28608c4 # 8 instructions; loads
spin main 288a380 288a388 # 3 instructions; loads
spin main 288a390 288a394 # 2 instructions; loads
spin main 288a3ac 288a3b4 # 3 instructions; loads
spin main 288a3c8 288a3d0 # 3 instructions; loads
spin main 288a3d8 288a3dc # 2 instructions; loads
spin main 288a464 288a46c # 3 instructions; loads
spin main 289bca0 289bcb8 # 7 instructions; loads
spin main 29094cc 2909514 # 3 instructions; loads
spin main 290e8a0 290e8c0 # 4 instructions; loads
spin main 290e980 290e9a0 # 4 instructions; loads
spin main 2917ccc 2917ccc 2917cd4 # 3 instructions; loads
spin main 2a88d64 2a88d6c # 3 instructions; loads
spin main 2a88d74 2a88d78 # 2 instructions; loads
spin main 2a88de4 2a88dec # 3 instructions; loads
spin main 2a89ba4 2a89bac # 3 instructions; loads
spin main 2a89bb4 2a89bb8 # 2 instructions; loads
spin main 2a89c24 2a89c2c # 3 instructions; loads
spin main 2a8a108 2a8a110 # 3 instructions; loads
spin main 2a8a118 2a8a11c # 2 instructions; loads
spin main 2a8a1dc 2a8a1e4 # 3 instructions; loads
spin main 2a8a864 2a8a86c # 3 instructions; loads
spin main 2a8a874 2a8a878 # 2 instructions; loads
spin main 2a8a8d4 2a8a8dc # 3 instructions; loads
spin main 2a8a918 2a8a920 # 3 instructions; loads
spin main 2a8a928 2a8a92c # 2 instructions; loads
spin main 2a8a954 2a8a95c # 3 instructions; loads
spin main 2a8a9a4 2a8a9ac # 3 instructions; loads
spin main 2a8a9b4 2a8a9b8 # 2 instructions; loads
spin main 2a8aac8 2a8aad0 # 3 instructions; loads
spin main 2a8aee8 2a8aef0 # 3 instructions; loads
spin main 2a8aef8 2a8aefc # 2 instructions; loads
spin main 2a8af68 2a8af70 # 3 instructions; loads
spin main 2a8afc0 2a8afc8 # 3 instructions; loads
spin main 2a8afd0 2a8afd4 # 2 instructions; loads
spin main 2a8aff8 2a8b000 # 3 instructions; loads
spin main 2a8b738 2a8b740 # 3 instructions; loads
spin main 2a8b748 2a8b74c # 2 instructions; loads
spin main 2a8b7a0 2a8b7a8 # 3 instructions; loads
spin main 2a8b890 2a8b898 # 3 instructions; loads
spin main 2a8b8a0 2a8b8a4 # 2 instructions; loads
spin main 2a8b940 2a8b948 # 3 instructions; loads
spin main 2a8b980 2a8b988 # 3 instructions; loads
spin main 2a8b990 2a8b994 # 2 instructions; loads
spin main 2a8b9ec 2a8b9f4 # 3 instructions; loads
spin main 2a8bb48 2a8bb50 # 3 instructions; loads
spin main 2a8bb58 2a8bb5c # 2 instructions; loads
spin main 2a8bbf8 2a8bc00 # 3 instructions; loads
spin main 2a8bc2c 2a8bc34 # 3 instructions; loads
spin main 2a8bc3c 2a8bc40 # 2 instructions; loads
spin main 2a8bc7c 2a8bc84 # 3 instructions; loads
spin main 2a8ce78 2a8ce80 # 3 instructions; loads
spin main 2a8ce88 2a8ce8c # 2 instructions; loads
spin main 2a8cee0 2a8cee8 # 3 instructions; loads
spin main 2a8cf24 2a8cf2c # 3 instructions; loads
spin main 2a8cf34 2a8cf38 # 2 instructions; loads
spin main 2a8d070 2a8d078 # 3 instructions; loads
spin main 2a8d0b0 2a8d0b8 # 3 instructions; loads
spin main 2a8d0c0 2a8d0c4 # 2 instructions; loads
spin main 2a8d1d0 2a8d1d8 # 3 instructions; loads
spin main 2a8e670 2a8e678 # 3 instructions; loads
spin main 2a8e680 2a8e684 # 2 instructions; loads
spin main 2a8e6f8 2a8e700 # 3 instructions; loads
spin main 2a8e728 2a8e730 # 3 instructions; loads
spin main 2a8e738 2a8e73c # 2 instructions; loads
spin main 2a8e804 2a8e80c # 3 instructions; loads
spin main 2a8f44c 2a8f454 # 3 instructions; loads
spin main 2a8f45c 2a8f460 # 2 instructions; loads
spin main 2a8f4e0 2a8f4e8 # 3 instructions; loads
spin main 2a8f51c 2a8f524 # 3 instructions; loads
spin main 2a8f52c 2a8f530 # 2 instructions; loads
spin main 2a8f580 2a8f588 # 3 instructions; loads
spin main 2a92710 2a92718 # 3 instructions; loads
spin main 2a92720 2a92724 # 2 instructions; loads
spin main 2a92740 2a92748 # 3 instructions; loads
spin main 2ac7fb8 2ac7fc4 2ac800c 2ac8028 # 29 instructions; loads
spin main 2b0e760 2b0e848 # 7 instructions; loads
spin subsdk0 2060 2074 # 6 instructions; loads
spin subsdk0 27804 27818 2787c 278c4 # 23 instructions; stc
spin subsdk0 baf78 bb020 bb028 # 12 instructions; loads
spin subsdk0 bb118 bb1c0 bb1c8 # 12 instructions; loads
spin subsdk0 be334 be348 # 6 instructions; loads
spin subsdk0 1bfbd4 1bfbe0 # 4 instructions; loads
spin subsdk0 1c1588 1c159c # 6 instructions; loads
spin subsdk0 1c4a78 1c4a94 1c4aa0 # 11 instructions; loads
spin subsdk0 1c4e54 1c4e60 # 4 instructions; loads
spin sdk 2d3654 2d3668 # 6 instructions; loads
spin sdk 443864 443884 # 9 instructions; loads
spin sdk 541e54 541f98 # 32 instructions; loads