diff --git a/config/seeds/5343000F.json b/config/seeds/5343000F.json new file mode 100644 index 000000000..761101b00 --- /dev/null +++ b/config/seeds/5343000F.json @@ -0,0 +1,37 @@ +[ + { + "start": "0x0002C1AE", + "observed": true, + "note": "PsCreateSystemThreadEx StartContext1 observed at runtime; decodes as a function body." + }, + { + "start": "0x000B86E0", + "observed": true, + "note": "PsCreateSystemThreadEx StartContext1 observed at runtime; decodes as a function body." + }, + { + "start": "0x001DF280", + "observed": true, + "note": "Indirect-call target observed at runtime; decodes as a function body." + }, + { + "start": "0x0023A0F0", + "observed": true, + "note": "PsCreateSystemThreadEx StartContext1 observed at runtime; decodes as a function body." + }, + { + "start": "0x0024AED0", + "observed": true, + "note": "PsCreateSystemThreadEx StartContext1 observed at runtime; decodes as a function body." + }, + { + "start": "0x002DE7B0", + "observed": true, + "note": "PsCreateSystemThreadEx StartContext1 observed at runtime; decodes as a function body." + }, + { + "start": "0x002EA576", + "observed": true, + "note": "Indirect-call target observed at runtime; decodes as a function body." + } +] \ No newline at end of file diff --git a/src/d3d/rhi.c b/src/d3d/rhi.c index 74c7e30b2..9d86be740 100644 --- a/src/d3d/rhi.c +++ b/src/d3d/rhi.c @@ -120,6 +120,23 @@ int rhi_image_copy(RhiImage *dst, RhiImage *src) { return g_rhi->image_copy ? g_rhi->image_copy(dst, src) : -1; } + +int rhi_occlusion_begin(void) +{ + return g_rhi->occlusion_begin ? g_rhi->occlusion_begin() : -1; +} + +void rhi_occlusion_end(RhiOcclusion *out) +{ + memset(out, 0, sizeof *out); + if (g_rhi->occlusion_end) + g_rhi->occlusion_end(out); +} + +int rhi_occlusion_result(const RhiOcclusion *t, uint64_t *samples) +{ + return g_rhi->occlusion_result ? g_rhi->occlusion_result(t, samples) : -1; +} int rhi_image_readback(RhiImage *img, uint32_t sub, void *dst, uint32_t dst_pitch) { return g_rhi->image_readback(img, sub, dst, dst_pitch); diff --git a/src/d3d/rhi.h b/src/d3d/rhi.h index 24cc0f937..a74e1c207 100644 --- a/src/d3d/rhi.h +++ b/src/d3d/rhi.h @@ -474,6 +474,23 @@ void rhi_draw_indexed(uint32_t index_count, uint32_t first_index, int32_t base_v void rhi_clear_color(RhiView *v, const float rgba[4]); void rhi_clear_depth(RhiView *v, uint32_t flags, float depth, uint8_t stencil); +/* ---- occlusion counting --------------------------------------------------------- */ + +/* Samples that pass the depth and stencil tests, counted over the draws made + * between rhi_occlusion_begin and rhi_occlusion_end: what an Xbox visibility + * test reports (D3DDevice_BeginVisibilityTest .. EndVisibilityTest). Each draw + * is counted by a query of its own, so a test may span render-target changes. + * rhi_occlusion_result waits for the GPU and returns 0 with the total in + * *samples, or -1 when the backend cannot count or the test's queries have + * been reused since (a result asked for long after its frame). */ +typedef struct RhiOcclusion { + uint32_t first, count, generation; +} RhiOcclusion; + +int rhi_occlusion_begin(void); /* 0 when counting has started */ +void rhi_occlusion_end(RhiOcclusion *out); +int rhi_occlusion_result(const RhiOcclusion *t, uint64_t *samples); + /* ---- backends ----------------------------------------------------------------- */ /* For a test that makes its own D3D11 device: hand it to the backend in diff --git a/src/d3d/rhi_backend.h b/src/d3d/rhi_backend.h index dff988cb3..000d7987d 100644 --- a/src/d3d/rhi_backend.h +++ b/src/d3d/rhi_backend.h @@ -81,6 +81,10 @@ typedef struct RhiBackend { /* Last, so a backend's positional initialiser that predates it leaves it * NULL, which rhi_image_copy reports as unsupported. */ int (*image_copy)(RhiImage *dst, RhiImage *src); + /* After image_copy for the same reason: NULL is "cannot count". */ + int (*occlusion_begin)(void); + void (*occlusion_end)(RhiOcclusion *); + int (*occlusion_result)(const RhiOcclusion *, uint64_t *); } RhiBackend; extern const RhiBackend rhi_d3d11_backend; diff --git a/src/d3d/rhi_d3d11.c b/src/d3d/rhi_d3d11.c index 6cd24e121..3a62a46bd 100644 --- a/src/d3d/rhi_d3d11.c +++ b/src/d3d/rhi_d3d11.c @@ -360,8 +360,11 @@ static int d_device_create(const RhiDeviceDesc *dd) return 0; } +static void occl_release(void); + static void d_device_destroy(void) { + occl_release(); staging_release(); release_back_buffer_view(); if (g_swap) { IDXGISwapChain_Release(g_swap); g_swap = NULL; } @@ -1291,6 +1294,74 @@ static void d_clear_depth(RhiView *v, uint32_t flags, float z, uint8_t s) ID3D11DeviceContext_ClearDepthStencilView(g_ctx, (ID3D11DepthStencilView *)v->v, flags, z, s); } +/* Occlusion counting (rhi_occlusion_*): one D3D11 occlusion query a test, + * which may span anything, from a ring; a slot's use count is the test's + * generation, so a test answered after its slot came round again reads as + * reused. */ +#define OCCL_SLOTS 256u +static ID3D11Query *g_occl[OCCL_SLOTS]; +static uint32_t g_occl_uses[OCCL_SLOTS]; +static uint32_t g_occl_next; +static int g_occl_open = -1; + +static void occl_release(void) +{ + uint32_t i; + for (i = 0; i < OCCL_SLOTS; i++) + if (g_occl[i]) { ID3D11Query_Release(g_occl[i]); g_occl[i] = NULL; } + g_occl_open = -1; +} + +static int d_occlusion_begin(void) +{ + uint32_t i = g_occl_next % OCCL_SLOTS; + + if (!g_dev || !g_ctx) + return -1; + if (!g_occl[i]) { + D3D11_QUERY_DESC qd; + memset(&qd, 0, sizeof qd); + qd.Query = D3D11_QUERY_OCCLUSION; + if (FAILED(ID3D11Device_CreateQuery(g_dev, &qd, &g_occl[i]))) + return -1; + } + g_occl_next++; + g_occl_uses[i]++; + ID3D11DeviceContext_Begin(g_ctx, (ID3D11Asynchronous *)g_occl[i]); + g_occl_open = (int)i; + return 0; +} + +static void d_occlusion_end(RhiOcclusion *out) +{ + if (g_occl_open < 0) + return; + ID3D11DeviceContext_End(g_ctx, (ID3D11Asynchronous *)g_occl[g_occl_open]); + out->first = (uint32_t)g_occl_open; + out->count = 1; + out->generation = g_occl_uses[g_occl_open]; + g_occl_open = -1; +} + +static int d_occlusion_result(const RhiOcclusion *t, uint64_t *samples) +{ + UINT64 n = 0; + HRESULT hr; + + *samples = 0; + if (!t->count) + return 0; + if (t->first >= OCCL_SLOTS || !g_occl[t->first] || g_occl_uses[t->first] != t->generation) + return -1; + while ((hr = ID3D11DeviceContext_GetData(g_ctx, (ID3D11Asynchronous *)g_occl[t->first], + &n, sizeof n, 0)) == S_FALSE) + ; + if (FAILED(hr)) + return -1; + *samples = n; + return 0; +} + const RhiBackend rhi_d3d11_backend = { "d3d11", d_device_create, d_device_destroy, d_device_ready, d_swapchain_view, d_swapchain_resize, @@ -1307,6 +1378,7 @@ const RhiBackend rhi_d3d11_backend = { d_set_uniform_buffers, d_set_textures, d_set_samplers, d_set_blend_state, d_set_depth_state, d_set_raster_state, d_draw, d_draw_indexed, d_clear_color, d_clear_depth, d_image_copy, + d_occlusion_begin, d_occlusion_end, d_occlusion_result, }; #endif /* _WIN32 */ diff --git a/src/d3d/rhi_vulkan.c b/src/d3d/rhi_vulkan.c index e38988c07..f9d81826d 100644 --- a/src/d3d/rhi_vulkan.c +++ b/src/d3d/rhi_vulkan.c @@ -88,6 +88,7 @@ int rhi_vk_dxc_compile(uint32_t stage, const RhiShaderSource *src, #define RING_CHUNK (8u << 20) #define MAX_VB 16 #define MAX_TEX 16 +#define OCCL_QUERIES 8192u /* rhi_occlusion_*: one a counted draw */ #define MAX_UBO 8 #define MAX_REFL 32 @@ -241,6 +242,7 @@ static struct { VmaAllocator vma; VkFormat depth24; /* D24S8, or D32S8 where there is none */ int feat_depth_clamp, feat_fill, feat_bias_clamp, feat_aniso, feat_border, feat_lod_bias; + int feat_query_reset, feat_precise; int validation; VkDescriptorSetLayout set_layout; @@ -317,6 +319,13 @@ static struct { uint32_t read_mask, write_mask, ref; float blend_const[4]; } dyn; + + /* Occlusion counting (rhi_occlusion_*): one query a draw while a test is + * open, taken in order from the pool and reset from the host when the + * pool is used up. A generation tells a test whose queries were reused. */ + VkQueryPool occl_pool; + uint32_t occl_next, occl_first, occl_gen; + int occl_counting, occl_overflow; } V; static struct { @@ -1414,6 +1423,11 @@ static int create_device(void) f2.features.depthClamp = V.feat_depth_clamp = have.features.depthClamp; f2.features.depthBiasClamp = V.feat_bias_clamp = have.features.depthBiasClamp; f2.features.samplerAnisotropy = V.feat_aniso = have.features.samplerAnisotropy; + /* Visibility tests (rhi_occlusion_*): the NV2A counts every sample that + * passes, so a precise count where the device has one; the pool is reset + * from the host, between frames' worth of draws. */ + f2.features.occlusionQueryPrecise = V.feat_precise = have.features.occlusionQueryPrecise; + f12.hostQueryReset = V.feat_query_reset = have12.hostQueryReset; f12.timelineSemaphore = VK_TRUE; f12.scalarBlockLayout = have12.scalarBlockLayout; f12.samplerMirrorClampToEdge = have12.samplerMirrorClampToEdge; @@ -1637,6 +1651,18 @@ static int create_frames(void) VKCHECK(vkAllocateCommandBuffers(V.dev, &ai, &V.frames[i].cmd), "command buffer"); VKCHECK(vkCreateSemaphore(V.dev, &sci, NULL, &V.frames[i].acquire), "semaphore"); } + if (V.feat_query_reset) { + VkQueryPoolCreateInfo qci = { VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO }; + qci.queryType = VK_QUERY_TYPE_OCCLUSION; + qci.queryCount = OCCL_QUERIES; + if (vkCreateQueryPool(V.dev, &qci, NULL, &V.occl_pool) == VK_SUCCESS) + vkResetQueryPool(V.dev, V.occl_pool, 0, OCCL_QUERIES); + else + V.occl_pool = VK_NULL_HANDLE; + } + if (!V.occl_pool) + fprintf(stderr, "[RHI] vulkan: no host query reset on %s; visibility tests are not " + "counted\n", V.props.deviceName); return 0; } @@ -1922,6 +1948,7 @@ static void v_device_destroy(void) destroy_swapchain_views(); if (SC.sc) vkDestroySwapchainKHR(V.dev, SC.sc, NULL); vkDestroySemaphore(V.dev, V.timeline, NULL); + if (V.occl_pool) vkDestroyQueryPool(V.dev, V.occl_pool, NULL); pcache_save(); vkDestroyPipelineCache(V.dev, V.pcache, NULL); vkDestroyPipelineLayout(V.dev, V.pipe_layout, NULL); @@ -3969,22 +3996,106 @@ static int prepare_draw(void) return push_descriptors(p); } +/* A draw inside an open visibility test runs inside a query of its own: + * a query begun in a rendering scope must end in it, and a test can span a + * render-target change, which ends the scope. 1 when this draw is counted. */ +static int occl_draw_begin(void) +{ + if (!V.occl_counting) + return 0; + if (V.occl_next >= OCCL_QUERIES) { + V.occl_overflow = 1; + return 0; + } + vkCmdBeginQuery(cmd(), V.occl_pool, V.occl_next, + V.feat_precise ? VK_QUERY_CONTROL_PRECISE_BIT : 0); + return 1; +} + +static void occl_draw_end(int counted) +{ + if (counted) + vkCmdEndQuery(cmd(), V.occl_pool, V.occl_next++); +} + static void v_draw(uint32_t n, uint32_t first) { - if (n && prepare_draw()) + int q; + if (n && prepare_draw()) { + q = occl_draw_begin(); vkCmdDraw(cmd(), n, 1, first, 0); + occl_draw_end(q); + } } static void v_draw_indexed(uint32_t n, uint32_t first, int32_t base) { BufVer *v; + int q; if (!n || !V.ib || !V.ib->nver || !prepare_draw()) return; v = &V.ib->ver[V.ib->cur]; v->used = V.submitted + 1; vkCmdBindIndexBuffer(cmd(), v->b, V.ib_offset, V.ib_bits == 32 ? VK_INDEX_TYPE_UINT32 : VK_INDEX_TYPE_UINT16); + q = occl_draw_begin(); vkCmdDrawIndexed(cmd(), n, 1, first, base, 0); + occl_draw_end(q); +} + +static int v_occlusion_begin(void) +{ + if (!V.occl_pool) + return -1; + /* Room for a test's draws, or start the pool again: everything recorded + * is finished first, so no query being reset is still in flight. A test + * still waiting for its answer from before then reads as reused. */ + if (V.occl_next > OCCL_QUERIES - OCCL_QUERIES / 8) { + flush_wait(); + vkResetQueryPool(V.dev, V.occl_pool, 0, OCCL_QUERIES); + V.occl_next = 0; + V.occl_gen++; + } + V.occl_counting = 1; + V.occl_overflow = 0; + V.occl_first = V.occl_next; + return 0; +} + +static void v_occlusion_end(RhiOcclusion *out) +{ + if (!V.occl_counting) + return; + out->first = V.occl_first; + out->count = V.occl_next - V.occl_first; + out->generation = V.occl_overflow ? ~V.occl_gen : V.occl_gen; + V.occl_counting = 0; +} + +static int v_occlusion_result(const RhiOcclusion *t, uint64_t *samples) +{ + uint64_t r[256]; + uint32_t done = 0; + + *samples = 0; + if (!V.occl_pool || t->generation != V.occl_gen) + return -1; + if (!t->count) + return 0; + flush_wait(); + while (done < t->count) { + uint32_t n = t->count - done, k; + if (n > 256) + n = 256; + if (vkGetQueryPoolResults(V.dev, V.occl_pool, t->first + done, n, sizeof r, r, + sizeof r[0], VK_QUERY_RESULT_64_BIT | + VK_QUERY_RESULT_WAIT_BIT) != VK_SUCCESS) + return -1; + for (k = 0; k < n; k++) + *samples += r[k]; + done += n; + } + return 0; } const RhiBackend rhi_vulkan_backend = { @@ -4003,6 +4114,7 @@ const RhiBackend rhi_vulkan_backend = { v_set_uniform_buffers, v_set_textures, v_set_samplers, v_set_blend_state, v_set_depth_state, v_set_raster_state, v_draw, v_draw_indexed, v_clear_color, v_clear_depth, v_image_copy, + v_occlusion_begin, v_occlusion_end, v_occlusion_result, }; #endif /* RHI_HAVE_VULKAN */ diff --git a/src/hle/hle_d3d8.c b/src/hle/hle_d3d8.c index 5de4de4ff..9efabbddf 100644 --- a/src/hle/hle_d3d8.c +++ b/src/hle/hle_d3d8.c @@ -4637,10 +4637,30 @@ HLE_EXPORT(D3DDevice_End) if (original_missing(hle_original_D3DDevice_End, "D3DDevice_End")) return; g_inline_end++; + /* The vertices since Begin, when the title's SetVertexData* are not + * replaced: they are in the push buffer, written by the title's own + * code. The 5960 online dashboard draws every screen into a target and + * puts it up with one such quad; skipped with End's own writes below, + * it collected no vertices and every frame stayed black. Walked here, + * while the group is open, they reach inline_vertex_data. */ + if (g_inline_on) + hle_d3d8_push_constants_sync(); HLE_CALL_ORIGINAL(D3DDevice_End); hle_d3d8_push_skip(); /* its own vertex writes: drawn here, not by the walk */ if (g_inline_on) { + /* Under the program the NV2A would run: one started through the push + * buffer since the API last selected a shader, if there is one. + * Constantine's Swap resolves its 1280x480 supersampled screen with a + * triangle drawn this way: the XDK starts a two-instruction program + * (oPos = v0, oT0 = v9) itself and calls Begin / End. Drawn under the + * title's own 2D program, which reads its texture coordinate from v2, + * every pixel sampled one texel and each frame came out one flat + * colour. */ + PushedProgramSave s; + + pushed_program_begin(&s); inline_draw(); + pushed_program_end(&s); g_inline_on = 0; g_inline_nverts = 0; } @@ -5883,6 +5903,7 @@ HLE_EXPORT(D3DDevice_UpdateOverlay) fflush(stderr); } d3d8_movie_draw(); + shadow_dump_frame(); /* F11 and the frame dumps see the movie too */ host_Swap(g_shadow, 0); } } @@ -6532,6 +6553,134 @@ HLE_EXPORT(D3DDevice_DrawIndexedVerticesUP) HLE_PTR(data), stride, 0); } +/* Visibility tests, counted on the host. + * + * D3DDevice_BeginVisibilityTest writes SET_ZPASS_PIXEL_COUNT_ENABLE 1, and + * EndVisibilityTest(Index) SET_ZPASS_PIXEL_COUNT_ENABLE 0 and GET_REPORT + * with the offset of the test's report slot. The NV2A writes the slot, and + * GetVisibilityTestResult reads it from a table of its own: slot Index & 0xFF + * of a 4 KB page, 16 bytes each, so bits 4-11 of the offset are the low byte + * of the index (XDK 5849, sub at Constantine 0x002A3B90). The push-buffer + * walk hands those methods here, the host counts the draws in between + * (rhi_occlusion_*), and GetVisibilityTestResult answers with the count of + * the newest closed test whose offset matches its index. + * + * Constantine culls its scenery and its hero by these tests, 53 a frame in + * its first level. Answered "0 pixels", Constantine himself and most of his + * apartment were never drawn. The count is in the guest's samples: a + * supersampled screen counts each of its samples, and a host that renders + * larger counts more. RECOMP_HLE_D3D8_VISIBILITY=0 goes back to answering + * without counting. */ +void d3d8_display_scene_size(UINT guest_w, UINT guest_h, UINT *scene_w, UINT *scene_h); + +#define VIS_TESTS 512u +static struct { + uint32_t offset; /* GET_REPORT's; 0 until it arrives */ + RhiOcclusion t; + double scale; + int have; /* counted: `pixels` holds the answer */ + uint32_t pixels, swap; +} g_vis[VIS_TESTS]; +static uint32_t g_vis_n; /* tests closed so far */ +static int g_vis_open; +static double g_vis_scale = 1.0; +static unsigned long g_vis_answered, g_vis_uncounted; + +static int visibility_on(void) +{ + static int on = -1; + if (on < 0) + on = xbox_EnvSwitch("RECOMP_HLE_D3D8_VISIBILITY", 1); + return on && g_shadow; +} + +void hle_d3d8_pb_zpass(uint32_t enable) +{ + if (!visibility_on()) + return; + if (enable) { + UINT sw = g_target_width, sh = g_target_height; + RhiOcclusion dropped; + + if (g_vis_open) + rhi_occlusion_end(&dropped); /* a second begin: the first is dropped */ + g_vis_open = rhi_occlusion_begin() == 0; + if (!g_vis_open && g_vis_uncounted++ == 0) + fprintf(stderr, "[HLE-D3D8] visibility tests: this renderer cannot count " + "samples; answered as before\n"); + if (g_target_is_screen && g_target_width && g_target_height) + d3d8_display_scene_size(g_target_width, g_target_height, &sw, &sh); + g_vis_scale = (sw && sh) ? (double)g_target_width * g_target_height / ((double)sw * sh) + : 1.0; + if (g_target_is_screen) + g_vis_scale *= (double)(g_target_aa_x * g_target_aa_y); + } else if (g_vis_open) { + uint32_t i = g_vis_n++ % VIS_TESTS; + + rhi_occlusion_end(&g_vis[i].t); + g_vis[i].offset = 0; + g_vis[i].scale = g_vis_scale; + g_vis[i].have = 0; + g_vis[i].swap = (uint32_t)g_shadow_swaps; + g_vis_open = 0; + } +} + +void hle_d3d8_pb_report(uint32_t value) +{ + uint32_t i; + + if (!visibility_on() || !g_vis_n) + return; + i = (g_vis_n - 1u) % VIS_TESTS; + if (!g_vis[i].offset) + g_vis[i].offset = value & 0x00FFFFFFu; +} + +/* Every closed test not yet counted, counted: the first waits for the GPU, + * the rest find their queries finished. A title asks for its tests after + * closing the whole batch (Constantine about 30 tests later), and drawing + * goes on between the asks, so answering one at a time waited on the GPU + * some 80 times a frame and cost a fifth of the frame rate. */ +static void visibility_count_all(void) +{ + uint32_t back, n = g_vis_n < VIS_TESTS ? g_vis_n : VIS_TESTS; + + for (back = 1; back <= n; back++) { + uint32_t i = (g_vis_n - back) % VIS_TESTS; + uint64_t samples; + + if (g_vis[i].have || !g_vis[i].offset) + continue; + if (rhi_occlusion_result(&g_vis[i].t, &samples) == 0) + g_vis[i].pixels = (uint32_t)((double)samples * g_vis[i].scale + 0.5); + else + g_vis[i].offset = 0; /* its queries were reused: no answer */ + g_vis[i].have = 1; + } +} + +/* The host's count for test `index`, from the newest closed test whose + * report offset names that slot. 0 with *pixels set, -1 when there is none. */ +static int visibility_count(uint32_t index, uint32_t *pixels) +{ + uint32_t back, n = g_vis_n < VIS_TESTS ? g_vis_n : VIS_TESTS; + + for (back = 1; back <= n; back++) { + uint32_t i = (g_vis_n - back) % VIS_TESTS; + + if (!g_vis[i].offset || ((g_vis[i].offset >> 4) & 0xFFu) != (index & 0xFFu)) + continue; + if (!g_vis[i].have) + visibility_count_all(); + if (!g_vis[i].offset) + return -1; + *pixels = g_vis[i].pixels; + return 0; + } + return -1; +} + /* HRESULT D3DDevice_GetVisibilityTestResult(DWORD Index, UINT *pResult, * ULONGLONG *pTimeStamp) * @@ -6577,6 +6726,23 @@ HLE_EXPORT(D3DDevice_GetVisibilityTestResult) HLE_CALL_ORIGINAL(D3DDevice_GetVisibilityTestResult); if (g_eax != 0x88760828u) /* not D3DERR_TESTINCOMPLETE */ return; + if (visibility_on()) { + uint32_t counted; + + hle_d3d8_push_constants_sync(); /* the End this asks about */ + if (visibility_count(index, &counted) == 0) { + if (result_va) + HLE_MEM32(result_va) = counted; + if (stamp_va) { + HLE_MEM32(stamp_va) = (uint32_t)g_shadow_swaps; + HLE_MEM32(stamp_va + 4) = 0; + } + if (g_vis_answered++ == 0) + fprintf(stderr, "[HLE-D3D8] visibility tests: counted on the host " + "(test %u: %u sample(s))\n", index, counted); + HLE_RETURN(0); /* S_OK */ + } + } if (executor < 0) { const char *e = getenv("RECOMP_HLE_D3D8_VISIBLE_PIXELS"); executor = getenv("RECOMP_PB_EXEC") != NULL; diff --git a/src/hle/hle_d3d8_vertex.c b/src/hle/hle_d3d8_vertex.c index e71947ef7..793ee1f24 100644 --- a/src/hle/hle_d3d8_vertex.c +++ b/src/hle/hle_d3d8_vertex.c @@ -485,6 +485,13 @@ static uint32_t g_clear_reg[5]; * SET_SURFACE_ZETA_OFFSET (0x0214), for a depth switch the title made * without a call this file replaces (hle_d3d8_pb_surface). */ void hle_d3d8_pb_surface(uint32_t color, uint32_t zeta); +/* ...and a visibility test's methods, SET_ZPASS_PIXEL_COUNT_ENABLE (0x17CC) + * and GET_REPORT (0x17D0): Begin / EndVisibilityTest write them, and a title + * may write them inline. */ +void hle_d3d8_pb_zpass(uint32_t enable); +void hle_d3d8_pb_report(uint32_t value); +#define PB_ZPASS_ENABLE 0x17CCu +#define PB_GET_REPORT 0x17D0u #define PB_SURFACE_COLOR 0x0210u #define PB_SURFACE_ZETA 0x0214u static uint32_t g_surface_color; @@ -977,6 +984,21 @@ void hle_d3d8_push_constants_sync(void) pb_program_method((w & 0x40000000u) ? method : method + 4u * k, HLE_MEM32(va + 4u + 4u * k), g_w_nodraw); } + /* Visibility tests: SET_ZPASS_PIXEL_COUNT_ENABLE opens and closes the + * count, GET_REPORT names the slot it is written to (hle_d3d8.c). */ + if (inl && method + 4u * count > PB_ZPASS_ENABLE && method <= PB_GET_REPORT && + guest_readable(va + 4u, 4ull * count)) { + uint32_t k; + for (k = 0; k < count; k++) { + uint32_t m = (w & 0x40000000u) ? method : method + 4u * k; + if (m == PB_ZPASS_ENABLE) { + pb_flush(); + hle_d3d8_pb_zpass(HLE_MEM32(va + 4u + 4u * k)); + } else if (m == PB_GET_REPORT) { + hle_d3d8_pb_report(HLE_MEM32(va + 4u + 4u * k)); + } + } + } if (inl && method + 4u * count > PB_SURFACE_COLOR && method <= PB_SURFACE_ZETA && guest_readable(va + 4u, 4ull * count)) { uint32_t k; diff --git a/src/kernel/kernel_hal.c b/src/kernel/kernel_hal.c index f1a1edd2e..964821aa4 100644 --- a/src/kernel/kernel_hal.c +++ b/src/kernel/kernel_hal.c @@ -1143,6 +1143,54 @@ ULONGLONG __stdcall xbox_KeQueryInterruptTime(void) * ========================================================================= */ #define XBOX_TSC_HZ 733333333ull +/* A thread that does nothing but read the clock is waiting for time to pass, + * and on the host it does so at the speed of the host: Constantine's Bink + * player asks "is the next frame due?" (a mutex taken and released) and reads + * the TSC round and round between movie frames, about two million kernel + * calls a second on one core -- heat and battery on a handheld for nothing. + * The console spins the same way, but there the CPU is its own. + * + * So a thread that reads the TSC POLL_READS times within a millisecond of + * host time, POLL_WINDOWS windows running, sleeps a millisecond (dropping the + * guest lock, so the thread it waits for runs) and reads the clock afresh. A + * loop waiting for a moment overshoots it by at most about that long; work + * that reads the clock as it goes reads it far less often than this. + * RECOMP_TSC_POLL_SLEEP=0 lets it spin. */ +#define POLL_READS 256u +#define POLL_WINDOWS 4u +static XBOX_THREAD_LOCAL LONGLONG t_poll_start; +static XBOX_THREAD_LOCAL uint32_t t_poll_reads, t_poll_windows; +static volatile LONG g_poll_sleeps; + +static int tsc_poll_backoff(LONGLONG now, LONGLONG freq) +{ + static int on = -1; + + if (on < 0) + on = xbox_EnvSwitch("RECOMP_TSC_POLL_SLEEP", 1); + if (!on) + return 0; + if (t_poll_reads++ == 0) { + t_poll_start = now; + return 0; + } + if (t_poll_reads < POLL_READS) + return 0; + t_poll_reads = 0; + if (now - t_poll_start >= freq / 1000) { + t_poll_windows = 0; /* reads spread out: work, not a poll */ + return 0; + } + if (++t_poll_windows < POLL_WINDOWS) + return 0; + if (InterlockedIncrement(&g_poll_sleeps) == 1) + fprintf(stderr, "[TIME] a thread polls the clock (%u reads within 1 ms, %u times " + "running): it sleeps 1 ms between polls (RECOMP_TSC_POLL_SLEEP=0 to " + "spin)\n", POLL_READS, POLL_WINDOWS); + xbox_GuestSleep(1); + return 1; +} + uint64_t xbox_ReadTimeStampCounter(void) { static LARGE_INTEGER freq; @@ -1156,6 +1204,8 @@ uint64_t xbox_ReadTimeStampCounter(void) freq.QuadPart = 1; } QueryPerformanceCounter(&now); + if (tsc_poll_backoff(now.QuadPart, freq.QuadPart)) + QueryPerformanceCounter(&now); { uint64_t ticks = (uint64_t)(now.QuadPart - origin.QuadPart); diff --git a/tools/disasm/functions.py b/tools/disasm/functions.py index f343cc7d3..258c86101 100644 --- a/tools/disasm/functions.py +++ b/tools/disasm/functions.py @@ -9,7 +9,9 @@ 5. Cross-validation and overlap resolution """ +import array import bisect +import sys from dataclasses import dataclass, field from typing import Dict, List, Optional, Set, Tuple @@ -274,6 +276,44 @@ def table_or_its_padding(addr: int) -> bool: # count; the first byte of real code (8B, 55, 83, 56, ...) is not. So # skip the index bytes, then int3/nop padding, and what is left is the # next function -- or another switch's 8B FF, which is not. + # + # The index walk stops at an address something else already names as + # code: a known start, an alias entry, or the target of a direct jump + # or call. An index table is data, nothing branches into it. A switch + # with more cases than the first opcode bytes' values cannot tell the + # two apart by value: Constantine's at 0x0004E220 has ~156 entries, so + # "56 8B F1" (push esi; mov esi, ecx) at 0x0004E490 -- a tail-jump + # target -- read as two more indices, a function was started at the + # F1 in the middle of the mov, and the lifted method lost "mov esi, + # ecx": its vtable calls went through whatever esi held, and the + # difficulty menu's options were never built. + # + # A method reached only through a vtable is named by nothing in the + # code at all -- 0x0004E490 is one -- and the pass that collects + # pointers from data runs after this one. So the data sections are + # searched here too, for words naming any byte an index walk could + # reach. A coincidental match only ends a walk early, which costs a + # recovery, never a split. + # Alias entries are not evidence here: one can come from the sweep's + # own junk (Constantine 0x00140010, index bytes taken for a tail-jump + # target), and stopping at it lost the real function at 0x00140020. + existing = set(self._candidates) | set(self.functions) + known = set(existing) + # Branches only from inside a function: the sweep decodes the index + # bytes too, and their garbage jumps land among the index bytes + # (Constantine lost five real functions after switch tables to that). + for insn in self.engine.instructions.values(): + if in_a_gap(insn.address): + continue + for t in (getattr(insn, "jump_target", None), + getattr(insn, "call_target", None)): + if t is not None: + known.add(t) + code_ranges = [(sec.virtual_addr, sec.virtual_addr + sec.virtual_size) + for sec in sections] + known |= self._data_words_naming( + {a for _tbl, end in tables for a in range(end, end + 256)}, + {sec.name for sec in sections}, code_ranges) after = 0 for tbl, end in tables: entries = (end - tbl) // 4 @@ -285,14 +325,22 @@ def table_or_its_padding(addr: int) -> bool: b = self.image.read_bytes_at_va(addr, 1) if not b or b[0] >= entries: break + # Named as code, and could be a function (the 0x0F rule below, + # and a body that decodes): 0x00140010 is named by a pointer + # and starts "00 00", index bytes, not code. + if (addr in known and b[0] >= 0x0F + and self.engine.probes_as_function_body(addr)): + break addr += 1 while True: b = self.image.read_bytes_at_va(addr, 1) if not b or b[0] not in (0xCC, 0x90): break addr += 1 - if (addr in self._candidates or addr in self.functions - or table_or_its_padding(addr) or not in_a_gap(addr)): + # Only an existing entry is a reason to skip it: an address the + # walk stopped at because code or a vtable names it is exactly + # the function this pass is here to find. + if (addr in existing or table_or_its_padding(addr) or not in_a_gap(addr)): continue # Opcodes 0x00-0x0E are `add`, `or` and segment pushes. No function # starts with one, but an index table does, because its bytes are @@ -322,6 +370,36 @@ def table_or_its_padding(addr: int) -> bool: + (f" ({after} after a switch table)" if after else "")) return added + def _data_words_naming(self, addrs: Set[int], code_names: Set[str], + code_ranges: List[Tuple[int, int]]) -> Set[int]: + """The members of `addrs` stored as an aligned dword outside the code + sections, beside another code pointer -- a vtable or a callback table, + not a lone word that matches by chance. Code sections by name, as + _pass_data_ptr_targets decides: an XBE flags .rdata executable, and + that is where vtables are.""" + def is_code(w: int) -> bool: + return any(lo <= w < hi for lo, hi in code_ranges) + + found: Set[int] = set() + sections = getattr(self.image, "sections", None) + if not addrs or not sections: + return found + for sec in sections: + if sec.name in code_names or sec.name == ".XTLID": + continue + data = self.image.get_section_data(sec) + if not data: + continue + words = array.array("I") + words.frombytes(bytes(data[:len(data) & ~3])) + if sys.byteorder != "little": + words.byteswap() + for i, w in enumerate(words): + if w in addrs and ((i > 0 and is_code(words[i - 1])) or + (i + 1 < len(words) and is_code(words[i + 1]))): + found.add(w) + return found + def _pass_seed_aliases(self) -> None: """ Turn seeded addresses that fell inside a function into alias entries. diff --git a/tools/disasm/test_gap_prologue.py b/tools/disasm/test_gap_prologue.py index 59f4c5f66..5b7e07815 100644 --- a/tools/disasm/test_gap_prologue.py +++ b/tools/disasm/test_gap_prologue.py @@ -23,11 +23,13 @@ class _Insn: - def __init__(self, addr, size, is_ret=False): + def __init__(self, addr, size, is_ret=False, jump_target=None): self.address = addr self.size = size self.end_address = addr + size self.is_ret = is_ret + self.jump_target = jump_target + self.call_target = None class _Section: @@ -83,6 +85,7 @@ def _detector(insns, functions, prologues, jump_tables=None, memory=None, det.image = _Image(memory) det.functions = {f.start: f for f in functions} det._candidates = {} + det._alias_entries = {} det.added = [] det._add_candidate = lambda addr, conf, why: det.added.append((addr, why)) return det @@ -209,6 +212,89 @@ def test_a_second_switchs_table_is_not_a_function(self): self.assertEqual(det.added, []) +class _Code: + """.text, for the code-pointer test beside a data word.""" + name = ".text" + virtual_addr = 0x00011000 + virtual_size = 0x00300000 + + +class BranchTargetAfterTableTest(unittest.TestCase): + """Constantine 0x0004E490: an index table ends straight into a function. + + The switch has more cases than "56" and "8B" are worth, so the bytes of + "push esi; mov esi, ecx" read as index bytes by value alone. 0x0004E490 is + a tail-jump target, which no index table can be. + """ + TABLE = (0x0004E220, 0x0004E220 + 4 * 160) + INDEX = TABLE[1] + FUNC = INDEX + 4 + BYTES = bytes.fromhex("01020304" "56" "8bf1" "8b4604") + + def _det(self, insns, aliases=None): + det = _detector(insns, [_Func(0x0004E100, 0x0004E21D)], prologues=set(), + jump_tables=dict([self.TABLE]), + memory={self.INDEX: self.BYTES}, + bodies={self.FUNC, self.FUNC + 2}) + det._alias_entries = dict(aliases or {}) + return det + + def test_a_jump_target_ends_the_index_bytes(self): + det = self._det([_Insn(0x0004E110, 5, jump_target=self.FUNC)]) + det._pass_gap_prologues([]) + # The function itself, not the F1 in the middle of its mov. + self.assertEqual(det.added, [(self.FUNC, "after_switch_table")]) + + def test_an_alias_entry_does_not_end_the_index_bytes(self): + # Aliases can be the sweep's own junk (Constantine 0x00140010). + det = self._det([], aliases={self.FUNC: self.FUNC + 0x1C}) + det._pass_gap_prologues([]) + self.assertEqual(det.added, [(self.FUNC + 2, "after_switch_table")]) + + def test_a_vtable_slot_ends_the_index_bytes(self): + # How 0x0004E490 is actually reached: only through a vtable in .rdata. + import struct + + class _Rdata: + name = ".rdata" + executable = True # as an XBE flags it + + det = self._det([]) + det.image.sections = [_Rdata()] + det.image.get_section_data = lambda sec: struct.pack("<3I", 0x0004E000, + self.FUNC, 0x0004E600) + det._pass_gap_prologues([_Code()]) + self.assertEqual(det.added, [(self.FUNC, "after_switch_table")]) + + def test_a_lone_data_word_does_not(self): + # A word that names an index byte with no code pointer beside it is a + # coincidence (Constantine 0x00140010): the walk goes on. + import struct + + class _Rdata: + name = ".rdata" + executable = True + + det = self._det([]) + det.image.sections = [_Rdata()] + det.image.get_section_data = lambda sec: struct.pack("<3I", 7, self.FUNC, 9) + det._pass_gap_prologues([_Code()]) + self.assertEqual(det.added, [(self.FUNC + 2, "after_switch_table")]) + + def test_a_jump_decoded_from_the_index_bytes_does_not(self): + # The sweep decodes index bytes as code; a jump from there is junk. + det = self._det([_Insn(self.INDEX, 2, jump_target=self.FUNC)]) + det._pass_gap_prologues([]) + self.assertEqual(det.added, [(self.FUNC + 2, "after_switch_table")]) + + def test_without_either_the_walk_runs_on(self): + # What happened before: the walk takes 56 and 8B as indices and + # starts a function at the F1 inside the mov. + det = self._det([]) + det._pass_gap_prologues([]) + self.assertEqual(det.added, [(self.FUNC + 2, "after_switch_table")]) + + class SehPrologueShapeTest(unittest.TestCase): """A function whose frame __SEH_prolog builds has no prologue to find.