mirror of
https://github.com/kuraeow/ultratears.git
synced 2026-11-07 10:16:00 -10:00
47 files changed
+2936
-243
No files matched your search
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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 = ∥
|
||||
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, ®, 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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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); }
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
@@ -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
@@ -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)
|
||||
|
||||
+11
-7
@@ -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);
|
||||
}
|
||||
|
||||
+12
@@ -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;
|
||||
|
||||
+11
@@ -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);
|
||||
|
||||
+23
-4
@@ -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);
|
||||
}
|
||||
|
||||
+2
@@ -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;
|
||||
|
||||
+13
@@ -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);
|
||||
}
|
||||
Vendored
+40
@@ -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);
|
||||
+25
@@ -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);
|
||||
}
|
||||
Vendored
+21
@@ -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
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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 '')]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
Reference in new issue
Block a user