diff --git a/config/regpromote/4D53006E.json b/config/regpromote/4D53006E.json new file mode 100644 index 000000000..7d9a9f349 --- /dev/null +++ b/config/regpromote/4D53006E.json @@ -0,0 +1,402 @@ +[ + "0x000120A0", + "0x000128D0", + "0x000149A0", + "0x00015000", + "0x00015810", + "0x0001F270", + "0x000202D0", + "0x000205A0", + "0x00020D80", + "0x00025800", + "0x00025AA0", + "0x00025AC0", + "0x00027EF0", + "0x00027FB0", + "0x000287C0", + "0x0002C4C0", + "0x0002CD90", + "0x000322A0", + "0x00039540", + "0x0003DE50", + "0x0003FC10", + "0x0003FF60", + "0x00046840", + "0x00046BD0", + "0x0004D180", + "0x000535B0", + "0x00057E90", + "0x00059460", + "0x000594D0", + "0x00059560", + "0x0005C9E0", + "0x0005DCD0", + "0x00072340", + "0x00075530", + "0x00076A00", + "0x00076AC0", + "0x0007A130", + "0x0007F010", + "0x000804B0", + "0x000804E0", + "0x000874B0", + "0x00087650", + "0x00087790", + "0x00088390", + "0x00088BC0", + "0x000894B0", + "0x00089C90", + "0x000A2D20", + "0x000AF920", + "0x000B0610", + "0x000B0730", + "0x000B0F60", + "0x000BC680", + "0x000D31E0", + "0x000DA050", + "0x000DA1B0", + "0x000DA2E0", + "0x000DB260", + "0x000DB600", + "0x000DB6E0", + "0x000DBAF0", + "0x000DC2F0", + "0x000DCA20", + "0x000DD220", + "0x000DD290", + "0x000DD320", + "0x000DD3A0", + "0x000DD6C0", + "0x000DE920", + "0x000DED90", + "0x000DF200", + "0x000DF310", + "0x000DF570", + "0x000DF980", + "0x000DFB80", + "0x000DFEB0", + "0x000E00B0", + "0x000E0B70", + "0x000E0F50", + "0x000E1210", + "0x000E1570", + "0x000E1810", + "0x000E1E80", + "0x000E24A0", + "0x000E2A00", + "0x000E2BC0", + "0x000E2FA0", + "0x000E3150", + "0x000E32E0", + "0x000E3B70", + "0x000E44C0", + "0x000E4520", + "0x000E4580", + "0x000E46B0", + "0x000E5070", + "0x000E58D0", + "0x000E5A00", + "0x000E5CA0", + "0x000E5DD0", + "0x000E5FA0", + "0x000E6050", + "0x000E6090", + "0x000E6A00", + "0x000E6FD0", + "0x000E8320", + "0x000E8450", + "0x000E8C90", + "0x000E9080", + "0x000E9500", + "0x000E9590", + "0x000F1BD0", + "0x000F20C0", + "0x000F28F0", + "0x000F2EE0", + "0x000F3370", + "0x000F3740", + "0x000F4270", + "0x000F4300", + "0x000F48F0", + "0x000F54D0", + "0x000F5530", + "0x000F55B0", + "0x000F5780", + "0x000F57A0", + "0x000F57C0", + "0x000F57E0", + "0x000F5800", + "0x000F5850", + "0x000F5890", + "0x000F5960", + "0x000F5B70", + "0x000F5C90", + "0x000F6030", + "0x000F60E0", + "0x000F62B0", + "0x000F63E0", + "0x000F66B0", + "0x000F6A70", + "0x000F6C60", + "0x000F7450", + "0x000F7710", + "0x000F9900", + "0x000F9BE0", + "0x000F9CF0", + "0x000F9DB0", + "0x000F9E60", + "0x000FA000", + "0x000FAAC0", + "0x000FB930", + "0x000FEAD0", + "0x000FF4F0", + "0x00100580", + "0x001005A0", + "0x00100820", + "0x00100FC0", + "0x00101450", + "0x00102D50", + "0x0010B610", + "0x00138590", + "0x001387A0", + "0x00139050", + "0x0013AA50", + "0x0013B030", + "0x0013FCC0", + "0x00169AB0", + "0x001B9300", + "0x001BA710", + "0x001BA7E0", + "0x001BA8B0", + "0x001C11A0", + "0x001C2570", + "0x001C2890", + "0x001C33A0", + "0x001C35B0", + "0x001C6F20", + "0x001CB8C0", + "0x001CB990", + "0x001CBB70", + "0x001CC8D0", + "0x001CE370", + "0x001CE580", + "0x001CF4A0", + "0x001D1230", + "0x001D1650", + "0x001D2060", + "0x001D20C0", + "0x001D22E0", + "0x001D2370", + "0x001D23E0", + "0x001D2650", + "0x001D2850", + "0x001D3020", + "0x001D3120", + "0x001D32B0", + "0x001D3320", + "0x001D3490", + "0x001D3610", + "0x001D3AA0", + "0x001D4290", + "0x001D4FD0", + "0x001D54C0", + "0x001D8DC0", + "0x001D8E00", + "0x001D8E80", + "0x001D97C0", + "0x001D9EF0", + "0x001D9F70", + "0x001DA100", + "0x001DA190", + "0x001DA670", + "0x001DA830", + "0x001DAA70", + "0x001DAB10", + "0x001DAD90", + "0x001DEA20", + "0x001DF6E0", + "0x001DFBA0", + "0x001E0A90", + "0x001E15F0", + "0x001E1B20", + "0x001E1F60", + "0x001E1FF0", + "0x001E25D0", + "0x001E2BF0", + "0x001E2FC0", + "0x001E33F0", + "0x001E3460", + "0x001E3A80", + "0x001E3B20", + "0x001E3C10", + "0x001E3D20", + "0x001E4580", + "0x001E53A0", + "0x001E53D0", + "0x001E5C60", + "0x001E5DE0", + "0x001E6410", + "0x001E6540", + "0x001E7B50", + "0x001E8120", + "0x001E8BD0", + "0x001EF6B0", + "0x001F04E0", + "0x001F3110", + "0x001F35B0", + "0x001F7C70", + "0x001F9040", + "0x001F93E0", + "0x001F9820", + "0x001FA140", + "0x001FAA60", + "0x002037F0", + "0x00207C30", + "0x00207E00", + "0x00213310", + "0x00215140", + "0x00215560", + "0x00215CD0", + "0x00216760", + "0x0021A1F0", + "0x0021ABF0", + "0x0021D000", + "0x0021E790", + "0x00222B50", + "0x002231D0", + "0x00223E40", + "0x00224890", + "0x0022C8B0", + "0x0022DC70", + "0x0022DF30", + "0x0022E120", + "0x0022E1A0", + "0x0022E7B0", + "0x0022ECD0", + "0x0022EF30", + "0x0022F880", + "0x0022FBF0", + "0x00230F90", + "0x00232E00", + "0x00233190", + "0x0023A3C0", + "0x0023A930", + "0x0023C9A0", + "0x00240070", + "0x00241110", + "0x00242070", + "0x00242210", + "0x00243E80", + "0x00243F70", + "0x00244030", + "0x00244390", + "0x00249810", + "0x00249920", + "0x0024B3A0", + "0x0024C070", + "0x0024C760", + "0x0024C900", + "0x0024CB70", + "0x0024CF30", + "0x0024D7B0", + "0x0024DDD0", + "0x0024E140", + "0x0024E2A0", + "0x0024FCA0", + "0x00250550", + "0x00250B60", + "0x002513D0", + "0x002515A0", + "0x002525D0", + "0x00256BA0", + "0x00256FF0", + "0x002587B0", + "0x00258A60", + "0x00259FB0", + "0x0025A6E0", + "0x0025A990", + "0x0025B3B0", + "0x0026D9C9", + "0x0026D9ED", + "0x0026DA3C", + "0x0026DB9F", + "0x0026DBCF", + "0x0026DCE6", + "0x0026DDA2", + "0x0026DE20", + "0x0026DF2C", + "0x0026E02A", + "0x0026E075", + "0x0026E45E", + "0x0026E62B", + "0x0026E72A", + "0x0026E930", + "0x0026EBC4", + "0x0026EFD5", + "0x00271310", + "0x00275670", + "0x002758A0", + "0x00275C30", + "0x00276910", + "0x00276D50", + "0x00277230", + "0x00278400", + "0x002787B0", + "0x00278860", + "0x002788E0", + "0x00278BE0", + "0x00279220", + "0x00279720", + "0x0027D040", + "0x0027FC80", + "0x0027FEB0", + "0x00286170", + "0x00286520", + "0x002F8078", + "0x002F8485", + "0x002F849B", + "0x002F852D", + "0x002F881D", + "0x002F8F46", + "0x002F95E7", + "0x002FB0CC", + "0x002FB107", + "0x002FB7FD", + "0x002FDFD5", + "0x002FE136", + "0x003B7500", + "0x003B7590", + "0x003B76C0", + "0x003B78D0", + "0x003B7A20", + "0x003B80D0", + "0x003B83F0", + "0x003B87F0", + "0x003B9CA0", + "0x003BAA40", + "0x003BB010", + "0x003BB080", + "0x003BB5D0", + "0x003BD490", + "0x003BD6C0", + "0x003BD810", + "0x003BDB90", + "0x003BE480", + "0x003BF0E0", + "0x003BF200", + "0x003BF270", + "0x003BFAC0", + "0x003C2E60", + "0x003C2FD0", + "0x003C3350", + "0x003C3540", + "0x003C4420", + "0x003C5F20", + "0x003C5FC0", + "0x003E7EFF", + "0x003F938D", + "0x003F9CAF", + "0x003F9D34", + "0x003F9E41", + "0x003FB3AF" +] diff --git a/config/seeds/4D53006E.json b/config/seeds/4D53006E.json new file mode 100644 index 000000000..3a2cfb9f4 --- /dev/null +++ b/config/seeds/4D53006E.json @@ -0,0 +1,6 @@ +[ + { + "start": "0x0019D240", + "note": "Post-race \"Updating Scoreboards\" state callback, reached only through this+0x1C of the race-end state machine at 0x00096C30; refused as an unresolved call target until lifted (forza1-source/src/fz_missing.c translates it by hand meanwhile)." + } +] diff --git a/scripts/recompile.py b/scripts/recompile.py index 1bea5c48a..d3f030c02 100644 --- a/scripts/recompile.py +++ b/scripts/recompile.py @@ -162,6 +162,8 @@ def build_commands(args, xbe: Path, analysis_json: Path): lift += ["--game-name", args.game_name] if args.trace_all_entries: lift.append("--trace-all-entries") + if getattr(args, "regpromote_file", None): + lift += ["--regpromote", str(args.regpromote_file)] # XDK functions with a name-keyed replacement (tools/recomp/hle.py), used # whenever tools.xdk_symbols has been run for this XBE. hle_symbols = xbe.with_name(xbe.stem + "_xdk_symbols.json") @@ -330,6 +332,14 @@ def main() -> int: args.data_sections = [e["pattern"] for e in entries] print(f"data sections: {', '.join(args.data_sections)} ({sec_file.name})") + # Functions whose guest registers are kept in C locals + # (tools/recomp/regpromote.py): the title's profile's hottest, per title. + args.regpromote_file = None + rp_file = config_file(args, "regpromote", title or "") + if title and rp_file.is_file(): + args.regpromote_file = rp_file + print(f"register promotion: {rp_file.name}") + if not args.dry_run and not xbe.is_file(): print(f"error: XBE not found: {xbe}", file=sys.stderr) return 1 diff --git a/src/apu/CMakeLists.txt b/src/apu/CMakeLists.txt index 0c0bc8466..50f724cb5 100644 --- a/src/apu/CMakeLists.txt +++ b/src/apu/CMakeLists.txt @@ -73,3 +73,7 @@ else() # Linux: platform layer (Win32 primitives on POSIX) + SDL3 audio + libm target_link_libraries(xbox_apu PRIVATE platform SDL3::SDL3 m) endif() +if(WIN32) + add_compile_definitions(_USE_MATH_DEFINES) +endif() + diff --git a/src/apu/apu_fault.c b/src/apu/apu_fault.c index afdb9137a..85d7983d5 100644 --- a/src/apu/apu_fault.c +++ b/src/apu/apu_fault.c @@ -92,8 +92,8 @@ static void say_undecoded(const char *what, recomp_fault *f, uint32_t va) fprintf(stderr, "[%s] undecoded access at 0x%08X, host insn %s\n", what, va, insn); fflush(stderr); } -#endif +#endif /* ---- the three range handlers -------------------------------------------- */ static int pcrtc_fault(recomp_fault *f, uint32_t va) diff --git a/src/apu/apu_mmio_hook.c b/src/apu/apu_mmio_hook.c index 8303f5cf7..ff9820657 100644 --- a/src/apu/apu_mmio_hook.c +++ b/src/apu/apu_mmio_hook.c @@ -422,8 +422,25 @@ int nv2a_intr_handle_write(PCONTEXT ctx, uintptr_t host_addr, uint64_t written; int size; - if (!mmio_decode_write_value(ctx, &written, &size)) + const uint8_t *ip = (const uint8_t *)ctx->Rip; + if (ip[0] == 0x83 && ((ip[1] >> 3) & 7) == 1 && (ip[1] >> 6) != 3) { + int modrm_len = decode_modrm_len(ip + 1, 0); + uint32_t imm = (uint32_t)(int32_t)(int8_t)ip[1 + modrm_len]; + uint32_t result = *(const volatile uint32_t *)host_addr | imm; + uint8_t parity = (uint8_t)result; + parity ^= parity >> 4; + parity ^= parity >> 2; + parity ^= parity >> 1; + ctx->EFlags = (ctx->EFlags & ~0x8C5u) | + ((result & 0x80000000u) ? 0x80u : 0) | + (result == 0 ? 0x40u : 0) | + ((parity & 1) == 0 ? 0x4u : 0); + ctx->Rip += 2 + modrm_len; + written = result; + size = 4; + } else if (!mmio_decode_write_value(ctx, &written, &size)) { return 0; + } if (nv2a_offset == NV2A_PCRTC_INTR_0_OFF) { uint32_t remaining = *pcrtc & ~(uint32_t)written; diff --git a/src/d3d/CMakeLists.txt b/src/d3d/CMakeLists.txt index 6e5dcfa5b..f9a312b49 100644 --- a/src/d3d/CMakeLists.txt +++ b/src/d3d/CMakeLists.txt @@ -146,4 +146,10 @@ else() # -Wno-missing-field-initializers: Vulkan's `{ VK_STRUCTURE_TYPE_... }` idiom. target_compile_options(xbox_d3d8 PRIVATE -Wall -Wextra -Wno-unused-parameter -Wno-missing-field-initializers -O2) + if(WIN32) + # The clang driver on Windows: CodeView debug info, so the PDB the + # title links (/DEBUG) names static functions in a sampled profile + # too, as /Zi does for MSVC. No effect on the generated code. + target_compile_options(xbox_d3d8 PRIVATE -g -gcodeview) + endif() endif() diff --git a/src/d3d/d3d8_clear.c b/src/d3d/d3d8_clear.c index 3ddc0d6a2..eaa86d41d 100644 --- a/src/d3d/d3d8_clear.c +++ b/src/d3d/d3d8_clear.c @@ -45,7 +45,7 @@ static struct { int tried, failed; RhiShader *vs, *ps; RhiBuffer *cb; - RhiBlendState *blend[2]; /* [writes colour] */ + RhiBlendState *blend[16]; /* [RHI_WRITE_* mask], made when first asked */ RhiDepthState *depth[4]; /* [writes depth | writes stencil << 1] */ RhiRasterState *raster; } g; @@ -99,9 +99,9 @@ static int create(void) bd.cpu_access = RHI_CPU_WRITE; if (!(g.cb = rhi_buffer_create(&bd, NULL))) { fail("CreateBuffer"); return 0; } - for (i = 0; i < 2; i++) { + for (i = 0; i < 16; i += 15) { memset(&bl, 0, sizeof bl); - bl.write_mask = i ? RHI_WRITE_ALL : 0; + bl.write_mask = (uint32_t)i; if (!(g.blend[i] = rhi_blend_state_create(&bl))) { fail("CreateBlendState"); return 0; } } for (i = 0; i < 4; i++) { @@ -131,13 +131,14 @@ static int create(void) } int d3d8_clear_rects(RhiView *rtv, RhiView *dsv, UINT width, UINT height, - const RhiRect *rects, UINT count, int color, int depth, int stencil, + const RhiRect *rects, UINT count, int color_mask, int depth, int stencil, const float rgba[4], float z, uint8_t stencil_value) { RhiOutputState saved; RhiViewport vp; float blend_factor[4] = { 1, 1, 1, 1 }; UINT i; + int color = color_mask & RHI_WRITE_ALL; if (!width || !height || (!color && !depth && !stencil)) return 0; @@ -167,7 +168,15 @@ int d3d8_clear_rects(RhiView *rtv, RhiView *dsv, UINT width, UINT height, rhi_set_shader(RHI_STAGE_PIXEL, g.ps); rhi_set_uniform_buffers(RHI_STAGE_VERTEX, 7, 1, &g.cb); rhi_set_uniform_buffers(RHI_STAGE_PIXEL, 7, 1, &g.cb); - rhi_set_blend_state(g.blend[color ? 1 : 0], blend_factor, 0xFFFFFFFF); + if (!g.blend[color]) { + RhiBlendDesc bl; + memset(&bl, 0, sizeof bl); + bl.write_mask = (uint32_t)color; + g.blend[color] = rhi_blend_state_create(&bl); + if (!g.blend[color]) + color = RHI_WRITE_ALL; /* the whole colour, as before */ + } + rhi_set_blend_state(g.blend[color], blend_factor, 0xFFFFFFFF); rhi_set_depth_state(g.depth[(depth ? 1 : 0) | (stencil ? 2 : 0)], stencil_value); rhi_set_raster_state(g.raster); diff --git a/src/d3d/d3d8_combiners.c b/src/d3d/d3d8_combiners.c index 4cb4a7361..bd3c39738 100644 --- a/src/d3d/d3d8_combiners.c +++ b/src/d3d/d3d8_combiners.c @@ -85,7 +85,7 @@ static uint32_t g_frame_counter = 0; * for our purposes and produces good distribution. * ================================================================ */ -static uint32_t fnv1a_hash(const void *data, size_t len) +__attribute__((unused)) static uint32_t fnv1a_hash(const void *data, size_t len) { const uint8_t *p = (const uint8_t *)data; uint32_t h = 0x811C9DC5u; @@ -102,9 +102,30 @@ static uint32_t fnv1a_hash(const void *data, size_t len) * on them made every distinct colour a new entry and a new D3DCompile in a * 128-entry table that then evicted and recompiled the same shaders in * steady state. */ +/* Eight bytes a step in two lanes. The key is over a kilobyte, and a byte + * at a time through one multiply chain was most of a lookup -- and Forza + * looks one up at nearly every draw, since its pixel shader changes that + * often. Only this process's table uses the value. */ static uint32_t combiner_state_hash(const NV2ACombinerState *state) { - return fnv1a_hash(state, NV2A_COMBINER_KEY_BYTES); + const uint8_t *p = (const uint8_t *)state; + size_t n = NV2A_COMBINER_KEY_BYTES, i = 0; + uint64_t a = 0x9E3779B97F4A7C15ull, b = 0xC2B2AE3D27D4EB4Full; + + for (; i + 16u <= n; i += 16u) { + uint64_t w0, w1; + memcpy(&w0, p + i, 8u); + memcpy(&w1, p + i + 8u, 8u); + a = (a ^ w0) * 0x100000001B3ull; + a ^= a >> 29; + b = (b ^ w1) * 0x9E3779B97F4A7C15ull; + b ^= b >> 31; + } + for (; i < n; i++) + a = (a ^ p[i]) * 0x100000001B3ull; + a ^= b * 0x165667B19E3779F9ull; + a ^= a >> 33; + return (uint32_t)(a ^ (a >> 32)); } static BOOL combiner_state_equal(const NV2ACombinerState *a, @@ -711,6 +732,17 @@ static int emit_shadow_compare(char *buf, int bufsize, int *poff, int i) * matrix is a texture stage state not forwarded yet), CLIPPLANE kills * nothing (PSCOMPAREMODE is not forwarded), DOT_ZW does not replace depth. * A mode at a stage the hardware does not allow it at reads zero. */ +static int proj2d_volume_r(void) +{ + static int on = -1; + + if (on < 0) { + const char *e = getenv("RECOMP_D3D8_PROJ2D_VOLUME_R"); + on = e && e[0] == '1'; + } + return on; +} + static int emit_xbox_textures(const NV2ACombinerState *state, char *buf, int bufsize, int *poff) { @@ -736,6 +768,29 @@ static int emit_xbox_textures(const NV2ACombinerState *state, char *buf, EMIT(" r_t%d = float4(0, 0, 0, 1);\n", i); break; case XTM_PROJECT2D: + if (state->tex_mode[i] == NV2A_TEXMODE_CUBEMAP) { + EMIT(" r_t%d = tex%d.Sample(samp%d, input.tc%d.xyz);\n", + i, i, i, i); + break; + } + if (state->tex_mode[i] == NV2A_TEXMODE_3D) { + /* PROJECT2D on a volume: the texture's own dimensions decide, + * so r may be a coordinate too: RECOMP_D3D8_PROJ2D_VOLUME_R=1 + * samples at r. Off by default; tried for Forza Motorsport's + * street lights, whose white road turned out to be empty + * stages (d3d8_device.c, empty_stage_view), and it changed + * nothing there. */ + if (proj2d_volume_r() && (state->vol_r & (1u << i))) + EMIT(" r_t%d = tex%d.Sample(samp%d, input.tc%d.xyz / qdiv(input.tc%d.w));\n", + i,i,i,i,i); + else + EMIT(" r_t%d = tex%d.Sample(samp%d, float3(input.tc%d.xy / qdiv(input.tc%d.w), 0.0));\n", + i,i,i,i,i); + break; + } + EMIT(" r_t%d = tex%d.Sample(samp%d, input.tc%d.xy / qdiv(input.tc%d.w) * tex_scale[%d].xy);\n", + i,i,i,i,i,i); + break; case XTM_BUMPENVMAP: case XTM_BUMPENVMAP_LUM: EMIT(" r_t%d = tex%d.Sample(samp%d, input.tc%d.xy / qdiv(input.tc%d.w)" @@ -884,6 +939,10 @@ int d3d8_combiners_generate_hlsl(const NV2ACombinerState *state, * (d3d8_shaders.c for fixed function, d3d8_vsh.c for a program). */ EMIT(" float fog : TEXCOORD4;\n"); EMIT(" float4 viewpos : TEXCOORD5;\n"); + /* The W-buffer depth, one copy perspective-correct and one not + * (d3d8_vsh.c, oWDep, and d3d8_vsh_wdepth_on). */ + EMIT(" float4 wdep : XBOXWDEPTH0;\n"); + EMIT(" noperspective float4 wlin : XBOXWLINEAR0;\n"); EMIT("};\n\n"); /* The fog unit, which runs before the combiners: the factor their fog @@ -943,7 +1002,10 @@ int d3d8_combiners_generate_hlsl(const NV2ACombinerState *state, } /* ---- Main function ---- */ - EMIT("float4 main(PS_IN input) : SV_TARGET {\n"); + EMIT("float4 main(PS_IN input, out float o_depth : SV_Depth) : SV_TARGET {\n"); + /* A W-buffered draw: the exact w, from the rasterised depth less what + * screen-space interpolation overstated (d3d8_vsh_wdepth_on). */ + EMIT(" o_depth = input.pos.z - input.wdep.y * max(input.wlin.x - input.wdep.x, 0.0);\n"); EMIT(" float shadow_dbg = 0.0;\n"); /* Initialize register file */ @@ -1515,6 +1577,58 @@ BOOL d3d8_combiners_prepare_draw(void) forget_last_shaders(); } + /* Sampling operation and resource dimension are independent on NV2A. + * In particular Forza binds 3D paint atlases under PROJECT2D, and its + * car paint's environment cube (stage 3) the same way: sampled as a + * cube, the paint has the gloss and reflections it has on the console + * (compared with xemu in the car selection and a race); sampled as 2D + * it was matte. On by default; RECOMP_D3D8_PROJECT2D_CUBES=0 for the + * old 2D sampling. */ + for (i=0;ilpVtbl->GetType(texture) : 0; + NV2ATextureMode mode = type == D3DRTYPE_VOLUMETEXTURE ? NV2A_TEXMODE_3D : + project2d_cubes && type == D3DRTYPE_CUBETEXTURE ? + NV2A_TEXMODE_CUBEMAP : NV2A_TEXMODE_2D; + if (g_combiner_state.tex_mode[i] != mode) { + g_combiner_state.tex_mode[i]=mode; + forget_last_shaders(); + } + /* Only a volume at least RECOMP_D3D8_PROJ2D_VOLUME_R_DEPTH + * slices deep (8 by default) samples at r. Forza's headlight + * volume is 128x128x8 and needs r; its 2-slice paint atlases and + * the volumes under its car shadows take r = 0, and sampled at + * their own r the shadows jumped from frame to frame. */ + { + static int min_depth = -1; + BYTE bit = (BYTE)(1u << i), want = 0; + if (min_depth < 0) { + const char *e = getenv("RECOMP_D3D8_PROJ2D_VOLUME_R_DEPTH"); + min_depth = e && atoi(e) > 0 ? atoi(e) : 8; + } + if (mode == NV2A_TEXMODE_3D) { + D3DVOLUME_DESC vd; + memset(&vd, 0, sizeof vd); + if (((IDirect3DVolumeTexture8 *)texture)->lpVtbl->GetLevelDesc( + (IDirect3DVolumeTexture8 *)texture, 0, &vd) == S_OK && + (int)vd.Depth >= min_depth) + want = bit; + } + if ((g_combiner_state.vol_r & bit) != want) { + g_combiner_state.vol_r = (BYTE)((g_combiner_state.vol_r & ~bit) | want); + forget_last_shaders(); + } + } + } + } + + /* Shadow-map stages come from the textures bound for this draw, not the * token: a depth-format texture is compared, not sampled. Part of the * shader key, so a change selects another shader. */ @@ -1579,75 +1693,67 @@ BOOL d3d8_combiners_prepare_draw(void) /* Bind the combiner pixel shader */ rhi_set_shader(RHI_STAGE_PIXEL, ps); - /* Update the PS constant buffer with current values. - * - * Even though the shader structure doesn't change, the constant - * values (C0, C1, fog, alpha ref) can change every frame via - * render state writes. So we always re-upload. */ - mapped = rhi_buffer_map(g_combiner_cb, RHI_MAP_WRITE_DISCARD); - if (mapped) { - NV2APSConstants *cb = (NV2APSConstants *)mapped; - - /* Per-stage constants */ + /* Fill the combiner constants, and upload only when they differ from + * the buffer the last draw sent. Fog, material colours and the bound + * textures stay put across long runs of draws; mapping the buffer on + * every one of them was a driver call per draw. */ + { + static NV2APSConstants sent; + static int sent_ok; + static unsigned long uploads, skipped; + static DWORD report_at; + NV2APSConstants cb; + + memset(&cb, 0, sizeof cb); for (i = 0; i < NV2A_MAX_COMBINER_STAGES; i++) { - d3dcolor_to_float4(g_combiner_state.c0[i], cb->c0[i]); - d3dcolor_to_float4(g_combiner_state.c1[i], cb->c1[i]); + d3dcolor_to_float4(g_combiner_state.c0[i], cb.c0[i]); + d3dcolor_to_float4(g_combiner_state.c1[i], cb.c1[i]); } - - /* Final combiner constants */ - d3dcolor_to_float4(g_combiner_state.final_c0, cb->final_c0); - d3dcolor_to_float4(g_combiner_state.final_c1, cb->final_c1); - - /* Fog color from render state */ - d3dcolor_to_float4(rs[D3DRS_FOGCOLOR], cb->fog_color); - - /* Alpha test parameters */ - cb->alpha_ref = rs[D3DRS_ALPHAREF] / 255.0f; - cb->alpha_func = rs[D3DRS_ALPHAFUNC]; - cb->alpha_test_enable = rs[D3DRS_ALPHATESTENABLE] ? 1 : 0; - cb->fog_enable = rs[D3DRS_FOGENABLE] ? 1 : 0; - cb->fog_src = NV2A_FOG_SRC_FACTOR; - cb->fog_range = 0; - memset(cb->fog_pad, 0, sizeof cb->fog_pad); - memset(cb->fog_params, 0, sizeof cb->fog_params); + d3dcolor_to_float4(g_combiner_state.final_c0, cb.final_c0); + d3dcolor_to_float4(g_combiner_state.final_c1, cb.final_c1); + d3dcolor_to_float4(rs[D3DRS_FOGCOLOR], cb.fog_color); + cb.alpha_ref = rs[D3DRS_ALPHAREF] / 255.0f; + cb.alpha_func = rs[D3DRS_ALPHAFUNC]; + cb.alpha_test_enable = rs[D3DRS_ALPHATESTENABLE] ? 1 : 0; + cb.fog_enable = rs[D3DRS_FOGENABLE] ? 1 : 0; + cb.fog_src = NV2A_FOG_SRC_FACTOR; if (rs[D3DRS_FOGENABLE]) { int program = d3d8_shaders_draw_is_program(); - memcpy(&cb->fog_params[0], &rs[D3DRS_FOGSTART], sizeof(float)); - memcpy(&cb->fog_params[1], &rs[D3DRS_FOGEND], sizeof(float)); - memcpy(&cb->fog_params[2], &rs[D3DRS_FOGDENSITY], sizeof(float)); - cb->fog_params[3] = (float)rs[D3DRS_FOGTABLEMODE]; - cb->fog_range = rs[D3DRS_RANGEFOGENABLE] ? 1 : 0; + memcpy(&cb.fog_params[0], &rs[D3DRS_FOGSTART], sizeof(float)); + memcpy(&cb.fog_params[1], &rs[D3DRS_FOGEND], sizeof(float)); + memcpy(&cb.fog_params[2], &rs[D3DRS_FOGDENSITY], sizeof(float)); + cb.fog_params[3] = (float)rs[D3DRS_FOGTABLEMODE]; + cb.fog_range = rs[D3DRS_RANGEFOGENABLE] ? 1 : 0; /* Only table fog is mapped here. With no table mode a program's * oFog stays the factor itself, as D3D vertex fog has it: mapped * linearly through start..end, Need for Speed Underground 2's * world (fog on, FOGTABLEMODE NONE) came out the fog colour, * a black screen. */ if (rs[D3DRS_FOGTABLEMODE] != D3DFOG_NONE) - cb->fog_src = program ? NV2A_FOG_SRC_PROGRAM_TABLE : NV2A_FOG_SRC_VIEW_TABLE; + cb.fog_src = program ? NV2A_FOG_SRC_PROGRAM_TABLE : NV2A_FOG_SRC_VIEW_TABLE; } for (i = 0; i < NV2A_MAX_TEXTURES; i++) { IDirect3DBaseTexture8 *tex = d3d8_GetStageTexture(i); D3DFORMAT format = d3d8_base_format(tex); UINT w = 0, h = 0; - cb->alpha_only[i] = format == D3DFMT_A8 || format == D3DFMT_LIN_A8; + cb.alpha_only[i] = format == D3DFMT_A8 || format == D3DFMT_LIN_A8; /* Linear textures are addressed in texels on the NV2A; see * NV2APSConstants.tex_scale. */ - cb->tex_scale[i][0] = cb->tex_scale[i][1] = 1.0f; - cb->tex_scale[i][2] = cb->tex_scale[i][3] = 0.0f; + cb.tex_scale[i][0] = cb.tex_scale[i][1] = 1.0f; if (tex && d3d8_format_is_linear(format) && d3d8_base_size(tex, &w, &h) && w && h) { - cb->tex_scale[i][0] = 1.0f / (float)w; - cb->tex_scale[i][1] = 1.0f / (float)h; + cb.tex_scale[i][0] = 1.0f / (float)w; + cb.tex_scale[i][1] = 1.0f / (float)h; } /* The host holds depth 0..1; the title's r/q is in the format's * own range, as the NV2A compares. */ switch (format) { case D3DFMT_D24S8: case D3DFMT_LIN_D24S8: - cb->shadow_max[i] = 16777215.0f; break; + cb.shadow_max[i] = 16777215.0f; break; case D3DFMT_D16: case D3DFMT_LIN_D16: - cb->shadow_max[i] = 65535.0f; break; + cb.shadow_max[i] = 65535.0f; break; default: - cb->shadow_max[i] = 1.0f; break; + cb.shadow_max[i] = 1.0f; break; } } { @@ -1663,10 +1769,9 @@ BOOL d3d8_combiners_prepare_draw(void) const char *v = getenv("RECOMP_D3D8_SHADOW_VIEW"); view = v ? atoi(v) : 0; } - cb->shadow_func = rs[D3DRS_SHADOWFUNC]; - cb->shadow_swap = (UINT)swap; - cb->shadow_pad[0] = (UINT)view; /* shadow_func.z in the HLSL */ - /* Say once what the first shadow-map draw compares with. */ + cb.shadow_func = rs[D3DRS_SHADOWFUNC]; + cb.shadow_swap = (UINT)swap; + cb.shadow_pad[0] = (UINT)view; /* shadow_func.z in the HLSL */ for (i = 0; i < NV2A_MAX_TEXTURES; i++) { static int said; if (g_combiner_state.shadow[i] && !said) { @@ -1679,8 +1784,29 @@ BOOL d3d8_combiners_prepare_draw(void) } } } - - rhi_buffer_unmap(g_combiner_cb); + if (sent_ok && memcmp(&cb, &sent, sizeof cb) == 0) { + skipped++; + } else { + mapped = rhi_buffer_map(g_combiner_cb, RHI_MAP_WRITE_DISCARD); + if (mapped) { + memcpy(mapped, &cb, sizeof cb); + rhi_buffer_unmap(g_combiner_cb); + sent = cb; + sent_ok = 1; + uploads++; + } + } + { + DWORD now = GetTickCount(); + + if (!report_at) + report_at = now; + else if (now - report_at >= 5000) { + fprintf(stderr, "[PS-CB] %lu uploaded, %lu unchanged\n", uploads, skipped); + uploads = skipped = 0; + report_at = now; + } + } } /* Bind the constant buffer to PS slot 0 */ diff --git a/src/d3d/d3d8_combiners.h b/src/d3d/d3d8_combiners.h index c10dd0189..0c18d6df4 100644 --- a/src/d3d/d3d8_combiners.h +++ b/src/d3d/d3d8_combiners.h @@ -277,7 +277,10 @@ typedef struct NV2ACombinerState { * at most twice the shaders, not eight times. Set per draw from the * render states, like `shadow`, not from the token. */ BYTE alpha_test; - BYTE key_pad[3]; /* zero: part of the key bytes, so never left unset */ + /* Bit N: stage N's PROJECT2D volume samples at r (proj2d_volume_r in + * d3d8_combiners.c). Set per draw from the bound volume's depth. */ + BYTE vol_r; + BYTE key_pad[2]; /* zero: part of the key bytes, so never left unset */ /* Everything above decides the generated shader and is its cache key * (NV2A_COMBINER_KEY_BYTES). Everything below is uploaded to the diff --git a/src/d3d/d3d8_device.c b/src/d3d/d3d8_device.c index 2a65b4c81..e6e95aa17 100644 --- a/src/d3d/d3d8_device.c +++ b/src/d3d/d3d8_device.c @@ -23,13 +23,15 @@ #include /* malloc: without its pointer is truncated to int. */ #include +#include +#include /* CRC32C, stable index stamps */ /* ================================================================ * Internal device state * ================================================================ */ /* Maximum tracked render states, texture stages, and transforms */ -#define MAX_RENDER_STATES 256 +#define MAX_RENDER_STATES 260 /* through D3DRS_POLY_FILL (259) */ #define MAX_TEXTURE_STAGES 4 #define MAX_TSS_STATES 32 #define MAX_TRANSFORMS 512 @@ -525,7 +527,15 @@ static BOOL window_size(const D3D8DeviceState *s, UINT *w, UINT *h) #endif } +static void present_scene_into(RhiView *target); static void present_scene(void) +{ + present_scene_into(NULL); +} + +/* target NULL: the swap chain's back buffer. Otherwise an image the size of + * the swap chain (paced presents, below). */ +static void present_scene_into(RhiView *target) { D3D8DeviceState *s = &g_device_state; D3D8DisplayFit fit; @@ -556,6 +566,10 @@ static void present_scene(void) d3d8_display_output_shape(s->width, s->height, &shape_w, &shape_h); fit = d3d8_display_fit(shape_w, shape_h, s->swap_width, s->swap_height); } + if (target) { + d3d8_display_resolve(s->rhi_scene_srv, s->width, s->height, target, fit); + return; + } d3d8_display_resolve(s->rhi_scene_srv, s->width, s->height, rhi_swapchain_view(), fit); keep_presented(); } @@ -635,6 +649,12 @@ BOOL d3d8_GetScissor(RhiRect *out) } UINT d3d8_GetGuestHeight(void) { return g_guest_height ? g_guest_height : g_device_state.present_height; } const DWORD *d3d8_GetRenderStates(void) { return g_device_state.render_states; } +/* The viewport's depth range, MaxZ - MinZ: what one unit of the vertex + * stage's z comes to in the depth buffer (d3d8_vsh.c, the W-buffer depth). */ +float d3d8_GetViewportDepthSpan(void) +{ + return g_device_state.viewport.MaxZ - g_device_state.viewport.MinZ; +} const DWORD *d3d8_GetTSS(DWORD stage) { return (stage < MAX_TEXTURE_STAGES) ? g_device_state.tss[stage] : NULL; } IDirect3DBaseTexture8 *d3d8_GetStageTexture(DWORD stage) { return (stage < 4) ? g_cur_textures[stage] : NULL; } const D3DMATRIX *d3d8_GetTransform(D3DTRANSFORMSTATETYPE type) { @@ -1055,6 +1075,7 @@ static HRESULT __stdcall dev_Clear(IDirect3DDevice8 *self, DWORD Count, const D3 { RhiRect host_rects[CLEAR_MAX_RECTS]; UINT n_rects = 0, target_w, target_h; + int clear_mask = RHI_WRITE_ALL; float clear_color[4] = { ((Color >> 16) & 0xFF) / 255.0f, /* R */ ((Color >> 8) & 0xFF) / 255.0f, /* G */ @@ -1075,6 +1096,30 @@ static HRESULT __stdcall dev_Clear(IDirect3DDevice8 *self, DWORD Count, const D3 * clear below. */ target_w = g_cur_rt ? g_cur_rt->width : g_device_state.width; target_h = g_cur_rt ? g_cur_rt->height : g_device_state.height; + /* Some colour channels only (D3D8_CLEAR_CHANNELS): a native clear has + * no channel mask, so it is the rectangle pass over the whole target, + * writing only those channels. */ + if ((Flags & D3D8_CLEAR_CHANNELS) && (Flags & D3DCLEAR_TARGET)) { + clear_mask = 0; + if (Flags & 0x1000u) clear_mask |= RHI_WRITE_R; + if (Flags & 0x2000u) clear_mask |= RHI_WRITE_G; + if (Flags & 0x4000u) clear_mask |= RHI_WRITE_B; + if (Flags & 0x8000u) clear_mask |= RHI_WRITE_A; + if (!Count || !pRects) { + host_rects[0].left = host_rects[0].top = 0; + host_rects[0].right = (int32_t)target_w; + host_rects[0].bottom = (int32_t)target_h; + if (d3d8_clear_rects(rtv, dsv, target_w, target_h, host_rects, 1, clear_mask, + (Flags & D3DCLEAR_ZBUFFER) != 0, + (Flags & D3DCLEAR_STENCIL) != 0, clear_color, Z, + (uint8_t)Stencil) == 0) { + if ((Flags & D3DCLEAR_ZBUFFER) && g_cur_ds == g_auto_ds) + g_wbuffer.valid = 0; + return S_OK; + } + clear_mask = RHI_WRITE_ALL; /* the pass failed: clear as before */ + } + } if (!clear_rects_to_host(Count, pRects, target_w, target_h, host_rects, &n_rects)) { /* A depth-only rectangle belongs to the 3D viewport it was worked * out for (Max Payne clears a portal's rectangle before drawing @@ -1097,7 +1142,8 @@ static HRESULT __stdcall dev_Clear(IDirect3DDevice8 *self, DWORD Count, const D3 } if (n_rects && d3d8_clear_rects(rtv, dsv, target_w, target_h, host_rects, n_rects, - (Flags & D3DCLEAR_TARGET) != 0, (Flags & D3DCLEAR_ZBUFFER) != 0, + (Flags & D3DCLEAR_TARGET) ? clear_mask : 0, + (Flags & D3DCLEAR_ZBUFFER) != 0, (Flags & D3DCLEAR_STENCIL) != 0, clear_color, Z, (uint8_t)Stencil) != 0) { static int said; @@ -1197,11 +1243,21 @@ static RhiView *empty_stage_view(void) RhiSubresourceData init; RhiViewDesc vd; RhiImage *img; - static const uint32_t white = 0xFFFFFFFFu; + static uint32_t white = 0xFFFFFFFFu; if (view || tried || !rhi_device_ready()) return view; tried = 1; + /* RECOMP_D3D8_EMPTY_STAGE=black: transparent black instead. Forza + * Motorsport draws its street lights four to a pass, one volume per + * stage, and leaves the stages of absent lights empty with their + * colour constants set: read as white, every such pass added the full + * constant and the road at night came out white. */ + { + const char *e = getenv("RECOMP_D3D8_EMPTY_STAGE"); + if (e && (e[0] == 'b' || e[0] == 'B' || e[0] == '0')) + white = 0; + } memset(&d, 0, sizeof d); d.type = RHI_IMAGE_2D; d.width = d.height = d.depth = d.mip_levels = d.samples = 1; @@ -1420,6 +1476,390 @@ static void up_ring_shutdown(void) up_ring_release(&g_up_index_ring); } +/* A converted mesh the expand cache keeps (hle_d3d8.c) used to be copied + * into the ring on every draw and again for the in-between frame: about + * 10 MB a frame, the largest single cost on the guest thread in a race. + * The bytes stay at one host address until their hash changes, so they are + * uploaded once into their own buffer and bound from there. + * RECOMP_STABLE_VB=0 goes back to copying every draw. */ +#define STABLE_VB_SLOTS 2048u +#define STABLE_VB_PROBE 24u +#define STABLE_VB_MAX (2u * 1024u * 1024u) + +struct stable_vb { + const void *ptr; + unsigned long long stamp; + UINT size; + uint32_t used; + RhiBuffer *buf; +}; + +static struct stable_vb g_svb[STABLE_VB_SLOTS]; +static const void *g_svb_note_ptr; +static unsigned long long g_svb_note_stamp; +static int g_svb_note_set, g_svb_on = -1; +static uint32_t g_svb_clock; +static unsigned long long g_svb_hits, g_svb_uploads; + +static int stable_vb_on(void) +{ + if (g_svb_on < 0) { + const char *v = getenv("RECOMP_STABLE_VB"); + g_svb_on = !(v && v[0] == '0'); + fprintf(stderr, "[D3D8] stable vertex buffers %s (RECOMP_STABLE_VB)\n", + g_svb_on ? "on" : "off"); + } + return g_svb_on; +} + +void d3d8_stable_vertex_note(const void *ptr, unsigned long long stamp) +{ + if (!ptr || !stable_vb_on()) + return; + g_svb_note_ptr = ptr; + g_svb_note_stamp = stamp; + g_svb_note_set = 1; +} + +static struct stable_vb *stable_vb_find(const void *ptr, int make) +{ + /* Large meshes come back page-aligned, so a shift alone piles them + * into one probe. The multiply spreads those addresses out. */ + uint32_t h = (uint32_t)((uintptr_t)ptr >> 4) * 0x9E3779B1u; + h ^= h >> 16; + uint32_t i; + struct stable_vb *empty = NULL; + + for (i = 0; i < STABLE_VB_PROBE; i++) { + struct stable_vb *s = &g_svb[(h + i) & (STABLE_VB_SLOTS - 1u)]; + if (s->buf && s->ptr == ptr) + return s; + if (!s->buf && !empty) + empty = s; + } + if (!make) + return NULL; + if (!empty) { + /* Only a slot this pointer hashes to, or the next lookup misses it. */ + struct stable_vb *old = &g_svb[h & (STABLE_VB_SLOTS - 1u)]; + for (i = 1; i < STABLE_VB_PROBE; i++) { + struct stable_vb *s = &g_svb[(h + i) & (STABLE_VB_SLOTS - 1u)]; + if (s->used < old->used) + old = s; + } + if (old->buf) + rhi_buffer_destroy(old->buf); + memset(old, 0, sizeof *old); + empty = old; + } + return empty; +} + +void d3d8_stable_vertex_drop(const void *ptr) +{ + struct stable_vb *s; + if (!ptr || g_svb_on == 0) + return; + s = stable_vb_find(ptr, 0); + if (!s) + return; + rhi_buffer_destroy(s->buf); + memset(s, 0, sizeof *s); + if (g_svb_note_ptr == ptr) + g_svb_note_set = 0; +} + +static RhiBuffer *stable_vb_create(struct stable_vb *s, const void *data, UINT size, + unsigned long long stamp) +{ + RhiBufferDesc bd; + RhiBuffer *buf; + + memset(&bd, 0, sizeof bd); + bd.size = size; + bd.usage = RHI_USAGE_DEFAULT; + bd.bind = RHI_BIND_VERTEX; + buf = rhi_buffer_create(&bd, data); + if (!buf) + return NULL; + if (s->buf) + rhi_buffer_destroy(s->buf); + s->ptr = data; + s->stamp = stamp; + s->size = size; + s->buf = buf; + s->used = ++g_svb_clock; + g_svb_uploads++; + return buf; +} + +/* The buffer for this draw, or NULL and the ring is used. A noted stamp + * that no longer matches re-uploads; a redraw of the same address binds + * what the frame already uploaded. */ +static RhiBuffer *stable_vb_take(const void *data, UINT size) +{ + struct stable_vb *s; + int noted; + unsigned long long stamp; + + if (!data || !size || size > STABLE_VB_MAX || !stable_vb_on()) + return NULL; + noted = g_svb_note_set && g_svb_note_ptr == data; + stamp = noted ? g_svb_note_stamp : 0; + s = stable_vb_find(data, noted); + if (!s) + return NULL; + if (s->buf && s->ptr == data && s->size == size) { + if (noted && s->stamp != stamp) { + rhi_buffer_update(s->buf, data); + s->stamp = stamp; + g_svb_uploads++; + } else { + g_svb_hits++; + } + s->used = ++g_svb_clock; + return s->buf; + } + if (!noted) + return NULL; + return stable_vb_create(s, data, size, stamp); +} + +void d3d8_stable_index_report(void); + +void d3d8_stable_vertex_report(void) +{ + if (g_svb_hits || g_svb_uploads) + fprintf(stderr, "[STABLE-VB] %llu draws from a kept buffer, %llu uploaded\n", + g_svb_hits, g_svb_uploads); + g_svb_hits = g_svb_uploads = 0; + d3d8_stable_index_report(); +} + +/* Index lists took the ring on every indexed draw: a map, a copy, and the + * same copy again into the in-between. The guest list's own bytes are the + * stamp. Unchanged, the converted list stays in the buffer uploaded for it. + * A list that already changed once this frame is left on the ring so the + * in-between still shows the first version. RECOMP_STABLE_VB=0 disables it + * with the vertex buffers. */ +#define STABLE_IB_SLOTS STABLE_VB_SLOTS +#define STABLE_IB_PROBE STABLE_VB_PROBE + +struct stable_ib { + const void *ptr; + UINT count, topology, size, min_index, vertex_span; + unsigned long frame; + unsigned long long stamp; + uint32_t used; + RhiBuffer *buf; +}; + +static struct stable_ib g_sib[STABLE_IB_SLOTS]; +static uint32_t g_sib_clock; +static unsigned long long g_sib_hits, g_sib_uploads; +static int g_sib_arm; +static RhiBuffer *g_sib_arm_buf; +static UINT g_sib_arm_bytes, g_sib_arm_count, g_sib_arm_topo; +static const void *g_sib_arm_key; +static const void *g_sib_pub_key; +static UINT g_sib_pub_count, g_sib_pub_topo, g_sib_pub_bytes; +static struct stable_ib *g_sib_pending; +static const void *g_sib_pending_ptr; +static UINT g_sib_pending_count, g_sib_pending_topo; +static UINT g_sib_pending_min, g_sib_pending_span; +static unsigned long g_sib_pending_frame; +static unsigned long long g_sib_pending_stamp; + +__attribute__((target("sse4.2"))) +static unsigned long long sib_hash(const uint16_t *idx, UINT count, + UINT *lo_out, UINT *hi_out) +{ + unsigned long long a = 0x9E3779B185EBCA87ull; + UINT lo = 0xFFFFu, hi = 0, i; + + for (i = 0; i < count; i++) { + UINT v = idx[i]; + if (v < lo) lo = v; + if (v > hi) hi = v; + a = _mm_crc32_u32((unsigned)a, v); + } + a ^= (unsigned long long)count * 0xC2B2AE3D27D4EB4Full; + *lo_out = lo; + *hi_out = hi; + return a; +} + +static struct stable_ib *sib_lookup(const void *ptr, UINT count, UINT topo, + int make, unsigned long frame) +{ + uint32_t h = (uint32_t)((uintptr_t)ptr >> 4) * 0x9E3779B1u; + uint32_t i; + struct stable_ib *empty = NULL; + + h ^= count * 0x85EBCA77u; + h ^= topo; + h ^= h >> 16; + for (i = 0; i < STABLE_IB_PROBE; i++) { + struct stable_ib *s = &g_sib[(h + i) & (STABLE_IB_SLOTS - 1u)]; + if (s->buf && s->ptr == ptr && s->count == count && s->topology == topo) + return s; + if (!s->buf && !empty) + empty = s; + } + if (!make) + return NULL; + if (!empty) { + struct stable_ib *old = NULL; + for (i = 0; i < STABLE_IB_PROBE; i++) { + struct stable_ib *s = &g_sib[(h + i) & (STABLE_IB_SLOTS - 1u)]; + if (s->frame == frame) + continue; + if (!old || s->used < old->used) + old = s; + } + if (!old) + return NULL; + if (old->buf) + rhi_buffer_destroy(old->buf); + memset(old, 0, sizeof *old); + empty = old; + } + return empty; +} + +static void sib_arm_bind(struct stable_ib *s) +{ + g_sib_arm = 1; + g_sib_arm_buf = s->buf; + g_sib_arm_bytes = s->size; + g_sib_arm_key = s->ptr; + g_sib_arm_count = s->count; + g_sib_arm_topo = s->topology; + s->used = ++g_sib_clock; +} + +void d3d8_stable_index_disarm(void) +{ + g_sib_arm = 0; + g_sib_arm_buf = NULL; + g_sib_pending = NULL; +} + +int d3d8_stable_index_prepare(const void *guest, UINT index_count, UINT topology, + unsigned long frame, UINT expect_bytes, + UINT *min_index, UINT *vertex_span, + unsigned long long *stamp) +{ + struct stable_ib *s; + UINT lo, hi; + unsigned long long sum; + + d3d8_stable_index_disarm(); + if (!guest || !index_count || index_count > (STABLE_VB_MAX / 2u) || !stable_vb_on()) + return -2; + sum = sib_hash((const uint16_t *)guest, index_count, &lo, &hi); + *min_index = lo; + *vertex_span = hi - lo + 1u; + *stamp = sum; + s = sib_lookup(guest, index_count, topology, 0, frame); + if (s && s->stamp == sum && s->size == expect_bytes) { + sib_arm_bind(s); + *min_index = s->min_index; + *vertex_span = s->vertex_span; + return 1; + } + if (s && s->frame == frame) + return -1; + g_sib_pending_ptr = guest; + g_sib_pending_count = index_count; + g_sib_pending_topo = topology; + g_sib_pending_min = *min_index; + g_sib_pending_span = *vertex_span; + g_sib_pending_frame = frame; + g_sib_pending_stamp = sum; + g_sib_pending = s; + return 0; +} + +int d3d8_stable_index_commit(const void *bytes, UINT size) +{ + struct stable_ib *s; + RhiBuffer *buf; + + if (!bytes || !size || size > STABLE_VB_MAX || !g_sib_pending_ptr) + return 0; + s = g_sib_pending; + if (!s || s->ptr != g_sib_pending_ptr || s->count != g_sib_pending_count || + s->topology != g_sib_pending_topo) + s = sib_lookup(g_sib_pending_ptr, g_sib_pending_count, g_sib_pending_topo, + 1, g_sib_pending_frame); + if (!s) + return 0; + if (s->buf && s->frame == g_sib_pending_frame && s->stamp != g_sib_pending_stamp) + return 0; + if (s->buf && s->size == size) { + rhi_buffer_update(s->buf, bytes); + buf = s->buf; + } else { + RhiBufferDesc bd; + memset(&bd, 0, sizeof bd); + bd.size = size; + bd.usage = RHI_USAGE_DEFAULT; + bd.bind = RHI_BIND_INDEX; + buf = rhi_buffer_create(&bd, bytes); + if (!buf) + return 0; + if (s->buf) + rhi_buffer_destroy(s->buf); + s->buf = buf; + s->size = size; + } + s->ptr = g_sib_pending_ptr; + s->count = g_sib_pending_count; + s->topology = g_sib_pending_topo; + s->min_index = g_sib_pending_min; + s->vertex_span = g_sib_pending_span; + s->frame = g_sib_pending_frame; + s->stamp = g_sib_pending_stamp; + g_sib_uploads++; + sib_arm_bind(s); + g_sib_pending = NULL; + return 1; +} + +int d3d8_stable_index_rebind(const void *guest, UINT index_count, UINT topology, UINT size) +{ + struct stable_ib *s; + + d3d8_stable_index_disarm(); + if (!guest || !stable_vb_on()) + return 0; + s = sib_lookup(guest, index_count, topology, 0, 0); + if (!s || !s->buf || s->size != size) + return 0; + sib_arm_bind(s); + return 1; +} + +const void *d3d8_stable_index_bound_key(UINT *index_count, UINT *topology, UINT *size) +{ + if (!g_sib_pub_key) + return NULL; + if (index_count) *index_count = g_sib_pub_count; + if (topology) *topology = g_sib_pub_topo; + if (size) *size = g_sib_pub_bytes; + return g_sib_pub_key; +} + +void d3d8_stable_index_report(void) +{ + if (!g_sib_hits && !g_sib_uploads) + return; + fprintf(stderr, "[STABLE-IB] %llu draws from a kept buffer, %llu uploaded\n", + g_sib_hits, g_sib_uploads); + g_sib_hits = g_sib_uploads = 0; +} + /* Append data to a ring, returns its byte offset. Returns (UINT)-1 on failure. */ static UINT up_ring_upload(UpRing *ring, const void *data, UINT size) { @@ -1619,7 +2059,34 @@ static void feedback_end(DWORD mask) } } -static HRESULT __stdcall dev_DrawPrimitive(IDirect3DDevice8 *self, D3DPRIMITIVETYPE PrimitiveType, UINT StartVertex, UINT PrimitiveCount) +/* Optional host-side timing. Draw time includes driver waits, not just CPU work. */ +static long long g_profile_draw_ticks,g_profile_texture_ticks; +static unsigned long long g_profile_draw_calls,g_profile_texture_calls; +long long d3d8_profile_clock(void) +{ + static int enabled=-1; + if(enabled<0){const char *v=getenv("RECOMP_FRAME_PROFILE");enabled=v && v[0]=='1';} + if(!enabled)return 0; + LARGE_INTEGER t;QueryPerformanceCounter(&t);return t.QuadPart; +} +void d3d8_profile_texture_end(long long start) +{ + if(!start)return; + g_profile_texture_ticks+=d3d8_profile_clock()-start;++g_profile_texture_calls; +} +void d3d8_profile_report(unsigned long frames) +{ + if(!frames || !d3d8_profile_clock())return; + LARGE_INTEGER freq;QueryPerformanceFrequency(&freq); + double ms=1000.0/(double)freq.QuadPart/(double)frames; + fprintf(stderr,"[FRAME-PROFILE] %lu frames: host draws %.3f ms/frame (%llu calls/frame), texture binding %.3f ms/frame (%llu calls/frame)\n", + frames,g_profile_draw_ticks*ms,g_profile_draw_calls/frames, + g_profile_texture_ticks*ms,g_profile_texture_calls/frames); + g_profile_draw_ticks=g_profile_texture_ticks=0; + g_profile_draw_calls=g_profile_texture_calls=0; +} + +static HRESULT __stdcall dev_DrawPrimitive_profile_impl(IDirect3DDevice8 *self, D3DPRIMITIVETYPE PrimitiveType, UINT StartVertex, UINT PrimitiveCount) { (void)self; g_d3d_draw_count++; @@ -1643,7 +2110,16 @@ static HRESULT __stdcall dev_DrawPrimitive(IDirect3DDevice8 *self, D3DPRIMITIVET return S_OK; } -static HRESULT __stdcall dev_DrawIndexedPrimitive(IDirect3DDevice8 *self, D3DPRIMITIVETYPE PrimitiveType, UINT MinVertexIndex, UINT NumVertices, UINT StartIndex, UINT PrimitiveCount) +static HRESULT __stdcall dev_DrawPrimitive(IDirect3DDevice8 *self, D3DPRIMITIVETYPE PrimitiveType, UINT StartVertex, UINT PrimitiveCount) +{ + long long start=d3d8_profile_clock(); + HRESULT hr=dev_DrawPrimitive_profile_impl(self,PrimitiveType,StartVertex,PrimitiveCount); + if(start){g_profile_draw_ticks+=d3d8_profile_clock()-start;++g_profile_draw_calls;} + return hr; +} + + +static HRESULT __stdcall dev_DrawIndexedPrimitive_profile_impl(IDirect3DDevice8 *self, D3DPRIMITIVETYPE PrimitiveType, UINT MinVertexIndex, UINT NumVertices, UINT StartIndex, UINT PrimitiveCount) { (void)self; (void)MinVertexIndex; (void)NumVertices; g_d3d_draw_count++; @@ -1667,6 +2143,15 @@ static HRESULT __stdcall dev_DrawIndexedPrimitive(IDirect3DDevice8 *self, D3DPRI return S_OK; } +static HRESULT __stdcall dev_DrawIndexedPrimitive(IDirect3DDevice8 *self, D3DPRIMITIVETYPE PrimitiveType, UINT MinVertexIndex, UINT NumVertices, UINT StartIndex, UINT PrimitiveCount) +{ + long long start=d3d8_profile_clock(); + HRESULT hr=dev_DrawIndexedPrimitive_profile_impl(self,PrimitiveType,MinVertexIndex,NumVertices,StartIndex,PrimitiveCount); + if(start){g_profile_draw_ticks+=d3d8_profile_clock()-start;++g_profile_draw_calls;} + return hr; +} + + UINT d3d8_up_vertices_read(D3DPRIMITIVETYPE type, UINT prims) { UINT n; @@ -1692,7 +2177,7 @@ UINT d3d8_up_indices_read(D3DPRIMITIVETYPE type, UINT prims) return n; } -static HRESULT __stdcall dev_DrawPrimitiveUP(IDirect3DDevice8 *self, D3DPRIMITIVETYPE PrimitiveType, UINT PrimitiveCount, const void *pVertexData, UINT VertexStreamZeroStride) +static HRESULT __stdcall dev_DrawPrimitiveUP_profile_impl(IDirect3DDevice8 *self, D3DPRIMITIVETYPE PrimitiveType, UINT PrimitiveCount, const void *pVertexData, UINT VertexStreamZeroStride) { (void)self; g_d3d_draw_count++; @@ -1719,6 +2204,17 @@ static HRESULT __stdcall dev_DrawPrimitiveUP(IDirect3DDevice8 *self, D3DPRIMITIV vb_size = vertex_count * VertexStreamZeroStride; + /* A mesh the expand cache kept is already on the GPU. */ + { + /* A fan or a quad was rewritten into a temporary list. That pointer + * dies with this draw, so it must not be kept as a stable mesh. */ + RhiBuffer *kept = converted ? NULL : stable_vb_take(draw_data, vb_size); + if (kept) { + rhi_set_vertex_buffer(0, kept, VertexStreamZeroStride, 0); + goto drawn_vertices; + } + } + /* Upload to ring buffer */ ring_offset = up_ring_upload(&g_up_vertex_ring, draw_data, vb_size); if (converted) free(converted); @@ -1727,6 +2223,7 @@ static HRESULT __stdcall dev_DrawPrimitiveUP(IDirect3DDevice8 *self, D3DPRIMITIV /* Bind ring buffer at the right offset */ rhi_set_vertex_buffer(0, g_up_vertex_ring.buffer, VertexStreamZeroStride, ring_offset); +drawn_vertices: /* Vertex shader: try programmable VS first, fall back to FVF fixed-function */ d3d8_shaders_prepare_draw(g_device_state.vertex_shader); /* VS, then the combiner PS or the fixed-function one */ @@ -1754,7 +2251,16 @@ static HRESULT __stdcall dev_DrawPrimitiveUP(IDirect3DDevice8 *self, D3DPRIMITIV return S_OK; } -static HRESULT __stdcall dev_DrawIndexedPrimitiveUP(IDirect3DDevice8 *self, D3DPRIMITIVETYPE PrimitiveType, UINT MinVertexIndex, UINT NumVertices, UINT PrimitiveCount, const void *pIndexData, D3DFORMAT IndexDataFormat, const void *pVertexData, UINT VertexStreamZeroStride) +static HRESULT __stdcall dev_DrawPrimitiveUP(IDirect3DDevice8 *self, D3DPRIMITIVETYPE PrimitiveType, UINT PrimitiveCount, const void *pVertexData, UINT VertexStreamZeroStride) +{ + long long start=d3d8_profile_clock(); + HRESULT hr=dev_DrawPrimitiveUP_profile_impl(self,PrimitiveType,PrimitiveCount,pVertexData,VertexStreamZeroStride); + if(start){g_profile_draw_ticks+=d3d8_profile_clock()-start;++g_profile_draw_calls;} + return hr; +} + + +static HRESULT __stdcall dev_DrawIndexedPrimitiveUP_profile_impl(IDirect3DDevice8 *self, D3DPRIMITIVETYPE PrimitiveType, UINT MinVertexIndex, UINT NumVertices, UINT PrimitiveCount, const void *pIndexData, D3DFORMAT IndexDataFormat, const void *pVertexData, UINT VertexStreamZeroStride) { (void)self; (void)MinVertexIndex; g_d3d_draw_count++; @@ -1763,10 +2269,18 @@ static HRESULT __stdcall dev_DrawIndexedPrimitiveUP(IDirect3DDevice8 *self, D3DP UINT vb_offset, ib_offset; UINT idx_bytes; - if (!pVertexData || !pIndexData || !VertexStreamZeroStride) return E_INVALIDARG; + g_sib_pub_key = NULL; + if (!pVertexData || !VertexStreamZeroStride) { + g_sib_arm = 0; + return E_INVALIDARG; + } + if (!pIndexData && !g_sib_arm) return E_INVALIDARG; topology = map_primitive_type(PrimitiveType, PrimitiveCount, &index_count); - if (index_count == 0) return E_INVALIDARG; + if (index_count == 0) { + g_sib_arm = 0; + return E_INVALIDARG; + } idx_bytes = (IndexDataFormat == D3DFMT_INDEX32) ? 4 : 2; @@ -1774,15 +2288,41 @@ static HRESULT __stdcall dev_DrawIndexedPrimitiveUP(IDirect3DDevice8 *self, D3DP ib_size = index_count * idx_bytes; /* Append vertices and indices to their rings; the draw reads them at - * the returned offsets. */ - vb_offset = up_ring_upload(&g_up_vertex_ring, pVertexData, vb_size); - if (vb_offset == (UINT)-1) return E_OUTOFMEMORY; - ib_offset = up_ring_upload(&g_up_index_ring, pIndexData, ib_size); - if (ib_offset == (UINT)-1) return E_OUTOFMEMORY; - - /* Bind, prepare, draw */ - rhi_set_vertex_buffer(0, g_up_vertex_ring.buffer, VertexStreamZeroStride, vb_offset); - rhi_set_index_buffer(g_up_index_ring.buffer, idx_bytes * 8, ib_offset); + * the returned offsets. A mesh the expand cache kept skips the vertex + * copy and is bound from the buffer uploaded when its bytes changed. */ + { + RhiBuffer *kept = stable_vb_take(pVertexData, vb_size); + if (kept) + rhi_set_vertex_buffer(0, kept, VertexStreamZeroStride, 0); + else { + vb_offset = up_ring_upload(&g_up_vertex_ring, pVertexData, vb_size); + if (vb_offset == (UINT)-1) { + g_sib_arm = 0; + return E_OUTOFMEMORY; + } + rhi_set_vertex_buffer(0, g_up_vertex_ring.buffer, VertexStreamZeroStride, vb_offset); + } + } + if (g_sib_arm && g_sib_arm_buf && ib_size == g_sib_arm_bytes && idx_bytes == 2) { + rhi_set_index_buffer(g_sib_arm_buf, 16, 0); + g_sib_pub_key = g_sib_arm_key; + g_sib_pub_count = g_sib_arm_count; + g_sib_pub_topo = g_sib_arm_topo; + g_sib_pub_bytes = g_sib_arm_bytes; + g_sib_hits++; + } else { + if (!pIndexData) { + g_sib_arm = 0; + return E_INVALIDARG; + } + ib_offset = up_ring_upload(&g_up_index_ring, pIndexData, ib_size); + if (ib_offset == (UINT)-1) { + g_sib_arm = 0; + return E_OUTOFMEMORY; + } + rhi_set_index_buffer(g_up_index_ring.buffer, idx_bytes * 8, ib_offset); + } + g_sib_arm = 0; /* Vertex shader: try programmable VS first, fall back to FVF fixed-function */ d3d8_shaders_prepare_draw(g_device_state.vertex_shader); /* VS, then the combiner PS or the fixed-function one */ @@ -1814,6 +2354,15 @@ static HRESULT __stdcall dev_DrawIndexedPrimitiveUP(IDirect3DDevice8 *self, D3DP return S_OK; } +static HRESULT __stdcall dev_DrawIndexedPrimitiveUP(IDirect3DDevice8 *self, D3DPRIMITIVETYPE PrimitiveType, UINT MinVertexIndex, UINT NumVertices, UINT PrimitiveCount, const void *pIndexData, D3DFORMAT IndexDataFormat, const void *pVertexData, UINT VertexStreamZeroStride) +{ + long long start=d3d8_profile_clock(); + HRESULT hr=dev_DrawIndexedPrimitiveUP_profile_impl(self,PrimitiveType,MinVertexIndex,NumVertices,PrimitiveCount,pIndexData,IndexDataFormat,pVertexData,VertexStreamZeroStride); + if(start){g_profile_draw_ticks+=d3d8_profile_clock()-start;++g_profile_draw_calls;} + return hr; +} + + static HRESULT __stdcall dev_CreateTexture(IDirect3DDevice8 *self, UINT Width, UINT Height, UINT Levels, DWORD Usage, D3DFORMAT Format, D3DPOOL Pool, IDirect3DTexture8 **ppTexture) { (void)self; (void)Pool; @@ -2783,6 +3332,225 @@ static HRESULT __stdcall dev_EndPush(IDirect3DDevice8 *self, DWORD *pPush) * about 136 frames a second to 27 and cut how far a run got by two thirds. */ static UINT g_present_interval = 1; +/* ================================================================ + * Paced presents (RECOMP_PACED_PRESENT=1, with frame interpolation) + * + * Frame interpolation draws its in-between frame while the title waits for + * the vblank, so the in-between can only be shown on time if the title's + * next frame is finished before the halfway point. Forza Motorsport takes + * 20-28 ms of a 33 ms period; the in-betweens came 10-16 ms late and the + * picture stuttered instead of running at 60. + * + * Here frames are not put on the screen where they are finished. Each is + * resolved into an image of its own with the moment it is due, and a + * thread of ours copies it to the swap chain and presents it then. The + * in-between N-1 -> N is drawn as soon as N is done and is due at N's + * vblank; N itself is due half a period later. That is half a period more + * latency, and an even 60. The device is made thread-safe for this + * (rhi_d3d11.c), and the thread only copies and presents. + * ================================================================ */ +enum { PACED_BETWEEN = 0, PACED_FRAME = 1, PACED_SLOTS = 2 }; +#if defined(_WIN32) +int rhi_d3d11_paced_present(RhiImage *img, uint32_t interval); /* rhi_d3d11.c */ + +static int g_paced = -1; +static RhiImage *g_paced_img[PACED_SLOTS]; +static RhiView *g_paced_rtv[PACED_SLOTS]; +static UINT g_paced_w, g_paced_h; +static volatile LONG g_paced_busy[PACED_SLOTS]; /* queued, not yet shown */ +static volatile LONGLONG g_paced_due[PACED_SLOTS]; +static LONGLONG g_paced_next_due; /* for the next in-between */ +static LONGLONG g_paced_qpf, g_paced_half; +static HANDLE g_paced_event; +static unsigned long g_paced_shown, g_paced_late, g_paced_waits; +static double g_paced_off_ms; +static LONGLONG g_paced_submit_ticks; +static unsigned long g_paced_submits; + +BOOL xbox_D3D8PacedOn(void) +{ + if (g_paced < 0) { + const char *e = getenv("RECOMP_PACED_PRESENT"); + g_paced = e && e[0] == '1'; + if (g_paced) { + LARGE_INTEGER f; + const char *hz = getenv("RECOMP_VBLANK_HZ"); + double rate = hz && atof(hz) >= 1.0 ? atof(hz) : 60.0; + QueryPerformanceFrequency(&f); + g_paced_qpf = f.QuadPart; + g_paced_half = (LONGLONG)((double)f.QuadPart / rate / 2.0); + fprintf(stderr, "D3D8: paced presents: each frame shown %.1f ms after it is " + "finished, an in-between before it (RECOMP_PACED_PRESENT)\n", + 1000.0 / rate / 2.0); + fflush(stderr); + } + } + return g_paced > 0; +} + +void xbox_D3D8PacedDue(long long qpc) +{ + g_paced_next_due = qpc; +} + +static double g_paced_hold_ms, g_paced_hold_max; + +static DWORD WINAPI paced_thread(LPVOID unused) +{ + (void)unused; + for (;;) { + LARGE_INTEGER now; + int k, pick = -1; + LONGLONG due = 0; + + for (k = 0; k < PACED_SLOTS; k++) + if (InterlockedCompareExchange(&g_paced_busy[k], 1, 1) == 1 && + (pick < 0 || g_paced_due[k] < due)) { + pick = k; + due = g_paced_due[k]; + } + if (pick < 0) { + WaitForSingleObject(g_paced_event, 5); + continue; + } + for (;;) { + LONGLONG left; + QueryPerformanceCounter(&now); + left = due - now.QuadPart; + if (left <= 0) + break; + if (left > g_paced_qpf / 500) /* more than 2 ms: sleep */ + Sleep(1); + else + YieldProcessor(); + } + { + double off = (double)(now.QuadPart - due) * 1000.0 / (double)g_paced_qpf; + g_paced_off_ms += off; + if (off > 4.0) + g_paced_late++; + } + /* Interval 0: a present that waits for the vblank holds the device's + * lock while it waits (the context is thread-safe now), and the + * title's own drawing stalled behind it -- Forza fell to 15 fps. + * The pacing is ours; the flip model still shows each picture + * whole at the display's next refresh. */ + { + LARGE_INTEGER h0, h1; + double ms; + QueryPerformanceCounter(&h0); + rhi_d3d11_paced_present(g_paced_img[pick], 0); + QueryPerformanceCounter(&h1); + ms = (double)(h1.QuadPart - h0.QuadPart) * 1000.0 / (double)g_paced_qpf; + g_paced_hold_ms += ms; + if (ms > g_paced_hold_max) + g_paced_hold_max = ms; + } + g_paced_shown++; + InterlockedExchange(&g_paced_busy[pick], 0); + } + return 0; +} + +/* Resolve the scene into slot k, due at `due`. FALSE if paced presents + * cannot be had, and the caller presents as before. */ +static BOOL paced_submit(int k, LONGLONG due) +{ + D3D8DeviceState *s = &g_device_state; + int i; + + if (!xbox_D3D8PacedOn() || !rhi_swapchain_view() || !s->rhi_scene_srv) + return FALSE; + if (!g_paced_event) { + g_paced_event = CreateEventW(NULL, FALSE, FALSE, NULL); + if (!g_paced_event || !CreateThread(NULL, 0, paced_thread, NULL, 0, NULL)) { + g_paced = 0; + return FALSE; + } + } + /* The slot's last picture must be on the screen before it is drawn over. */ + { + int spins = 0; + while (InterlockedCompareExchange(&g_paced_busy[k], 1, 1) == 1 && spins < 100) { + Sleep(1); + spins++; + } + if (spins) + g_paced_waits++; + } + /* Images the size of the swap chain. A window resize during the + * resolve below leaves one picture the old size; its copy is refused + * and the next frame makes them again. */ + if (g_paced_w != s->swap_width || g_paced_h != s->swap_height) { + for (i = 0; i < PACED_SLOTS; i++) { + while (InterlockedCompareExchange(&g_paced_busy[i], 1, 1) == 1) + Sleep(1); + rhi_view_destroy(g_paced_rtv[i]); g_paced_rtv[i] = NULL; + rhi_image_destroy(g_paced_img[i]); g_paced_img[i] = NULL; + } + g_paced_w = g_paced_h = 0; + for (i = 0; i < PACED_SLOTS; i++) { + RhiImageDesc d; + memset(&d, 0, sizeof d); + d.type = RHI_IMAGE_2D; + d.width = s->swap_width; + d.height = s->swap_height; + d.depth = 1; + d.mip_levels = 1; + d.format = RHI_FORMAT_R8G8B8A8_UNORM; + d.samples = 1; + d.usage = RHI_USAGE_DEFAULT; + d.bind = RHI_BIND_RENDER_TARGET | RHI_BIND_SAMPLED; + g_paced_img[i] = rhi_image_create(&d, NULL); + g_paced_rtv[i] = g_paced_img[i] ? + rhi_view_create(g_paced_img[i], RHI_VIEW_RENDER_TARGET, NULL) : NULL; + if (!g_paced_rtv[i]) { + fprintf(stderr, "D3D8: paced presents: no image %ux%u; presenting as " + "before\n", s->swap_width, s->swap_height); + g_paced = 0; + return FALSE; + } + } + g_paced_w = s->swap_width; + g_paced_h = s->swap_height; + } + { + LARGE_INTEGER a, b; + QueryPerformanceCounter(&a); + present_scene_into(g_paced_rtv[k]); + QueryPerformanceCounter(&b); + g_paced_submit_ticks += b.QuadPart - a.QuadPart; + g_paced_submits++; + } + g_paced_due[k] = due; + InterlockedExchange(&g_paced_busy[k], 1); + SetEvent(g_paced_event); + return TRUE; +} + +/* The five-second report, from the HLE's. */ +void xbox_D3D8PacedReport(void) +{ + if (g_paced <= 0 || !g_paced_shown) + return; + fprintf(stderr, "D3D8: paced presents: %lu shown, %.2f ms from their moment on " + "average, %lu more than 4 ms late, %lu waits for a slot; resolve %.2f ms each; present %.2f ms each, %.2f worst\n", + g_paced_shown, g_paced_off_ms / (double)g_paced_shown, g_paced_late, g_paced_waits, + g_paced_submits ? (double)g_paced_submit_ticks * 1000.0 / (double)g_paced_qpf / + (double)g_paced_submits : 0.0, + g_paced_hold_ms / (double)g_paced_shown, g_paced_hold_max); + g_paced_hold_ms = g_paced_hold_max = 0.0; + g_paced_shown = g_paced_late = g_paced_waits = 0; + g_paced_submit_ticks = 0; + g_paced_submits = 0; + g_paced_off_ms = 0.0; +} +#else +BOOL xbox_D3D8PacedOn(void) { return FALSE; } +void xbox_D3D8PacedDue(long long qpc) { (void)qpc; } +void xbox_D3D8PacedReport(void) {} +#endif + void xbox_D3D8SetPresentInterval(UINT interval) { g_present_interval = interval; @@ -2802,6 +3570,21 @@ static HRESULT __stdcall dev_Swap(IDirect3DDevice8 *self, DWORD Flags) DispatchMessageA(&msg); } +#if defined(_WIN32) + if (xbox_D3D8PacedOn()) { + LARGE_INTEGER now; + QueryPerformanceCounter(&now); + follow_vrr(); + two_d_tag_frame(); + if (paced_submit(PACED_FRAME, now.QuadPart + g_paced_half)) { + d3d8_display_frame_done(); + return S_OK; + } + present_scene(); + d3d8_display_frame_done(); + return (HRESULT)rhi_present(g_present_interval); + } +#endif present_resolve(); return (HRESULT)rhi_present(g_present_interval); } @@ -2893,6 +3676,10 @@ void xbox_D3D8InterpPresent(void) if (!g_interp_on) return; follow_vrr(); +#if defined(_WIN32) + if (xbox_D3D8PacedOn() && paced_submit(PACED_BETWEEN, g_paced_next_due)) + return; +#endif present_scene(); rhi_present(g_present_interval); } diff --git a/src/d3d/d3d8_internal.h b/src/d3d/d3d8_internal.h index f2d8878ba..de3e7a89f 100644 --- a/src/d3d/d3d8_internal.h +++ b/src/d3d/d3d8_internal.h @@ -53,7 +53,7 @@ void d3d8_SetTwoDSqueeze(BOOL on); * depth and stencil as asked, the scissor and viewport ignored. 0 done (or * nothing to do), -1 if the pass could not be made. */ int d3d8_clear_rects(RhiView *rtv, RhiView *dsv, UINT width, UINT height, - const RhiRect *rects, UINT count, int color, int depth, int stencil, + const RhiRect *rects, UINT count, int color_mask, int depth, int stencil, const float rgba[4], float z, uint8_t stencil_value); void d3d8_clear_shutdown(void); @@ -85,6 +85,8 @@ UINT d3d8_GetBackbufferHeight(void); /* Current render state array accessor */ const DWORD *d3d8_GetRenderStates(void); +/* MaxZ - MinZ of the current viewport. */ +float d3d8_GetViewportDepthSpan(void); const DWORD *d3d8_GetTSS(DWORD stage); /* Base texture currently bound to a texture stage (2D/cube/volume), diff --git a/src/d3d/d3d8_movie.c b/src/d3d/d3d8_movie.c index 6135d5364..0e0acbff7 100644 --- a/src/d3d/d3d8_movie.c +++ b/src/d3d/d3d8_movie.c @@ -36,11 +36,15 @@ static struct { uint32_t frame_w, frame_h; int dirty; /* not uploaded yet */ int have; /* a frame to draw */ + uint32_t bars; /* black rows above and below the picture */ + uint32_t grow_to; /* bars a run of frames has shown, not taken yet */ + int grow_frames; } g = { 0, 0, NULL, NULL, 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, SRWLOCK_INIT }; typedef struct { float x, y, w, h; /* the picture's rectangle, clip space */ - float tex_w, tex_h, pad0, pad1; + float tex_w, vis_h; /* the texels shown: all columns, vis_h rows */ + float first_row, tex_h; /* from this row; the texture's own height */ } MovieConstants; static const char kShaderSource[] = @@ -53,12 +57,12 @@ static const char kShaderSource[] = " float2 c = float2(id & 1, id >> 1);\n" " VSOut o;\n" " o.pos = float4(rect.x + c.x * rect.z, rect.y - c.y * rect.w, 0, 1);\n" - " o.uv = c * size.xy;\n" + " o.uv = float2(c.x * size.x, size.z + c.y * size.y);\n" " return o;\n" "}\n" "Texture2D tex : register(t9);\n" "float4 texel(int2 p) {\n" - " p = clamp(p, int2(0, 0), int2(size.xy) - 1);\n" + " p = clamp(p, int2(0, 0), int2(size.xw) - 1);\n" " return tex.Load(int3(p, 0));\n" "}\n" "float4 ps_main(VSOut i) : SV_Target {\n" @@ -172,6 +176,69 @@ static int movie_texture(uint32_t w, uint32_t h) return 1; } +/* Rows of near-black from the top (dir 1) or the bottom (dir -1) of a BGRA + * frame, looking at every eighth pixel of a row. */ +static uint32_t black_rows(const uint8_t *bgra, uint32_t w, uint32_t h, int dir) +{ + uint32_t n, x; + + for (n = 0; n < h / 2u; n++) { + const uint8_t *row = bgra + (size_t)(dir > 0 ? n : h - 1u - n) * w * 4u; + for (x = 0; x < w; x += 8u) { + const uint8_t *q = row + (size_t)x * 4u; + if (q[0] > 28 || q[1] > 28 || q[2] > 28) + return n; + } + } + return n; +} + +/* A widescreen movie inside a 4:3 frame -- Forza Motorsport's attract loop and + * its "press start" screen are 16:9 pictures between black bars -- would + * be drawn small in the middle of a wide window, with black all round. The + * bars are found and left off, so the picture itself fills the window's + * height at its own shape. Never cut into the picture: a frame whose picture + * reaches into the bars takes them back at once; wider bars are only taken + * once 20 frames in a row have shown them, so one dark frame does not zoom. + * A frame that is black almost throughout says nothing and changes nothing. + * RECOMP_MOVIE_LETTERBOX=0 shows the whole frame, as before. */ +static void note_bars(const uint8_t *bgra, uint32_t w, uint32_t h) +{ + static int on = -1; + uint32_t top, bottom, b; + + if (on < 0) { + const char *v = getenv("RECOMP_MOVIE_LETTERBOX"); + on = !(v && v[0] == '0'); + } + if (!on || h < 64u) { + g.bars = 0; + return; + } + top = black_rows(bgra, w, h, 1); + bottom = black_rows(bgra, w, h, -1); + b = top < bottom ? top : bottom; + if (b >= h / 4u) + return; /* nearly all black: no news */ + if (b < h / 24u) + b = 0; /* a thin edge is not a bar */ + b &= ~1u; + if (b <= g.bars) { + g.bars = b; + g.grow_frames = 0; + return; + } + if (g.grow_frames && b == g.grow_to) { + if (++g.grow_frames >= 20) { + g.bars = b; + g.grow_frames = 0; + } + } else { + g.grow_to = b; + g.grow_frames = 1; + } +} + void d3d8_movie_set_frame(const uint8_t *bgra, uint32_t width, uint32_t height) { size_t bytes = (size_t)width * height * 4u; @@ -180,6 +247,8 @@ void d3d8_movie_set_frame(const uint8_t *bgra, uint32_t width, uint32_t height) return; AcquireSRWLockExclusive(&g.lock); if (g.frame_w != width || g.frame_h != height) { + g.bars = 0; + g.grow_frames = 0; free(g.frame); g.frame = (uint8_t *)malloc(bytes); g.frame_w = g.frame ? width : 0; @@ -188,6 +257,7 @@ void d3d8_movie_set_frame(const uint8_t *bgra, uint32_t width, uint32_t height) if (g.frame) { memcpy(g.frame, bgra, bytes); g.dirty = g.have = 1; + note_bars(bgra, width, height); } ReleaseSRWLockExclusive(&g.lock); } @@ -208,7 +278,7 @@ void d3d8_movie_draw(void) MovieConstants c; float blend_factor[4] = { 1, 1, 1, 1 }, black[4] = { 0, 0, 0, 1 }; float fa, ba; - uint32_t w, h; + uint32_t w, h, bars; void *mapped; if (!rtv || !bw || !bh || !g.have || !movie_create()) @@ -225,6 +295,7 @@ void d3d8_movie_draw(void) rhi_image_update(g.texture, 0, NULL, g.frame, w * 4u, 0); g.dirty = 0; } + bars = g.bars; ReleaseSRWLockExclusive(&g.lock); rhi_output_save(&saved); @@ -239,7 +310,9 @@ void d3d8_movie_draw(void) rhi_clear_color(rtv, black); if (!(mapped = rhi_buffer_map(g.cb, RHI_MAP_WRITE_DISCARD))) goto restore; - fa = 4.0f / 3.0f; + /* 4:3 for the whole frame; the rows between the bars are that much + * wider for their height. */ + fa = 4.0f / 3.0f * (float)h / (float)(h - 2u * bars); { UINT sw = bw, sh = bh; @@ -251,8 +324,9 @@ void d3d8_movie_draw(void) c.x = -c.w / 2.0f; c.y = c.h / 2.0f; c.tex_w = (float)w; + c.vis_h = (float)(h - 2u * bars); + c.first_row = (float)bars; c.tex_h = (float)h; - c.pad0 = c.pad1 = 0.0f; memcpy(mapped, &c, sizeof c); rhi_buffer_unmap(g.cb); diff --git a/src/d3d/d3d8_resources.c b/src/d3d/d3d8_resources.c index 84ac98579..f59396b86 100644 --- a/src/d3d/d3d8_resources.c +++ b/src/d3d/d3d8_resources.c @@ -8,6 +8,7 @@ #include "d3d8_internal.h" #include "d3d8_swizzle.h" +#include "dxt5_decode.h" #include #include #include @@ -1457,6 +1458,18 @@ static void d3d8_upload_mip_level(RhiImage *image, D3DFORMAT fmt, if (!rhi_device_ready() || !image) return; + RhiImageDesc storage; + rhi_image_get_desc(image, &storage); + if ((fmt == D3DFMT_DXT5 || fmt == D3DFMT_DXT1) && storage.format == RHI_FORMAT_R8G8B8A8_UNORM) { + BYTE *rgba = (BYTE *)malloc((size_t)w * h * 4); + if (!rgba) return; + if (fmt == D3DFMT_DXT1) xbr_decode_dxt1_rgba(src_data, src_row_pitch, w, h, rgba); + else xbr_decode_dxt5_rgba(src_data, src_row_pitch, w, h, rgba); + rhi_image_update(image, subresource, NULL, rgba, w * 4, w * h * 4); + free(rgba); + return; + } + /* Step 1: unswizzle swizzled formats into linear storage. */ if (!d3d8_format_is_compressed(fmt) && d3d8_format_is_swizzled(fmt)) { linear = (BYTE *)malloc((size_t)w * h * bpp); @@ -1558,6 +1571,8 @@ HRESULT d3d8_CreateTextureImpl(UINT Width, UINT Height, UINT Levels, DWORD Usage tex->d3d8_format = Format; tex->host_format = d3d8_to_dxgi_format(Format); + if ((Format == D3DFMT_DXT5 || Format == D3DFMT_DXT1) && ((Width & 3) || (Height & 3))) + tex->host_format = RHI_FORMAT_R8G8B8A8_UNORM; /* F16 is float depth, which D3D11 has no format for. As a texture it * mapped to R16_FLOAT and was then bound as depth (it is a depth format), * which no backend accepts: MK Shaolin Monks' 256x256 LIN_F16 shadow @@ -2031,6 +2046,30 @@ BOOL d3d8_cube_level(IDirect3DBaseTexture8 *texture, UINT face, UINT level, face >= 6 || level >= cube->levels || !bits || !pitch || !rows) return FALSE; w = cube_level_width(cube, level); + /* A capture must start with the GPU's existing environment map, not + * the zero-filled CPU allocation of a render target. Titles update + * only some faces per frame. These formats have identical host/Xbox + * byte channels; only the Xbox swizzle needs restoring for replay. */ + if (cube->usage & D3DUSAGE_RENDERTARGET) { + BYTE *linear; + if (cube->d3d8_format != D3DFMT_A8R8G8B8 && + cube->d3d8_format != D3DFMT_LIN_A8R8G8B8 && + cube->d3d8_format != D3DFMT_X8R8G8B8 && + cube->d3d8_format != D3DFMT_LIN_X8R8G8B8) + return FALSE; + linear = (BYTE *)malloc((size_t)w * w * 4); + if (!linear) return FALSE; + if (rhi_image_readback(cube->image, face * cube->levels + level, + linear, w * 4) != 0) { + free(linear); + return FALSE; + } + if (d3d8_format_is_swizzled(cube->d3d8_format)) + xbox_swizzle_rect(cube_level_ptr(cube, face, level), linear, w, w, 4); + else + memcpy(cube_level_ptr(cube, face, level), linear, (size_t)w * w * 4); + free(linear); + } *bits = cube_level_ptr(cube, face, level); *pitch = d3d8_row_pitch(cube->d3d8_format, w); *rows = d3d8_format_is_compressed(cube->d3d8_format) ? (w + 3) / 4 : w; diff --git a/src/d3d/d3d8_screencopy.c b/src/d3d/d3d8_screencopy.c index b972ccdc1..2e7b22bb9 100644 --- a/src/d3d/d3d8_screencopy.c +++ b/src/d3d/d3d8_screencopy.c @@ -55,6 +55,18 @@ static const char kSource[] = "}\n" "Texture2D screen : register(t9);\n" "float4 ps_main(VSOut i) : SV_Target {\n" + /* Preserve supersampling during integer downscales into a title's + * postprocess texture. Point loading discarded the extra samples. */ + " float2 taps = round(scale.xy);\n" + " if (all(taps >= 1) && all(taps <= 4) &&\n" + " all(abs(taps - scale.xy) < 0.00001) && any(taps > 1)) {\n" + " int2 first = int2(floor((i.pos.xy - 0.5) * scale.xy + scale.zw));\n" + " float4 sum = 0;\n" + " [loop] for (int y = 0; y < (int)taps.y; ++y)\n" + " [loop] for (int x = 0; x < (int)taps.x; ++x)\n" + " sum += screen.Load(int3(first + int2(x, y), 0));\n" + " return sum / (taps.x * taps.y);\n" + " }\n" " int2 p = int2(i.pos.xy * scale.xy + scale.zw);\n" " return screen.Load(int3(p, 0));\n" "}\n"; diff --git a/src/d3d/d3d8_shaders.c b/src/d3d/d3d8_shaders.c index eb0e1529a..8a7ccb338 100644 --- a/src/d3d/d3d8_shaders.c +++ b/src/d3d/d3d8_shaders.c @@ -98,6 +98,9 @@ static const char g_vs_source[] = " float4 tex3 : TEXCOORD3;\n" " float fog : TEXCOORD4;\n" " float4 viewpos : TEXCOORD5;\n" /* view-space position (per-pixel fog) */ + /* d3d8_vsh.c, oWDep: zero here, nothing to write */ + " float4 wdep : XBOXWDEPTH0;\n" + " float4 wlin : XBOXWLINEAR0;\n" /* Vulkan: a vertex stage feeding a point list must write the point * size, where D3D11 has none (every point is one pixel). __spirv__ is * DXC's own macro for a SPIR-V compile, so D3DCompile never sees this, @@ -172,6 +175,8 @@ static const char g_vs_source[] = " o.fog = 1.0;\n" " o.specular = float4(0, 0, 0, 0);\n" " o.viewpos = float4(0, 0, 0, 1);\n" + " o.wdep = float4(0, 0, 0, 0);\n" + " o.wlin = float4(0, 0, 0, 0);\n" "\n" " if (Flags & FLAG_PRETRANSFORMED) {\n" " o.pos.x = (input.pos.x / ScreenSize.x) * 2.0 - 1.0;\n" @@ -322,6 +327,8 @@ static const char g_ps_body[] = " float4 tex3 : TEXCOORD3;\n" " float fog : TEXCOORD4;\n" " float4 viewpos : TEXCOORD5;\n" + " float4 wdep : XBOXWDEPTH0;\n" + " noperspective float4 wlin : XBOXWLINEAR0;\n" "};\n" "\n" "// D3DFOGMODE: 0=NONE, 1=EXP, 2=EXP2, 3=LINEAR\n" @@ -394,7 +401,10 @@ static const char g_ps_body[] = " return a1 * a2;\n" /* fallback = MODULATE */ "}\n" "\n" - "float4 main(PS_IN input) : SV_TARGET {\n" + "float4 main(PS_IN input, out float o_depth : SV_Depth) : SV_TARGET {\n" + /* d3d8_vsh.c, d3d8_vsh_wdepth_on: under a program, a W-buffered + * draw's exact w; zero correction from the fixed-function stage. */ + " o_depth = input.pos.z - input.wdep.y * max(input.wlin.x - input.wdep.x, 0.0);\n" " float4 current = input.diffuse;\n" " float4 texels[4];\n" "\n" diff --git a/src/d3d/d3d8_states.c b/src/d3d/d3d8_states.c index 42cb7ca99..2b8bf5f3a 100644 --- a/src/d3d/d3d8_states.c +++ b/src/d3d/d3d8_states.c @@ -116,11 +116,57 @@ static DWORD hash_blend_states(const DWORD *rs) (rs[D3DRS_COLORWRITEENABLE] << 16); } -static DWORD hash_raster_states(const DWORD *rs, BOOL scissor) +/* NV2A polygon-offset bias is an OpenGL-style unit count (one unit is one + * minimum depth step). D3D11's DepthBias is that same count on a UNORM + * depth buffer. A non-finite or absurd value is no offset. */ +static float poly_float(DWORD bits) { + float f; + memcpy(&f, &bits, sizeof f); + return f; +} + +static int poly_bias_units(float f) +{ + if (!(f > -1.0e6f && f < 1.0e6f)) + return 0; + if (f >= 0.0f) + return (int)(f + 0.5f); + return (int)(f - 0.5f); +} + +/* The offset applies only while the enable for this fill mode is set. + * NV2A leaves all three off, so a draw that never asked for offset stays + * unbiased. */ +static void poly_offset(const DWORD *rs, float *slope, int *bias) +{ + DWORD enable; + float s, b; + + switch (rs[D3DRS_FILLMODE]) { + case D3DFILL_POINT: enable = rs[D3DRS_POLY_POINT]; break; + case D3DFILL_WIREFRAME: enable = rs[D3DRS_POLY_LINE]; break; + default: enable = rs[D3DRS_POLY_FILL]; break; + } + s = poly_float(rs[D3DRS_POLY_SLOPE]); + b = poly_float(rs[D3DRS_POLY_BIAS]); + if (!enable || !(s > -1.0e6f && s < 1.0e6f)) + s = 0.0f; + if (!enable) + b = 0.0f; + *slope = s; + *bias = enable ? poly_bias_units(b) : 0; +} + +static DWORD hash_raster_states(const DWORD *rs, BOOL scissor, float slope, int bias) +{ + DWORD sb; + memcpy(&sb, &slope, sizeof sb); return rs[D3DRS_CULLMODE] ^ (rs[D3DRS_FILLMODE] << 4) ^ - (scissor ? 0x100u : 0u); + (scissor ? 0x100u : 0u) ^ + (sb * 0x9E3779B1u) ^ + ((DWORD)bias * 0x85EBCA77u); } /* ================================================================ @@ -208,11 +254,68 @@ static int depth_clamp(void) return on; } +/* The values Forza actually writes, a few distinct ones, so a bias that + * rounds away to nothing (a tiny fraction) is visible in the log. */ +static void note_poly_offset(const DWORD *rs, float slope, int bias) +{ + static DWORD seen_s[16], seen_b[16], seen_e[16]; + static int n; + static DWORD tick; + static unsigned long draws, enabled; + DWORD e, now; + int i; + float raw_s, raw_b; + + draws++; + if (slope != 0.0f || bias != 0) + enabled++; + raw_s = poly_float(rs[D3DRS_POLY_SLOPE]); + raw_b = poly_float(rs[D3DRS_POLY_BIAS]); + e = rs[D3DRS_POLY_POINT] | (rs[D3DRS_POLY_LINE] << 1) | + (rs[D3DRS_POLY_FILL] << 2) | (rs[D3DRS_FILLMODE] << 8); + if ((rs[D3DRS_POLY_SLOPE] || rs[D3DRS_POLY_BIAS] || e) && n < 16) { + for (i = 0; i < n; i++) + if (seen_s[i] == rs[D3DRS_POLY_SLOPE] && + seen_b[i] == rs[D3DRS_POLY_BIAS] && seen_e[i] == e) + break; + if (i == n) { + seen_s[n] = rs[D3DRS_POLY_SLOPE]; + seen_b[n] = rs[D3DRS_POLY_BIAS]; + seen_e[n] = e; + n++; + fprintf(stderr, "[HLE-D3D8] polygon offset: slope %g (0x%08lX) " + "bias %g (0x%08lX) -> depth %d slope %g, fill %lu, " + "enable point/line/solid %lu/%lu/%lu\n", + raw_s, (unsigned long)rs[D3DRS_POLY_SLOPE], + raw_b, (unsigned long)rs[D3DRS_POLY_BIAS], + bias, slope, (unsigned long)rs[D3DRS_FILLMODE], + (unsigned long)rs[D3DRS_POLY_POINT], + (unsigned long)rs[D3DRS_POLY_LINE], + (unsigned long)rs[D3DRS_POLY_FILL]); + } + } + now = GetTickCount(); + if (!tick) + tick = now; + if (now - tick < 5000u) + return; + fprintf(stderr, "[POLY] %lu draws, offset on for %lu\n", draws, enabled); + tick = now; + draws = 0; + enabled = 0; +} + static void update_rasterizer_state(const DWORD *rs, BOOL scissor) { - DWORD hash = hash_raster_states(rs, scissor); + float slope; + int bias; + DWORD hash; RhiRasterDesc rd; + poly_offset(rs, &slope, &bias); + note_poly_offset(rs, slope, bias); + hash = hash_raster_states(rs, scissor, slope, bias); + if (hash == g_last_raster_hash && g_raster_state) return; g_last_raster_hash = hash; @@ -243,6 +346,12 @@ static void update_rasterizer_state(const DWORD *rs, BOOL scissor) rd.scissor = scissor ? 1 : 0; rd.multisample = 0; rd.antialiased_lines = 0; + /* Pulls a coplanar decal toward the camera when the title's bias is + * negative, which is the usual "draw this on top of the road" offset. + * The sign matches OpenGL and D3D11: positive bias increases depth. */ + rd.depth_bias = bias; + rd.slope_scaled_depth_bias = slope; + rd.depth_bias_clamp = 0.0f; g_raster_state = rhi_raster_state_create(&rd); if (!g_raster_state) @@ -366,7 +475,7 @@ void d3d8_states_apply_sampler(DWORD stage) tss[D3DTSS_MIPFILTER]); sd.address_u = d3d8_to_rhi_address(tss[D3DTSS_ADDRESSU] ? tss[D3DTSS_ADDRESSU] : D3DTADDRESS_WRAP); sd.address_v = d3d8_to_rhi_address(tss[D3DTSS_ADDRESSV] ? tss[D3DTSS_ADDRESSV] : D3DTADDRESS_WRAP); - sd.address_w = RHI_ADDRESS_WRAP; + sd.address_w = d3d8_to_rhi_address(tss[D3DTSS_ADDRESSW] ? tss[D3DTSS_ADDRESSW] : D3DTADDRESS_WRAP); sd.max_anisotropy = tss[D3DTSS_MAXANISOTROPY] ? tss[D3DTSS_MAXANISOTROPY] : 1; /* What a BORDER stage reads outside 0..1: the title's D3DTSS_BORDERCOLOR diff --git a/src/d3d/d3d8_vsh.c b/src/d3d/d3d8_vsh.c index 7fd838490..cdacacf7b 100644 --- a/src/d3d/d3d8_vsh.c +++ b/src/d3d/d3d8_vsh.c @@ -68,6 +68,7 @@ static uint16_t g_vsh_vdata_fed; /* the mask g_vsh_vdata.fed holds */ /* Constant registers (192 float4) */ static NV2AVSConstants g_vsh_constants; static BOOL g_vsh_constants_dirty = TRUE; +static unsigned long g_vsh_const_uploads, g_vsh_const_same; /* Constant buffer for VS constants */ static RhiBuffer *g_vsh_cb = NULL; @@ -771,6 +772,24 @@ int d3d8_vsh_generate_hlsl(const NV2AVshProgram *program, "};\n" "\n", NV2A_VS_MAX_CONSTANTS, NV2A_VS_MAX_CONSTANTS, NV2A_VS_MAX_INPUTS); + /* RECOMP_D3D8_CLIP_W=0: no clip distance at w <= 0 (see VS_OUT). */ + { + static int clip_w = -1; + if (clip_w < 0) { + const char *e = getenv("RECOMP_D3D8_CLIP_W"); + clip_w = !(e && e[0] == '0'); + } + sb_append(&sb, "static const int xboxClipW = %d;\n", clip_w); + } + { + static int abs_w = -1; + if (abs_w < 0) { + const char *e = getenv("RECOMP_D3D8_ABS_W"); + abs_w = e && e[0] == '1'; + } + sb_append(&sb, "static const int xboxAbsW = %d;\n\n", abs_w); + } + /* Input structure - only declare used inputs */ sb_append(&sb, "struct VS_IN {\n"); for (i = 0; i < NV2A_VS_MAX_INPUTS; i++) { @@ -804,9 +823,28 @@ int d3d8_vsh_generate_hlsl(const NV2AVshProgram *program, " float4 oT3 : TEXCOORD3;\n" " float oFog : TEXCOORD4;\n" " float4 oViewPos : TEXCOORD5;\n" + /* The W-buffer depth (see the screen-space transform below): x is + * w over the depth scale, y the viewport's depth span when the + * pixel stage is to write it, else 0. Twice, since the pixel + * stages read one copy perspective-correct and one not. Straight + * after the view position, so a pixel stage that does not read + * them still matches by register. */ + " float4 oWDep : XBOXWDEPTH0;\n" + " float4 oWLin : XBOXWLINEAR0;\n" " float oPts : PSIZE;\n" " float4 oB0 : TEXCOORD6;\n" " float4 oB1 : TEXCOORD7;\n" + /* D3D11 only: the near plane, as a clip distance. Depth clipping is + * off (d3d8_states.c, depth_clamp: BF2:MC's menus sit a tenth of a + * depth unit in front of it), and with it went the near plane: a + * wall that runs past the camera kept the sliver between the eye + * and the near plane, drawn at depth 0 over everything. In Forza + * Motorsport those slivers were black slabs with straight edges + * across the road. 1e-4 of the range in front of the plane is kept. + * The Vulkan device does not enable shaderClipDistance. */ + "#ifndef __spirv__\n" + " float oClipW : SV_ClipDistance0;\n" + "#endif\n" /* Vulkan: a vertex stage feeding a point list must write the point * size; D3D11 has none (oPts above goes nowhere, every point is one * pixel). __spirv__ is DXC's macro for a SPIR-V compile, so @@ -983,19 +1021,40 @@ int d3d8_vsh_generate_hlsl(const NV2AVshProgram *program, * them has no area. */ sb_append(&sb, " /* Undo the Xbox screen-space transform */\n" + /* xboxClipZ: the program's own depth in clip space, kept for the + * near-plane clip distance (VS_OUT oClipW) before a W-buffered draw + * replaces z with w. */ + " float xboxClipZ;\n" + " float4 xboxWDep = float4(0, 0, 0, 0);\n" " if (xboxScreenspaceEnable.x != 0) {\n" " float xboxW = oPos.w;\n" " oPos -= xboxScreenspaceOffset;\n" " oPos /= xboxScreenspaceScale;\n" - " if (xboxScreenspaceEnable.y != 0 && xboxScreenspaceScale.z != 0)\n" + " float xboxZ = oPos.z;\n" + " if (xboxScreenspaceEnable.y != 0 && xboxScreenspaceScale.z != 0) {\n" " oPos.z = xboxW / xboxScreenspaceScale.z;\n" + " if (xboxScreenspaceEnable.y > 1.5)\n" + " xboxWDep = float4(oPos.z, xboxScreenspaceEnable.w, 0, 0);\n" + " }\n" " if (oPos.w == 0) {\n" " if (abs(oPos.x) > 1.0e4 || abs(oPos.y) > 1.0e4)\n" " oPos = float4(0, 0, 0, 0); /* through the eye: clipped */\n" " else\n" " oPos.w = 1;\n" " }\n" + /* The NV2A rasterises the screen-space positions the program + * wrote, divided by w already, and uses w only to interpolate: a + * vertex behind the eye stays where the divide put it rather than + * being clipped against w = 0 as homogeneous clipping would. So |w| + * goes to the host under RECOMP_D3D8_ABS_W=1. OFF: tried for Forza + * Motorsport's horizon quads (corners far behind the camera), it + * wrecked every frame, since ordinary scenery crosses w = 0 all the + * time and the NV2A evidently clips it. Kept as an experiment. */ + " if (xboxAbsW != 0) oPos.w = abs(oPos.w);\n" " oPos.xyz *= oPos.w;\n" + " xboxClipZ = (xboxZ + 1.0e-4) * oPos.w;\n" + " } else {\n" + " xboxClipZ = oPos.z + 1.0e-4 * oPos.w;\n" " }\n\n"); /* Populate output structure */ @@ -1013,9 +1072,14 @@ int d3d8_vsh_generate_hlsl(const NV2AVshProgram *program, /* Table fog reads -viewpos.z (or its length, for range fog) as the * distance; on the Xbox that distance is the program's oFog. */ " o.oViewPos = float4(0, 0, -oFog.x, 1);\n" + " o.oWDep = xboxWDep;\n" + " o.oWLin = xboxWDep;\n" " o.oPts = oPts.x;\n" " o.oB0 = saturate(oB0);\n" " o.oB1 = saturate(oB1);\n" + "#ifndef __spirv__\n" + " o.oClipW = (xboxClipW != 0 && (xboxScreenspaceEnable.x == 0 || xboxScreenspaceEnable.z != 0)) ? xboxClipZ : 1.0;\n" + "#endif\n" "#ifdef __spirv__\n" " o.vk_point_size = 1.0;\n" "#endif\n" @@ -1798,43 +1862,50 @@ void d3d8_vsh_interp_registers(int *projection, int *affine_first, int *affine_c void d3d8_vsh_set_constant(int start_reg, const float *data, int count) { - int end_reg; - float hp; + int end_reg, changed = 0; + float hp = 1.0f; + int reg_hp = -1; - if (!data || start_reg < 0) + if (!data || start_reg < 0 || count <= 0) return; end_reg = start_reg + count; if (end_reg > NV2A_VS_MAX_CONSTANTS) end_reg = NV2A_VS_MAX_CONSTANTS; - for (int i = start_reg; i < end_reg; i++) { - int src_offset = (i - start_reg) * 4; - g_vsh_constants.c[i][0] = data[src_offset + 0]; - g_vsh_constants.c[i][1] = data[src_offset + 1]; - g_vsh_constants.c[i][2] = data[src_offset + 2]; - g_vsh_constants.c[i][3] = data[src_offset + 3]; + /* Hor+ at upload (RECOMP_HOR_PLUS_TARGETS=all) folds into the stored + * register, once. The compare is against that stored result, so a draw + * that sets the same matrix again does not upload the whole bank. */ + if (hor_plus_all_targets()) { + hp = hor_plus_factor(); + if (hp != 1.0f && !g_hor_plus_claimed) + reg_hp = d3d8_vsh_hor_plus_reg(); } - /* Hor+ (hor_plus_factor) is applied as the constants reach the host - * (d3d8_vsh_prepare_draw), so it can depend on where the draw goes. */ - if (!hor_plus_all_targets()) { - g_vsh_constants_dirty = TRUE; - return; - } - hp = hor_plus_factor(); - if (hp != 1.0f && !g_hor_plus_claimed) { - int reg = d3d8_vsh_hor_plus_reg(); - - if (reg >= start_reg && reg < end_reg) { - g_vsh_constants.c[reg][0] *= hp; - g_vsh_constants.c[reg][1] *= hp; - g_vsh_constants.c[reg][2] *= hp; - g_vsh_constants.c[reg][3] *= hp; + for (int i = start_reg; i < end_reg; i++) { + int src_offset = (i - start_reg) * 4; + float v[4]; + + v[0] = data[src_offset + 0]; + v[1] = data[src_offset + 1]; + v[2] = data[src_offset + 2]; + v[3] = data[src_offset + 3]; + if (i == reg_hp) { + v[0] *= hp; + v[1] *= hp; + v[2] *= hp; + v[3] *= hp; + } + if (memcmp(g_vsh_constants.c[i], v, sizeof v) != 0) { + memcpy(g_vsh_constants.c[i], v, sizeof v); + changed = 1; } } - g_vsh_constants_dirty = TRUE; + if (changed) + g_vsh_constants_dirty = TRUE; + else + g_vsh_const_same++; } HRESULT d3d8_vsh_set_declaration(DWORD handle, const D3D8VshInput *inputs, int count) @@ -1892,6 +1963,33 @@ int d3d8_vsh_wbuffer_on(void) return on; } +/* Whether a W-buffered draw's depth is written per pixel + * (RECOMP_D3D8_WDEPTH, on unless 0). + * + * The W the NV2A keeps is exact at every pixel; the host rasterises the + * depth the vertex stage gives it linearly in screen space, where w is not + * linear but 1/w is. Across a triangle that overstates w -- the more, the + * bigger the triangle on screen -- so two surfaces in one plane cut into + * different triangles get different depths and cut through each other. + * Forza Motorsport's billboards are a board and its advert in one plane: + * the white board came through the advert in wedges along the triangles' + * edges, changing with every move of the camera. The pixel stage now + * writes the exact w (an attribute interpolated perspective-correct), + * minus the overstatement, from the rasterised depth, so a depth bias the + * draw has is kept. Plain SV_Depth: SV_DepthLessEqual (never more than the + * rasterised depth, which holds) wants the position read centroid, and gains + * nothing under the LESS tests the titles draw with. */ +int d3d8_vsh_wdepth_on(void) +{ + static int on = -1; + + if (on < 0) { + const char *e = getenv("RECOMP_D3D8_WDEPTH"); + on = !(e && e[0] == '0'); + } + return on; +} + void d3d8_vsh_get_screenspace(float scale[4], float offset[4], int *enabled) { if (scale) @@ -1994,10 +2092,27 @@ BOOL d3d8_vsh_prepare_draw(DWORD handle) const DWORD *rs = d3d8_GetRenderStates(); float w = (rs && rs[D3DRS_ZENABLE] == 2u /* D3DZB_USEW */ && d3d8_vsh_wbuffer_on()) ? 1.0f : 0.0f; - if (g_vsh_screen.enable[1] != w) { + /* enable.y = 2: the pixel stage writes the W depth per pixel, and + * enable.w is the viewport's depth span it is written in. */ + float span = d3d8_GetViewportDepthSpan(); + if (w != 0.0f && d3d8_vsh_wdepth_on()) + w = 2.0f; + if (g_vsh_screen.enable[1] != w || g_vsh_screen.enable[3] != span) { g_vsh_screen.enable[1] = w; + g_vsh_screen.enable[3] = span; g_vsh_screen_dirty = TRUE; } + /* enable.z: the draw tests depth. The near-plane clip distance + * applies only then: with Z off the NV2A ignores the depth a program + * writes, and Forza's minimap track line comes out at z = -8e6 in + * some frames, which the clip removed. */ + { + float zt = (rs && rs[D3DRS_ZENABLE] != 0u) ? 1.0f : 0.0f; + if (g_vsh_screen.enable[2] != zt) { + g_vsh_screen.enable[2] = zt; + g_vsh_screen_dirty = TRUE; + } + } } /* The microcode's hash, taken when the program was loaded */ @@ -2078,11 +2193,25 @@ BOOL d3d8_vsh_prepare_draw(DWORD handle) } } rhi_buffer_unmap(g_vsh_cb); + g_vsh_const_uploads++; } g_vsh_constants_dirty = FALSE; zoomed = want; widened = widen; } + { + static DWORD report_at; + DWORD now = GetTickCount(); + + if (!report_at) + report_at = now; + else if (now - report_at >= 5000) { + fprintf(stderr, "[VS-CB] %lu uploads, %lu sets already stored\n", + g_vsh_const_uploads, g_vsh_const_same); + g_vsh_const_uploads = g_vsh_const_same = 0; + report_at = now; + } + } } /* Bind constant buffer to slot b1 */ diff --git a/src/d3d/d3d8_xbox.h b/src/d3d/d3d8_xbox.h index 70873f80f..7ce25e3d1 100644 --- a/src/d3d/d3d8_xbox.h +++ b/src/d3d/d3d8_xbox.h @@ -415,6 +415,16 @@ typedef enum D3DRENDERSTATETYPE { * with the texture coordinate's r/q (a shadow map). A D3DCMPFUNC; 0 * while the title has not set it. */ D3DRS_SHADOWFUNC = 254, + /* Xbox polygon offset, forwarded as D3D11 depth bias. Slope and bias + * are float bits (NV097_SET_POLYGON_OFFSET_SCALE_FACTOR and + * NV097_SET_POLYGON_OFFSET_BIAS). The three enables select which fill + * mode the offset applies to. Host-only numbers, past the last Xbox + * state the combiners read. */ + D3DRS_POLY_SLOPE = 255, + D3DRS_POLY_BIAS = 256, + D3DRS_POLY_POINT = 257, + D3DRS_POLY_LINE = 258, + D3DRS_POLY_FILL = 259, } D3DRENDERSTATETYPE; #define D3D8_PSTEXTUREMODES_XBOX 0x80000000u @@ -450,6 +460,7 @@ typedef enum D3DTEXTURESTAGESTATETYPE { D3DTSS_COLORARG0 = 26, D3DTSS_ALPHAARG0 = 27, D3DTSS_RESULTARG = 28, + D3DTSS_ADDRESSW = 29, /* host extension: depth-axis addressing */ } D3DTEXTURESTAGESTATETYPE; typedef enum D3DTEXTUREOP { @@ -1350,6 +1361,16 @@ void xbox_D3D8DepthCopyShutdown(void); waits to be shown; Present shows it without counting a frame; End puts the scene target back. FALSE from Begin: no second image, draw nothing. */ BOOL xbox_D3D8InterpBegin(void); +/* Paced presents (d3d8_device.c): on, the next in-between's due moment + * (QueryPerformanceCounter), and the five-second report. */ +BOOL xbox_D3D8PacedOn(void); +void xbox_D3D8PacedDue(long long qpc); +void xbox_D3D8PacedReport(void); + +/* A Clear flag beside the PC's D3DCLEAR_*: only some colour channels are + * cleared, the ones in bits 12-15 (R 0x1000, G 0x2000, B 0x4000, A 0x8000), + * as the Xbox's D3DCLEAR_TARGET_R/G/B/A ask (hle d3d8_xbox_map.h). */ +#define D3D8_CLEAR_CHANNELS 0x00010000u void xbox_D3D8InterpPresent(void); void xbox_D3D8InterpEnd(void); diff --git a/src/d3d/dxt5_decode.h b/src/d3d/dxt5_decode.h new file mode 100644 index 000000000..21a2d01aa --- /dev/null +++ b/src/d3d/dxt5_decode.h @@ -0,0 +1,98 @@ +#ifndef XBR_DXT5_DECODE_H +#define XBR_DXT5_DECODE_H +#include +#include +#include + +static void xbr_decode_dxt5_rgba(const uint8_t *src, unsigned pitch, + unsigned width, unsigned height, uint8_t *dst) +{ + for (unsigned by = 0; by < (height + 3) / 4; ++by) { + for (unsigned bx = 0; bx < (width + 3) / 4; ++bx) { + const uint8_t *b = src + by * pitch + bx * 16; + unsigned alpha[8] = {b[0], b[1]}; + uint64_t ai = 0; + uint32_t ci = 0; + unsigned rgb[4][3]; + for (unsigned i = 0; i < 6; ++i) ai |= (uint64_t)b[2+i] << (8*i); + for (unsigned i = 0; i < 4; ++i) ci |= (uint32_t)b[12+i] << (8*i); + if (alpha[0] > alpha[1]) { + for (unsigned i = 1; i <= 6; ++i) + alpha[i+1] = ((7-i)*alpha[0] + i*alpha[1]) / 7; + } else { + for (unsigned i = 1; i <= 4; ++i) + alpha[i+1] = ((5-i)*alpha[0] + i*alpha[1]) / 5; + alpha[6] = 0; alpha[7] = 255; + } + for (unsigned i = 0; i < 2; ++i) { + unsigned c = b[8+i*2] | ((unsigned)b[9+i*2] << 8); + unsigned r = (c >> 11) & 31, g = (c >> 5) & 63, bl = c & 31; + rgb[i][0] = (r << 3) | (r >> 2); + rgb[i][1] = (g << 2) | (g >> 4); + rgb[i][2] = (bl << 3) | (bl >> 2); + } + for (unsigned c = 0; c < 3; ++c) { + rgb[2][c] = (2*rgb[0][c] + rgb[1][c]) / 3; + rgb[3][c] = (rgb[0][c] + 2*rgb[1][c]) / 3; + } + for (unsigned y = 0; y < 4; ++y) for (unsigned x = 0; x < 4; ++x) { + unsigned p = y*4+x, dx = bx*4+x, dy = by*4+y; + if (dx >= width || dy >= height) continue; + uint8_t *out = dst + ((size_t)dy*width+dx)*4; + unsigned color = (ci >> (p*2)) & 3; + for (unsigned c = 0; c < 3; ++c) out[c] = (uint8_t)rgb[color][c]; + out[3] = (uint8_t)alpha[(ai >> (p*3)) & 7]; + } + } + } +} +static void xbr_decode_dxt1_rgba(const uint8_t *src, unsigned pitch, + unsigned width, unsigned height, uint8_t *dst) +{ + for (unsigned by=0;by<(height+3)/4;++by) for(unsigned bx=0;bx<(width+3)/4;++bx) { + const uint8_t *b=src+(size_t)by*pitch+bx*8; + unsigned colors[4][4]={{0}}, endpoints[2]; + uint32_t indices=0; + for(unsigned i=0;i<4;++i) indices|=(uint32_t)b[4+i]<<(8*i); + for(unsigned i=0;i<2;++i) { + unsigned c=b[i*2]|((unsigned)b[i*2+1]<<8); + endpoints[i]=c; + unsigned r=(c>>11)&31,g=(c>>5)&63,bl=c&31; + colors[i][0]=(r<<3)|(r>>2);colors[i][1]=(g<<2)|(g>>4); + colors[i][2]=(bl<<3)|(bl>>2);colors[i][3]=255; + } + for(unsigned c=0;c<3;++c) { + if(endpoints[0]>endpoints[1]) { + colors[2][c]=(2*colors[0][c]+colors[1][c])/3; + colors[3][c]=(colors[0][c]+2*colors[1][c])/3; + } else colors[2][c]=(colors[0][c]+colors[1][c])/2; + } + colors[2][3]=255;colors[3][3]=endpoints[0]>endpoints[1]?255:0; + for(unsigned y=0;y<4;++y) for(unsigned x=0;x<4;++x) { + unsigned dx=bx*4+x,dy=by*4+y; + if(dx>=width||dy>=height) continue; + unsigned index=(indices>>(2*(y*4+x)))&3; + for(unsigned c=0;c<4;++c) dst[((size_t)dy*width+dx)*4+c]=(uint8_t)colors[index][c]; + } + } +} +/* Xbox compressed volumes store groups of up to four depth slices per + * 4x4 XY block, rather than a whole 2D image per depth slice. */ +static void xbr_decode_dxt_volume_slice(const uint8_t *src, unsigned width, + unsigned height, unsigned depth, unsigned z, int bc1, uint8_t *dst) +{ + unsigned nx=(width+3)/4,ny=(height+3)/4; + unsigned group=z&~3u,group_depth=depth-group; + if(group_depth>4)group_depth=4; + unsigned block_bytes=bc1?8:16; + for(unsigned by=0;by4)count=4; + for(unsigned y=0;y<4 && by*4+yres) + return -1; + present_lock(); + if (SUCCEEDED(IDXGISwapChain_GetBuffer(g_swap, 0, &IID_ID3D11Texture2D, (void **)&bb))) { + D3D11_TEXTURE2D_DESC a, b; + ID3D11Texture2D_GetDesc(bb, &a); + ID3D11Texture2D_GetDesc((ID3D11Texture2D *)img->res, &b); + if (a.Width == b.Width && a.Height == b.Height && a.Format == b.Format) + ID3D11DeviceContext_CopyResource(g_ctx, (ID3D11Resource *)bb, img->res); + ID3D11Texture2D_Release(bb); + rc = (int)IDXGISwapChain_Present(g_swap, interval, flags); + } + present_unlock(); + return rc; +} + static int d_device_create(const RhiDeviceDesc *dd) { + if (!g_present_cs_ready) { + InitializeCriticalSection(&g_present_cs); + g_present_cs_ready = 1; + } + { static const struct { DXGI_SWAP_EFFECT effect; UINT flags; const char *name; } tries[] = { { DXGI_SWAP_EFFECT_FLIP_DISCARD, D3D11_SC_FLAG_ALLOW_TEARING, "flip model, tearing allowed (variable refresh possible)" }, @@ -351,13 +453,21 @@ static int d_device_create(const RhiDeviceDesc *dd) g_swap = NULL; g_dev = NULL; g_ctx = NULL; + bind_cache_clear(); return -1; } if (make_back_buffer_view() != 0) { fprintf(stderr, "D3D8: the swap chain's back buffer has no render-target view\n"); return -1; } + { + const char *e = getenv("RECOMP_PACED_PRESENT"); + if (e && e[0] == '1') + make_context_thread_safe(); + } + bind_cache_clear(); return 0; + } } static void d_device_destroy(void) @@ -366,6 +476,7 @@ static void d_device_destroy(void) release_back_buffer_view(); if (g_swap) { IDXGISwapChain_Release(g_swap); g_swap = NULL; } if (g_ctx) { ID3D11DeviceContext_Release(g_ctx); g_ctx = NULL; } + bind_cache_clear(); if (g_dev) { ID3D11Device_Release(g_dev); g_dev = NULL; } } @@ -387,16 +498,20 @@ static int d_swapchain_resize(uint32_t w, uint32_t h) if (!g_swap) return -1; + present_lock(); /* ResizeBuffers wants every reference to the old back buffer gone, * including any the context still holds. */ ID3D11DeviceContext_OMGetRenderTargets(g_ctx, 1, &saved_rtv, &saved_dsv); ID3D11DeviceContext_OMSetRenderTargets(g_ctx, 0, NULL, NULL); + g_bind.srv_ok = 0; release_back_buffer_view(); if (SUCCEEDED(IDXGISwapChain_ResizeBuffers(g_swap, 0, w, h, DXGI_FORMAT_UNKNOWN, g_swap_flags))) ok = make_back_buffer_view(); ID3D11DeviceContext_OMSetRenderTargets(g_ctx, 1, &saved_rtv, saved_dsv); + g_bind.srv_ok = 0; if (saved_rtv) ID3D11RenderTargetView_Release(saved_rtv); if (saved_dsv) ID3D11DepthStencilView_Release(saved_dsv); + present_unlock(); return ok; } @@ -426,7 +541,14 @@ static int32_t d_present(uint32_t interval) { UINT flags = (interval == 0 && g_vrr && g_tearing) ? DXGI_PRESENT_ALLOW_TEARING : 0; - return g_swap ? (int32_t)IDXGISwapChain_Present(g_swap, interval, flags) : -1; + int32_t rc; + + if (!g_swap) + return -1; + present_lock(); + rc = (int32_t)IDXGISwapChain_Present(g_swap, interval, flags); + present_unlock(); + return rc; } static int d_image_readback(RhiImage *img, uint32_t sub, void *dst, uint32_t dst_pitch); @@ -1117,6 +1239,9 @@ static void d_set_render_target(RhiView *color, RhiView *depth) ID3D11RenderTargetView *rtv = color ? (ID3D11RenderTargetView *)color->v : NULL; ID3D11DeviceContext_OMSetRenderTargets(g_ctx, 1, &rtv, depth ? (ID3D11DepthStencilView *)depth->v : NULL); + /* Binding a target drops any shader view of that resource. The next + * SetTextures has to reach the context even if the pointer matches. */ + g_bind.srv_ok = 0; } static void d_output_save(RhiOutputState *s) @@ -1164,6 +1289,7 @@ static void d_output_restore(RhiOutputState *s) ID3D11DepthStencilView *dsv = (ID3D11DepthStencilView *)s->depth; ID3D11DeviceContext_OMSetRenderTargets(g_ctx, 1, &rtv, dsv); + g_bind.srv_ok = 0; if (s->viewport_count) d_set_viewports(s->viewport_count, s->viewports); if (rtv) ID3D11RenderTargetView_Release(rtv); @@ -1188,85 +1314,196 @@ static void d_set_scissor(const RhiRect *r) static void d_set_topology(uint32_t t) { + if (g_bind.topo_ok && g_bind.topo == t) + return; + g_bind.topo = t; + g_bind.topo_ok = 1; ID3D11DeviceContext_IASetPrimitiveTopology(g_ctx, (D3D11_PRIMITIVE_TOPOLOGY)t); } static void d_set_vertex_layout(RhiVertexLayout *l) { - ID3D11DeviceContext_IASetInputLayout(g_ctx, l ? l->il : NULL); + ID3D11InputLayout *il = l ? l->il : NULL; + if (g_bind.layout_ok && g_bind.layout == il) + return; + g_bind.layout = il; + g_bind.layout_ok = 1; + ID3D11DeviceContext_IASetInputLayout(g_ctx, il); } static void d_set_vertex_buffer(uint32_t slot, RhiBuffer *b, uint32_t stride, uint32_t offset) { ID3D11Buffer *nb = b ? b->b : NULL; UINT st = stride, off = offset; + if (slot == 0 && g_bind.vb_ok && g_bind.vb == nb && + g_bind.vb_stride == st && g_bind.vb_off == off) + return; ID3D11DeviceContext_IASetVertexBuffers(g_ctx, slot, 1, &nb, &st, &off); + if (slot == 0) { + g_bind.vb = nb; + g_bind.vb_stride = st; + g_bind.vb_off = off; + g_bind.vb_ok = 1; + } else { + g_bind.vb_ok = 0; + } } static void d_set_index_buffer(RhiBuffer *b, uint32_t bits, uint32_t offset) { - ID3D11DeviceContext_IASetIndexBuffer(g_ctx, b ? b->b : NULL, - bits == 32 ? DXGI_FORMAT_R32_UINT : DXGI_FORMAT_R16_UINT, - offset); + ID3D11Buffer *nb = b ? b->b : NULL; + DXGI_FORMAT fmt = bits == 32 ? DXGI_FORMAT_R32_UINT : DXGI_FORMAT_R16_UINT; + if (g_bind.ib_ok && g_bind.ib == nb && g_bind.ib_fmt == fmt && g_bind.ib_off == offset) + return; + g_bind.ib = nb; + g_bind.ib_fmt = fmt; + g_bind.ib_off = offset; + g_bind.ib_ok = 1; + ID3D11DeviceContext_IASetIndexBuffer(g_ctx, nb, fmt, offset); } static void d_set_shader(uint32_t stage, RhiShader *s) { - if (stage == RHI_STAGE_VERTEX) - ID3D11DeviceContext_VSSetShader(g_ctx, s ? (ID3D11VertexShader *)s->sh : NULL, NULL, 0); - else - ID3D11DeviceContext_PSSetShader(g_ctx, s ? (ID3D11PixelShader *)s->sh : NULL, NULL, 0); + if (stage == RHI_STAGE_VERTEX) { + ID3D11VertexShader *sh = s ? (ID3D11VertexShader *)s->sh : NULL; + if (g_bind.vs_ok && g_bind.vs == sh) + return; + g_bind.vs = sh; + g_bind.vs_ok = 1; + ID3D11DeviceContext_VSSetShader(g_ctx, sh, NULL, 0); + } else { + ID3D11PixelShader *sh = s ? (ID3D11PixelShader *)s->sh : NULL; + if (g_bind.ps_ok && g_bind.ps == sh) + return; + g_bind.ps = sh; + g_bind.ps_ok = 1; + ID3D11DeviceContext_PSSetShader(g_ctx, sh, NULL, 0); + } } static void d_set_uniform_buffers(uint32_t stage, uint32_t slot, uint32_t n, RhiBuffer *const *b) { ID3D11Buffer *nb[14]; - uint32_t i; + uint32_t i, si; + int same; if (n > 14) n = 14; for (i = 0; i < n; i++) nb[i] = b[i] ? b[i]->b : NULL; + si = stage == RHI_STAGE_VERTEX ? 0u : 1u; + same = slot + n <= 8u; + for (i = 0; same && i < n; i++) { + uint32_t k = slot + i; + if (!(g_bind.cb_ok[si] & (1u << k)) || g_bind.cb[si][k] != nb[i]) + same = 0; + } + if (same) + return; if (stage == RHI_STAGE_VERTEX) ID3D11DeviceContext_VSSetConstantBuffers(g_ctx, slot, n, nb); else ID3D11DeviceContext_PSSetConstantBuffers(g_ctx, slot, n, nb); + if (slot + n <= 8u) { + for (i = 0; i < n; i++) { + g_bind.cb[si][slot + i] = nb[i]; + g_bind.cb_ok[si] |= (uint8_t)(1u << (slot + i)); + } + } else { + g_bind.cb_ok[si] = 0; + } } static void d_set_textures(uint32_t slot, uint32_t n, RhiView *const *v) { ID3D11ShaderResourceView *srv[16]; uint32_t i; + int same; if (n > 16) n = 16; for (i = 0; i < n; i++) srv[i] = v[i] ? (ID3D11ShaderResourceView *)v[i]->v : NULL; + same = slot + n <= 8u; + for (i = 0; same && i < n; i++) { + uint32_t k = slot + i; + if (!(g_bind.srv_ok & (1u << k)) || g_bind.srv[k] != srv[i]) + same = 0; + } + if (same) + return; ID3D11DeviceContext_PSSetShaderResources(g_ctx, slot, n, srv); + if (slot + n <= 8u) { + for (i = 0; i < n; i++) { + g_bind.srv[slot + i] = srv[i]; + g_bind.srv_ok |= (uint8_t)(1u << (slot + i)); + } + } else { + g_bind.srv_ok = 0; + } } static void d_set_samplers(uint32_t slot, uint32_t n, RhiSampler *const *s) { ID3D11SamplerState *ns[16]; uint32_t i; + int same; if (n > 16) n = 16; for (i = 0; i < n; i++) ns[i] = s[i] ? s[i]->s : NULL; + same = slot + n <= 8u; + for (i = 0; same && i < n; i++) { + uint32_t k = slot + i; + if (!(g_bind.samp_ok & (1u << k)) || g_bind.samp[k] != ns[i]) + same = 0; + } + if (same) + return; ID3D11DeviceContext_PSSetSamplers(g_ctx, slot, n, ns); + if (slot + n <= 8u) { + for (i = 0; i < n; i++) { + g_bind.samp[slot + i] = ns[i]; + g_bind.samp_ok |= (uint8_t)(1u << (slot + i)); + } + } else { + g_bind.samp_ok = 0; + } } static void d_set_blend_state(RhiBlendState *s, const float *factor, uint32_t mask) { - ID3D11DeviceContext_OMSetBlendState(g_ctx, s ? s->s : NULL, factor, mask); + ID3D11BlendState *st = s ? s->s : NULL; + if (g_bind.blend_ok && g_bind.blend == st && g_bind.mask == mask && + factor && memcmp(g_bind.factor, factor, sizeof g_bind.factor) == 0) + return; + g_bind.blend = st; + g_bind.mask = mask; + g_bind.blend_ok = 1; + if (factor) + memcpy(g_bind.factor, factor, sizeof g_bind.factor); + else + memset(g_bind.factor, 0, sizeof g_bind.factor); + ID3D11DeviceContext_OMSetBlendState(g_ctx, st, factor, mask); } static void d_set_depth_state(RhiDepthState *s, uint32_t ref) { - ID3D11DeviceContext_OMSetDepthStencilState(g_ctx, s ? s->s : NULL, ref); + ID3D11DepthStencilState *st = s ? s->s : NULL; + if (g_bind.depth_ok && g_bind.depth == st && g_bind.ref == ref) + return; + g_bind.depth = st; + g_bind.ref = ref; + g_bind.depth_ok = 1; + ID3D11DeviceContext_OMSetDepthStencilState(g_ctx, st, ref); } static void d_set_raster_state(RhiRasterState *s) { - ID3D11DeviceContext_RSSetState(g_ctx, s ? s->s : NULL); + ID3D11RasterizerState *st = s ? s->s : NULL; + if (g_bind.raster_ok && g_bind.raster == st) + return; + g_bind.raster = st; + g_bind.raster_ok = 1; + ID3D11DeviceContext_RSSetState(g_ctx, st); } static void d_draw(uint32_t n, uint32_t first) diff --git a/src/hle/CMakeLists.txt b/src/hle/CMakeLists.txt index 7742ba202..b5d3d80d3 100644 --- a/src/hle/CMakeLists.txt +++ b/src/hle/CMakeLists.txt @@ -11,6 +11,11 @@ if(MSVC) # /Zi so the PDB names this library's static functions in a sampled # profile (RECOMP_SAMPLE); see src/kernel/CMakeLists.txt. add_compile_options(/Zi) +elseif(WIN32) + # The same for the clang driver, which is not MSVC: without CodeView + # info a profile credits shadow_expand_vertices, shadow_wndproc and every + # other static in hle_d3d8.c to hle_d3d8_shadow_swaps. + add_compile_options(-g -gcodeview) endif() add_library(xbox_hle STATIC diff --git a/src/hle/audio_output_xaudio2.cpp b/src/hle/audio_output_xaudio2.cpp index 7c398fef5..a3c47feda 100644 --- a/src/hle/audio_output_xaudio2.cpp +++ b/src/hle/audio_output_xaudio2.cpp @@ -73,8 +73,14 @@ struct Submix { }; constexpr uint32_t kSubmixCount = 32; -/* Voices may play up to this much faster than they are submitted. */ -constexpr float kMaxFrequencyRatio = 8.0f; +/* The source voice is created at the buffer's original rate. Pitch and + * Doppler are a frequency ratio from that, and the output clock + * (hle_dsound.c) submits at the pitched rate. XAudio2 accepts + * 1/XAUDIO2_MAX_FREQ_RATIO .. XAUDIO2_MAX_FREQ_RATIO. A cap of 8 played a + * 22050 Hz buffer no slower than 2756 Hz, while Forza's idle loop is clocked + * at 1000 Hz, so the voice consumed the loop about three times as fast as + * the clock submitted it and the slot ran dry every few pumps. */ +constexpr float kMaxFrequencyRatio = XAUDIO2_MAX_FREQ_RATIO; std::atomic critical_error{S_OK}; @@ -804,6 +810,10 @@ extern "C" int recomp_audio_output_submit( if (voice.fed && state.BuffersQueued == 0) { ++voice.starved; ++starved_total; + /* A source voice that runs out of buffers stops, and the next + * SubmitSourceBuffer stays queued until Start. Without this the + * looping voice is silent from the first underrun on. */ + voice.source->Start(); } { const unsigned long long now = GetTickCount64(); diff --git a/src/hle/d3d8_capture.c b/src/hle/d3d8_capture.c index 62a46b104..25cc677aa 100644 --- a/src/hle/d3d8_capture.c +++ b/src/hle/d3d8_capture.c @@ -75,6 +75,8 @@ static int cap_put(D3D8CapWriter *w, const void *p, size_t n) return 0; } +static volatile char g_cap_lock; + int d3d8cap_chunk(D3D8CapWriter *w, uint32_t type, const void *p0, size_t n0, const void *p1, size_t n1, @@ -94,11 +96,20 @@ int d3d8cap_chunk(D3D8CapWriter *w, uint32_t type, } head[0] = type; head[1] = (uint32_t)total; + /* One chunk at a time: Forza's background loader uploads textures on a + * thread of its own while the frame is being written, and two chunks + * written into each other left a capture nothing could read past the + * first car. */ + while (__atomic_test_and_set(&g_cap_lock, __ATOMIC_ACQUIRE)) + ; if (cap_put(w, head, sizeof head) || cap_put(w, p0, n0) || cap_put(w, p1, n1) || cap_put(w, p2, n2) || - cap_put(w, zeros, cap_pad(total))) + cap_put(w, zeros, cap_pad(total))) { + __atomic_clear(&g_cap_lock, __ATOMIC_RELEASE); return -1; + } w->chunks++; + __atomic_clear(&g_cap_lock, __ATOMIC_RELEASE); return 0; } @@ -277,7 +288,7 @@ const char *d3d8cap_chunk_name(uint32_t type) "vs_screenspace", "ps_token", "depth_surface", "set_render_target", "vs_vertex_data", "cube_texture", "scissors", "material", "light", "light_enable", "two_d_placement", "cube_level", "palette", "screen_copy", "scene", - "depth_copy" + "depth_copy", "volume_texture", "depth_alias" }; return type < D3D8CAP_CHUNK_KINDS ? names[type] : "unknown"; diff --git a/src/hle/d3d8_capture.h b/src/hle/d3d8_capture.h index b10b28b04..7593b5a25 100644 --- a/src/hle/d3d8_capture.h +++ b/src/hle/d3d8_capture.h @@ -78,8 +78,8 @@ * the host's system-memory copy, which rendering never writes, so a * target drawn in an earlier frame and only sampled in this one replays * as zeros. - * - volume textures, which shadow mode does not create. One bound on a - * stage would be recorded as nothing bound. + * - volume formats other than expanded linear RGBA. Version 11 records + * expanded RGBA volumes and all their mip slices. * - the contents of a cube a title renders into (they live on the GPU and * are drawn again on replay). A cube filled from memory has its texels * recorded (CUBE_LEVEL, version 7). @@ -109,7 +109,7 @@ extern "C" { * version 8 the screen copies (a title reading its own frame back into a * texture), version 9 the scene's pixels as the frame begins, version 10 * the depth copies (a title reading its depth buffer as a colour texture). */ -#define D3D8CAP_VERSION 10u +#define D3D8CAP_VERSION 12u /* The oldest version the reader still accepts. A bump that only adds chunk * kinds leaves an older file a valid newer one that happens not to contain * them (version 6 added three; version 5 one), so it leaves this alone. A @@ -159,7 +159,9 @@ enum { D3D8CAP_SCREEN_COPY = 31, /* D3D8CapScreenCopy (version 8) */ D3D8CAP_SCENE = 32, /* D3D8CapScene + rows (version 9) */ D3D8CAP_DEPTH_COPY = 33, /* D3D8CapScreenCopy, has_rect 0 (version 10) */ - D3D8CAP_CHUNK_KINDS = 34 /* one past the last, for per-kind counters */ + D3D8CAP_VOLUME_TEXTURE = 34, /* D3D8CapVolumeTexture + packed levels */ + D3D8CAP_DEPTH_ALIAS = 35, /* two uint32 words: depth id, texture id */ + D3D8CAP_CHUNK_KINDS = 36 /* one past the last, for per-kind counters */ }; typedef struct { @@ -235,6 +237,10 @@ typedef struct { typedef struct { uint32_t id, format, width, height, levels, usage; } D3D8CapTexture; +typedef struct { + uint32_t id, format, width, height, depth, levels, usage; +} D3D8CapVolumeTexture; + typedef struct { uint32_t pitch, rows, bytes; } D3D8CapLevel; /* One level filled again: shadow mode locked and unlocked a texture the diff --git a/src/hle/d3d8_xbox_map.h b/src/hle/d3d8_xbox_map.h index d0e32158d..cb1fa330e 100644 --- a/src/hle/d3d8_xbox_map.h +++ b/src/hle/d3d8_xbox_map.h @@ -35,6 +35,13 @@ static __inline DWORD xbox_clear_flags_to_host(uint32_t xbox_flags) if (xbox_flags & 0xF0) host |= D3DCLEAR_TARGET; + /* Some of the colour channels only (D3DCLEAR_TARGET_R 0x10, G 0x20, + * B 0x40, A 0x80): passed on as D3D8_CLEAR_CHANNELS (d3d8_xbox.h) so the + * host clears just those. Forza's car selection clears the alpha + * channel alone (0x83) over its showroom backdrop; cleared whole, the + * backdrop went black behind the car. */ + if ((xbox_flags & 0xF0) && (xbox_flags & 0xF0) != 0xF0) + host |= D3D8_CLEAR_CHANNELS | ((xbox_flags & 0xF0u) << 8); if (xbox_flags & 0x01) host |= D3DCLEAR_ZBUFFER; if (xbox_flags & 0x02) diff --git a/src/hle/hle_d3d8.c b/src/hle/hle_d3d8.c index fe47c3d35..90818c386 100644 --- a/src/hle/hle_d3d8.c +++ b/src/hle/hle_d3d8.c @@ -78,6 +78,7 @@ #include "rhi.h" /* rhi_set_wait_hooks */ #include "input_bindings.h" /* the mouse, for mouse look and mouse: bindings */ #include "../kernel/xbox_memory_layout.h" /* the guest lock */ +#include /* CRC32C, ec_hash */ void hle_d3d8_push_constants_sync(void); /* hle_d3d8_vertex.c */ int hle_d3d8_pb_walking(void); /* hle_d3d8_vertex.c */ @@ -85,6 +86,16 @@ void hle_d3d8_apply_pending_textures(void); /* hle_d3d8_texture.c */ void hle_d3d8_apply_pending_constants(void); /* hle_d3d8_vertex.c */ void hle_d3d8_push_skip(void); int hle_d3d8_pb_inline_on(void); +/* d3d8_device.c: a converted mesh that stays put is uploaded once. */ +void d3d8_stable_vertex_note(const void *ptr, unsigned long long stamp); +void d3d8_stable_vertex_drop(const void *ptr); +void d3d8_stable_vertex_report(void); +int d3d8_stable_index_prepare(const void *guest, unsigned index_count, unsigned topology, + unsigned long frame, unsigned expect_bytes, + unsigned *min_index, unsigned *vertex_span, + unsigned long long *stamp); +int d3d8_stable_index_commit(const void *bytes, unsigned size); +void d3d8_stable_index_disarm(void); /* Every replacement starts here. Beyond the one-time log line, it brings the * push-buffer walk up to date first (hle_d3d8_vertex.c): what the title wrote @@ -236,12 +247,16 @@ static int movie_layer_mode(void) return mode; } -/* After a draw reached the host. */ +/* After a draw reached the host. With deferred frames the stages are only + * bound when the queue runs, so the draw wrappers' call comes too early and + * the queued draw reports itself instead (hle_d3d8_note_deferred_draw): + * judged at recording, Forza Motorsport's menu movie counted as unsampled + * and the movie layer covered the whole menu. */ static void note_draw_sampled_movie(void) { int s; - if (!g_movie_phys) + if (!g_movie_phys || hle_d3d8_defer_recording()) return; for (s = 0; s < 4; s++) if (g_stage_texels[s] == g_movie_phys) @@ -249,6 +264,11 @@ static void note_draw_sampled_movie(void) } static uint32_t g_shadow_last_color; +void hle_d3d8_note_deferred_draw(void) +{ + note_draw_sampled_movie(); +} + /* RECOMP_HLE_D3D8_TRACE_SWAPS=-: one line per render-target set, * clear, back-buffer query, copy and swap while the swap count is in that * range, so the order of a title's frame can be read rather than inferred @@ -1595,7 +1615,7 @@ static unsigned long g_draws_off_thread; * after the title's own body ran: the return addresses are still there. */ static void trace_draw_callers(void) { - static long want = -2; + static long want = -2, want_host = -1; static int shown; static uint32_t seen[16]; extern uint32_t g_xbox_code_lo, g_xbox_code_hi; @@ -1605,8 +1625,13 @@ static void trace_draw_callers(void) if (want == -2) { const char *e = getenv("RECOMP_HLE_D3D8_TRACE_DRAW"); want = e && *e ? (long)strtoul(e, NULL, 0) : -1; + e = getenv("RECOMP_HLE_D3D8_TRACE_HOST_DRAW"); + want_host = e && *e ? (long)strtoul(e, NULL, 0) : -1; } - if (want < 0 || (uint32_t)want != g_shadow_vs || shown >= 16) + if (shown >= 16 || + !((want >= 0 && (uint32_t)want == g_shadow_vs) || + (want_host >= 0 && g_shadow_vs_slot >= 0 && + g_programs[g_shadow_vs_slot].host == (DWORD)want_host))) return; for (i = -8; i < 120 && n < 6; i++) { v = HLE_MEM32(g_esp + 4u * (uint32_t)i); @@ -1900,6 +1925,15 @@ static int ff_from_declaration(uint32_t handle, uint32_t stride) static UINT g_draw_overread; static unsigned long g_draws_overread; +/* Read-only correlation between a title material and host draw commands. */ +static uint32_t g_material_trace_object, g_material_trace_pass, g_material_trace_type; +static int g_material_trace_active; +void hle_d3d8_material_trace_context(uint32_t object,uint32_t pass,uint32_t type,int active) +{ + g_material_trace_object=object;g_material_trace_pass=pass; + g_material_trace_type=type;g_material_trace_active=active; +} + static int shadow_can_draw(uint32_t xpt, uint32_t stride) { if (g_cpu_bb_locked) @@ -2023,6 +2057,11 @@ static int shadow_can_draw(uint32_t xpt, uint32_t stride) * from its own state arrays (hle_d3d8_state.c). */ hle_d3d8_shadow_apply_states(g_shadow); hle_d3d8_sync_palettes(g_shadow); + if(g_material_trace_active) + fprintf(stderr,"[MATDRAW] swap %lu draw %lu object %08X pass %u type %u hostvs %08X stride %u screen %d target %ux%u\n", + g_shadow_swaps,g_frame_draws,g_material_trace_object,g_material_trace_pass,g_material_trace_type, + g_shadow_vs_slot>=0?(unsigned)g_programs[g_shadow_vs_slot].host:0u,stride, + g_target_is_screen,g_target_width,g_target_height); g_frame_draws++; if (g_target_is_screen) g_screen_changes++; @@ -2058,9 +2097,22 @@ void hle_d3d8_beside_exe(char *out, size_t bytes, const char *name) } } +/* RECOMP_F11_SERIES=: F11 writes the next n frames, one a swap, and + * the next n in-between frames (RECOMP_INTERP_DUMP), each named by its + * swap -- the two side by side show what an in-between draws differently + * from the frames either side of it (flicker). */ +static int g_dump_series; + static void shadow_dump_next_frame(void) { + const char *e = getenv("RECOMP_F11_SERIES"); + int n = e ? atoi(e) : 0; + g_dump_requested = 1; + if (n > 0) { + g_dump_series = n; + hle_d3d8_interp_dump_now(n); + } } /* RECOMP_HLE_D3D8_BRIGHT=: every n swaps, say how bright the finished frame @@ -2201,9 +2253,16 @@ static void ram_dump_at_swap(void) static int parsed; static unsigned long want[16]; static int nwant; - const char *prefix = getenv("RECOMP_RAM_DUMP"); + static const char *prefix; + static int looked; int i; + /* Once: getenv walks the whole environment, and this runs every Swap. */ + if (!looked) { + prefix = getenv("RECOMP_RAM_DUMP"); + looked = 1; + } + if (!prefix || !*prefix) return; if (!parsed) { @@ -2287,6 +2346,10 @@ static void shadow_dump_frame(void) * no prefix was given -- so pressing the key is the whole procedure. */ asked = g_dump_requested; g_dump_requested = 0; + if (g_dump_series > 0) { + asked = 2; /* one of an F11 series */ + g_dump_series--; + } if (asked && (!prefix || !*prefix)) { hle_d3d8_beside_exe(asked_prefix, sizeof asked_prefix, "frame"); prefix = asked_prefix; @@ -2314,6 +2377,8 @@ static void shadow_dump_frame(void) snprintf(path, sizeof path, "%s_s%08lu.bmp", prefix, g_shadow_swaps); snprintf(slot, sizeof ring[0], "%s", path); written++; + } else if (asked == 2) { + snprintf(path, sizeof path, "%s_n%05lu.bmp", prefix, g_shadow_swaps); } else { snprintf(path, sizeof path, "%s%03d.bmp", prefix, written++); } @@ -2694,6 +2759,23 @@ HLE_EXPORT(D3DDevice_Clear) hle_d3d8_push_skip(); /* its own clear methods: cleared here, not by the walk */ if (g_shadow) { float z; + static int keep_depth = -1; + + /* RECOMP_HLE_D3D8_KEEP_DEPTH_ON_ZCLEAR=1: a clear of the screen's + * depth and stencil alone (no colour) clears the stencil only. + * Forza Motorsport draws its road and the road's light, then + * clears depth and stencil and draws the rest of the scene with + * colour writes off for alpha; a far ground plane drawn after that + * clear covered the road in the light buffer the road's second pass + * multiplies by, and dark slabs lay across the road. Kept, the + * road's depth holds it back, as the console's picture shows. Why + * the console keeps it is not known; off by default. */ + if (keep_depth < 0) { + const char *e = getenv("RECOMP_HLE_D3D8_KEEP_DEPTH_ON_ZCLEAR"); + keep_depth = e && e[0] == '1'; + } + if (keep_depth && g_target_is_screen && !(flags & 0xF0u) && (flags & 0x01u)) + flags &= ~0x01u; memcpy(&z, &z_bits, sizeof z); if (g_shadow_clears < 4) @@ -2787,6 +2869,74 @@ static unsigned long g_swap_timed; static long long g_swap_flush_ticks, g_swap_present_ticks; static long long g_swap_flush_max, g_swap_present_max; +extern void d3d8_profile_report(unsigned long frames); +static void vertex_cache_report(void); +static void expand_cache_report(void); + +/* RECOMP_HLE_PEEK=:[,:...]: guest memory printed + * with every five-second report, sixteen dwords a line. For watching a + * title's own state change (a queue, a flag) while it runs, without + * stopping it. */ +static int hle_peek_readable(uint32_t va, uint32_t bytes) +{ + MEMORY_BASIC_INFORMATION mbi; + const char *p = (const char *)HLE_PTR(va); + if (!bytes) + return 1; + if (!VirtualQuery(p, &mbi, sizeof mbi) || mbi.State != MEM_COMMIT || + (mbi.Protect & (PAGE_NOACCESS | PAGE_GUARD))) + return 0; + if ((const char *)mbi.BaseAddress + mbi.RegionSize < p + bytes) + return hle_peek_readable(va + (uint32_t)((const char *)mbi.BaseAddress + mbi.RegionSize - p), + bytes - (uint32_t)((const char *)mbi.BaseAddress + mbi.RegionSize - p)); + return 1; +} + +static void hle_peek_report(void) +{ + static int parsed; + static uint32_t va[8], n[8], hop[8][4]; + static int nva, nhop[8]; + + if (!parsed) { + const char *e = getenv("RECOMP_HLE_PEEK"); + parsed = 1; + while (e && *e && nva < 8) { + char *end; + va[nva] = (uint32_t)strtoul(e, &end, 0); + nhop[nva] = 0; + /* addr@off@off...: follow a pointer at each '@' and add off. */ + while (*end == '@' && nhop[nva] < 4) + hop[nva][nhop[nva]++] = (uint32_t)strtoul(end + 1, &end, 0); + n[nva] = 16; + if (*end == ':') + n[nva] = (uint32_t)strtoul(end + 1, &end, 0); + if (n[nva] > 256) + n[nva] = 256; + nva++; + e = *end == ',' ? end + 1 : NULL; + } + } + for (int i = 0; i < nva; i++) { + uint32_t a = va[i]; + int ok = hle_peek_readable(a, 4); + for (int h = 0; ok && h < nhop[i]; h++) { + a = HLE_MEM32(a) + hop[i][h]; + ok = a >= 0x10000u && hle_peek_readable(a, 4); + } + if (!ok || !hle_peek_readable(a, n[i] * 4)) { + fprintf(stderr, "[PEEK] #%d: %08X not readable\n", i, a); + continue; + } + for (uint32_t k = 0; k < n[i]; k++) { + if (k % 16 == 0) + fprintf(stderr, "%s[PEEK] #%d %08X:", k ? "\n" : "", i, a + k * 4); + fprintf(stderr, " %08X", HLE_MEM32(a + k * 4)); + } + fprintf(stderr, "\n"); + } +} + static void swap_timing_report(void) { LARGE_INTEGER qpf; @@ -2794,6 +2944,7 @@ static void swap_timing_report(void) if (!g_swap_timed) return; + hle_peek_report(); QueryPerformanceFrequency(&qpf); ms = 1000.0 / (double)qpf.QuadPart / (double)g_swap_timed; fprintf(stderr, "[HLE-D3D8] swap timing over %lu frames: gate wait %.2f ms, " @@ -2806,6 +2957,9 @@ static void swap_timing_report(void) (double)g_swap_flush_max * ms * (double)g_swap_timed, (double)g_swap_present_ticks * ms, (double)g_swap_present_max * ms * (double)g_swap_timed); + d3d8_profile_report(g_swap_timed); + vertex_cache_report(); + expand_cache_report(); g_swap_gate_ticks = g_swap_body_ticks = g_swap_frame_ticks = 0; g_swap_flush_ticks = g_swap_present_ticks = 0; g_swap_flush_max = g_swap_present_max = 0; @@ -3139,6 +3293,7 @@ static void frame_end_shadow(void) hle_d3d8_texture_frame_end(); g_shadow_swaps++; + xbox_WatchdogNotePresent(); /* RECOMP_WATCHDOG_STALL */ /* The frame boundary for capture: closes the frame being recorded, or * starts recording if this is the requested swap. */ hle_d3d8_capture_swap(g_shadow_swaps, g_shadow_width, g_shadow_height); @@ -3222,6 +3377,7 @@ static void frame_end_shadow(void) g_skipped_fullscreen); swap_timing_report(); hle_d3d8_interp_report(); + xbox_D3D8PacedReport(); if (g_slot_reloads) fprintf(stderr, "[HLE-D3D8] shadow vertex programs: %lu loads answered from " "the %d cached host programs\n", g_slot_reloads, g_slot_program_count); @@ -3365,15 +3521,33 @@ HLE_EXPORT(D3DDevice_Swap) QueryPerformanceCounter(&t0); if (g_swap_last.QuadPart) g_swap_frame_ticks += t0.QuadPart - g_swap_last.QuadPart; - xbox_Nv2aFlipGateArm(); - QueryPerformanceCounter(&t1); - g_in_swap_body = 1; - g_swap_body_starts = g_pushed_starts; - HLE_CALL_ORIGINAL(D3DDevice_Swap); - g_in_swap_body = 0; - QueryPerformanceCounter(&t2); - g_swap_gate_ticks += t1.QuadPart - t0.QuadPart; - g_swap_body_ticks += t2.QuadPart - t1.QuadPart; + /* With frame interpolation the title's own Swap runs before the + * gate's wait, since the in-between is drawn in that wait from what + * the frame drew: Forza draws one last full-screen pass inside its + * Swap, and an in-between made before it showed every menu without + * it on every other picture -- sharp, then soft, sixty times a + * second. */ + if (hle_d3d8_interp_gate_after_swap()) { + g_in_swap_body = 1; + g_swap_body_starts = g_pushed_starts; + HLE_CALL_ORIGINAL(D3DDevice_Swap); + g_in_swap_body = 0; + QueryPerformanceCounter(&t1); + xbox_Nv2aFlipGateArm(); + QueryPerformanceCounter(&t2); + g_swap_body_ticks += t1.QuadPart - t0.QuadPart; + g_swap_gate_ticks += t2.QuadPart - t1.QuadPart; + } else { + xbox_Nv2aFlipGateArm(); + QueryPerformanceCounter(&t1); + g_in_swap_body = 1; + g_swap_body_starts = g_pushed_starts; + HLE_CALL_ORIGINAL(D3DDevice_Swap); + g_in_swap_body = 0; + QueryPerformanceCounter(&t2); + g_swap_gate_ticks += t1.QuadPart - t0.QuadPart; + g_swap_body_ticks += t2.QuadPart - t1.QuadPart; + } g_swap_last = t2; g_swap_timed++; } @@ -4978,7 +5152,11 @@ HLE_EXPORT(D3DDevice_SetViewport) uint32_t viewport = HLE_ARG(0); first_call(&seen, "D3DDevice_SetViewport", viewport); - if (viewport && xbox_EnvSwitch("RECOMP_HLE_D3D8_TRACE_VIEWPORT", 0)) { + static int trace_viewport = -1; + + if (trace_viewport < 0) + trace_viewport = xbox_EnvSwitch("RECOMP_HLE_D3D8_TRACE_VIEWPORT", 0); + if (viewport && trace_viewport) { /* Every distinct viewport the title sets, once, with its caller: * where a title computes the rectangle it draws its 3D into. */ static struct { uint32_t x, y, w, h, from; } seenvp[64]; @@ -5204,23 +5382,26 @@ static uint32_t depth_texture_of(uint32_t surface) * host texture SetTexture binds when the title samples it * (hle_d3d8_render_texture), so what a shadow pass writes is what the next * pass reads. NULL if the host cannot hold it. */ -static IDirect3DSurface8 *depth_texture_surface(uint32_t texture_va) +static IDirect3DSurface8 *depth_host_surface(IDirect3DTexture8 *t,uint32_t texture_va) { static struct { IDirect3DTexture8 *texture; IDirect3DSurface8 *surface; } cache[8]; - IDirect3DTexture8 *t = hle_d3d8_render_texture(g_shadow, texture_va); int i; if (!t) return NULL; for (i = 0; i < 8; i++) - if (cache[i].texture == t) + if (cache[i].texture == t) { + host_NoteDepthAlias(cache[i].surface,t); return cache[i].surface; + } for (i = 0; i < 8 && cache[i].texture; i++) ; if (i == 8) i = 0; /* reuse the oldest; keeps its ref */ if (cache[i].surface) cache[i].surface->lpVtbl->Release(cache[i].surface); + if(cache[i].texture)host_ReleaseTexture(cache[i].texture); + t->lpVtbl->AddRef(t); cache[i].texture = t; cache[i].surface = NULL; if (FAILED(t->lpVtbl->GetSurfaceLevel(t, 0, &cache[i].surface))) @@ -5228,8 +5409,13 @@ static IDirect3DSurface8 *depth_texture_surface(uint32_t texture_va) else fprintf(stderr, "[HLE-D3D8] shadow map 0x%08X: depth renders into the " "texture the title samples\n", texture_va); + host_NoteDepthAlias(cache[i].surface,t); return cache[i].surface; } +static IDirect3DSurface8 *depth_texture_surface(uint32_t va) +{ + return depth_host_surface(hle_d3d8_render_texture(g_shadow,va),va); +} static int nodepth_zscale_on(void) { @@ -5506,6 +5692,9 @@ static void shadow_set_render_target(uint32_t rt, uint32_t zs) if (!depth && kind != 0 && kind != 3 && own && g_device_depth && texture && share_device_depth_on() && device_depth_fits(texture, level)) depth = g_device_depth; + /* Forza samples a bare depth surface through another container. */ + if(!depth && kind!=0 && zw==w && zh==h && is_depth_format(zfmt) && shadow_maps_on()) + depth=depth_host_surface(hle_d3d8_render_surface(g_shadow,zs),zs); if (!depth) depth = depth_surface(w, h); g_target_shadow_map = depth && depth_texture_of(zs) && zw == w && zh == h && @@ -6079,13 +6268,146 @@ static void packed_current(const struct shadow_program *p, int k, uint8_t *dst) } } +/* The converted copy of a draw's vertices (shadow_expand_vertices) is made + * for most of a race's draws -- Forza's car and track programs read packed + * normals -- and was calloc'd and freed for every one of them. A sampled + * race (profil-race5) spent ~2% of the guest thread in the heap for it and + * ~4% in out-of-line memmove calls, one per vertex for 30-60 bytes each. + * + * The copy only lives until the host draw returns: host_DrawPrimitiveUP and + * host_DrawIndexedPrimitiveUP hand it to the device, which copies it into + * its own vertex buffer, and a deferred or captured draw copies it too + * (hle_d3d8_record.c). So one buffer per thread is reused, zeroed as calloc + * zeroed it; a copy too big for it is still calloc'd. expand_release frees + * whichever it was. */ +#if defined(_WIN32) +#define EXPAND_TLS __declspec(thread) +#else +#define EXPAND_TLS __thread +#endif +#define EXPAND_SCRATCH_MAX (8u * 1024u * 1024u) +static EXPAND_TLS uint8_t *g_expand_scratch; +static EXPAND_TLS size_t g_expand_scratch_size; + +static uint8_t *expand_alloc(size_t n, size_t size) +{ + size_t bytes; + + if (!n || !size || n > (size_t)-1 / size) + return calloc(n, size); + bytes = n * size; + if (bytes > EXPAND_SCRATCH_MAX) + return calloc(n, size); + if (bytes > g_expand_scratch_size) { + size_t want = g_expand_scratch_size ? g_expand_scratch_size : 64u * 1024u; + + while (want < bytes) + want *= 2u; + free(g_expand_scratch); + g_expand_scratch = (uint8_t *)malloc(want); + g_expand_scratch_size = g_expand_scratch ? want : 0u; + if (!g_expand_scratch) + return calloc(n, size); + } + memset(g_expand_scratch, 0, bytes); + return g_expand_scratch; +} + +static EXPAND_TLS uint8_t *g_ecache_lent; /* expand_cached: lent, not given */ + +/* For hle_d3d8_record.c: vertices the cache owns and keeps unchanged until + * the next frame (an entry drawn this frame is neither converted again nor + * dropped before then), so a draw kept for an in-between can point at them + * instead of copying them. */ +int hle_d3d8_vertices_stable(const void *p) +{ + return p && p == (const void *)g_ecache_lent; +} + +static void expand_release(uint8_t *p) +{ + if (p == g_ecache_lent) { + g_ecache_lent = NULL; /* the cache's own; it stays there */ + return; + } + if (p && p != g_expand_scratch) + free(p); +} + +/* Rebased index lists were malloc'd and freed on every indexed draw. One + * buffer per thread covers them; a list past 1 MB is still malloc'd and the + * caller frees that one. The host copies the indices before this returns. */ +static EXPAND_TLS uint8_t *g_index_scratch; +static EXPAND_TLS size_t g_index_scratch_size; + +static void *index_scratch(size_t bytes) +{ + if (!bytes) + return NULL; + if (bytes <= 1024u * 1024u) { + if (bytes > g_index_scratch_size) { + size_t want = g_index_scratch_size ? g_index_scratch_size : 4096u; + uint8_t *n; + + while (want < bytes) + want *= 2u; + n = (uint8_t *)realloc(g_index_scratch, want); + if (!n) + return malloc(bytes); + g_index_scratch = n; + g_index_scratch_size = want; + } + return g_index_scratch; + } + return malloc(bytes); +} + +static void index_scratch_free(void *p) +{ + if (p && p != g_index_scratch) + free(p); +} + +/* A vertex's few dozen bytes copied inline. memcpy with a size not known at + * compile time is a call into the C runtime, made once per vertex; fixed + * 16/8/4-byte pieces are a load and a store. The function using it is + * marked no_builtin so clang does not fold the loop back into that call. */ +static inline void copy_small(uint8_t *d, const uint8_t *s, size_t n) +{ + while (n >= 16u) { + __builtin_memcpy(d, s, 16u); + d += 16u; s += 16u; n -= 16u; + } + if (n >= 8u) { + __builtin_memcpy(d, s, 8u); + d += 8u; s += 8u; n -= 8u; + } + if (n >= 4u) { + __builtin_memcpy(d, s, 4u); + d += 4u; s += 4u; n -= 4u; + } + while (n) { + *d++ = *s++; + n--; + } +} + +#if defined(__clang__) +#define EXPAND_NO_BUILTIN __attribute__((no_builtin("memcpy", "memmove"))) +#else +#define EXPAND_NO_BUILTIN +#endif + /* The vertices a program with NORMPACKED3 registers reads: each vertex copied * behind its unpacked normals, as shadow_read_declaration laid them out. The * bits are x:11, y:11, z:10, signed, divided by 1023, 1023 and 511 * (Cxbx-Reloaded's vertex buffer conversion). Returns NULL with *failed clear * when the current program packs nothing, and NULL with *failed set when the * copy cannot be made; otherwise the copy, and *stride grows to match. */ -static uint8_t *shadow_expand_vertices(const void *verts, UINT vertices, UINT *stride, +/* Fixed-size copies inside stay builtins under no_builtin. */ +#define memcpy(d, s, n) __builtin_memcpy((d), (s), (n)) +EXPAND_NO_BUILTIN +static uint8_t *shadow_expand_vertices_uncached(const void *verts, UINT vertices, UINT *stride, uint32_t first, int *failed) { const struct shadow_program *p; @@ -6117,7 +6439,7 @@ static uint8_t *shadow_expand_vertices(const void *verts, UINT vertices, UINT *s } } out_stride = g_ff.out_stride; - out = calloc((size_t)vertices, out_stride); + out = expand_alloc((size_t)vertices, out_stride); if (!out) { *failed = 1; return NULL; @@ -6143,7 +6465,8 @@ static uint8_t *shadow_expand_vertices(const void *verts, UINT vertices, UINT *s memcpy(dst + g_ff.out[k], bgra, 4); } } else { - memcpy(dst + g_ff.out[k], f, (size_t)g_ff.floats[k] * sizeof f[0]); + copy_small(dst + g_ff.out[k], (const uint8_t *)f, + (size_t)g_ff.floats[k] * sizeof f[0]); } } } @@ -6163,14 +6486,14 @@ static uint8_t *shadow_expand_vertices(const void *verts, UINT vertices, UINT *s UINT span = g_draw_overread; out_stride = (span + 3u) & ~3u; - out = calloc((size_t)vertices, out_stride); + out = expand_alloc((size_t)vertices, out_stride); if (!out) { *failed = 1; return NULL; } for (v = 0; v < vertices; v++) { size_t n = AVAIL(v) < span ? AVAIL(v) : span; - memcpy(out + (size_t)v * out_stride, (const uint8_t *)verts + (size_t)v * in_stride, n); + copy_small(out + (size_t)v * out_stride, (const uint8_t *)verts + (size_t)v * in_stride, n); } *stride = out_stride; return out; @@ -6206,7 +6529,7 @@ static uint8_t *shadow_expand_vertices(const void *verts, UINT vertices, UINT *s UINT span = g_draw_overread > in_stride ? g_draw_overread : in_stride; out_stride = shift + ((span + 3u) & ~3u); } - out = calloc((size_t)vertices, out_stride); + out = expand_alloc((size_t)vertices, out_stride); if (!out) { *failed = 1; return NULL; @@ -6227,7 +6550,7 @@ static uint8_t *shadow_expand_vertices(const void *verts, UINT vertices, UINT *s at = base[k] + (size_t)v * sstride[k] + p->packed_offset[k]; if (p->packed_raw[k]) { - memcpy(dst + p->packed_out[k], at, p->packed_size[k]); + copy_small(dst + p->packed_out[k], at, p->packed_size[k]); continue; } if (p->packed_format[k] == 0x16u) { /* NORMPACKED3 */ @@ -6259,17 +6582,449 @@ static uint8_t *shadow_expand_vertices(const void *verts, UINT vertices, UINT *s n[c] = (float)s; } } - memcpy(dst + p->packed_out[k], n, (size_t)count * sizeof n[0]); + copy_small(dst + p->packed_out[k], (const uint8_t *)n, (size_t)count * sizeof n[0]); } { size_t span = g_draw_overread > in_stride ? g_draw_overread : in_stride; - memcpy(dst + shift, src, AVAIL(v) < span ? AVAIL(v) : span); + copy_small(dst + shift, src, AVAIL(v) < span ? AVAIL(v) : span); } } #undef AVAIL *stride = out_stride; return out; } +#undef memcpy +/* Memoize only single-stream conversions. Compare all source bytes and the + * whole declaration before reuse; multi-stream/current-register paths stay + * uncached. Returned copies retain the caller's existing free() ownership. */ +#define VERTEX_CACHE_SLOTS 64 +#define VERTEX_CACHE_LIMIT (32u*1024u*1024u) +static struct vertex_cache_entry { + const void *source; + UINT vertices,in_stride,out_stride; + struct shadow_program program; + uint8_t *input,*output; + size_t input_bytes,output_bytes; + unsigned long used; +} g_vertex_cache[VERTEX_CACHE_SLOTS]; +static size_t g_vertex_cache_bytes; +static unsigned long g_vertex_cache_hits,g_vertex_cache_misses,g_vertex_cache_verified,g_vertex_cache_mismatch; +static void vertex_cache_report(void) +{ + fprintf(stderr,"[VERTEX-CACHE] hits %lu, misses %lu, verified %lu, mismatches %lu, %.1f MiB\n", + g_vertex_cache_hits,g_vertex_cache_misses,g_vertex_cache_verified,g_vertex_cache_mismatch, + (double)g_vertex_cache_bytes/(1024.0*1024.0)); + g_vertex_cache_hits=g_vertex_cache_misses=0; +} + +/* ---- converted vertices kept across frames ------------------------------- + * + * shadow_expand_vertices_uncached rebuilds every vertex of a draw whose + * program reads a packed format (NORMPACKED3 and the like), at every draw, + * every frame. Forza's car and track programs all do, and their meshes do + * not change from one frame to the next: in a sampled race (profil-race6) + * the conversion was 11% of the guest thread on its own. + * + * So a conversion is kept, keyed by where the vertices are, how many, their + * stride and everything about the program that shapes the copy, and reused + * while a hash of the guest bytes still matches. Hashing reads the vertices + * once at memory speed; converting them read, unpacked and wrote every one. + * A changed mesh hashes differently and is converted again. + * + * Only what the copy is a function of alone is kept: one stream, a program + * from the declaration, no fixed function. Draws reading other streams, or + * a register's current value, are converted every time as before. + * + * One table per thread, made the first time that thread draws, so nothing + * here needs a lock. RECOMP_EXPAND_CACHE=0 turns it off. */ +#define ECACHE_SLOTS 4096u /* power of two */ +#define ECACHE_PROBE 8u +#define ECACHE_BUDGET (96u * 1024u * 1024u) +#define ECACHE_MAX_INPUT (4u * 1024u * 1024u) +#define ECACHE_AGE 240u /* swaps unused before it may go */ + +struct ecache_entry { + const void *src; + uint64_t layout; /* program layout + stride + overread */ + uint64_t content; /* hash of the guest bytes */ + UINT vertices, out_stride; + uint8_t *out; + size_t out_bytes, out_cap; + unsigned long used; + unsigned long hashed; /* swap its bytes were last hashed */ + unsigned long changed; /* swap they last came out different */ +}; +struct ecache { + struct ecache_entry e[ECACHE_SLOTS]; + size_t bytes; +}; +static EXPAND_TLS struct ecache *g_ecache; +static unsigned long g_ec_hits, g_ec_converted, g_ec_new, g_ec_uncacheable, g_ec_full; +static unsigned long g_ec_again, g_ec_frame_skip, g_ec_alt_skip; +static unsigned long long g_ec_hashed; +/* Direct map of (pointer, byte count) hashed earlier this frame. A collision + * under-counts; it never changes a draw. Tells whether a second hash of the + * same mesh is common enough to be worth a same-frame skip. */ +static struct { const void *src; size_t bytes; unsigned long frame; } g_ec_seen[4096]; + +/* Four CRC32C lanes. The old multiply-rotate hash read every mesh on every + * draw (about 10 MB a frame in a race, several milliseconds). Hardware CRC + * is the same check — a changed byte lands in a different lane — and moves + * at a memory load per instruction. SSE4.2 is the floor: every x64 CPU this + * renderer runs on has it. */ +__attribute__((target("sse4.2"))) +static uint64_t ec_hash(const uint8_t *p, size_t n, uint64_t seed) +{ + uint64_t a = seed ^ 0x9E3779B185EBCA87ull; + uint64_t b = seed ^ 0xC2B2AE3D27D4EB4Full; + uint64_t c = seed ^ 0x165667B19E3779F9ull; + uint64_t d = seed ^ 0x85EBCA77C2B2AE63ull; + size_t i = 0; + + for (; i + 32u <= n; i += 32u) { + uint64_t w0, w1, w2, w3; + __builtin_memcpy(&w0, p + i, 8u); + __builtin_memcpy(&w1, p + i + 8u, 8u); + __builtin_memcpy(&w2, p + i + 16u, 8u); + __builtin_memcpy(&w3, p + i + 24u, 8u); + a = _mm_crc32_u64(a, w0); + b = _mm_crc32_u64(b, w1); + c = _mm_crc32_u64(c, w2); + d = _mm_crc32_u64(d, w3); + } + for (; i + 8u <= n; i += 8u) { + uint64_t w; + __builtin_memcpy(&w, p + i, 8u); + a = _mm_crc32_u64(a, w); + } + for (; i < n; i++) + a = _mm_crc32_u8((uint32_t)a, p[i]); + a ^= (b << 32) ^ c ^ (d << 17) ^ ((uint64_t)n * 0xC2B2AE3D27D4EB4Full); + a ^= a >> 33; + a *= 0x9E3779B185EBCA87ull; + a ^= a >> 29; + return a; +} + +static void ec_drop(struct ecache *c, struct ecache_entry *e) +{ + d3d8_stable_vertex_drop(e->out); + c->bytes -= e->out_cap; + free(e->out); + memset(e, 0, sizeof *e); +} + +/* Make room: everything not drawn for ECACHE_AGE swaps goes. */ +static void ec_sweep(struct ecache *c, unsigned long now) +{ + unsigned i; + + for (i = 0; i < ECACHE_SLOTS; i++) + if (c->e[i].out && now - c->e[i].used > ECACHE_AGE) + ec_drop(c, &c->e[i]); +} + +static void expand_cache_report(void) +{ + unsigned long total = g_ec_hits + g_ec_converted + g_ec_new + g_ec_uncacheable; + + if (!total) + return; + fprintf(stderr, "[EXPAND-CACHE] %lu draws: %lu reused, %lu converted again (changed), " + "%lu new, %lu not cacheable, %lu not kept (full), %lu hashed the same bytes again, " + "%lu taken unhashed (checked earlier this frame), %lu on alternate frames; " + "%.1f MiB hashed, %.1f MiB held\n", + total, g_ec_hits, g_ec_converted, g_ec_new, g_ec_uncacheable, g_ec_full, g_ec_again, + g_ec_frame_skip, g_ec_alt_skip, + (double)g_ec_hashed / (1024.0 * 1024.0), + g_ecache ? (double)g_ecache->bytes / (1024.0 * 1024.0) : 0.0); + g_ec_hits = g_ec_converted = g_ec_new = g_ec_uncacheable = g_ec_full = g_ec_again = 0; + g_ec_frame_skip = g_ec_alt_skip = 0; + g_ec_hashed = 0; + d3d8_stable_vertex_report(); +} + +static int expand_cache_on(void) +{ + static int on = -1; + if (on < 0) { + const char *v = getenv("RECOMP_EXPAND_CACHE"); + on = !(v && v[0] == '0'); + fprintf(stderr, "[EXPAND-CACHE] converted vertices %s across frames " + "(RECOMP_EXPAND_CACHE)\n", on ? "kept" : "not kept"); + } + return on; +} + +/* NULL with *handled clear: not a draw this cache takes. Otherwise the + * converted vertices (owned by the cache, see expand_release) or NULL with + * *failed set, as the uncached conversion would answer. */ +static uint8_t *expand_cached(const void *verts, UINT vertices, UINT *stride, + uint32_t first, int *failed, int *handled) +{ + const struct shadow_program *p; + struct ecache *c; + struct ecache_entry *e = NULL, *victim = NULL; + uint64_t layout, content, kh; + UINT in_stride = *stride; + size_t in_bytes; + unsigned long now = g_shadow_swaps; + uint8_t *out; + unsigned i; + int k; + + *handled = 0; + if (g_ff_active || !g_shadow_vs_is_program || g_shadow_vs_slot < 0 || + !verts || !vertices || !in_stride) + return NULL; + p = &g_programs[g_shadow_vs_slot]; + if (!p->packed_count && !g_draw_overread) + return NULL; /* nothing to convert: no copy at all */ + for (k = 0; k < p->packed_count; k++) + if (p->packed_stream[k]) { + g_ec_uncacheable++; + return NULL; + } + in_bytes = (size_t)vertices * in_stride; + if (in_bytes > ECACHE_MAX_INPUT) { + g_ec_uncacheable++; + return NULL; + } + if (!g_ecache) { + g_ecache = (struct ecache *)calloc(1, sizeof *g_ecache); + if (!g_ecache) + return NULL; + } + c = g_ecache; + + /* Everything the copy's shape depends on besides the bytes. */ + { + uint32_t sh[8 + 6 * SHADOW_MAX_PACKED]; + int n = 0; + sh[n++] = (uint32_t)p->packed_count; + sh[n++] = (uint32_t)p->expanded_bytes; + sh[n++] = (uint32_t)in_stride; + sh[n++] = (uint32_t)g_draw_overread; + for (k = 0; k < p->packed_count && k < SHADOW_MAX_PACKED; k++) { + sh[n++] = p->packed_offset[k]; + sh[n++] = p->packed_out[k]; + sh[n++] = p->packed_format[k]; + sh[n++] = p->packed_size[k]; + sh[n++] = p->packed_raw[k]; + sh[n++] = 0; + } + layout = ec_hash((const uint8_t *)sh, (size_t)n * sizeof sh[0], 0x51ED270B27u); + } + kh = ((uint64_t)(uintptr_t)verts * 0x9E3779B97F4A7C15ull) ^ layout ^ + ((uint64_t)vertices * 0xC2B2AE3D27D4EB4Full); + kh ^= kh >> 29; + for (i = 0; i < ECACHE_PROBE; i++) { + struct ecache_entry *s = &c->e[(kh + i) & (ECACHE_SLOTS - 1u)]; + if (s->out && s->src == verts && s->vertices == vertices && s->layout == layout) { + e = s; + break; + } + if (!victim || !s->out || (victim->out && s->used < victim->used)) + victim = s; + } + *handled = 1; + /* Checked already this frame: a third of a race's draws are a mesh drawn + * again (its shadow, the mirror, a second pass), and hashing it again + * read the same bytes for the same answer. An entry used this frame had + * its bytes hashed this frame -- a hit, or converted and stored. A mesh + * rewritten between two draws of one frame would be missed; Forza never + * does it (the "not kept" count, which catches exactly that, stays 0), + * and RECOMP_EXPAND_CACHE_FRAME=0 hashes at every draw again. */ + { + static int frame_skip = -1; + if (frame_skip < 0) { + const char *v = getenv("RECOMP_EXPAND_CACHE_FRAME"); + frame_skip = !(v && v[0] == '0'); + } + if (frame_skip && e && e->used == now) { + g_ec_hits++; + g_ec_frame_skip++; + *stride = e->out_stride; + *failed = 0; + g_ecache_lent = e->out; + d3d8_stable_vertex_note(e->out, e->content); + return e->out; + } + /* A mesh that has not changed for four seconds and was hashed last + * frame is taken as it is on every other frame (half the entries one + * frame, half the next). The bytes are still checked at least every + * second frame, so a change -- a car's body deforming in a crash -- + * shows at most one frame late, and from then on that mesh counts as + * changing and is hashed at every draw again. Anything that moves + * often (and the "changed" count says a few do) never gets here. + * RECOMP_EXPAND_CACHE_ALTERNATE=0 hashes every frame. */ + { + static int alternate = -1; + if (alternate < 0) { + const char *v = getenv("RECOMP_EXPAND_CACHE_ALTERNATE"); + alternate = !(v && v[0] == '0'); + } + if (alternate && e && e->hashed + 1u == now && now - e->changed >= 120u && + ((((uintptr_t)e >> 6) ^ now) & 1u)) { + e->used = now; + g_ec_hits++; + g_ec_alt_skip++; + *stride = e->out_stride; + *failed = 0; + g_ecache_lent = e->out; + d3d8_stable_vertex_note(e->out, e->content); + return e->out; + } + } + } + { + uint64_t sk = ((uint64_t)(uintptr_t)verts * 0x9E3779B97F4A7C15ull) ^ (uint64_t)in_bytes; + unsigned slot = (unsigned)(sk ^ (sk >> 32)) & (unsigned)(sizeof g_ec_seen / sizeof g_ec_seen[0] - 1u); + + if (g_ec_seen[slot].frame == now && g_ec_seen[slot].src == verts && + g_ec_seen[slot].bytes == in_bytes) + g_ec_again++; + else { + g_ec_seen[slot].src = verts; + g_ec_seen[slot].bytes = in_bytes; + g_ec_seen[slot].frame = now; + } + } + content = ec_hash((const uint8_t *)verts, in_bytes, 0); + g_ec_hashed += in_bytes; + if (e && e->content == content) { + e->used = now; + e->hashed = now; + g_ec_hits++; + *stride = e->out_stride; + *failed = 0; + g_ecache_lent = e->out; + d3d8_stable_vertex_note(e->out, content); + return e->out; + } + + out = shadow_expand_vertices_uncached(verts, vertices, stride, first, failed); + if (!out || *failed) + return out; /* the scratch, or nothing to draw */ + if (e && e->used == now) { + /* Drawn already this frame with other bytes: an in-between may be + * pointing at those, so they stay until the frame is over. */ + g_ec_full++; + return out; + } + { + size_t bytes = (size_t)vertices * *stride; + + if (!e) { + e = victim; + if (e->out && now - e->used <= 1u) { + /* Everything in reach is in use this frame. */ + g_ec_full++; + return out; + } + if (e->out) + ec_drop(c, e); + g_ec_new++; + } else { + g_ec_converted++; + } + if (bytes > e->out_cap) { + uint8_t *n; + if (c->bytes - e->out_cap + bytes > ECACHE_BUDGET) + ec_sweep(c, now); + if (c->bytes - (e->out ? e->out_cap : 0) + bytes > ECACHE_BUDGET || + !(n = (uint8_t *)malloc(bytes))) { + if (e->out) + ec_drop(c, e); + g_ec_full++; + return out; + } + if (e->out) { + d3d8_stable_vertex_drop(e->out); + c->bytes -= e->out_cap; + free(e->out); + } + e->out = n; + e->out_cap = bytes; + c->bytes += bytes; + } + memcpy(e->out, out, bytes); + e->src = verts; + e->vertices = vertices; + e->layout = layout; + e->content = content; + e->out_stride = *stride; + e->out_bytes = bytes; + e->used = now; + e->hashed = e->changed = now; + g_ecache_lent = e->out; /* the same bytes, and they stay */ + d3d8_stable_vertex_note(e->out, content); + return e->out; + } +} + +static uint8_t *shadow_expand_vertices(const void *verts,UINT vertices,UINT *stride, + uint32_t first,int *failed) +{ + static int enabled=-1; + if(enabled<0){const char *v=getenv("RECOMP_VERTEX_CACHE");enabled=v && v[0]=='1';} + if (!enabled && expand_cache_on()) { + int handled; + uint8_t *out = expand_cached(verts, vertices, stride, first, failed, &handled); + if (handled) + return out; + return shadow_expand_vertices_uncached(verts, vertices, stride, first, failed); + } + if(!enabled || g_vertex_cache_mismatch || g_ff_active || !g_shadow_vs_is_program || + g_shadow_vs_slot<0 || g_draw_overread || !verts || !vertices || !*stride) + return shadow_expand_vertices_uncached(verts,vertices,stride,first,failed); + const struct shadow_program *p=&g_programs[g_shadow_vs_slot]; + if(!p->packed_count || p->other_streams) + return shadow_expand_vertices_uncached(verts,vertices,stride,first,failed); + for(int k=0;kpacked_count;++k)if(p->packed_stream[k]) + return shadow_expand_vertices_uncached(verts,vertices,stride,first,failed); + size_t input_bytes=(size_t)vertices**stride; + if(input_bytes>1024u*1024u) + return shadow_expand_vertices_uncached(verts,vertices,stride,first,failed); + unsigned victim=0;UINT in_stride=*stride; + for(unsigned j=0;jinput && e->source==verts && e->vertices==vertices && e->in_stride==in_stride && + !memcmp(&e->program,p,sizeof *p) && !memcmp(e->input,verts,input_bytes)){ + uint8_t *out=malloc(e->output_bytes); + if(!out)break; + memcpy(out,e->output,e->output_bytes);e->used=g_shadow_swaps; + *stride=e->out_stride;*failed=0;++g_vertex_cache_hits; + if(g_vertex_cache_verified<32){ + UINT reference_stride=in_stride;int bad=0; + uint8_t *reference=shadow_expand_vertices_uncached(verts,vertices,&reference_stride,first,&bad); + ++g_vertex_cache_verified; + if(!reference || bad || reference_stride!=e->out_stride || + memcmp(reference,out,e->output_bytes)){ + ++g_vertex_cache_mismatch;free(out);*stride=reference_stride;*failed=bad;return reference; + } + expand_release(reference); + } + return out; + } + if(!e->input || (g_vertex_cache[victim].input && e->usedinput_bytes+e->output_bytes; + if(output_bytes>2u*1024u*1024u || g_vertex_cache_bytes-old+input_bytes+output_bytes>VERTEX_CACHE_LIMIT)return out; + uint8_t *input=malloc(input_bytes),*copy=malloc(output_bytes); + if(!input || !copy){free(input);free(copy);return out;} + memcpy(input,verts,input_bytes);memcpy(copy,out,output_bytes); + free(e->input);free(e->output);g_vertex_cache_bytes=g_vertex_cache_bytes-old+input_bytes+output_bytes; + e->source=verts;e->vertices=vertices;e->in_stride=in_stride;e->out_stride=*stride; + e->program=*p;e->input=input;e->output=copy;e->input_bytes=input_bytes;e->output_bytes=output_bytes;e->used=g_shadow_swaps; + return out; +} + /* The host half of every non-indexed draw: the UP draw below and the vertex * buffer draws in hle_d3d8_vertex.c. `verts` is already a host pointer to the @@ -6309,7 +7064,7 @@ void hle_d3d8_shadow_draw(uint32_t xpt, uint32_t count, const void *verts, if (xpt == XPT_LINELOOP) { /* close the loop: repeat vertex 0 */ loop = malloc((size_t)(count + 1) * host_stride); if (!loop) { - free(expanded); + expand_release(expanded); g_draws_failed++; return; } @@ -6319,7 +7074,7 @@ void hle_d3d8_shadow_draw(uint32_t xpt, uint32_t count, const void *verts, } hr = host_DrawPrimitiveUP(g_shadow, pt, prims, verts, host_stride); free(loop); - free(expanded); + expand_release(expanded); if (FAILED(hr)) g_draws_failed++; else if (from_buffer) @@ -6342,7 +7097,8 @@ void hle_d3d8_shadow_draw_indexed(uint32_t xpt, uint32_t count, const uint16_t * uint8_t *expanded; D3DPRIMITIVETYPE pt; UINT prims, vertices = 0, i, n = 0, host_stride = stride, min_index = 0; - int failed; + int failed, index_keep = -2; + UINT index_expect = 0; HRESULT hr; if (!g_shadow || !idx || !verts || !stride || !count) @@ -6364,22 +7120,54 @@ void hle_d3d8_shadow_draw_indexed(uint32_t xpt, uint32_t count, const uint16_t * * of vertices in: TimeSplitters 2's Siberia draws 340 pieces a frame with * indices up to 35,000, and copying every vertex below the highest one * for each of them moved hundreds of megabytes a frame and ran at 14 fps. - * The range a draw actually uses is a few kilobytes. */ + * The range a draw actually uses is a few kilobytes. + * An unchanged guest list is already in a kept index buffer: the scan + * that found its range also checked the bytes, and the draw binds that + * buffer instead of rebuilding the list. */ { - UINT lo = 0xFFFFu, hi = 0; - for (i = 0; i < count; i++) { - if (idx[i] < lo) lo = idx[i]; - if (idx[i] > hi) hi = idx[i]; + unsigned long long stamp = 0; + index_expect = count * (UINT)sizeof(uint16_t); + if (xpt == XPT_TRIANGLEFAN || xpt == XPT_POLYGON) + index_expect = prims * 3u * (UINT)sizeof(uint16_t); + else if (xpt == XPT_QUADLIST) + index_expect = prims * 6u * (UINT)sizeof(uint16_t); + else if (xpt == XPT_LINELOOP) + index_expect = (count + 1u) * (UINT)sizeof(uint16_t); + index_keep = d3d8_stable_index_prepare(idx, count, xpt, g_shadow_swaps, index_expect, + &min_index, &vertices, &stamp); + + if (index_keep < 0 && index_keep != -1) { + UINT lo = 0xFFFFu, hi = 0; + for (i = 0; i < count; i++) { + if (idx[i] < lo) lo = idx[i]; + if (idx[i] > hi) hi = idx[i]; + } + min_index = lo; + vertices = hi - lo + 1; + } + verts = (const uint8_t *)verts + (size_t)min_index * stride; + /* A converted list is longer than the guest one. Skipping the + * rebuild is only safe when the pointer passed below is the guest + * list and it is exactly as long as the draw reads. */ + if (index_keep == 1 && index_expect == count * (UINT)sizeof(uint16_t) && + !hle_d3d8_capture_active()) { + /* The live draw can use the retained, rebased GPU indices, but + * a capture needs the matching CPU list too. Otherwise it saves + * guest indices against a vertex range already shifted by min, + * and replay reads past that range into unrelated geometry. */ + /* The kept list was stored after this conversion. */ + if (xpt == XPT_TRIANGLEFAN || xpt == XPT_POLYGON || xpt == XPT_QUADLIST) + pt = D3DPT_TRIANGLELIST; + if (xpt == XPT_QUADLIST) + prims *= 2; + goto index_ready; } - min_index = lo; - vertices = hi - lo + 1; - verts = (const uint8_t *)verts + (size_t)lo * stride; } switch (xpt) { case XPT_TRIANGLEFAN: case XPT_POLYGON: - list = malloc((size_t)prims * 3 * sizeof *list); + list = index_scratch((size_t)prims * 3 * sizeof *list); for (i = 0; list && i < prims; i++) { list[n++] = (uint16_t)(idx[0] - min_index); list[n++] = (uint16_t)(idx[i + 1] - min_index); @@ -6388,7 +7176,7 @@ void hle_d3d8_shadow_draw_indexed(uint32_t xpt, uint32_t count, const uint16_t * pt = D3DPT_TRIANGLELIST; break; case XPT_QUADLIST: - list = malloc((size_t)prims * 6 * sizeof *list); + list = index_scratch((size_t)prims * 6 * sizeof *list); for (i = 0; list && i < prims; i++) { const uint16_t *q = idx + i * 4; uint16_t a = (uint16_t)(q[0] - min_index), b = (uint16_t)(q[1] - min_index); @@ -6400,7 +7188,7 @@ void hle_d3d8_shadow_draw_indexed(uint32_t xpt, uint32_t count, const uint16_t * prims *= 2; break; case XPT_LINELOOP: - list = malloc(((size_t)count + 1) * sizeof *list); + list = index_scratch(((size_t)count + 1) * sizeof *list); for (i = 0; list && i < count; i++) list[i] = (uint16_t)(idx[i] - min_index); if (list) @@ -6408,7 +7196,7 @@ void hle_d3d8_shadow_draw_indexed(uint32_t xpt, uint32_t count, const uint16_t * break; default: if (min_index) { - list = malloc((size_t)count * sizeof *list); + list = index_scratch((size_t)count * sizeof *list); for (i = 0; list && i < count; i++) list[i] = (uint16_t)(idx[i] - min_index); } @@ -6416,24 +7204,40 @@ void hle_d3d8_shadow_draw_indexed(uint32_t xpt, uint32_t count, const uint16_t * } if ((xpt == XPT_TRIANGLEFAN || xpt == XPT_POLYGON || xpt == XPT_QUADLIST || xpt == XPT_LINELOOP || min_index) && !list) { + d3d8_stable_index_disarm(); g_draws_failed++; return; } +index_ready: + if (index_keep == 0) { + UINT kept_n = count; + if (list) { + if (xpt == XPT_LINELOOP) + kept_n = count + 1u; + else if (n) + kept_n = n; + } + if (kept_n * (UINT)sizeof(uint16_t) == index_expect) + d3d8_stable_index_commit(list ? (const void *)list : (const void *)idx, + index_expect); + } expanded = shadow_expand_vertices(verts, vertices, &host_stride, from_buffer && g_draw_first != NO_FIRST_VERTEX ? g_draw_first + min_index : NO_FIRST_VERTEX, &failed); g_draw_first = NO_FIRST_VERTEX; if (failed) { - free(list); + d3d8_stable_index_disarm(); + index_scratch_free(list); g_draws_failed++; return; } hr = host_DrawIndexedPrimitiveUP( g_shadow, pt, 0, vertices, prims, list ? list : idx, D3DFMT_INDEX16, expanded ? expanded : verts, host_stride); - free(list); - free(expanded); + d3d8_stable_index_disarm(); + index_scratch_free(list); + expand_release(expanded); if (FAILED(hr)) g_draws_failed++; else if (from_buffer) diff --git a/src/hle/hle_d3d8_interp.c b/src/hle/hle_d3d8_interp.c index 6fb8ea003..ac835ad02 100644 --- a/src/hle/hle_d3d8_interp.c +++ b/src/hle/hle_d3d8_interp.c @@ -129,6 +129,15 @@ static int g_prep_cap; /* This frame, in the gate. */ static int g_prepared, g_frame_ok, g_slot; +/* What the frame still drew after its in-between was drawn from it (the + * title's Swap body runs after the gate's wait): missing from the + * in-between, so it shows on every other picture only. */ +static size_t g_redrawn_ops; +static int g_redrawn_draws; +static unsigned long g_late_draws, g_late_frames; +#define INTERP_LATE_RUN 15 +static int g_late_run, g_pause_left, g_pause_len; +static unsigned long g_pause_ended, g_paused_frames; static LONGLONG g_last_present, g_qpf, g_redraw_est, g_presented_at; /* RECOMP_INTERP_DUMP=: in-between frames from swap @@ -138,7 +147,7 @@ static LONGLONG g_last_present, g_qpf, g_redraw_est, g_presented_at; * side. */ static const char *g_dump_prefix; static unsigned long g_dump_from, g_swaps; -static int g_dumped; +static int g_dumped, g_dump_max = 24; /* For the report. */ static unsigned long g_frames, g_shown, g_late, g_held_2d, g_held_match, g_held_cut, @@ -147,6 +156,29 @@ static double g_matched_sum, g_redraw_ms_sum, g_redraw_ms_max; static double g_alpha_sum, g_off_ms_sum, g_off_ms_max; static unsigned long g_redraws, g_matched_frames; +static volatile int g_hold_req; /* hle_d3d8_interp_hold, this frame */ +/* Frames left of an F11 series (hle_d3d8_interp_dump_now): every one gets + * its in-between, however late -- writing the pictures makes each frame + * late, and the pause that would follow would leave the series none. */ +static int g_force_left; + +void hle_d3d8_interp_hold(void) +{ + g_hold_req = 1; +} + +void hle_d3d8_interp_dump_now(int n) +{ + if (!g_dump_prefix || n <= 0) + return; + g_dump_from = g_swaps + 1; + g_dumped = 0; + g_dump_max = n; + g_force_left = n + 2; + g_pause_left = 0; + g_late_run = 0; +} + unsigned long hle_d3d8_interp_presents(void) { return g_presents_total; @@ -171,8 +203,10 @@ static int grow(uint8_t **buf, size_t *cap, size_t need) return 1; } -static void push(uint8_t **buf, size_t *len, size_t *cap, void (*fn)(const void *), - const void *arg, size_t size, int draw, int *whole) +/* arg NULL: room only, which the caller fills through the pointer returned + * (hle_d3d8_interp_draw_reserve) -- one copy of a draw's vertices, not two. */ +static void *push(uint8_t **buf, size_t *len, size_t *cap, void (*fn)(const void *), + const void *arg, size_t size, int draw, int *whole) { size_t need = sizeof(op_header) + INTERP_ALIGN(size); op_header *h; @@ -180,31 +214,54 @@ static void push(uint8_t **buf, size_t *len, size_t *cap, void (*fn)(const void if (*len + need > INTERP_LIST_LIMIT || !grow(buf, cap, *len + need)) { if (whole) *whole = 0; /* not the whole frame: not drawn again */ - return; + return NULL; } h = (op_header *)(*buf + *len); h->size = (uint32_t)size; h->draw = draw; h->fn = fn; - if (size) + if (size && arg) memcpy(h + 1, arg, size); *len += need; + return h + 1; } +static LONGLONG g_op_ticks; +static unsigned long g_op_count; + void hle_d3d8_interp_op(void (*fn)(const void *arg), const void *arg, size_t size) { InterpList *l = &g_list[g_cur]; push(&l->ops, &l->len, &l->cap, fn, arg, size, -1, &l->whole); + g_op_count++; } void hle_d3d8_interp_draw(void (*fn)(const void *arg), const void *arg, size_t size, const HleInterpDrawKey *key) +{ + void *p = hle_d3d8_interp_draw_reserve(fn, size, key); + if (p && size) + memcpy(p, arg, size); +} + +/* Time spent keeping draws (hle_d3d8_record.c), for the report. */ +static LONGLONG g_keep_ticks; +static double g_keep_bytes; +void hle_d3d8_interp_keep_ticks(long long t) +{ + g_keep_ticks += t; +} + +void *hle_d3d8_interp_draw_reserve(void (*fn)(const void *arg), size_t size, + const HleInterpDrawKey *key) { InterpList *l = &g_list[g_cur]; const float *c = hle_d3d8_interp_constants(); float *r; + g_keep_bytes += (double)size; + if (l->ndraws >= l->cap_draws) { int n = l->cap_draws ? l->cap_draws * 2 : 1024; InterpDraw *d = (InterpDraw *)realloc(l->draws, (size_t)n * sizeof *d); @@ -212,14 +269,14 @@ void hle_d3d8_interp_draw(void (*fn)(const void *arg), const void *arg, size_t s if (!d) { l->whole = 0; - return; + return NULL; } l->draws = d; g = (float *)realloc(l->regs, (size_t)n * (size_t)(g_nregs ? g_nregs : 1) * 4 * sizeof(float)); if (!g) { l->whole = 0; - return; + return NULL; } l->regs = g; l->cap_draws = n; @@ -242,8 +299,12 @@ void hle_d3d8_interp_draw(void (*fn)(const void *arg), const void *arg, size_t s } if (g_aff_count) memcpy(r, c + g_aff_first * 4, (size_t)g_aff_count * 4 * sizeof(float)); - push(&l->ops, &l->len, &l->cap, fn, arg, size, l->ndraws, &l->whole); - l->ndraws++; + { + void *p = push(&l->ops, &l->len, &l->cap, fn, NULL, size, l->ndraws, &l->whole); + if (p) + l->ndraws++; + return p; + } } /* Run what the list put off, with nothing kept while it runs: these are the @@ -741,6 +802,8 @@ static void redraw(double a) return; } hle_d3d8_interp_rec = 0; /* what is drawn now is not kept */ + g_redrawn_ops = cur->len; + g_redrawn_draws = cur->ndraws; for (off = 0; off < cur->len; ) { op_header *h = (op_header *)(cur->ops + off); if (h->draw >= 0) @@ -748,7 +811,7 @@ static void redraw(double a) h->fn(h->size ? (const void *)(h + 1) : NULL); off += sizeof(op_header) + INTERP_ALIGN(h->size); } - if (g_dump_prefix && g_swaps + 1 >= g_dump_from && g_dumped < 24) { + if (g_dump_prefix && g_swaps + 1 >= g_dump_from && g_dumped < g_dump_max) { char path[512]; snprintf(path, sizeof path, "%s_i%05lu_%d.bmp", g_dump_prefix, g_swaps + 1, g_slot); if (hle_d3d8_dump_back_buffer(path) == 0) { @@ -792,13 +855,36 @@ static long long gate_idle(long long now, long long release) if (!g_prepared) { g_prepared = 1; g_frame_ok = 0; - if (hle_d3d8_capture_active() || hle_d3d8_movie_layer_shown()) + if (hle_d3d8_capture_active() || hle_d3d8_movie_layer_shown() || g_hold_req) g_held_other++; else g_frame_ok = prepare_frame(); } - if (!g_frame_ok || span <= g_qpf / 1000 || span > g_qpf / 10) + if (!g_frame_ok || (!g_force_left && (span <= g_qpf / 1000 || span > g_qpf / 10))) return 0; + /* Paced presents (d3d8_device.c, RECOMP_PACED_PRESENT): the in-between + * is drawn at once and shown at this frame's vblank, the frame itself + * half a period after. Nothing has to be finished by the halfway point, + * which a title that takes most of its period never is. Two frames a + * period only: the device keeps one image for each. */ + if (g_multiple == 2 && xbox_D3D8PacedOn()) { + if (g_slot < 2) { + LARGE_INTEGER a, b; + double ms; + xbox_D3D8PacedDue(release); + QueryPerformanceCounter(&a); + redraw(0.5); + QueryPerformanceCounter(&b); + ms = (double)(b.QuadPart - a.QuadPart) * 1000.0 / (double)g_qpf; + g_redraw_ms_sum += ms; + g_redraws++; + if (ms > g_redraw_ms_max) + g_redraw_ms_max = ms; + g_alpha_sum += 0.5; + g_slot = 2; + } + return 0; + } while (g_slot < g_multiple) { LONGLONG due = g_last_present + span * g_slot / g_multiple; LONGLONG start = due - g_redraw_est; @@ -916,7 +1002,10 @@ void hle_d3d8_interp_init(void) g_dump_prefix = NULL; { const char *from = getenv("RECOMP_INTERP_DUMP_FROM"); + const char *max = getenv("RECOMP_INTERP_DUMP_MAX"); /* more than 24, for a run of them */ g_dump_from = from ? (unsigned long)atol(from) : 0; + if (max && atoi(max) > 0) + g_dump_max = atoi(max); } xbox_Nv2aFlipGateSetIdle(gate_idle); fprintf(stderr, "[INTERP] frame interpolation: %d frames shown for each the title " @@ -924,10 +1013,17 @@ void hle_d3d8_interp_init(void) fflush(stderr); } +int hle_d3d8_interp_gate_after_swap(void) +{ + return g_multiple && g_on; +} + void hle_d3d8_interp_frame_end(void) { LARGE_INTEGER now; + int held_req = g_hold_req; + g_hold_req = 0; /* asked for the frame just shown */ if (!g_multiple) return; QueryPerformanceCounter(&now); @@ -943,6 +1039,54 @@ void hle_d3d8_interp_frame_end(void) return; } follow_registers(); + /* A title that runs out of its period has no wait to lend, and then + * keeping its frames only makes it later still: Forza's pack of eight + * cars at a race start went from 33 to 39 ms a frame keeping draws no + * in-between was ever drawn from. After INTERP_LATE_RUN frames in a row + * that gave the gate no wait, nothing is kept for a while; a pause that + * runs straight into another is made longer, up to 20 seconds. */ + if (g_redrawn_ops && g_list[g_cur].ndraws > g_redrawn_draws) { + g_late_draws += (unsigned long)(g_list[g_cur].ndraws - g_redrawn_draws); + g_late_frames++; + } + g_redrawn_ops = 0; + g_redrawn_draws = 0; + if (g_force_left > 0) { + /* Late, so the gate had no wait to lend and gate_idle never ran: + * draw the in-between now, after the frame. Its place on the screen + * is wrong for the moment, its picture is what is being looked at. */ + if (!g_prepared && hle_d3d8_interp_rec) { + g_prepared = 1; + if (!hle_d3d8_capture_active() && !held_req && prepare_frame()) + redraw(0.5); + } + g_force_left--; + g_late_run = 0; + g_prepared = 1; + } + if (!g_prepared) + g_late_run++; + else + g_late_run = 0; + if (g_pause_left > 0 || g_late_run >= INTERP_LATE_RUN) { + if (g_pause_left <= 0) { + g_pause_len = g_frames - g_pause_ended < 30 && g_pause_len + ? (g_pause_len * 2 > 1200 ? 1200 : g_pause_len * 2) + : 150; + g_pause_left = g_pause_len; + g_late_run = 0; + } + if (--g_pause_left == 0) + g_pause_ended = g_frames; + g_paused_frames++; + hle_d3d8_interp_rec = 0; + list_retire(&g_list[0]); + list_retire(&g_list[1]); + g_prepared = 0; + g_frame_ok = 0; + g_slot = 1; + return; + } /* The frame just shown becomes the one before; what the one before it * put off can happen now, since neither will be drawn again. */ list_retire(&g_list[g_cur ^ 1]); @@ -973,6 +1117,25 @@ void hle_d3d8_interp_report(void) g_redraws ? g_off_ms_sum / (double)g_redraws : 0.0, g_off_ms_max, g_redraws ? g_alpha_sum / (double)g_redraws : 0.0, g_on ? "" : " (off)"); + if (g_frames || g_keep_ticks) + fprintf(stderr, "[INTERP] keeping the frame's draws: %.2f ms a frame (%.1f MB a frame)\n", + g_frames ? (double)g_keep_ticks * 1000.0 / (double)g_qpf / (double)g_frames : 0.0, + g_frames ? g_keep_bytes / 1048576.0 / (double)g_frames : 0.0); + if (g_frames) + fprintf(stderr, "[INTERP] keeping the frame's other calls: %lu a frame\n", + g_op_count / g_frames); + if (g_late_frames) + fprintf(stderr, "[INTERP] %lu frames drew %lu draws after their in-between " + "was made (in the title's Swap)\n", g_late_frames, g_late_draws); + g_late_frames = g_late_draws = 0; + if (g_paused_frames) + fprintf(stderr, "[INTERP] %lu frames kept nothing: the title had no time to spare " + "for an in-between\n", g_paused_frames); + g_paused_frames = 0; + g_op_ticks = 0; + g_op_count = 0; + g_keep_ticks = 0; + g_keep_bytes = 0.0; fflush(stderr); g_frames = g_shown = g_late = g_held_2d = g_held_match = g_held_cut = 0; g_held_other = 0; diff --git a/src/hle/hle_d3d8_record.c b/src/hle/hle_d3d8_record.c index 7f65631c0..ffc7d988f 100644 --- a/src/hle/hle_d3d8_record.c +++ b/src/hle/hle_d3d8_record.c @@ -26,8 +26,8 @@ * is what the device actually had. * * Not handled: - * - a stage bound to a volume texture is recorded as nothing bound, and - * counted. src/hle does not create them, so this does not happen today. + * - a volume outside the expanded linear RGBA diagnostic path is + * recorded as nothing bound, and counted. * - more than CAPTURE_MAX_TEXTURES distinct textures in one frame: the rest * are recorded as nothing bound, and counted. * - calls made while the capture file is being closed on another thread; @@ -49,13 +49,14 @@ /* hle_d3d8.c: the shadow device, or NULL. */ IDirect3DDevice8 *hle_d3d8_shadow_device(void); +void hle_d3d8_note_deferred_draw(void); /* hle_d3d8.c */ #define CAPTURE_DEFAULT_SWAP 120 #define CAPTURE_MAX_TEXTURES 1024 static int g_max_files = 24; /* RECOMP_D3D8_CAPTURE_MAX, as RECOMP_HLE_D3D8_DUMP */ #define CAPTURE_MAX_LEVELS 16 #define CAPTURE_STAGES 4 /* d3d8_device.c MAX_TEXTURE_STAGES */ -#define CAPTURE_RENDER_STATES 256 /* d3d8_device.c MAX_RENDER_STATES */ +#define CAPTURE_RENDER_STATES 260 /* d3d8_device.c MAX_RENDER_STATES */ #define CAPTURE_STAGE_STATES 32 /* d3d8_device.c MAX_TSS_STATES */ static D3D8CapWriter *g_cap; @@ -131,8 +132,11 @@ static void cube_level_write(IDirect3DBaseTexture8 *object, uint32_t id, const BYTE *bits; UINT pitch, rows; - if (!d3d8_cube_level(object, face, level, &bits, &pitch, &rows)) + if (!d3d8_cube_level(object, face, level, &bits, &pitch, &rows)) { + fprintf(stderr, "[HLE-D3D8] capture: cube %u face %u level %u " + "could not be read; reflection snapshot incomplete\n", id, face, level); return; + } c.id = id; c.face = face; c.level = level; @@ -143,10 +147,10 @@ static void cube_level_write(IDirect3DBaseTexture8 *object, uint32_t id, g_texture_bytes += c.bytes; } -/* A cube texture's creation, then its texels unless the title renders into - * it: a cube filled from memory (hle_d3d8_texture.c, static_cube) would - * otherwise replay empty, and every surface lit through it -- a - * normalisation cube's dot product, an environment map -- with it. +/* A cube texture's creation, then its starting texels. Render targets need + * a GPU snapshot too: a title may update only three faces this frame, using + * the other three from previous frames. d3d8_cube_level reads supported + * render-target formats back into Xbox packing for the existing replay. * d3d8_cube_info is also the type test: it answers only for a cube. */ static int cube_write(IDirect3DBaseTexture8 *object, uint32_t id) { @@ -162,7 +166,7 @@ static int cube_write(IDirect3DBaseTexture8 *object, uint32_t id) head.usage = info.usage; chunk(D3D8CAP_CUBE_TEXTURE, &head, sizeof head, NULL, 0, NULL, 0); g_texture_writes++; - if (!(info.usage & D3DUSAGE_RENDERTARGET)) { + { UINT f, l; for (f = 0; f < 6; f++) @@ -215,6 +219,25 @@ static int texture_write(IDirect3DBaseTexture8 *object, uint32_t id) return 1; } +/* Record the host's expanded RGBA volume, including every mip slice. */ +static int volume_write(IDirect3DBaseTexture8 *object, uint32_t id) +{ + if (object->lpVtbl->GetType(object) != D3DRTYPE_VOLUMETEXTURE) return 0; + D3D8VolumeTexture *v=(D3D8VolumeTexture *)object; + if (v->d3d8_format != D3DFMT_LIN_A8B8G8R8 || !v->sys_mem || !v->levels || + v->levels>CAPTURE_MAX_LEVELS) return 0; + D3D8CapVolumeTexture head={id,v->d3d8_format,v->width,v->height,v->depth,v->levels,v->usage}; + size_t bytes=0; + for (unsigned l=0;llevels;++l) { + unsigned w=v->width>>l,h=v->height>>l,d=v->depth>>l; + if(!w)w=1;if(!h)h=1;if(!d)d=1; + bytes+=(size_t)w*h*d*4; + } + chunk(D3D8CAP_VOLUME_TEXTURE,&head,sizeof head,v->sys_mem,bytes,NULL,0); + g_texture_writes++;g_texture_bytes+=bytes; + return 1; +} + /* The capture id for a texture about to be bound, writing it first if the * capture does not hold it yet. 0 for NULL, and for a texture that cannot be * recorded (counted). */ @@ -229,7 +252,8 @@ static uint32_t texture_id(IDirect3DBaseTexture8 *object) return g_textures[i].id; if (g_texture_count >= CAPTURE_MAX_TEXTURES || (!texture_write(object, g_next_texture_id) && - !cube_write(object, g_next_texture_id))) { + !cube_write(object, g_next_texture_id) && + !volume_write(object, g_next_texture_id))) { g_unrecorded_binds++; return 0; } @@ -434,6 +458,16 @@ static void rec_ps_token(DWORD token) /* The capture id for a depth surface, writing it first if needed. 0 for * NULL, and for one that cannot be recorded (the caller counts it). */ +static struct {IDirect3DSurface8 *surface;IDirect3DTexture8 *texture;} g_depth_alias[16]; +void host_NoteDepthAlias(IDirect3DSurface8 *surface,IDirect3DTexture8 *texture) +{ + if(!surface||!texture)return; + unsigned i; + for(i=0;i<16;++i)if(!g_depth_alias[i].surface || g_depth_alias[i].surface==surface)break; + if(i==16)i=0; + g_depth_alias[i].surface=surface;g_depth_alias[i].texture=texture; +} + static uint32_t depth_id(IDirect3DSurface8 *object) { D3DSURFACE_DESC desc; @@ -452,6 +486,10 @@ static uint32_t depth_id(IDirect3DSurface8 *object) return 0; g_depth_objects[g_depth_count++] = object; c.id = (uint32_t)g_depth_count; + for(unsigned alias=0;alias<16;++alias)if(g_depth_alias[alias].surface==object){ + uint32_t id=texture_id((IDirect3DBaseTexture8 *)g_depth_alias[alias].texture); + if(id){uint32_t pair[2]={c.id,id};chunk(D3D8CAP_DEPTH_ALIAS,pair,sizeof pair,NULL,0,NULL,0);return c.id;} + } c.width = desc.Width; c.height = desc.Height; c.format = (uint32_t)desc.Format; @@ -909,6 +947,19 @@ static void retain_call(void (*fn)(const void *), const void *fixed, size_t fixe uint8_t stack[256], *p = stack; size_t total = DEFER_ALIGN(fixed_size) + DEFER_ALIGN(a_size) + b_size; + /* A draw is laid out straight into the list: copying it here first and + * there again, through a malloc, was most of what keeping a frame cost. */ + if (key) { + p = (uint8_t *)hle_d3d8_interp_draw_reserve(fn, total, key); + if (!p) + return; + memcpy(p, fixed, fixed_size); + if (a_size) + memcpy(p + DEFER_ALIGN(fixed_size), a, a_size); + if (b_size) + memcpy(p + DEFER_ALIGN(fixed_size) + DEFER_ALIGN(a_size), b, b_size); + return; + } if (total > sizeof stack) { p = (uint8_t *)malloc(total); if (!p) @@ -947,6 +998,26 @@ static uint64_t hash_bytes(uint64_t h, const void *data, size_t n) return h; } +/* The same, for a draw's identity, reading at most about a kilobyte: the + * whole buffer hashed cost Forza's race several milliseconds a frame. A + * small buffer (2D text, sprites) is still read whole, so two strings of + * the same length are told apart; a large one is told by its length and + * sixteen spans across it, and the key carries the program, the textures + * and the counts besides. */ +static uint64_t hash_sampled(uint64_t h, const void *data, size_t n) +{ + const uint8_t *b = (const uint8_t *)data; + uint64_t len = (uint64_t)n; + size_t i; + + if (n <= 2048) + return hash_bytes(h, data, n); + h = hash_bytes(h, &len, sizeof len); + for (i = 0; i < 16; i++) + h = hash_bytes(h, b + (n - 64) * i / 15, 64); + return h; +} + static void draw_key(HleInterpDrawKey *k, DWORD type, UINT prims, UINT stride, DWORD index_format) { @@ -1316,8 +1387,9 @@ typedef struct { DWORD type; UINT prims, stride; } dq_draw_up; static void op_draw_up(const void *arg) { const dq_draw_up *p = (const dq_draw_up *)arg; - host_DrawPrimitiveUP(hle_d3d8_shadow_device(), (D3DPRIMITIVETYPE)p->type, p->prims, - DEFER_PART_A(arg, dq_draw_up), p->stride); + if (SUCCEEDED(host_DrawPrimitiveUP(hle_d3d8_shadow_device(), (D3DPRIMITIVETYPE)p->type, + p->prims, DEFER_PART_A(arg, dq_draw_up), p->stride))) + hle_d3d8_note_deferred_draw(); } typedef struct { @@ -1330,12 +1402,62 @@ typedef struct { static void op_draw_indexed_up(const void *arg) { const dq_draw_indexed_up *p = (const dq_draw_indexed_up *)arg; - host_DrawIndexedPrimitiveUP(hle_d3d8_shadow_device(), (D3DPRIMITIVETYPE)p->type, + if (SUCCEEDED(host_DrawIndexedPrimitiveUP(hle_d3d8_shadow_device(), (D3DPRIMITIVETYPE)p->type, p->min_index, p->num_vertices, p->prims, DEFER_PART_A(arg, dq_draw_indexed_up), (D3DFORMAT)p->index_format, DEFER_PART_B(arg, dq_draw_indexed_up, p->index_bytes), - p->stride); + p->stride))) + hle_d3d8_note_deferred_draw(); +} + +/* A kept draw whose vertices stay where they are until the in-between is + * drawn (hle_d3d8_vertices_stable): only where they are is kept. A race + * start kept 30 MB of them a frame, 3 ms of copying. */ +typedef struct { + DWORD type, prims, stride; + const void *vertices; +} dq_draw_up_ref; + +static void op_draw_up_ref(const void *arg) +{ + const dq_draw_up_ref *p = (const dq_draw_up_ref *)arg; + if (SUCCEEDED(host_DrawPrimitiveUP(hle_d3d8_shadow_device(), (D3DPRIMITIVETYPE)p->type, + p->prims, p->vertices, p->stride))) + hle_d3d8_note_deferred_draw(); +} + +int d3d8_stable_index_rebind(const void *guest, unsigned index_count, unsigned topology, + unsigned size); +const void *d3d8_stable_index_bound_key(unsigned *index_count, unsigned *topology, + unsigned *size); +void d3d8_stable_index_disarm(void); + +typedef struct { + DWORD type, min_index, num_vertices, prims, stride, index_format; + uint32_t index_bytes; + const void *vertices; + const void *index_key; /* kept list: no index bytes follow */ + UINT index_count, index_topology; +} dq_draw_indexed_up_ref; + +static void op_draw_indexed_up_ref(const void *arg) +{ + const dq_draw_indexed_up_ref *p = (const dq_draw_indexed_up_ref *)arg; + const void *indices; + + if (p->index_key) { + if (!d3d8_stable_index_rebind(p->index_key, p->index_count, p->index_topology, + p->index_bytes)) + return; + indices = p->index_key; + } else { + indices = DEFER_PART_A(arg, dq_draw_indexed_up_ref); + } + if (SUCCEEDED(host_DrawIndexedPrimitiveUP(hle_d3d8_shadow_device(), (D3DPRIMITIVETYPE)p->type, + p->min_index, p->num_vertices, p->prims, + indices, (D3DFORMAT)p->index_format, p->vertices, p->stride))) + hle_d3d8_note_deferred_draw(); } HRESULT host_DrawPrimitiveUP(IDirect3DDevice8 *dev, D3DPRIMITIVETYPE type, @@ -1373,12 +1495,25 @@ HRESULT host_DrawPrimitiveUP(IDirect3DDevice8 *dev, D3DPRIMITIVETYPE type, dq_draw_up p; HleInterpDrawKey k; + LARGE_INTEGER keep_t0, keep_t1; + QueryPerformanceCounter(&keep_t0); draw_key(&k, (DWORD)type, prims, stride, 0); - k.content = hash_bytes(0xCBF29CE484222325ull, vertices, (size_t)bytes); - p.type = (DWORD)type; - p.prims = prims; - p.stride = stride; - retain_call(op_draw_up, &p, sizeof p, vertices, (size_t)bytes, NULL, 0, &k); + k.content = hash_sampled(0xCBF29CE484222325ull, vertices, (size_t)bytes); + if (hle_d3d8_vertices_stable(vertices)) { + dq_draw_up_ref r; + r.type = (DWORD)type; + r.prims = prims; + r.stride = stride; + r.vertices = vertices; + retain_call(op_draw_up_ref, &r, sizeof r, NULL, 0, NULL, 0, &k); + } else { + p.type = (DWORD)type; + p.prims = prims; + p.stride = stride; + retain_call(op_draw_up, &p, sizeof p, vertices, (size_t)bytes, NULL, 0, &k); + } + QueryPerformanceCounter(&keep_t1); + hle_d3d8_interp_keep_ticks(keep_t1.QuadPart - keep_t0.QuadPart); } return hr; } @@ -1393,6 +1528,7 @@ HRESULT host_DrawIndexedPrimitiveUP(IDirect3DDevice8 *dev, D3DPRIMITIVETYPE type dq_draw_indexed_up p; UINT index_size = index_format == D3DFMT_INDEX32 ? 4u : 2u; uint64_t ibytes = (uint64_t)d3d8_up_indices_read(type, prims) * index_size; + d3d8_stable_index_disarm(); /* From the base the indices are relative to, through the last vertex. */ uint64_t vbytes = (uint64_t)(min_index + num_vertices) * stride; if (ibytes > DEFER_LIMIT || vbytes > DEFER_LIMIT) @@ -1441,18 +1577,42 @@ HRESULT host_DrawIndexedPrimitiveUP(IDirect3DDevice8 *dev, D3DPRIMITIVETYPE type dq_draw_indexed_up p; HleInterpDrawKey k; + LARGE_INTEGER keep_t0, keep_t1; + QueryPerformanceCounter(&keep_t0); draw_key(&k, (DWORD)type, prims, stride, (DWORD)index_format); - k.content = hash_bytes(hash_bytes(0xCBF29CE484222325ull, indices, (size_t)ibytes), - vertices, (size_t)vbytes); - p.type = (DWORD)type; - p.min_index = min_index; - p.num_vertices = num_vertices; - p.prims = prims; - p.stride = stride; - p.index_format = (DWORD)index_format; - p.index_bytes = (uint32_t)ibytes; - retain_call(op_draw_indexed_up, &p, sizeof p, indices, (size_t)ibytes, - vertices, (size_t)vbytes, &k); + k.content = hash_sampled(hash_sampled(0xCBF29CE484222325ull, indices, (size_t)ibytes), + vertices, (size_t)vbytes); + if (hle_d3d8_vertices_stable(vertices)) { + dq_draw_indexed_up_ref r; + UINT ik_count = 0, ik_topo = 0, ik_size = 0; + const void *ikey = d3d8_stable_index_bound_key(&ik_count, &ik_topo, &ik_size); + r.type = (DWORD)type; + r.min_index = min_index; + r.num_vertices = num_vertices; + r.prims = prims; + r.stride = stride; + r.index_format = (DWORD)index_format; + r.vertices = vertices; + r.index_key = ikey; + r.index_count = ik_count; + r.index_topology = ik_topo; + r.index_bytes = ikey ? ik_size : (uint32_t)ibytes; + retain_call(op_draw_indexed_up_ref, &r, sizeof r, + ikey ? NULL : indices, ikey ? 0 : (size_t)ibytes, + NULL, 0, &k); + } else { + p.type = (DWORD)type; + p.min_index = min_index; + p.num_vertices = num_vertices; + p.prims = prims; + p.stride = stride; + p.index_format = (DWORD)index_format; + p.index_bytes = (uint32_t)ibytes; + retain_call(op_draw_indexed_up, &p, sizeof p, indices, (size_t)ibytes, + vertices, (size_t)vbytes, &k); + } + QueryPerformanceCounter(&keep_t1); + hle_d3d8_interp_keep_ticks(keep_t1.QuadPart - keep_t0.QuadPart); } return hr; } @@ -1808,8 +1968,31 @@ void host_vsh_set_constant(int first_reg, const float *data, int count) d3d8_vsh_set_constant(first_reg, data, count); } +/* A program's layout changes inside a frame: the inline and register draws + * (hle_d3d8.c draw_registers) set sixteen float4 registers for theirs and + * put the program's own back after. Not kept, an in-between drew every + * draw with the layout the frame ended on -- Forza's menus came out as flat + * colours every other frame. */ +typedef struct { DWORD handle; int count; } dq_declaration; + +static void op_vsh_declaration(const void *arg) +{ + const dq_declaration *p = (const dq_declaration *)arg; + d3d8_vsh_set_declaration(p->handle, + p->count ? (const D3D8VshInput *)DEFER_PART_A(arg, dq_declaration) + : NULL, + p->count); +} + HRESULT host_vsh_set_declaration(DWORD handle, const D3D8VshInput *inputs, int count) { + if (hle_d3d8_interp_rec && count >= 0 && (count == 0 || inputs)) { + dq_declaration p; + p.handle = handle; + p.count = count; + retain_call(op_vsh_declaration, &p, sizeof p, inputs, + (size_t)count * sizeof *inputs, NULL, 0, NULL); + } if (g_cap) rec_vs_declaration(handle, inputs, count); return d3d8_vsh_set_declaration(handle, inputs, count); @@ -1916,6 +2099,16 @@ static void rec_screen_copy(IDirect3DTexture8 *dst, const RECT *src, const POINT HRESULT host_CopyBackBufferToTexture(IDirect3DTexture8 *dst) { + /* Deferred frames: the copy reads what the frame drew before it, so it + * runs in the queue, in its place, not now -- now the queue's draws + * have not happened and it copied the frame before. */ + if (hle_d3d8_defer_recording()) { + dq_screen_copy p; + memset(&p, 0, sizeof p); + p.dst = dst; + hle_d3d8_defer_op(op_screen_copy, &p, sizeof p); + return S_OK; + } if (g_cap && dst) rec_screen_copy(dst, NULL, NULL); if (hle_d3d8_interp_rec) { @@ -1930,6 +2123,17 @@ HRESULT host_CopyBackBufferToTexture(IDirect3DTexture8 *dst) HRESULT host_CopyBackBufferRectToTexture(IDirect3DTexture8 *dst, const RECT *src, const POINT *at) { + if (hle_d3d8_defer_recording() && src) { + dq_screen_copy p; + memset(&p, 0, sizeof p); + p.dst = dst; + p.has_rect = 1; + p.src = *src; + if (at) + p.at = *at; + hle_d3d8_defer_op(op_screen_copy, &p, sizeof p); + return S_OK; + } if (g_cap && dst && src) rec_screen_copy(dst, src, at); if (hle_d3d8_interp_rec && src) { @@ -1958,6 +2162,10 @@ static void op_depth_copy(const void *arg) HRESULT host_CopyDepthToTexture(IDirect3DTexture8 *dst) { + if (hle_d3d8_defer_recording()) { + hle_d3d8_defer_op(op_depth_copy, &dst, sizeof dst); + return S_OK; + } if (g_cap && dst) { D3D8CapScreenCopy c; diff --git a/src/hle/hle_d3d8_record.h b/src/hle/hle_d3d8_record.h index 737d5e576..23f7c0a2e 100644 --- a/src/hle/hle_d3d8_record.h +++ b/src/hle/hle_d3d8_record.h @@ -108,6 +108,15 @@ typedef struct { } HleInterpDrawKey; void hle_d3d8_interp_op(void (*fn)(const void *arg), const void *arg, size_t size); +void *hle_d3d8_interp_draw_reserve(void (*fn)(const void *arg), size_t size, + const HleInterpDrawKey *key); +void hle_d3d8_interp_keep_ticks(long long t); +/* hle_d3d8.c: TRUE for converted vertices that stay unchanged until the + * next frame, which a kept draw may point at instead of copying. */ +int hle_d3d8_vertices_stable(const void *p); +/* hle_d3d8_interp.c: TRUE while in-betweens are drawn, so Swap runs the + * title's own Swap body before the gate's wait, not after. */ +int hle_d3d8_interp_gate_after_swap(void); void hle_d3d8_interp_draw(void (*fn)(const void *arg), const void *arg, size_t size, const HleInterpDrawKey *key); /* Run fn(arg) once the frame being kept is no longer needed: a release or a @@ -128,6 +137,13 @@ int hle_d3d8_dump_back_buffer(const char *path); * presented. */ void hle_d3d8_interp_init(void); void hle_d3d8_interp_frame_end(void); +/* The frame being drawn is not to have an in-between drawn from it: the + * title knows it is one whose draws cannot be redrawn faithfully (a menu + * fading in or out). Forza's front end asks it at every frame. */ +void hle_d3d8_interp_hold(void); +/* RECOMP_INTERP_DUMP: write the next n in-between frames, from now (F11 + * under RECOMP_F11_SERIES). */ +void hle_d3d8_interp_dump_now(int n); /* The five-second report's line, and in-between frames shown so far. */ void hle_d3d8_interp_report(void); unsigned long hle_d3d8_interp_presents(void); @@ -233,4 +249,7 @@ void host_vsh_set_vertex_data(int reg, const float value[4]); void host_combiners_set_pixel_shader(DWORD token); +void host_NoteDepthAlias(IDirect3DSurface8 *surface,IDirect3DTexture8 *texture); + #endif /* XBOXRECOMP_HLE_D3D8_RECORD_H */ + diff --git a/src/hle/hle_d3d8_state.c b/src/hle/hle_d3d8_state.c index f3b421bf2..81b2777ea 100644 --- a/src/hle/hle_d3d8_state.c +++ b/src/hle/hle_d3d8_state.c @@ -126,7 +126,15 @@ static void unmapped(const char *what, uint32_t value) } } -static int state_ready(void) +static int state_ready_check(void); + +/* Asked several times a draw; the answer is settled after the first. */ +static inline int state_ready(void) +{ + return g_state_ready >= 0 ? g_state_ready : state_ready_check(); +} + +static int state_ready_check(void) { uint32_t rs = hle_var_D3D_g_RenderState; @@ -417,6 +425,15 @@ static const struct { /* XDK 5849 numbering: SHADOWFUNC sits after MULTISAMPLERENDERTARGETMODE. * The compare a shadow-map stage makes (d3d8_combiners.c). */ { 157, D3DRS_SHADOWFUNC, CMP }, + /* Polygon offset. The slope and the bias are float bits; the three + * enables gate point, line and solid. Applied as D3D11 depth bias for + * the current fill mode, so a decal or a contact shadow sits on the + * road instead of fighting it. */ + { 77, D3DRS_POLY_SLOPE, AS_IS }, + { 78, D3DRS_POLY_BIAS, AS_IS }, + { 79, D3DRS_POLY_POINT, AS_IS }, + { 80, D3DRS_POLY_LINE, AS_IS }, + { 81, D3DRS_POLY_FILL, AS_IS }, }; static DWORD rs_to_host(uint8_t kind, uint32_t v) @@ -444,6 +461,7 @@ static const struct { } g_ts_map[] = { { 0, D3DTSS_ADDRESSU, TS_ADDRESS }, { 1, D3DTSS_ADDRESSV, TS_ADDRESS }, + { 2, D3DTSS_ADDRESSW, TS_ADDRESS }, { 3, D3DTSS_MAGFILTER, TS_FILTER }, { 4, D3DTSS_MINFILTER, TS_FILTER }, { 5, D3DTSS_MIPFILTER, TS_FILTER }, @@ -1083,6 +1101,52 @@ int hle_d3d8_state_store_simple(uint32_t method, uint32_t value) return 0; } +/* Face culling as the push buffer sets it: NV097_SET_CULL_FACE_ENABLE + * (0x0308), NV097_SET_CULL_FACE (0x039C: 0x404 front, 0x405 back, 0x408 + * both) and NV097_SET_FRONT_FACE (0x03A0: 0x900 CW, 0x901 CCW), folded into + * the D3DRS_CULLMODE slot (the faces of that winding are culled). A title + * that records its own push buffers sets culling in them, and only there: + * the slot kept whatever the API last set, and Forza Motorsport's + * billboards, signs and grandstands drew their back faces too, fighting + * the front ones for the same depth -- flicker as the camera moved. + * RECOMP_PB_CULL=0 leaves the slot to the API alone. */ +void hle_d3d8_state_store_cull(uint32_t enable, uint32_t face, uint32_t front) +{ + static int on = -1; + uint32_t v; + + if (on < 0) { + const char *e = getenv("RECOMP_PB_CULL"); + on = e ? atoi(e) : 1; + } + if (!on || !hle_var_D3D_g_RenderState || state_ready() != 1 || rs_slot(147u) < 0) + return; + if (!enable) + v = 0; + else if (face == 0x408u) + return; /* both: no host equivalent */ + else { + /* The XDK writes the D3DCULL value itself as the front face (its + * screen is y-down, the winding names the culled side): Forza's own + * D3DCULL_CCW came with back faces culled and the front CCW. */ + v = front == 0x900u ? 0x900u : 0x901u; + if (face == 0x404u) + v ^= 1u; /* the front is culled instead */ + if (on == 2) + v ^= 1u; /* the other reading, to compare */ + } + { + static int said; + uint32_t old = HLE_MEM32(hle_var_D3D_g_RenderState + 4u * (uint32_t)rs_slot(147u)); + if (said < 24 && old != v) { + said++; + fprintf(stderr, "[PB-CULL] enable %u face 0x%03X front 0x%03X: cull mode " + "0x%03X (was 0x%03X)\n", enable, face, front, v, old); + } + } + HLE_MEM32(hle_var_D3D_g_RenderState + 4u * (uint32_t)rs_slot(147u)) = v; +} + uint32_t hle_d3d8_push_position(void); /* hle_d3d8_vertex.c */ int hle_d3d8_queue_simple_state(uint32_t lo, uint32_t hi, uint32_t method, uint32_t value); diff --git a/src/hle/hle_d3d8_texture.c b/src/hle/hle_d3d8_texture.c index ce05bdbbd..7356b31c0 100644 --- a/src/hle/hle_d3d8_texture.c +++ b/src/hle/hle_d3d8_texture.c @@ -44,7 +44,9 @@ #include "d3d8_xbox.h" #include "d3d8_internal.h" +#include "dxt5_decode.h" #include "hle_d3d8_record.h" +#include /* CRC32C, level0_checksum */ /* Reads a RECOMP_* switch; see xbox_memory_layout.h. Declared here rather than * including the kernel header, which this file otherwise has no need of. */ @@ -105,6 +107,28 @@ typedef struct { static texture_entry g_textures[TEXTURE_CACHE]; static int g_texture_count; + +/* The entries the host draws into (rendered), for the lookups that only + * want those. Every bind asked rendered_surface_for, and that walked the + * whole cache -- hundreds of entries, a thousand binds a frame in a race. + * The list is the same entries in the same order, rebuilt when one becomes + * rendered (the only way into it; an entry cleared or reused fails the + * checks the lookups make anyway). */ +static unsigned g_rendered_gen = 1, g_rendered_list_gen; +static int g_rendered_list[TEXTURE_CACHE], g_rendered_n; + +static void rendered_list_refresh(void) +{ + int i; + + if (g_rendered_list_gen == g_rendered_gen) + return; + g_rendered_n = 0; + for (i = 0; i < g_texture_count; i++) + if (g_textures[i].rendered) + g_rendered_list[g_rendered_n++] = i; + g_rendered_list_gen = g_rendered_gen; +} static IDirect3DTexture8 *g_bound[MAX_STAGES]; /* The Size word of what each stage last had bound: the image rectangle and * pitch the hardware keeps when SwitchTexture changes only the offset and @@ -339,6 +363,35 @@ static int read_layout(uint32_t va, texture_layout *t) * Word at a time, so hashing the few megabytes a frame binds costs about a * millisecond. RECOMP_HLE_D3D8_TEX_SAMPLED=1 restores the old sample, for * comparison. */ +/* Four CRC32C lanes, as the vertex cache's hash (hle_d3d8.c ec_hash): a + * memory load per instruction, where one multiply chain waited on itself + * every eight bytes. Compared only with this process's own earlier sums. */ +__attribute__((target("sse4.2"))) +static uint32_t level0_crc(const uint8_t *p, uint32_t n) +{ + uint64_t a = 0x9E3779B185EBCA87ull, b = 0xC2B2AE3D27D4EB4Full; + uint64_t c = 0x165667B19E3779F9ull, d = 0x85EBCA77C2B2AE63ull; + uint32_t i = 0; + + for (; i + 32u <= n; i += 32u) { + uint64_t w0, w1, w2, w3; + memcpy(&w0, p + i, 8u); + memcpy(&w1, p + i + 8u, 8u); + memcpy(&w2, p + i + 16u, 8u); + memcpy(&w3, p + i + 24u, 8u); + a = _mm_crc32_u64(a, w0); + b = _mm_crc32_u64(b, w1); + c = _mm_crc32_u64(c, w2); + d = _mm_crc32_u64(d, w3); + } + for (; i < n; i++) + a = _mm_crc32_u8((uint32_t)a, p[i]); + a ^= (b << 32) ^ c ^ (d << 17) ^ ((uint64_t)n * 0xC2B2AE3D27D4EB4Full); + a ^= a >> 33; + a *= 0x9E3779B185EBCA87ull; + return (uint32_t)(a ^ (a >> 32)); +} + static uint32_t level0_checksum(const texture_layout *t) { static int sampled = -1; @@ -358,19 +411,7 @@ static uint32_t level0_checksum(const texture_layout *t) } return h; } - { - uint64_t h = 0x9E3779B97F4A7C15ull, w; - uint32_t n = t->bytes, i; - - for (i = 0; i + 8 <= n; i += 8) { - memcpy(&w, p + i, 8); - h = (h ^ w) * 0x100000001B3ull; - h ^= h >> 29; - } - for (; i < n; i++) - h = (h ^ p[i]) * 0x100000001B3ull; - return (uint32_t)(h ^ (h >> 32)); - } + return level0_crc(p, t->bytes); } /* A cheap look at level 0: 1,024 eight-byte words spread across it. @@ -389,14 +430,24 @@ static uint32_t texel_sample(const texture_layout *t) 0x9E3779B97F4A7C15ull, 0x7F4A7C159E3779B9ull }; uint64_t w; uint32_t i, k, words = n / 8, step; + /* RECOMP_HLE_D3D8_TEX_SAMPLE_WORDS (default 256, was 1024): each word is + * a cache line and often a page of its own, so the sample of a race's + * few hundred textures a frame was mostly waiting on memory. A texture + * rewritten wholesale still moves it; a small edit is the full hash's, + * every RECOMP_HLE_D3D8_TEX_FULL_EVERY frames, as before. */ + static uint32_t want; + if (!want) { + const char *v = getenv("RECOMP_HLE_D3D8_TEX_SAMPLE_WORDS"); + want = v && atoi(v) > 0 ? (uint32_t)atoi(v) : 256u; + } if (!words) { uint32_t s = 2166136261u; for (i = 0; i < n; i++) s = (s ^ p[i]) * 16777619u; return s; } - step = words > 1024 ? words / 1024 : 1; + step = words > want ? words / want : 1; for (i = 0, k = 0; i < words; i += step, k = (k + 1) & 3) { memcpy(&w, p + (size_t)i * 8, 8); h[k] = (h[k] ^ w) * 0x100000001B3ull; @@ -762,6 +813,7 @@ static IDirect3DTexture8 *framebuffer_texture(IDirect3DDevice8 *dev, uint32_t va } e->va = va; e->rendered = 1; + g_rendered_gen++; e->framebuffer = source; fprintf(stderr, "[HLE-D3D8] the title binds its own %s as a texture " "0x%08X (%ux%u, format 0x%02X, data 0x%08X); filling it from the " @@ -834,6 +886,102 @@ static int tex_trace_data(uint32_t data, unsigned long now) return 1; } +/* The last textures a frame bound. A race binds about a thousand times a + * frame and almost all of them are the same few; scanning the whole cache + * for each one was the bind. A slot the cache reuses fails the header check + * and falls through to the scan. */ +#define TEX_HOT 24 +struct tex_hot_slot { + int idx; + uint32_t va, data, format, size; +}; +static struct tex_hot_slot g_tex_hot[TEX_HOT]; +static int g_tex_hot_n; +static texture_entry *g_tex_resolved; + +static texture_entry *tex_hot_find(uint32_t va, uint32_t data, uint32_t format, uint32_t size) +{ + int i; + + for (i = 0; i < g_tex_hot_n; i++) { + struct tex_hot_slot *h = &g_tex_hot[i]; + texture_entry *e; + + if (h->va != va || h->data != data || h->format != format || h->size != size) + continue; + if (h->idx < 0 || h->idx >= g_texture_count) + continue; + e = &g_textures[h->idx]; + if (e->host && e->va == va && e->data == data && e->format == format && e->size == size) + return e; + } + return NULL; +} + +/* Behind the hot list: a direct-mapped index of where each texture was + * last found, checked like the hot list is. A race binds a few hundred + * different textures a frame, more than the hot list holds, and each miss + * walked the whole cache. A stale slot fails the check and the walk runs. */ +#define TEX_INDEX 4096u +static int16_t g_tex_index[TEX_INDEX]; +static int g_tex_index_ready; + +static unsigned tex_index_slot(uint32_t va, uint32_t data, uint32_t format, uint32_t size) +{ + uint64_t k = ((uint64_t)va * 0x9E3779B97F4A7C15ull) ^ ((uint64_t)data * 0xC2B2AE3D27D4EB4Full) ^ + ((uint64_t)format * 0x165667B19E3779F9ull) ^ size; + k ^= k >> 29; + return (unsigned)(k ^ (k >> 32)) & (TEX_INDEX - 1u); +} + +static texture_entry *tex_index_find(uint32_t va, uint32_t data, uint32_t format, uint32_t size) +{ + int idx; + texture_entry *e; + + if (!g_tex_index_ready) + return NULL; + idx = g_tex_index[tex_index_slot(va, data, format, size)]; + if (idx < 0 || idx >= g_texture_count) + return NULL; + e = &g_textures[idx]; + if (e->host && e->va == va && e->data == data && e->format == format && e->size == size) + return e; + return NULL; +} + +static void tex_index_put(texture_entry *e) +{ + if (!g_tex_index_ready) { + memset(g_tex_index, 0xFF, sizeof g_tex_index); + g_tex_index_ready = 1; + } + g_tex_index[tex_index_slot(e->va, e->data, e->format, e->size)] = (int16_t)(e - g_textures); +} + +static void tex_hot_remember(texture_entry *e) +{ + int idx = (int)(e - g_textures); + int i, n; + struct tex_hot_slot h; + + tex_index_put(e); + h.idx = idx; + h.va = e->va; + h.data = e->data; + h.format = e->format; + h.size = e->size; + for (i = 0; i < g_tex_hot_n; i++) + if (g_tex_hot[i].idx == idx) + break; + n = i < g_tex_hot_n ? i : (g_tex_hot_n < TEX_HOT ? g_tex_hot_n : TEX_HOT - 1); + if (n == g_tex_hot_n) + g_tex_hot_n++; + if (n > 0) + memmove(&g_tex_hot[1], &g_tex_hot[0], (size_t)n * sizeof g_tex_hot[0]); + g_tex_hot[0] = h; +} + static IDirect3DTexture8 *host_texture(IDirect3DDevice8 *dev, uint32_t va) { unsigned long now = hle_d3d8_shadow_swaps(); @@ -842,6 +990,8 @@ static IDirect3DTexture8 *host_texture(IDirect3DDevice8 *dev, uint32_t va) uint32_t data = obj_field(va, 4), format = obj_field(va, 12), size = obj_field(va, 16); int i; + g_tex_resolved = NULL; + /* Memory the title rendered into as a bare surface: the host's drawing * is the content (hle_d3d8_render_surface). First, even over frame-buffer * memory: Forza borrows its idle front buffer as a 320x240 scratch target @@ -905,11 +1055,16 @@ static IDirect3DTexture8 *host_texture(IDirect3DDevice8 *dev, uint32_t va) return fb; } - for (i = 0; i < g_texture_count; i++) { - texture_entry *c = &g_textures[i]; - if (c->host && c->va == va && c->data == data && c->format == format && c->size == size) { - e = c; - break; + e = tex_hot_find(va, data, format, size); + if (!e) + e = tex_index_find(va, data, format, size); + if (!e) { + for (i = 0; i < g_texture_count; i++) { + texture_entry *c = &g_textures[i]; + if (c->host && c->va == va && c->data == data && c->format == format && c->size == size) { + e = c; + break; + } } } /* RECOMP_HLE_D3D8_TEX_PROBE=x: every lookup of a texture that size, @@ -972,6 +1127,8 @@ static IDirect3DTexture8 *host_texture(IDirect3DDevice8 *dev, uint32_t va) } } } + g_tex_resolved = e; + tex_hot_remember(e); return e->host; } @@ -1014,6 +1171,8 @@ static IDirect3DTexture8 *host_texture(IDirect3DDevice8 *dev, uint32_t va) g_uploads++; entry_count(e, &t); enforce_budget(now); /* this one was used now, so it stays */ + g_tex_resolved = e; + tex_hot_remember(e); return e->host; } @@ -1484,8 +1643,8 @@ static void upload_static_cube(IDirect3DCubeTexture8 *host, uint32_t phys, uint3 static IDirect3DCubeTexture8 *static_cube(IDirect3DDevice8 *dev, uint32_t va) { static int full_every = -1; - uint32_t common = HLE_MEM32(va + 0), data = HLE_MEM32(va + 4); - uint32_t format = HLE_MEM32(va + 12), size = HLE_MEM32(va + 16); + uint32_t common = obj_field(va, 0), data = obj_field(va, 4); + uint32_t format = obj_field(va, 12), size = obj_field(va, 16); uint32_t fmt = (format >> 8) & 0xFF, levels = (format >> 16) & 0xF; uint32_t edge = 1u << ((format >> 20) & 0xF); uint32_t phys = data & 0x0FFFFFFFu, chain, stride, total; @@ -1551,6 +1710,109 @@ static IDirect3DCubeTexture8 *static_cube(IDirect3DDevice8 *dev, uint32_t va) return g_static_cubes[i].host; } +/* D3D11 cannot sample BC1/BC3 as a 3D image. Expand each guest slice to + * RGBA before uploading, retaining the volume and its mip chain. */ +static IDirect3DVolumeTexture8 *static_volume(IDirect3DDevice8 *dev, uint32_t va) +{ + static struct { uint32_t data, format, hash; unsigned long checked, used; + IDirect3DVolumeTexture8 *host; } cache[512]; + static unsigned count; + uint32_t format = obj_field(va, 12), data = obj_field(va, 4); + uint32_t fmt = (format >> 8) & 255, levels = (format >> 16) & 15; + uint32_t w = 1u << ((format >> 20) & 15), h = 1u << ((format >> 24) & 15); + uint32_t d = 1u << ((format >> 28) & 15), phys = data & 0x0FFFFFFF; + uint64_t total = 0; + unsigned i, l; + unsigned long now = hle_d3d8_shadow_swaps(); + if (((format >> 4) & 15) != 3 || (fmt != D3DFMT_DXT5 && fmt != D3DFMT_DXT1) || !levels || + !data || obj_field(va, 16)) return NULL; + for (l = 0; l < levels; ++l) + total += (uint64_t)d3d8_row_pitch((D3DFORMAT)fmt, level_dim(w,l)) * + level_rows(fmt, level_dim(h,l)) * level_dim(d,l); + if ((uint64_t)phys + total > CONTIG_SIZE) return NULL; + /* Optional bounded guest-byte dump: compare decoding with the actual + * source slab, independently of the host capture's expanded copy. */ + { + static unsigned dumped; + static const char *prefix; + static int looked; + if(!looked){prefix=getenv("RECOMP_HLE_D3D8_VOLUME_DUMP");looked=1;} + if(prefix && *prefix && dumped<8) { + static uint32_t seen_data[8],seen_format[8]; + unsigned n; + for(n=0;nlpVtbl->GetTexture(dev,stage,&bound[stage]); + for (unsigned candidate=0;candidatelpVtbl->Release(bound[stage]); + if (victim==512) return NULL; + i=victim; + } + IDirect3DVolumeTexture8 *host = NULL; + if (FAILED(dev->lpVtbl->CreateVolumeTexture(dev,w,h,d,levels,0, + D3DFMT_LIN_A8B8G8R8,D3DPOOL_MANAGED,&host)) || !host) return NULL; + if (ilpVtbl->LockBox(cache[i].host,l,&box,NULL,0))) { + free(rgba); return cache[i].host; + } + for (uint32_t z=0;zlpVtbl->UnlockBox(cache[i].host,l); + free(rgba); src+=(size_t)slice*ld; + } + cache[i].hash=hash; + return cache[i].host; +} + /* The host render target for a 2D guest texture the title renders into, for * hle_d3d8.c's SetRenderTarget. The same cache entry SetTexture finds, so a * texture drawn into and then bound samples what was drawn. A texture already @@ -1606,6 +1868,7 @@ IDirect3DTexture8 *hle_d3d8_render_texture(IDirect3DDevice8 *dev, uint32_t va) e->format = format; e->size = size; e->rendered = 1; + g_rendered_gen++; e->checked_swap = e->used_swap = now; fprintf(stderr, "[HLE-D3D8] shadow render target texture 0x%08X: format 0x%02X " "%ux%u, %u level(s)\n", va, t.fmt, t.width, t.height, t.levels); @@ -1665,6 +1928,7 @@ IDirect3DTexture8 *hle_d3d8_render_surface(IDirect3DDevice8 *dev, uint32_t surfa e->format = format; e->size = size; e->rendered = 1; + g_rendered_gen++; e->checked_swap = e->used_swap = e->drawn_swap = now; fprintf(stderr, "[HLE-D3D8] shadow render target surface 0x%08X: format 0x%02X %ux%u " "at 0x%08X, sampled by any texture over that memory\n", @@ -1678,8 +1942,9 @@ static void rendered_surface_drawn(uint32_t data, uint32_t format, uint32_t size { int i; - for (i = 0; i < g_texture_count; i++) { - texture_entry *c = &g_textures[i]; + rendered_list_refresh(); + for (i = 0; i < g_rendered_n; i++) { + texture_entry *c = &g_textures[g_rendered_list[i]]; if (c->host && c->rendered && c->va == 0 && c->data == data && same_pixels(c->format, c->size, format, size)) c->drawn_swap = now; @@ -1693,8 +1958,9 @@ static int rendered_surface_recent(uint32_t data, uint32_t format, uint32_t size { int i; - for (i = 0; i < g_texture_count; i++) { - texture_entry *c = &g_textures[i]; + rendered_list_refresh(); + for (i = 0; i < g_rendered_n; i++) { + texture_entry *c = &g_textures[g_rendered_list[i]]; if (c->host && c->rendered && c->va == 0 && c->data == data && same_pixels(c->format, c->size, format, size)) return now - c->drawn_swap <= 2u; @@ -1709,9 +1975,15 @@ static IDirect3DTexture8 *rendered_surface_for(uint32_t data, uint32_t format, u { int i; - for (i = 0; i < g_texture_count; i++) { - texture_entry *c = &g_textures[i]; - if (c->host && c->rendered && c->va == 0 && c->data == data && + rendered_list_refresh(); + for (i = 0; i < g_rendered_n; i++) { + texture_entry *c = &g_textures[g_rendered_list[i]]; + /* Depth images may be rendered through a parent texture and sampled + * through another container. Match their pixels, not container identity. + * Keep colour aliasing limited to the existing bare-surface path. */ + if (c->host && c->rendered && + (c->va == 0 || d3d8_format_is_depth((D3DFORMAT)((format >> 8) & 0xFFu))) && + (c->data & 0x0FFFFFFFu) == (data & 0x0FFFFFFFu) && same_pixels(c->format, c->size, format, size)) { c->used_swap = now; return c->host; @@ -1801,10 +2073,44 @@ static void report(void) HLE_ORIGINAL(D3DDevice_SetTexture); static void shadow_set_texture(uint32_t stage, uint32_t texture); +/* Deferred binds carry the texture object's header as it was when the title + * bound it: Forza Motorsport points one object at its movie frame, at small + * glyph pages and at the screen within a single frame, and read when the + * queue ran, every bind saw the last of them -- the menu came out as one + * colour. The header is put back for the bind and the current one restored + * after. a[2] says whether a[3..8] hold it. */ +#define DEFER_TEX_WORDS 6 static void op_set_texture(const void *arg) { const uint32_t *a = (const uint32_t *)arg; - shadow_set_texture(a[0], a[1]); + uint32_t va = a[1], now[DEFER_TEX_WORDS]; + int k, swap = a[2] && va && !is_synth(va); + + if (swap) + for (k = 0; k < DEFER_TEX_WORDS; k++) { + now[k] = HLE_MEM32(va + 4u * k); + HLE_MEM32(va + 4u * k) = a[3 + k]; + } + shadow_set_texture(a[0], va); + if (swap) + for (k = 0; k < DEFER_TEX_WORDS; k++) + HLE_MEM32(va + 4u * k) = now[k]; +} + +static void defer_set_texture(uint32_t stage, uint32_t va) +{ + uint32_t a[3 + DEFER_TEX_WORDS]; + int k; + + memset(a, 0, sizeof a); + a[0] = stage; + a[1] = va; + if (va && !is_synth(va)) { + a[2] = 1; + for (k = 0; k < DEFER_TEX_WORDS; k++) + a[3 + k] = HLE_MEM32(va + 4u * k); + } + hle_d3d8_defer_op(op_set_texture, a, sizeof a); } /* HRESULT D3DDevice_SetTexture(DWORD Stage, IDirect3DBaseTexture8 *pTexture) */ @@ -1946,10 +2252,7 @@ HLE_EXPORT(D3DDevice_SetTexture) * executed, so the texels are read then -- as the NV2A reads them when it * reaches the draw, not when the title issued it. */ if (hle_d3d8_defer_recording()) { - uint32_t a[2]; - a[0] = stage; - a[1] = texture; - hle_d3d8_defer_op(op_set_texture, a, sizeof a); + defer_set_texture(stage, texture); } else { shadow_set_texture(stage, texture); } @@ -2052,7 +2355,7 @@ void hle_d3d8_texture_by_state(uint32_t stage, uint32_t data, uint32_t format) /* Not one SetTexture has bound: a swizzled or compressed texture is * described completely by its Format, so it is mirrored from that. */ if (!va && !d3d8_format_is_linear((D3DFORMAT)((format >> 8) & 0xFFu)) && - ((format >> 4) & 0xFu) == 2u && !(format & FORMAT_CUBEMAP)) + (((format >> 4) & 0xFu) == 2u || ((format >> 4) & 0xFu) == 3u) && !(format & FORMAT_CUBEMAP)) va = synth_key(data, format); if (!va) { if (g_switch_misses++ < 3) @@ -2063,17 +2366,14 @@ void hle_d3d8_texture_by_state(uint32_t stage, uint32_t data, uint32_t format) } g_switch_hits++; if (hle_d3d8_defer_recording()) { - uint32_t a[2]; - a[0] = stage; - a[1] = va; - hle_d3d8_defer_op(op_set_texture, a, sizeof a); + defer_set_texture(stage, va); } else { shadow_set_texture(stage, va); } } } -static void shadow_set_texture(uint32_t stage, uint32_t texture) +static void shadow_set_texture_profile_impl(uint32_t stage, uint32_t texture) { { IDirect3DDevice8 *dev = hle_d3d8_shadow_device(); @@ -2103,30 +2403,49 @@ static void shadow_set_texture(uint32_t stage, uint32_t texture) report(); return; } + IDirect3DVolumeTexture8 *volume = static_volume(dev, texture); + if (volume) { + g_bound[stage] = NULL; + g_bound_entry[stage] = NULL; + host_SetTexture(dev, stage, (IDirect3DBaseTexture8 *)volume); + g_bound_count++; + return; + } host = host_texture(dev, texture); } hle_d3d8_note_stage_texels(stage, texture ? obj_field(texture, 4) & 0x0FFFFFFFu : 0u); g_bound[stage] = host; g_bound_entry[stage] = NULL; + /* The entry behind the host texture, and for stage 0 whether this + * draw is one of the title's full-screen passes over its own frame: + * nothing else binds the frame buffer as stage 0, so it identifies + * them without matching a shader or a size. One pass over the cache + * for both; they were two, each up to every entry, at every bind. */ { - int i; - for (i = 0; host && i < g_texture_count; i++) - if (g_textures[i].host == host) { + int i, want_fb = stage == 0; + + /* host_texture just resolved this entry. The scan below is the + * path for a surface or a frame buffer it answered itself. */ + if (g_tex_resolved && g_tex_resolved->host == host) { + g_bound_entry[stage] = g_tex_resolved; + if (want_fb) + g_stage0_framebuffer = g_tex_resolved->framebuffer == FB_SCREEN; + } else { + if (want_fb) + g_stage0_framebuffer = 0; + for (i = 0; host && i < g_texture_count; i++) { + if (g_textures[i].host != host) + continue; + if (!g_bound_entry[stage]) g_bound_entry[stage] = &g_textures[i]; - break; - } - } - if (stage == 0) { - /* Whether this draw is one of the title's full-screen passes over - * its own frame. Nothing else binds the frame buffer as stage 0, - * so it identifies them without matching a shader or a size. */ - int i; - g_stage0_framebuffer = 0; - for (i = 0; host && i < g_texture_count; i++) - if (g_textures[i].host == host && g_textures[i].framebuffer == FB_SCREEN) { + if (want_fb && g_textures[i].framebuffer == FB_SCREEN) { g_stage0_framebuffer = 1; - break; + want_fb = 0; } + if (!want_fb) + break; + } + } } /* The host pixel shader samples every stage whatever its operation * (d3d8_shaders.c), and an unbound D3D11 slot reads as zero, so a @@ -2160,6 +2479,16 @@ static void shadow_set_texture(uint32_t stage, uint32_t texture) } } +extern long long d3d8_profile_clock(void); +extern void d3d8_profile_texture_end(long long start); +static void shadow_set_texture(uint32_t stage,uint32_t texture) +{ + long long start=d3d8_profile_clock(); + shadow_set_texture_profile_impl(stage,texture); + d3d8_profile_texture_end(start); +} + + /* Does the draw about to be made sample the title's own frame at stage 0? * * That is what its full-screen passes do and what nothing else does, so it @@ -2235,3 +2564,4 @@ HLE_EXPORT(D3DDevice_SetPalette) set_pal_data(stage, data); } } + diff --git a/src/hle/hle_d3d8_vertex.c b/src/hle/hle_d3d8_vertex.c index e71947ef7..d97713a35 100644 --- a/src/hle/hle_d3d8_vertex.c +++ b/src/hle/hle_d3d8_vertex.c @@ -332,7 +332,7 @@ static uint32_t g_pb_load; /* the NV2A's constant load pointer */ static float g_pb_cur[4]; static float g_pb_run[MAX_CONSTANT_REGISTERS][4]; static int g_pb_run_first = -1, g_pb_run_count; -static unsigned long g_pb_constants, g_pb_resyncs, g_pb_walks; +static unsigned long g_pb_constants, g_pb_resyncs, g_pb_walks, g_pb_fast; static void pb_flush(void) { @@ -469,6 +469,7 @@ static int pb_array_method(uint32_t method) * which are hardware state, not the title's: stored in the array the XDK * itself reads, they broke TimeSplitters 2's front end. */ int hle_d3d8_state_store_simple(uint32_t method, uint32_t value); +void hle_d3d8_state_store_cull(uint32_t enable, uint32_t face, uint32_t front); int hle_d3d8_title_ltcg(void); int hle_d3d8_simple_states_by_walk(void); /* below */ /* ...and the title's own clears: SET_ZSTENCIL_CLEAR_VALUE (0x1D8C), @@ -848,26 +849,59 @@ int hle_d3d8_pb_walking(void) static struct { uint32_t lo, hi, method, value; } g_simple_q[SIMPLE_QUEUE]; static uint32_t g_simple_n; static unsigned long g_simple_by_walk; +/* The preview loader and render thread both reach this queue. A concurrent + * flush used to reset the count between a match and its removal, underflowing + * the memmove length to almost 64 GiB. Keep the count and entries together. */ +static SRWLOCK g_simple_lock = SRWLOCK_INIT; int hle_d3d8_queue_simple_state(uint32_t lo, uint32_t hi, uint32_t method, uint32_t value) { - if (!hle_d3d8_simple_states_by_walk() || g_simple_n >= SIMPLE_QUEUE) + if (!hle_d3d8_simple_states_by_walk()) return 0; + AcquireSRWLockExclusive(&g_simple_lock); + if (g_simple_n >= SIMPLE_QUEUE) { + ReleaseSRWLockExclusive(&g_simple_lock); + return 0; + } g_simple_q[g_simple_n].lo = lo; g_simple_q[g_simple_n].hi = hi; g_simple_q[g_simple_n].method = method; g_simple_q[g_simple_n].value = value; g_simple_n++; g_simple_by_walk++; + ReleaseSRWLockExclusive(&g_simple_lock); return 1; } /* A simple-state method the walk met at addr: stored if an API call put it * there. A range that wrapped round the buffer has hi below lo. */ -static void walk_simple_state(uint32_t addr, uint32_t method) +/* Face culling the push buffer sets (hle_d3d8_state_store_cull). The + * hardware's defaults until a buffer says otherwise. */ +static uint32_t g_cull_enable, g_cull_face = 0x405u, g_cull_front = 0x901u; + +#define PB_CULL_ENABLE 0x0308u +#define PB_CULL_FACE 0x039Cu +#define PB_FRONT_FACE 0x03A0u + +static void walk_cull_method(uint32_t method, uint32_t value) +{ + if (method == PB_CULL_ENABLE) + g_cull_enable = value != 0u; + else if (method == PB_CULL_FACE) + g_cull_face = value; + else if (method == PB_FRONT_FACE) + g_cull_front = value; + else + return; + pb_flush(); /* the draws before it keep theirs */ + hle_d3d8_state_store_cull(g_cull_enable, g_cull_face, g_cull_front); +} + +static int walk_simple_state(uint32_t addr, uint32_t method) { uint32_t i; + AcquireSRWLockExclusive(&g_simple_lock); for (i = 0; i < g_simple_n; i++) { uint32_t lo = g_simple_q[i].lo, hi = g_simple_q[i].hi; int in = lo <= hi ? (addr >= lo && addr < hi) : (addr >= lo || addr < hi); @@ -877,17 +911,40 @@ static void walk_simple_state(uint32_t addr, uint32_t method) (void)hle_d3d8_state_store_simple(method, g_simple_q[i].value); g_simple_n--; memmove(&g_simple_q[i], &g_simple_q[i + 1], (g_simple_n - i) * sizeof g_simple_q[0]); - return; + ReleaseSRWLockExclusive(&g_simple_lock); + return 1; } + ReleaseSRWLockExclusive(&g_simple_lock); + return 0; +} + +/* Polygon offset that no API call put in the buffer: a push buffer the title + * recorded itself sets it there, and only there. Forza Motorsport draws its + * billboards' adverts over a blank board in the same plane, pushed forward + * this way; without it the board won wherever its depth came out a hair + * nearer -- white wedges over the advert that changed every frame the + * camera moved. RECOMP_PB_POLYOFFSET=0 leaves them to the API alone. */ +static int pb_polyoffset_method(uint32_t m) +{ + static int on = -1; + + if (on < 0) { + const char *e = getenv("RECOMP_PB_POLYOFFSET"); + on = !(e && e[0] == '0'); + } + return on && (m == 0x0384u || m == 0x0388u || m == 0x0330u || m == 0x0334u || + m == 0x0338u); } static void flush_simple_states(void) { uint32_t i; + AcquireSRWLockExclusive(&g_simple_lock); for (i = 0; i < g_simple_n; i++) (void)hle_d3d8_state_store_simple(g_simple_q[i].method, g_simple_q[i].value); g_simple_n = 0; + ReleaseSRWLockExclusive(&g_simple_lock); } void hle_d3d8_push_constants_sync(void) @@ -895,6 +952,7 @@ void hle_d3d8_push_constants_sync(void) uint32_t device, put, va, ret = 0, words = 0; int jumps = 0; int inl = hle_d3d8_pb_inline_on(); + int trace, strace; /* Not re-entrant: an immediate-mode draw from the walk goes through * hle_d3d8_shadow_draw, which syncs before drawing. The walk in progress * is already applying everything in order, so the nested call has @@ -916,9 +974,84 @@ void hle_d3d8_push_constants_sync(void) g_pb_scan = put; return; } + /* Most syncs find the buffer where the last walk left it: a draw, a + * vertex, a texture call, and nothing was pushed between them. Parsing + * that empty span was most of this function's time in a race. Anything + * a finished walk still owes (a constant run, a simple state, a bind + * held past the put pointer) is flushed, which is what falling through + * the empty walk did. */ + if (put == g_pb_scan) { + if (g_pb_run_first >= 0) + pb_flush(); + flush_simple_states(); + hle_d3d8_texture_queue_flush(); + return; + } g_pb_walks++; walking = 1; + /* Read once: the swap does not move during a walk, and asking per method + * header was two calls each. */ + trace = pb_trace(); + strace = pb_surface_trace(); va = g_pb_scan; + /* A replaced call walks its own push so the scan stays level with put, + * then draws the vertices itself. When that span is only the methods + * the walk would ignore (inline vertices, its own begin/end, its own + * element list), the interpreter's per-word dispatch is the whole cost. + * Trace stays on the slow walk so a method log is not missing lines. */ + if (g_w_nodraw && inl && !trace && !strace && va <= put) { + uint32_t scan = va; + int pure = 1; + + while (scan != put) { + uint32_t w, count, method, n, next; + int noninc; + + if (!guest_readable(scan, 4u)) { + pure = 0; + break; + } + w = HLE_MEM32(scan); + if ((w & 0xE0000003u) == 0x20000000u || (w & 3u) == 1u || (w & 3u) == 2u || + w == 0x00020000u || (w & 0xA0030003u) != 0u) { + pure = 0; + break; + } + count = (w >> 18) & 0x7FFu; + method = w & 0x1FFCu; + if (count && !guest_readable(scan + 4u, 4ull * count)) { + pure = 0; + break; + } + noninc = (w & 0x40000000u) != 0u; + for (n = 0; n < count; n++) { + uint32_t m = noninc ? method : method + 4u * n; + + if (!(pb_inline_method(m) || m == PB_ELEM16 || m == PB_ELEM32 || + m == PB_DRAW_ARRAYS || m == PB_INLINE_ARRAY)) { + pure = 0; + break; + } + } + if (!pure) + break; + next = scan + 4u + 4u * count; + if (next < scan || next > put) { + pure = 0; + break; + } + scan = next; + } + if (pure && scan == put) { + g_pb_fast++; + pb_flush(); + flush_simple_states(); + hle_d3d8_texture_queue_flush(); + g_pb_scan = put; + walking = 0; + return; + } + } while (va != put) { uint32_t w, count, method, i; @@ -930,7 +1063,7 @@ void hle_d3d8_push_constants_sync(void) uint32_t target = (w & 3u) == 1u ? (w & 0xFFFFFFFCu) : (w & 0x1FFFFFFCu); if (++jumps > PB_MAX_JUMPS) goto resync; - if (pb_trace()) + if (trace) fprintf(stderr, "[PB swap %lu] 0x%08X jump -> 0x%08X\n", hle_d3d8_shadow_swaps(), va, CONTIG_BASE | (target & 0x0FFFFFFFu)); va = CONTIG_BASE | (target & 0x0FFFFFFFu); @@ -941,7 +1074,7 @@ void hle_d3d8_push_constants_sync(void) goto resync; ret = va + 4u; va = CONTIG_BASE | ((w & 0xFFFFFFFCu) & 0x0FFFFFFFu); - if (pb_trace()) + if (trace) fprintf(stderr, "[PB swap %lu] call -> 0x%08X\n", hle_d3d8_shadow_swaps(), va); continue; } @@ -958,7 +1091,7 @@ void hle_d3d8_push_constants_sync(void) method = w & 0x1FFCu; if (inl) hle_d3d8_texture_queue_reach(va); - if (pb_trace()) { + if (trace) { uint32_t k; for (k = 0; k < count && k < 64u && guest_readable(va + 4u + 4u * k, 4u); k++) fprintf(stderr, "[PB swap %lu] 0x%08X method 0x%04X = 0x%08X%s\n", @@ -990,7 +1123,7 @@ void hle_d3d8_push_constants_sync(void) } } } - if (pb_surface_trace() && method + 4u * count > 0x0200u && method < 0x0220u) { + if (strace && method + 4u * count > 0x0200u && method < 0x0220u) { uint32_t k; for (k = 0; k < count && guest_readable(va + 4u + 4u * k, 4u); k++) fprintf(stderr, "[PB-SURF swap %lu] method 0x%04X = 0x%08X%s\n", @@ -1004,23 +1137,45 @@ void hle_d3d8_push_constants_sync(void) (method + 4u * count > PB_ARRAYS_FIRST && method < PB_ARRAYS_END) || (method >= PB_ELEM16 && method <= PB_INLINE_ARRAY) || (method + 4u * count > PB_SIMPLE_FIRST && method < PB_SIMPLE_END) || + (method + 4u * count > PB_CULL_FACE && method <= PB_FRONT_FACE) || (method + 4u * count > PB_CLEAR_FIRST && method < PB_CLEAR_END) || method == PB_SWATH))) { int noninc = (w & 0x40000000u) != 0u; if (!guest_readable(va + 4u, 4ull * count)) goto resync; + /* A run of vertex shader constants, which is most of what a race + * pushes: straight to the four-word register, without asking + * each word about every other kind of method. The same as the + * loop below gives them, by way of pb_method. */ + if (!noninc && method >= PB_CONST_FIRST && method + 4u * count <= PB_CONST_END) { + for (i = 0; i < count; i++) { + uint32_t comp = ((method + 4u * i - PB_CONST_FIRST) / 4u) & 3u; + uint32_t v = HLE_MEM32(va + 4u + 4u * i); + + memcpy(&g_pb_cur[comp], &v, sizeof v); + if (comp == 3u) + pb_constant(g_pb_load++, g_pb_cur); + } + } else for (i = 0; i < count; i++) { uint32_t m = noninc ? method : method + 4u * i; uint32_t v = HLE_MEM32(va + 4u + 4u * i); - if (inl && ((m >= PB_SIMPLE_FIRST && m < PB_SIMPLE_END) || m == PB_SWATH)) { + if (inl && (m == PB_CULL_ENABLE || m == PB_CULL_FACE || m == PB_FRONT_FACE)) { + walk_cull_method(m, v); + } else if (inl && m >= PB_SIMPLE_END && m < PB_FRONT_FACE + 4u) { + /* between the simple states and the cull methods: not ours */ + } else if (inl && ((m >= PB_SIMPLE_FIRST && m < PB_SIMPLE_END) || m == PB_SWATH)) { if (!g_w_nodraw && hle_d3d8_title_ltcg()) (void)hle_d3d8_state_store_simple(m, v); - else - walk_simple_state(va + 4u + 4u * i, m); + else if (!walk_simple_state(va + 4u + 4u * i, m) && + !g_w_nodraw && pb_polyoffset_method(m)) { + pb_flush(); + (void)hle_d3d8_state_store_simple(m, v); + } } else if (inl && m >= PB_CLEAR_FIRST && m < PB_CLEAR_END) { g_clear_reg[(m - PB_CLEAR_FIRST) / 4u] = v; - if (pb_surface_trace()) + if (strace) fprintf(stderr, "[PB-SURF swap %lu] method 0x%04X = 0x%08X%s\n", hle_d3d8_shadow_swaps(), m, v, g_w_nodraw ? " (replaced call)" : ""); @@ -1150,7 +1305,8 @@ static void report(void) g_skip_no_stream, g_skip_range, g_push_draws, g_push_failed); if (g_pb_walks) fprintf(stderr, "[HLE-D3D8] push buffer constants: %lu applied over %lu walks, " - "%lu resync(s)\n", g_pb_constants, g_pb_walks, g_pb_resyncs); + "%lu of them only the call's own vertices, %lu resync(s)\n", + g_pb_constants, g_pb_walks, g_pb_fast, g_pb_resyncs); last = now; } } @@ -1271,6 +1427,37 @@ static void wait_point(const char *name, int *seen) } } +/* Optional timing at the wait boundary. Parent waits may contain nested + * calls, so the per-function totals must not be added together. */ +struct wait_profile {const char *name;long long since,sync,body,max;unsigned long calls,slow;}; +static struct wait_profile g_wait_profile[5]={ + {"CDevice_KickOff"},{"KickOffAndWaitForIdle"},{"BlockOnTime"}, + {"BlockOnResource"},{"ResourceBlockUntilNotBusy"}}; +static long long wait_profile_clock(void) +{ + static int enabled=-1; + if(enabled<0){const char *v=getenv("RECOMP_WAIT_PROFILE");enabled=v && v[0]=='1';} + if(!enabled)return 0; + LARGE_INTEGER t;QueryPerformanceCounter(&t);return t.QuadPart; +} +static void wait_profile_note(unsigned slot,long long start,long long middle) +{ + if(!start)return; + LARGE_INTEGER frequency;QueryPerformanceFrequency(&frequency); + long long end=wait_profile_clock(),body=end-middle; + struct wait_profile *p=&g_wait_profile[slot]; + if(!p->since)p->since=start; + p->sync+=middle-start;p->body+=body;++p->calls; + if(body>p->max)p->max=body; + if(body>frequency.QuadPart/10000)++p->slow; + if(end-p->since>=frequency.QuadPart*5){ + double ms=1000.0/(double)frequency.QuadPart; + fprintf(stderr,"[WAIT-PROFILE] %s over %.2fs: %lu calls, sync %.3f ms, original %.3f ms, max %.3f ms, %lu original calls over100us\n", + p->name,(end-p->since)*ms/1000.0,p->calls,p->sync*ms,p->body*ms,p->max*ms,p->slow); + p->since=end;p->sync=p->body=p->max=0;p->calls=p->slow=0; + } +} + HLE_ORIGINAL(CDevice_KickOff); HLE_ORIGINAL(D3D_KickOffAndWaitForIdle); HLE_ORIGINAL(D3D_BlockOnTime); @@ -1279,42 +1466,57 @@ HLE_ORIGINAL(D3DResource_BlockUntilNotBusy); HLE_EXPORT(CDevice_KickOff) { + long long start=wait_profile_clock(); static int seen; wait_point("CDevice_KickOff", &seen); + long long middle=start?wait_profile_clock():0; if (hle_original_CDevice_KickOff) HLE_CALL_ORIGINAL(CDevice_KickOff); + wait_profile_note(0,start,middle); } HLE_EXPORT(D3D_KickOffAndWaitForIdle) { + long long start=wait_profile_clock(); static int seen; wait_point("D3D_KickOffAndWaitForIdle", &seen); + long long middle=start?wait_profile_clock():0; if (hle_original_D3D_KickOffAndWaitForIdle) HLE_CALL_ORIGINAL(D3D_KickOffAndWaitForIdle); + wait_profile_note(1,start,middle); } HLE_EXPORT(D3D_BlockOnTime) { + long long start=wait_profile_clock(); static int seen; wait_point("D3D_BlockOnTime", &seen); + long long middle=start?wait_profile_clock():0; if (hle_original_D3D_BlockOnTime) HLE_CALL_ORIGINAL(D3D_BlockOnTime); + wait_profile_note(2,start,middle); } HLE_EXPORT(D3D_BlockOnResource) { + long long start=wait_profile_clock(); static int seen; wait_point("D3D_BlockOnResource", &seen); + long long middle=start?wait_profile_clock():0; if (hle_original_D3D_BlockOnResource) HLE_CALL_ORIGINAL(D3D_BlockOnResource); + wait_profile_note(3,start,middle); } HLE_EXPORT(D3DResource_BlockUntilNotBusy) { + long long start=wait_profile_clock(); static int seen; wait_point("D3DResource_BlockUntilNotBusy", &seen); + long long middle=start?wait_profile_clock():0; if (hle_original_D3DResource_BlockUntilNotBusy) HLE_CALL_ORIGINAL(D3DResource_BlockUntilNotBusy); + wait_profile_note(4,start,middle); } /* A replaced draw with no stream buffer to read: its stream setup was diff --git a/src/hle/hle_dsound.c b/src/hle/hle_dsound.c index 9d0692a71..908eea2a2 100644 --- a/src/hle/hle_dsound.c +++ b/src/hle/hle_dsound.c @@ -84,7 +84,8 @@ enum { TAG_ADPCM = 0x0069u, SLOTS = 256u, - PRE_ROLL_MS = 50u, /* silence ahead of a buffer's sound; see prime() */ + PRE_ROLL_MS = 50u, /* silence ahead of a buffer's first sound; see prime() */ + LEAD_MS = 80u, /* real audio re-queued when a looping voice ran dry */ SERVICE_PERIOD_US = 5000, /* the service thread's cadence; see service_thread() */ MAX_NOTIFY = 64u, /* positions per buffer */ @@ -333,6 +334,10 @@ static void prime(Buffer *b, uint32_t rate, uint32_t played_rate, uint32_t chann if (!recomp_audio_output_position(slot_of(b), &played, &queued) || queued != 0u) return; b->dry++; + /* The voice ran out. Another block of silence would be a hole in the + * sound every time it happens. pump() has already lengthened this + * slice with the loop itself, which is what gets submitted next. */ + return; } if (bytes == 0u || bytes > sizeof silence) return; @@ -574,6 +579,31 @@ static void notify_stop(Buffer *b, uint64_t now, int stop) } } +/* XAUDIO2_MAX_FREQ_RATIO. A mixed voice is created at the buffer's original + * rate and will not play outside this multiple of it. */ +enum { VOICE_FREQ_RATIO_MAX = 1024u }; + +/* The sample rate the host voice consumes when asked for `want` against a + * voice created at `original`. */ +static uint32_t voice_playable_hz(uint32_t original, uint32_t want) +{ + uint64_t lo, hi; + + if (original == 0u || want == 0u) + return want; + lo = (uint64_t)original / (uint64_t)VOICE_FREQ_RATIO_MAX; + if (lo < 1u) + lo = 1u; + hi = (uint64_t)original * (uint64_t)VOICE_FREQ_RATIO_MAX; + if (hi > 200000u) + hi = 200000u; + if ((uint64_t)want < lo) + return (uint32_t)lo; + if ((uint64_t)want > hi) + return (uint32_t)hi; + return want; +} + /* Send the samples the output clock has consumed since the last pump. */ static void pump(Buffer *b, uint64_t now) { @@ -618,6 +648,40 @@ static void pump(Buffer *b, uint64_t now) bytes = recomp_dsound_buffer_consume(out, now, &offset); if (bytes == 0u || b->data == 0u || bytes > sizeof pcm) return; + /* A looping voice with nothing left queued plays a hole until the next + * pump. Silence in that hole is the stutter. Read further around the + * loop so this submit covers LEAD_MS, and let the copy below leave the + * cursor at the end of what it read. */ + int refilled = 0; + if ((out->play_flags & RECOMP_DSOUND_PLAY_LOOPING) && b->primed) { + uint32_t queued = 0u; + uint32_t frame = adpcm ? channels * 2u : channels * (bits / 8u); + + if (frame && + recomp_audio_output_position(slot_of(b), NULL, &queued) && + queued == 0u) { + uint32_t lead = (uint32_t)((uint64_t)out->sample_rate / 1000u * (uint64_t)LEAD_MS * frame); + uint32_t room = out->size_bytes / frame * frame; + static int said; + + if (lead > (uint32_t)sizeof pcm) + lead = (uint32_t)sizeof pcm / frame * frame; + if (room && lead > room) + lead = room; + if (lead > bytes) { + if (said < 4) { + uint32_t rate = out->sample_rate ? out->sample_rate : 1u; + said++; + fprintf(stderr, "[DSOUND] slot %u refilling a dry loop: %u bytes " + "(%u ms) at %u Hz, queuing %u bytes (%u ms)\n", + slot_of(b), bytes, bytes / frame * 1000u / rate, + out->sample_rate, lead, LEAD_MS); + } + bytes = lead; + refilled = 1; + } + } + } if (adpcm) { uint32_t block_bytes = XBOX_ADPCM_BLOCK_BYTES * channels; @@ -652,6 +716,8 @@ static void pump(Buffer *b, uint64_t now) if (offset == out->size_bytes) offset = out->loop_start_bytes; } } + if (refilled) + out->cursor_bytes = offset; { int32_t volume = (int32_t)setting(b->iface, SET_VOLUME); RecompAudioMix mix; @@ -692,16 +758,32 @@ static void pump(Buffer *b, uint64_t now) recomp_audio_output_submit(slot_of(b), pcm, bytes, rate, channels, bits, volume); /* The Doppler shift changes how fast the buffer is consumed, as it * does on the console -- from now: after this slice was taken, or - * the output clock would step over it. */ + * the output clock would step over it. The host voice was created at + * the buffer's original rate and only plays within + * VOICE_FREQ_RATIO_MAX of that (audio_output_xaudio2.cpp). The title's + * cursor follows the pitch that was asked for; the output clock + * follows the rate the voice will actually consume, so a loop is not + * submitted slower than it is played. */ if (b->mixed) { uint32_t asked = b->asked_rate ? b->asked_rate : b->model.original_sample_rate; + uint32_t original = out->original_sample_rate; uint32_t want = (uint32_t)lrintf((float)asked * doppler); + uint32_t play; + static int said_pitch; if (want < 1000u) want = 1000u; if (want > 200000u) want = 200000u; - if (want != b->model.sample_rate) { - (void)recomp_dsound_buffer_set_frequency(&b->model, want, now); - (void)recomp_dsound_buffer_set_frequency(&b->output_model, want, now); + play = voice_playable_hz(original, want); + if (said_pitch < 6 && (play != want || want <= 1000u)) { + said_pitch++; + fprintf(stderr, "[DSOUND] slot %u pitch: asked %u Hz x doppler %.4f " + "-> %u Hz, original %u Hz, voice plays %u Hz\n", + slot_of(b), asked, (double)doppler, want, original, play); + fflush(stderr); } + if (want != b->model.sample_rate) + (void)recomp_dsound_buffer_set_frequency(&b->model, want, now); + if (play != out->sample_rate) + (void)recomp_dsound_buffer_set_frequency(out, play, now); } /* The release has run out: the buffer stops, as the hardware stops * the voice. */ @@ -1765,8 +1847,12 @@ static uint32_t set_rate(uint32_t iface, uint32_t frequency) pump(b, now); b->asked_rate = frequency; result = recomp_dsound_buffer_set_frequency(&b->model, frequency, now); - if (result == RECOMP_DSOUND_OK && b->model.sample_rate != before) - (void)recomp_dsound_buffer_set_frequency(&b->output_model, frequency, now); + if (result == RECOMP_DSOUND_OK && b->model.sample_rate != before) { + uint32_t original = b->output_model.original_sample_rate; + uint32_t play = voice_playable_hz(original ? original : frequency, + b->model.sample_rate); + (void)recomp_dsound_buffer_set_frequency(&b->output_model, play, now); + } } unlock(); return result; diff --git a/src/hle/hle_dsound_stream.c b/src/hle/hle_dsound_stream.c index d0c3af93c..a00daca31 100644 --- a/src/hle/hle_dsound_stream.c +++ b/src/hle/hle_dsound_stream.c @@ -464,6 +464,71 @@ static Stream *find_by_object(uint32_t object) return best; } +/* Called after a successful guest SetFormat by a title binding. The format + * captured at CreateSoundStream is only the initial one: Forza creates PCM + * 48 kHz streams and then changes them before supplying the first packet. + * Keeping the initial format plays 44.1 kHz music too fast and reads ADPCM + * effect packets as PCM noise. Preserve the guest's own setter and update + * only the host side, once its old packets have all been drained. */ +void hle_dsound_stream_format_changed(uint32_t object, uint32_t wfx) +{ + Stream *s; + uint32_t tag, channels, rate, align, bits; + static unsigned reported; + + if (!wfx || !guest_readable(wfx, 16u)) + return; + tag = HLE_MEM32(wfx) & 0xFFFFu; + channels = HLE_MEM32(wfx) >> 16; + rate = HLE_MEM32(wfx + 4u); + align = HLE_MEM32(wfx + 12u) & 0xFFFFu; + bits = HLE_MEM32(wfx + 12u) >> 16; + if (!channels || !rate || !align) + return; + + lock(); + s = find_by_iface(object); + if (!s) s = find_by_object(object); + if (!s) { + unlock(); + return; + } + if (s->count) { + /* Reinterpreting packets already counted in the old format would + * corrupt completion positions. A setter on a busy stream needs + * the title to flush it first; retain the evidence rather than + * silently throwing its packets away. */ + fprintf(stderr, "[DSOUND] SetFormat stream %08X not applied: %d packets pending\n", + s->iface, s->count); + unlock(); + return; + } + if (s->tag != tag || s->channels != channels || s->rate != rate || + s->align != align || s->bits != bits) { + release_voice(s); + s->tag = tag; + s->channels = channels; + s->rate = rate; + s->align = align; + s->bits = bits; + s->output = channels >= 1u && channels <= 2u && + rate >= 1000u && rate <= 200000u && + ((tag == TAG_PCM && (bits == 8u || bits == 16u) && + align == channels * (bits / 8u)) || + (tag == TAG_ADPCM && bits == 4u && + align == XBOX_ADPCM_BLOCK_BYTES * channels)); + s->consumed_base = s->sent = s->queued; + s->play_origin = s->queued; + s->play_anchored = 0; + s->resumed_ms = 0; + if (reported++ < 32u) + fprintf(stderr, "[DSOUND] SetFormat stream %08X: tag %04X %u ch %u Hz " + "%u bit align %u, %s\n", s->iface, tag, channels, rate, + bits, align, s->output ? "output" : "clock only"); + } + unlock(); +} + /* Complete one packet: status, size, then the callback or the event. The * callback is LPFNXMEDIAOBJECTCALLBACK, __stdcall (pStreamContext, * pPacketContext, dwStatus); its `ret 12` pops the arguments and the dummy diff --git a/src/kernel/CMakeLists.txt b/src/kernel/CMakeLists.txt index ec23cb96e..12a1ef833 100644 --- a/src/kernel/CMakeLists.txt +++ b/src/kernel/CMakeLists.txt @@ -75,6 +75,11 @@ if(MSVC) target_compile_definitions(xbox_kernel PRIVATE _CRT_SECURE_NO_WARNINGS) else() target_compile_options(xbox_kernel PRIVATE -Wall -Wextra -Wno-unused-parameter -O2) + if(WIN32) + # The clang driver on Windows: same reason as /Zi above. Without it a + # sampled profile credits nv2a_ack_thread to xbox_GetMappingHandle. + target_compile_options(xbox_kernel PRIVATE -g -gcodeview) + endif() endif() target_compile_definitions(xbox_kernel PRIVATE $<$:_DEBUG>) diff --git a/src/kernel/kernel_bridge.c b/src/kernel/kernel_bridge.c index 980c30c86..644b89f81 100644 --- a/src/kernel/kernel_bridge.c +++ b/src/kernel/kernel_bridge.c @@ -2407,6 +2407,8 @@ static void bridge_NtPulseEvent(void) */ static HANDLE bridge_resolve_handle(uint32_t token); +static volatile LONG s_long_wait_lines; + static void bridge_NtWaitForSingleObjectEx(void) { HANDLE handle = bridge_resolve_handle(STACK_ARG(0)); @@ -2423,6 +2425,38 @@ static void bridge_NtWaitForSingleObjectEx(void) if (bridge_wait_on_self(STACK_ARG(0), alertable, timeout_ptr)) return; + /* A wait with no end that does not end says nothing on its own: a hang + * shows as a still picture and a log that stops. So an infinite wait, + * not alertable, is made in ten-second pieces, and each piece that + * runs out says which object and from where (the guest stack above the + * arguments: the wrapper's and its callers' return addresses) -- then + * waits on, exactly as before. Forza froze once on the way into its + * Arcade menu with nothing in the log to say what it waited for. */ + if (!timeout_ptr && !alertable && handle) { + DWORD r, waited = 0; + for (;;) { + r = WaitForSingleObjectEx(handle, 10000, FALSE); + if (r != WAIT_TIMEOUT) + break; + waited += 10; + /* Idle workers wait like this too, for work that does not come: + * a cap keeps them from filling the log. */ + if (InterlockedIncrement(&s_long_wait_lines) <= 300) { + uint32_t k; + fprintf(stderr, " [KERNEL] still waiting after %lus: token=0x%08X " + "handle=%p thread %lu; guest stack from 0x%08X:", + (unsigned long)waited, STACK_ARG(0), handle, + (unsigned long)GetCurrentThreadId(), g_esp); + for (k = 0; k < 24; k++) + fprintf(stderr, " %08X", (uint32_t)BRIDGE_MEM32(g_esp + 16 + k * 4)); + fprintf(stderr, "\n"); + fflush(stderr); + } + } + g_eax = r == WAIT_OBJECT_0 ? 0u + : r == WAIT_ABANDONED_0 ? 0x00000080u /* STATUS_ABANDONED */ + : 0xC0000001u; + } else g_eax = (uint32_t)xbox_NtWaitForSingleObjectEx( handle, (KPROCESSOR_MODE)wait_mode, (BOOLEAN)alertable, XBOX_TO_NATIVE(timeout_ptr)); @@ -4681,8 +4715,14 @@ static void bridge_NtReadFile(void) /* What a read actually delivered. A decoder that rejects its input cannot * say whether the bytes were wrong or the read was, and the two look - * identical from inside the title -- the first bytes settle it. */ - { + * identical from inside the title -- the first bytes settle it. + * + * RECOMP_READ_LOG=1 only: a streaming title reads all race long, and two + * lines and an fflush per read cost Forza about 1 ms a frame. */ + static int read_log = -1; + if (read_log < 0) + read_log = xbox_EnvSwitch("RECOMP_READ_LOG", 0); + if (read_log) { const uint8_t *p = (const uint8_t *)XBOX_TO_NATIVE(buffer_va); uint32_t got = (uint32_t)ios.Information; /* The offset matters as much as the length. A title streaming a pack @@ -4779,9 +4819,10 @@ static void bridge_NtReadFile(void) * loader thread with exit status 0x103, which is STATUS_PENDING, so * whether this call is the source of that is exactly the question * the log has to be able to answer. */ - fprintf(stderr, " [READ] async: event=0x%08X apc=0x%08X -> 0x%08X%s\n", - STACK_ARG(1), STACK_ARG(2), g_eax, - g_eax == 0x00000103u ? " STATUS_PENDING" : ""); + if (read_log) + fprintf(stderr, " [READ] async: event=0x%08X apc=0x%08X -> 0x%08X%s\n", + STACK_ARG(1), STACK_ARG(2), g_eax, + g_eax == 0x00000103u ? " STATUS_PENDING" : ""); /* RECOMP_FILE_SYNC=1 reports the read finished, for a title whose * loader does not come back for the result. Burnout 2 needs the * opposite -- its stream reader only accepts a short count on the diff --git a/src/kernel/xbox_memory_layout.c b/src/kernel/xbox_memory_layout.c index 629ad787b..02ca0347c 100644 --- a/src/kernel/xbox_memory_layout.c +++ b/src/kernel/xbox_memory_layout.c @@ -154,6 +154,26 @@ static void *g_contig_memory = NULL; #define XBOX_NV2A_SIZE (16u * 1024u * 1024u) static void *g_nv2a_memory = NULL; +/* Acknowledgement at the title's known PFB flush polling point. + * This clears the same busy bit as NV2A_ACK, after its writer has set it; + * no interrupt, FIFO pointer, or other register bit is changed. On by + * default: without it every CDevice_KickOff spins until the ack thread's + * next pass, about 4 ms a frame in a Forza race (RECOMP_SAMPLE, 30 Oct + * 2026). RECOMP_PFB_INLINE_ACK=0 turns it off. */ +void xbox_Nv2aPfbAck(uint32_t guest_base) +{ + static int enabled=-1; + if(enabled<0){const char *v=getenv("RECOMP_PFB_INLINE_ACK");enabled=!(v&&v[0]=='0');} + if(!enabled || guest_base!=XBOX_NV2A_BASE || !g_nv2a_memory)return; + volatile LONG *reg=(volatile LONG *)((uint8_t *)g_nv2a_memory+0x100410); + LONG previous=InterlockedAnd(reg,~(LONG)0x00010000); + if(previous&0x00010000){ + static unsigned long acknowledged; + if(!acknowledged++)fprintf(stderr,"[PFB-ACK] inline flush acknowledgement active\n"); + } +} + + /* MCPX southbridge register span: APU 0xFE800000 through NIC 0xFEF00000. */ #define XBOX_MCPX_BASE 0xFE800000u #define XBOX_MCPX_SIZE (8u * 1024u * 1024u) @@ -2296,6 +2316,20 @@ static void fence_mirrors_tick(void) *fence = put; g_ack_activity++; } + /* The GPU's own copy of that time. D3D_BlockOnTime with a + * nonzero second argument (Forza's KickOffAndWaitForIdle) also + * spins until bits 2..6 of PGRAPH 0x400B10 equal the completed + * time << 2. Nothing set it, so the wait passed only when the + * time happened to be a multiple of 32: Forza hung at random + * loading a race, at "Updating scoreboards" and at simulated + * qualifying, spinning in sub_003BD220 (30 Oct 2026). */ + if (g_nv2a_memory) { + volatile uint32_t *pg = + (volatile uint32_t *)((uint8_t *)g_nv2a_memory + 0x400B10); + uint32_t want = (put << 2) & 0x7Cu; + if ((*pg & 0x7Cu) != want) + *pg = (*pg & ~0x7Cu) | want; + } } } } @@ -3077,19 +3111,37 @@ static uint32_t *s_watchdog_esp; static uint32_t *s_watchdog_regs[6]; static unsigned s_watchdog_secs; -static DWORD WINAPI xbox_watchdog_thread(LPVOID unused) +/* RECOMP_WATCHDOG_STALL=: the same picture, but of the thread that + * presents frames, once it has presented none for that many seconds -- and + * the title runs on. A frozen screen with music still playing (Forza + * Motorsport's white screen on the way into a race) is a render thread that + * stopped; when it stopped is not known in advance, so a fixed time is no + * use. Each present hands its thread's registers over + * (xbox_WatchdogNotePresent). */ +static volatile LONG s_watchdog_presents; +static unsigned s_watchdog_stall; + +void xbox_WatchdogNotePresent(void) { - const uint8_t *mem; - uint32_t esp, i; + if (!s_watchdog_stall) + return; + s_watchdog_esp = &g_esp; + s_watchdog_regs[0] = &g_eax; s_watchdog_regs[1] = &g_ecx; + s_watchdog_regs[2] = &g_edx; s_watchdog_regs[3] = &g_ebx; + s_watchdog_regs[4] = &g_esi; s_watchdog_regs[5] = &g_edi; + InterlockedIncrement(&s_watchdog_presents); +} - (void)unused; - Sleep(s_watchdog_secs * 1000u); +static void xbox_watchdog_dump(const char *why, unsigned secs) +{ + const uint8_t *mem; + uint32_t esp, i, top; mem = (const uint8_t *)g_memory_offset; esp = s_watchdog_esp ? *s_watchdog_esp : 0; - fprintf(stderr, "[WATCHDOG] no exit after %us; guest esp=0x%08X\n" + fprintf(stderr, "[WATCHDOG] %s after %us; guest esp=0x%08X\n" " regs: eax=%08X ecx=%08X edx=%08X ebx=%08X esi=%08X edi=%08X\n", - s_watchdog_secs, esp, + why, secs, esp, s_watchdog_regs[0] ? *s_watchdog_regs[0] : 0, s_watchdog_regs[1] ? *s_watchdog_regs[1] : 0, s_watchdog_regs[2] ? *s_watchdog_regs[2] : 0, @@ -3143,22 +3195,70 @@ static DWORD WINAPI xbox_watchdog_thread(LPVOID unused) * is as much a hang question as a crash one (Max Payne's heap spin). */ xbox_watch_scan_on_crash(); + /* A thread the title created has its stack elsewhere + * (xbox_AllocThreadStack): read on to the end of guest memory there. */ + top = (esp >= XBOX_STACK_BASE && esp < XBOX_STACK_TOP) ? XBOX_STACK_TOP + : XBOX_TOTAL_RAM - 4u; for (i = 0; i < 400 && esp; i++) { uint32_t a = esp + i * 4; - if (a < XBOX_STACK_BASE || a >= XBOX_STACK_TOP) break; + if (a < XBOX_BASE_ADDRESS || a >= top) break; fprintf(stderr, " GS %08X %08X\n", a, *(const uint32_t *)(mem + a)); } fflush(stderr); +} + +static DWORD WINAPI xbox_watchdog_thread(LPVOID unused) +{ + (void)unused; + Sleep(s_watchdog_secs * 1000u); + xbox_watchdog_dump("no exit", s_watchdog_secs); _exit(3); return 0; } +/* Three pictures per stall (at 1x, 2x and 4x the limit), so a loop shows as + * the same frames and a slow load as different ones; a new stall after + * presents resume gets its own. */ +static DWORD WINAPI xbox_watchdog_stall_thread(LPVOID unused) +{ + LONG last = 0; + unsigned idle = 0; + + (void)unused; + for (;;) { + LONG now; + Sleep(1000); + now = s_watchdog_presents; + if (now != last) { + last = now; + idle = 0; + continue; + } + if (!now) + continue; + idle++; + if (idle == s_watchdog_stall || idle == 2 * s_watchdog_stall || + idle == 4 * s_watchdog_stall) + xbox_watchdog_dump("no present", idle); + } + return 0; +} + void xbox_WatchdogStart(void) { const char *secs = getenv("RECOMP_WATCHDOG_SECS"); + const char *stall = getenv("RECOMP_WATCHDOG_STALL"); HANDLE h; + if (stall && atoi(stall) > 0) { + s_watchdog_stall = (unsigned)atoi(stall); + h = CreateThread(NULL, 0, xbox_watchdog_stall_thread, NULL, 0, NULL); + if (h) + CloseHandle(h); + fprintf(stderr, "[WATCHDOG] guest stack of the presenting thread after %us without a present\n", + s_watchdog_stall); + } if (!secs || !*secs) return; s_watchdog_secs = (unsigned)atoi(secs); diff --git a/src/kernel/xbox_memory_layout.h b/src/kernel/xbox_memory_layout.h index e6dcaa81b..a55481b5b 100644 --- a/src/kernel/xbox_memory_layout.h +++ b/src/kernel/xbox_memory_layout.h @@ -365,6 +365,8 @@ void xbox_ProtectMirrorsForDebug(void); * RECOMP_WATCHDOG_SECS seconds. Call from the thread that runs guest code; * does nothing unless that variable is set. */ void xbox_WatchdogStart(void); +/* Each present: the thread RECOMP_WATCHDOG_STALL watches. */ +void xbox_WatchdogNotePresent(void); /* Frame rate at the HLE boundary (recomp_fps.c). RECOMP_FPS= prints * swaps and delivered vblanks per window; the counts cost an interlocked diff --git a/src/launcher/CMakeLists.txt b/src/launcher/CMakeLists.txt index 6afcda3aa..b6340e032 100644 --- a/src/launcher/CMakeLists.txt +++ b/src/launcher/CMakeLists.txt @@ -24,13 +24,14 @@ # ask which host it is on. function(recomp_add_launcher TARGET GAME_EXE) - cmake_parse_arguments(_launcher "NO_WIDESCREEN" "" "" ${ARGN}) + cmake_parse_arguments(_launcher "NO_WIDESCREEN" "XBE_SHA1" "" ${ARGN}) set(_dir ${CMAKE_CURRENT_FUNCTION_LIST_DIR}) if(WIN32) add_executable(${TARGET} WIN32 ${_dir}/launcher.c ${_dir}/launcher_win32.c + ${_dir}/launcher_iso_win32.c ${_dir}/launcher_theme.c ${_dir}/launcher_theme_gdi.c ${_dir}/launcher_bindings.c @@ -102,7 +103,7 @@ function(recomp_add_launcher TARGET GAME_EXE) target_link_libraries(${TARGET} PRIVATE xbox_config xbox_input xbox_net) if(WIN32) # The rest is in every Windows. - target_link_libraries(${TARGET} PRIVATE user32 gdi32) + target_link_libraries(${TARGET} PRIVATE user32 gdi32 comdlg32 bcrypt) else() target_link_libraries(${TARGET} PRIVATE SDL3::SDL3 "-framework CoreText" "-framework CoreGraphics" @@ -118,6 +119,13 @@ function(recomp_add_launcher TARGET GAME_EXE) if(_launcher_NO_WIDESCREEN) target_compile_definitions(${TARGET} PRIVATE LAUNCHER_NO_WIDESCREEN=1) endif() + # recomp_add_launcher( XBE_SHA1 ): the XBE the code + # was lifted from. A port shared without the game takes the game from + # the player's disc image (launcher_iso_win32.c) and checks it is that one. + if(_launcher_XBE_SHA1) + target_compile_definitions(${TARGET} PRIVATE + LAUNCHER_XBE_SHA1="${_launcher_XBE_SHA1}") + endif() if(MSVC) target_compile_options(${TARGET} PRIVATE /W4 $<$>:/O2>) diff --git a/src/launcher/launcher_iso_win32.c b/src/launcher/launcher_iso_win32.c new file mode 100644 index 000000000..ef0825909 --- /dev/null +++ b/src/launcher/launcher_iso_win32.c @@ -0,0 +1,428 @@ +/* + * launcher_iso_win32.c -- the game's files from the player's own disc image. + * + * A port is shared without the game: the executable carries the runtime and + * the code lifted from one known XBE, and the files that make the game -- + * that XBE included -- come from the disc the player owns. So the first time + * the launcher finds no game beside itself, it asks for the image (a full + * Redump .iso or a trimmed xiso), copies the XDVDFS file system out of it + * into game\ beside the executable, and checks the XBE against the one the + * code was lifted from (LAUNCHER_XBE_SHA1, recomp_add_launcher XBE_SHA1). + * + * XDVDFS: the volume descriptor is sector 32 of the game partition, which + * starts at 0 in an xiso and further in for a full image (XGD1 0x18300000; + * XGD2/XGD3 offsets are tried too). A directory is a binary tree of entries, + * each { u16 left, u16 right (in dwords from the table's start), u32 sector, + * u32 size, u8 attributes, u8 name length, name }; 0xFFFF fills the rest of + * a sector. + */ +#define WIN32_LEAN_AND_MEAN +#include +#include +#include + +#include +#include +#include +#include + +#include "launcher.h" + +#ifndef LAUNCHER_GAME_XBE +#define LAUNCHER_GAME_XBE "default.xbe" +#endif + +#define XDVDFS_SECTOR 2048ull +#define ISO_BUF (4u << 20) + +static const uint64_t k_partitions[] = { + 0ull, 0x18300000ull, 0x2080000ull, 0xFD90000ull +}; + +typedef struct { + HANDLE iso; + uint64_t base; /* the game partition's offset */ + uint64_t total, done; /* bytes, for the progress line */ + unsigned files; + int failed; + wchar_t error[512]; + uint8_t *buf; +} IsoJob; + +static volatile LONG g_iso_progress; /* tenths of a percent */ +static volatile LONG g_iso_finished; +static IsoJob g_job; +static wchar_t g_iso_path[MAX_PATH]; +static wchar_t g_game_dir[MAX_PATH]; + +static int read_at(HANDLE h, uint64_t off, void *dst, DWORD n) +{ + LARGE_INTEGER li; + DWORD got = 0; + + li.QuadPart = (LONGLONG)off; + if (!SetFilePointerEx(h, li, NULL, FILE_BEGIN)) + return 0; + return ReadFile(h, dst, n, &got, NULL) && got == n; +} + +static int find_partition(HANDLE h, uint64_t *base, uint32_t *root_sector, uint32_t *root_size) +{ + size_t i; + uint8_t vd[0x800]; + + for (i = 0; i < sizeof k_partitions / sizeof k_partitions[0]; i++) { + uint64_t b = k_partitions[i]; + if (!read_at(h, b + 32 * XDVDFS_SECTOR, vd, sizeof vd)) + continue; + if (memcmp(vd, "MICROSOFT*XBOX*MEDIA", 20) != 0 || + memcmp(vd + 0x7EC, "MICROSOFT*XBOX*MEDIA", 20) != 0) + continue; + *base = b; + memcpy(root_sector, vd + 20, 4); + memcpy(root_size, vd + 24, 4); + return 1; + } + return 0; +} + +static void job_fail(IsoJob *j, const wchar_t *what, const wchar_t *path) +{ + if (j->failed) + return; + j->failed = 1; + _snwprintf(j->error, sizeof j->error / sizeof j->error[0] - 1, L"%s\n%s", + what, path ? path : L""); +} + +static int copy_file(IsoJob *j, uint32_t sector, uint32_t size, const wchar_t *dst) +{ + HANDLE out; + uint64_t off = j->base + (uint64_t)sector * XDVDFS_SECTOR; + uint32_t left = size; + + out = CreateFileW(dst, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + if (out == INVALID_HANDLE_VALUE) { + job_fail(j, L"Could not write / Konnte nicht schreiben:", dst); + return 0; + } + while (left) { + DWORD n = left > ISO_BUF ? ISO_BUF : left, wrote = 0; + if (!read_at(j->iso, off, j->buf, n)) { + job_fail(j, L"The image could not be read / Das Image ist nicht lesbar:", g_iso_path); + CloseHandle(out); + return 0; + } + if (!WriteFile(out, j->buf, n, &wrote, NULL) || wrote != n) { + job_fail(j, L"Could not write (disk full?) / Schreiben fehlgeschlagen (Platte voll?):", dst); + CloseHandle(out); + return 0; + } + off += n; + left -= n; + j->done += n; + if (j->total) + InterlockedExchange(&g_iso_progress, (LONG)(j->done * 1000ull / j->total)); + } + CloseHandle(out); + j->files++; + return 1; +} + +/* Walk one directory table; with dst NULL only the sizes are added up. */ +static int walk_dir(IsoJob *j, uint32_t sector, uint32_t size, const wchar_t *dst, int depth) +{ + uint8_t *table; + uint32_t *stack; + uint32_t cap; + int sp = 0; + + if (depth > 32 || size == 0) + return 1; + if (size > (64u << 20)) { + job_fail(j, L"The image looks damaged / Das Image scheint beschaedigt:", g_iso_path); + return 0; + } + table = (uint8_t *)malloc(size); + if (!table || !read_at(j->iso, j->base + (uint64_t)sector * XDVDFS_SECTOR, table, size)) { + free(table); + job_fail(j, L"The image could not be read / Das Image ist nicht lesbar:", g_iso_path); + return 0; + } + /* Every entry is at least 14 bytes, so no more can be pending. */ + cap = size / 14u + 2u; + stack = (uint32_t *)malloc(cap * sizeof *stack); + if (!stack) { + free(table); + job_fail(j, L"Out of memory / Kein Speicher frei", NULL); + return 0; + } + stack[sp++] = 0; + while (sp > 0 && !j->failed) { + uint32_t off = stack[--sp]; + uint16_t lft, rgt; + uint32_t esec, esize; + uint8_t attr, nlen; + char name[256]; + wchar_t wname[256], path[MAX_PATH]; + + if (off + 14 > size) + continue; + memcpy(&lft, table + off, 2); + memcpy(&rgt, table + off + 2, 2); + if (lft == 0xFFFF && rgt == 0xFFFF) + continue; + memcpy(&esec, table + off + 4, 4); + memcpy(&esize, table + off + 8, 4); + attr = table[off + 12]; + nlen = table[off + 13]; + if (off + 14 + nlen > size) + continue; + if (lft && (uint32_t)sp < cap) + stack[sp++] = (uint32_t)lft * 4u; + if (rgt && (uint32_t)sp < cap) + stack[sp++] = (uint32_t)rgt * 4u; + memcpy(name, table + off + 14, nlen); + name[nlen] = 0; + if (!nlen || strchr(name, '/') || strchr(name, '\\') || strstr(name, "..")) + continue; + if (!MultiByteToWideChar(1252, 0, name, -1, wname, 256)) + continue; + if (attr & 0x10) { + if (dst) { + _snwprintf(path, MAX_PATH - 1, L"%s\\%s", dst, wname); + path[MAX_PATH - 1] = 0; + CreateDirectoryW(path, NULL); + walk_dir(j, esec, esize, path, depth + 1); + } else { + walk_dir(j, esec, esize, NULL, depth + 1); + } + } else if (dst) { + _snwprintf(path, MAX_PATH - 1, L"%s\\%s", dst, wname); + path[MAX_PATH - 1] = 0; + copy_file(j, esec, esize, path); + } else { + j->total += esize; + } + } + free(stack); + free(table); + return !j->failed; +} + +static DWORD WINAPI extract_thread(LPVOID unused) +{ + IsoJob *j = &g_job; + uint32_t root_sector = 0, root_size = 0; + + (void)unused; + j->iso = CreateFileW(g_iso_path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, + FILE_FLAG_SEQUENTIAL_SCAN, NULL); + if (j->iso == INVALID_HANDLE_VALUE) { + job_fail(j, L"The image could not be opened / Das Image laesst sich nicht oeffnen:", g_iso_path); + } else if (!find_partition(j->iso, &j->base, &root_sector, &root_size)) { + job_fail(j, L"This is not an Xbox disc image / Das ist kein Xbox-Disc-Image:", g_iso_path); + } else if ((j->buf = (uint8_t *)malloc(ISO_BUF)) == NULL) { + job_fail(j, L"Out of memory / Kein Speicher frei", NULL); + } else { + CreateDirectoryW(g_game_dir, NULL); + if (walk_dir(j, root_sector, root_size, NULL, 0)) + walk_dir(j, root_sector, root_size, g_game_dir, 0); + } + if (j->iso != INVALID_HANDLE_VALUE) + CloseHandle(j->iso); + free(j->buf); + InterlockedExchange(&g_iso_finished, 1); + return 0; +} + +#ifdef LAUNCHER_XBE_SHA1 +/* SHA-1 of a file, as 40 lower-case hex digits. 0 when it cannot be read. */ +static int sha1_file(const wchar_t *path, char hex[41]) +{ + BCRYPT_ALG_HANDLE alg = NULL; + BCRYPT_HASH_HANDLE hash = NULL; + HANDLE f; + uint8_t buf[65536], digest[20]; + DWORD got; + int ok = 0, i; + + f = CreateFileW(path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); + if (f == INVALID_HANDLE_VALUE) + return 0; + if (BCryptOpenAlgorithmProvider(&alg, BCRYPT_SHA1_ALGORITHM, NULL, 0) == 0 && + BCryptCreateHash(alg, &hash, NULL, 0, NULL, 0, 0) == 0) { + ok = 1; + while (ReadFile(f, buf, sizeof buf, &got, NULL) && got) { + if (BCryptHashData(hash, buf, got, 0) != 0) { + ok = 0; + break; + } + } + if (ok && BCryptFinishHash(hash, digest, sizeof digest, 0) != 0) + ok = 0; + } + if (hash) + BCryptDestroyHash(hash); + if (alg) + BCryptCloseAlgorithmProvider(alg, 0); + CloseHandle(f); + if (ok) + for (i = 0; i < 20; i++) + sprintf(hex + 2 * i, "%02x", digest[i]); + return ok; +} +#endif + +/* ------------------------------------------------------------ the window */ + +static HWND g_text; + +static LRESULT CALLBACK progress_proc(HWND h, UINT msg, WPARAM wp, LPARAM lp) +{ + if (msg == WM_TIMER) { + wchar_t line[256]; + LONG p = InterlockedCompareExchange(&g_iso_progress, 0, 0); + + _snwprintf(line, 255, + L"Copying the game from your disc image...\r\n" + L"Das Spiel wird aus deinem Disc-Image kopiert...\r\n\r\n%ld.%ld %%", + p / 10, p % 10); + line[255] = 0; + SetWindowTextW(g_text, line); + if (InterlockedCompareExchange(&g_iso_finished, 0, 0)) + DestroyWindow(h); + return 0; + } + if (msg == WM_CLOSE) + return 0; /* not while copying */ + if (msg == WM_DESTROY) { + PostQuitMessage(0); + return 0; + } + return DefWindowProcW(h, msg, wp, lp); +} + +static void run_progress_window(HINSTANCE inst) +{ + WNDCLASSW wc; + HWND w; + MSG m; + + memset(&wc, 0, sizeof wc); + wc.lpfnWndProc = progress_proc; + wc.hInstance = inst; + wc.hCursor = LoadCursorW(NULL, (LPCWSTR)IDC_WAIT); + wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); + wc.lpszClassName = L"xboxrecomp_iso_import"; + RegisterClassW(&wc); + w = CreateWindowW(wc.lpszClassName, L"Disc image / Disc-Image", + WS_OVERLAPPED | WS_CAPTION, CW_USEDEFAULT, CW_USEDEFAULT, 460, 170, + NULL, NULL, inst, NULL); + g_text = CreateWindowW(L"STATIC", L"", WS_CHILD | WS_VISIBLE | SS_CENTER, + 10, 20, 420, 90, w, NULL, inst, NULL); + ShowWindow(w, SW_SHOW); + SetTimer(w, 1, 100, NULL); + while (GetMessageW(&m, NULL, 0, 0) > 0) { + TranslateMessage(&m); + DispatchMessageW(&m); + } +} + +static int file_exists_w(const wchar_t *p) +{ + DWORD a = GetFileAttributesW(p); + return a != INVALID_FILE_ATTRIBUTES && !(a & FILE_ATTRIBUTE_DIRECTORY); +} + +/* 1 when the game is there (already, or now). 0: the player said no or the + * copy failed, and was told why; the launcher ends. */ +int launcher_iso_ensure_game(void) +{ + wchar_t exe[MAX_PATH], xbe[MAX_PATH], *slash; + wchar_t xbe_name[64]; + OPENFILENAMEW ofn; + HANDLE t; + + if (getenv("RECOMP_GAME_DIR")) + return 1; /* somebody else's arrangement */ + if (!GetModuleFileNameW(NULL, exe, MAX_PATH)) + return 1; + slash = wcsrchr(exe, L'\\'); + if (!slash) + return 1; + *slash = 0; + MultiByteToWideChar(CP_ACP, 0, LAUNCHER_GAME_XBE, -1, xbe_name, 64); + _snwprintf(g_game_dir, MAX_PATH - 1, L"%s\\game", exe); + _snwprintf(xbe, MAX_PATH - 1, L"%s\\%s", g_game_dir, xbe_name); + if (file_exists_w(xbe)) + return 1; +#ifdef LAUNCHER_GAME_DIR + { + /* A development tree: the game where the title's main.c says. */ + wchar_t dev[MAX_PATH], rel[MAX_PATH]; + MultiByteToWideChar(CP_ACP, 0, LAUNCHER_GAME_DIR, -1, rel, MAX_PATH); + _snwprintf(dev, MAX_PATH - 1, L"%s\\%s\\%s", exe, rel, xbe_name); + if (file_exists_w(dev)) + return 1; + } +#endif + + if (MessageBoxW(NULL, + L"The game's files are not here yet. This port does not include the game:\n" + L"it needs an image of your own original disc (.iso / .xiso).\n\n" + L"Die Spieldateien fehlen noch. Dieser Port enthaelt das Spiel nicht:\n" + L"er braucht ein Image deiner eigenen Original-Disc (.iso / .xiso).\n\n" + L"Choose the disc image now? / Jetzt das Disc-Image auswaehlen?", + L"Disc image needed / Disc-Image benoetigt", + MB_YESNO | MB_ICONINFORMATION) != IDYES) + return 0; + + memset(&ofn, 0, sizeof ofn); + g_iso_path[0] = 0; + ofn.lStructSize = sizeof ofn; + ofn.lpstrFilter = L"Xbox disc image (*.iso, *.xiso)\0*.iso;*.xiso\0All files\0*.*\0"; + ofn.lpstrFile = g_iso_path; + ofn.nMaxFile = MAX_PATH; + ofn.lpstrTitle = L"Original disc image / Original-Disc-Image"; + ofn.Flags = OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST | OFN_HIDEREADONLY; + if (!GetOpenFileNameW(&ofn)) + return 0; + + memset(&g_job, 0, sizeof g_job); + g_job.iso = INVALID_HANDLE_VALUE; + t = CreateThread(NULL, 0, extract_thread, NULL, 0, NULL); + if (!t) + return 0; + run_progress_window(GetModuleHandleW(NULL)); + WaitForSingleObject(t, INFINITE); + CloseHandle(t); + + if (g_job.failed || !file_exists_w(xbe)) { + MessageBoxW(NULL, g_job.failed ? g_job.error : + L"The image has no game on it / Auf dem Image ist kein Spiel.", + L"Disc image / Disc-Image", MB_OK | MB_ICONERROR); + return 0; + } +#ifdef LAUNCHER_XBE_SHA1 + { + char hex[41]; + if (sha1_file(xbe, hex) && _stricmp(hex, LAUNCHER_XBE_SHA1) != 0) { + MessageBoxW(NULL, + L"The files are copied, but this disc is a different version from the one\n" + L"this port was made from. The game will probably not start.\n\n" + L"Die Dateien sind kopiert, aber diese Disc ist eine andere Version als die,\n" + L"aus der dieser Port gemacht wurde. Das Spiel startet wahrscheinlich nicht.", + L"Disc image / Disc-Image", MB_OK | MB_ICONWARNING); + return 1; + } + } +#endif + { + wchar_t done[200]; + _snwprintf(done, 199, L"%u files copied. / %u Dateien kopiert.", g_job.files, g_job.files); + done[199] = 0; + MessageBoxW(NULL, done, L"Disc image / Disc-Image", MB_OK | MB_ICONINFORMATION); + } + return 1; +} diff --git a/src/launcher/launcher_win32.c b/src/launcher/launcher_win32.c index 50d8ad39e..8e6a2b70b 100644 --- a/src/launcher/launcher_win32.c +++ b/src/launcher/launcher_win32.c @@ -14,6 +14,9 @@ #include "launcher.h" #include "launcher_theme.h" +/* launcher_iso_win32.c: the game from the player's own disc image. */ +int launcher_iso_ensure_game(void); + static HWND g_hwnd; /* ------------------------------------------------------------ the host */ @@ -257,6 +260,10 @@ int WINAPI WinMain(HINSTANCE inst, HINSTANCE prev, LPSTR cmd, int show) * glows shows as a blur over all of it. */ SetProcessDPIAware(); + /* No game beside us yet: from the player's disc image, or not at all. */ + if (!launcher_iso_ensure_game()) + return 0; + launcher_init(); memset(&wc, 0, sizeof wc); diff --git a/src/replay/d3d8_replay.c b/src/replay/d3d8_replay.c index 662000243..901a1d0bd 100644 --- a/src/replay/d3d8_replay.c +++ b/src/replay/d3d8_replay.c @@ -888,6 +888,55 @@ static void do_cube_texture(Replay *r, const D3D8CapChunk *c) slot->is_cube = 1; } +/* The diagnostic volume format stores tightly packed RGBA mip slabs. */ +static void do_volume_texture(Replay *r,const D3D8CapChunk *c) +{ + const D3D8CapVolumeTexture *t=c->data; + if(c->bytesformat!=D3DFMT_LIN_A8B8G8R8 || !t->width || + !t->height || !t->depth || t->width>2048 || t->height>2048 || t->depth>2048 || + !t->levels || t->levels>16) {r->malformed++;return;} + size_t total=0; + for(unsigned l=0;llevels;++l){ + unsigned w=t->width>>l,h=t->height>>l,d=t->depth>>l; + if(!w)w=1;if(!h)h=1;if(!d)d=1; + total+=(size_t)w*h*d*4; + } + const uint8_t *src=d3d8cap_tail(c,sizeof *t,total); + if(!src){r->malformed++;return;} + ReplayTexture *slot=texture_slot(r,t->id,1); + if(!slot){r->malformed++;return;} + IDirect3DVolumeTexture8 *v=NULL; + if(FAILED(r->dev->lpVtbl->CreateVolumeTexture(r->dev,t->width,t->height,t->depth, + t->levels,t->usage,(D3DFORMAT)t->format,D3DPOOL_MANAGED,&v)) || !v){r->failed++;return;} + for(unsigned l=0;llevels;++l){ + unsigned w=t->width>>l,h=t->height>>l,d=t->depth>>l; + if(!w)w=1;if(!h)h=1;if(!d)d=1; + D3DLOCKED_BOX box; + if(FAILED(v->lpVtbl->LockBox(v,l,&box,NULL,0))){v->lpVtbl->Release(v);r->failed++;return;} + for(unsigned z=0;zlpVtbl->UnlockBox(v,l))){v->lpVtbl->Release(v);r->failed++;return;} + src+=(size_t)w*h*d*4; + } + release_slot(slot);slot->tex=(IDirect3DBaseTexture8 *)v;slot->is_cube=0;slot->p8=0; +} + +static void do_depth_alias(Replay *r,const D3D8CapChunk *c) +{ + if(c->bytes!=8){r->malformed++;return;} + const uint32_t *pair=c->data; + ReplayTexture *tex=texture_slot(r,pair[1],0); + if(!pair[0] || pair[0]>REPLAY_MAX_DEPTHS || !tex || !tex->tex || + tex->tex->lpVtbl->GetType(tex->tex)!=D3DRTYPE_TEXTURE){r->malformed++;return;} + IDirect3DSurface8 *surface=NULL; + IDirect3DTexture8 *object=(IDirect3DTexture8 *)tex->tex; + if(FAILED(object->lpVtbl->GetSurfaceLevel(object,0,&surface)) || !surface){r->failed++;return;} + IDirect3DSurface8 **slot=&r->depths[pair[0]-1]; + if(*slot)(*slot)->lpVtbl->Release(*slot); + *slot=surface; +} + static void do_depth_surface(Replay *r, const D3D8CapChunk *c) { const D3D8CapDepthSurface *d = c->data; @@ -1562,6 +1611,12 @@ static void replay_chunk(Replay *r, const D3D8CapChunk *c) case D3D8CAP_DRAW_INDEXED_UP: do_draw_indexed_up(r, c); break; + case D3D8CAP_DEPTH_ALIAS: + do_depth_alias(r,c); + break; + case D3D8CAP_VOLUME_TEXTURE: + do_volume_texture(r,c); + break; case D3D8CAP_TEXTURE: do_texture(r, c); break; diff --git a/tools/recomp/__main__.py b/tools/recomp/__main__.py index 896e52596..2eeeabbb4 100644 --- a/tools/recomp/__main__.py +++ b/tools/recomp/__main__.py @@ -311,6 +311,12 @@ def main(): help="JSON list of addresses to emit an entry trace " "for (RECOMP_TRACE_ENTER). For bring-up: shows " "which call in an init chain is not returning") + parser.add_argument("--regpromote", metavar="FILE", + help="JSON list of function addresses (or \"all\") " + "whose guest registers are kept in C locals " + "(tools/recomp/regpromote.py). A title lists its " + "profile's hottest functions; scripts/recompile.py " + "passes config/regpromote/.json") parser.add_argument("--trace-all-entries", action="store_true", help="Emit an entry hook in every function. With " "RECOMP_TRACE_PROFILE set at run time this reports " @@ -381,6 +387,8 @@ def main(): print(f"Loading data files...", file=sys.stderr) t0 = time.time() + from . import regpromote as _regpromote + _promote = _regpromote.load(args.regpromote) translator = BatchTranslator( xbe_path=args.xbe_path, func_json_path=data_files["functions"], @@ -401,6 +409,11 @@ def main(): os.path.dirname(os.path.abspath(__file__)), "output"), "icall_sites.json"), ) + translator.translator.regpromote = _promote + if _promote: + print(f"Register promotion: " + f"{'every function' if _promote == 'all' else f'{len(_promote)} function(s)'} " + f"({args.regpromote})", file=sys.stderr) t_load = time.time() - t0 print(f"Loaded {len(translator.func_db)} functions, " diff --git a/tools/recomp/regpromote.py b/tools/recomp/regpromote.py new file mode 100644 index 000000000..ba6278914 --- /dev/null +++ b/tools/recomp/regpromote.py @@ -0,0 +1,189 @@ +"""Guest registers in C locals inside chosen lifted functions. + +Lifted code names the guest registers eax..edi and esp as fields of the +thread-local g_cpu, and xmm0..7 as thread-locals of their own. Every store to +guest memory may alias them as far as the compiler knows, so after each one it +reloads every register it is about to use, and keeps none of them in a host +register across a statement. Measured on Forza Motorsport (profil-race5..9): +the hottest lifted function went from 385 thread-local accesses to 58 and from +721 instructions to 343, and the race from 40 ms of work a frame to 26 ms -- +20 fps to 30 -- with the 400 hottest functions done this way. + +For each chosen function this rewrites the translated text so that + +* the registers are locals, read from g_cpu on entry, +* they are written back before every call (direct, guarded or refused + indirect, tail jump) and every return, and read again after every call, +* the call itself runs with the names mapped to the globals again, so a macro + that sets eax or g_esp itself (RECOMP_ICALL_SAFE on a refused target) sets + what the callee and the reload see. + +A function is left as it is when it uses anything this cannot see through: +setjmp/longjmp, MMX registers, a register named by its global outside the +indirect-call capture, the ABI checker, an HLE hook, or a call to a C function +not known to leave the guest registers alone (PURE below). The x87 stack and +everything else in g_cpu stay where they are. + +Which functions: a JSON list of addresses (tools.recomp --regpromote FILE, +scripts/recompile.py takes config/regpromote/.json), or "all". +Cold code gains nothing, so a title lists its profile's hottest functions. +""" +import json +import re + +GPRS = ('eax', 'ecx', 'edx', 'esp', 'ebx', 'esi', 'edi') + +# Called from lifted code without touching the guest register file. +PURE = frozenset(''' +MEM8 MEM16 MEM32 MEM64 SMEM8 SMEM16 SMEM32 SMEM64 MEMF MEMD XBOX_PTR +fp_push fp_pop fp_top fp_st fp_st1 RECOMP_FP_PC RECOMP_FCMP RECOMP_FCMP_CC +LO8 HI8 SET_LO8 SET_HI8 LO16 SET_LO16 ZX8 ZX16 SX8 SX16 RECOMP_PARITY8 +RECOMP_DF_STEP RECOMP_BACKEDGE RECOMP_IDIV_CHECK RECOMP_DIV_CHECK +PUSH32 POP32 PUSH16 POP16 +fabs fabsf sqrt sqrtf atan2 atan2f sin cos sinf cosf floor ceil floorf ceilf +tan tanf log2 exp2 ldexp memcpy memmove memset +recomp_cvtss2si recomp_cvttss2si recomp_fp_round24 recomp_parity8 +recomp_frndint recomp_fist xbox_ReadTimeStampCounter +if while for switch sizeof return +void uint32_t int32_t uint16_t int16_t uint8_t int8_t uint64_t int64_t +float double int +'''.split()) + +# Calls that leave the function: everything around them is synchronised. +_CALL = re.compile(r'\b(RECOMP_ABI_CALL_POP|RECOMP_ABI_CALL|RECOMP_ICALL_SAFE_AT_CC|' + r'RECOMP_ICALL_SAFE_AT|RECOMP_ICALL_SAFE_CC|RECOMP_ICALL_SAFE|' + r'RECOMP_ITAIL|sub_[0-9A-F]+\w*)\s*\(') +_REFUSE = re.compile(r'setjmp|longjmp|\bg_(?:eax|ecx|edx|esp|ebx|esi|edi|xmm\d)\b|' + r'\bmm[0-7]\b|RECOMP_ABI_CHECK|HLE_|RECOMP_ICALL_FLAT|RECOMP_DISPATCH') +_ICALL_CAPTURE = 'uint32_t _icall_esp = g_esp;' + + +def load(path): + """The functions to promote: a set of addresses, or "all".""" + if not path: + return None + with open(path, 'r', encoding='utf-8') as fh: + entries = json.load(fh) + if entries == 'all': + return 'all' + if isinstance(entries, dict): + entries = list(entries.keys()) + out = set() + for e in entries: + if isinstance(e, dict): + e = e.get('start') or e.get('address') + if isinstance(e, str): + e = e[4:] if e.startswith('sub_') else e + out.add(int(e, 16)) + else: + out.add(int(e)) + return out + + +def _strip_comments(s): + s = re.sub(r'/\*.*?\*/', ' ', s, flags=re.S) + return re.sub(r'//[^\n]*', ' ', s) + + +def _match_paren(s, i): + depth = 0 + for j in range(i, len(s)): + if s[j] == '(': + depth += 1 + elif s[j] == ')': + depth -= 1 + if depth == 0: + return j + raise ValueError('unbalanced parentheses') + + +def _unknown_calls(code): + code = re.sub(r'^\s*#.*$', '', _strip_comments(code), flags=re.M) + out = set() + for m in re.finditer(r'\b([A-Za-z_]\w*)\s*\(', code): + n = m.group(1) + if n in PURE or n.startswith(('XMM_', 'CMP_', 'TEST_')) or _CALL.match(n + '('): + continue + out.add(n) + return out + + +def _defines(xmms, target): + s = ''.join('#undef %s\n#define %s %s_%s\n' % (r, r, target, r) for r in GPRS) + return s + ''.join('#undef xmm%s\n#define xmm%s %s_xmm%s\n' % (x, x, target, x) + for x in xmms) + + +def promote_body(name, body): + """body: 'void NAME(void)\\n{\\n ... \\n}\\n'. Returns (new body or None, why).""" + body = body.replace(_ICALL_CAPTURE, 'uint32_t _icall_esp = esp;') + plain = _strip_comments(body) + why = _REFUSE.search(plain) + if why: + return None, 'refused: ' + why.group(0) + unknown = _unknown_calls(body) + if unknown: + return None, 'unknown calls: ' + ' '.join(sorted(unknown)) + xmms = sorted(set(re.findall(r'\bxmm([0-7])\b', plain))) + open_i = body.index('{\n') + 2 + head, rest = body[:open_i], body[open_i:] + close_i = rest.rstrip().rfind('}') + inner, tail = rest[:close_i], rest[close_i:] + to_global, to_local = _defines(xmms, 'g'), _defines(xmms, 'l') + + res, i, ncalls = [], 0, 0 + while True: + m = _CALL.search(inner, i) + if not m: + res.append(inner[i:]) + break + line_start = inner.rfind('\n', 0, m.start()) + 1 + seg = inner[line_start:m.start()] + if '/*' in seg and '*/' not in seg[seg.rfind('/*'):]: + res.append(inner[i:m.end()]) + i = m.end() + continue + j = _match_paren(inner, m.end() - 1) + 1 + while j < len(inner) and inner[j] in ' \t': + j += 1 + if j >= len(inner) or inner[j] != ';': + return None, 'call not a statement' + j += 1 + res.append(inner[i:m.start()]) + res.append('{ RP_OUT();\n' + to_global + inner[m.start():j] + '\n' + + to_local + 'RP_IN(); }') + ncalls += 1 + i = j + inner = ''.join(res) + inner, nret = re.subn(r'\breturn\s*;', '{ RP_OUT(); return; }', inner) + + out_parts = ['g_cpu.r_%s = l_%s;' % (r, r) for r in GPRS] + \ + ['g_xmm%s = l_xmm%s;' % (x, x) for x in xmms] + in_parts = ['l_%s = g_cpu.r_%s;' % (r, r) for r in GPRS] + \ + ['l_xmm%s = g_xmm%s;' % (x, x) for x in xmms] + decl = ' uint32_t ' + ', '.join('l_%s = g_cpu.r_%s' % (r, r) for r in GPRS) + ';\n' + decl += ''.join(' RecompXmm l_xmm%s = g_xmm%s;\n' % (x, x) for x in xmms) + pre = ('/* regpromote: guest registers in locals inside this function */\n' + '#define RP_OUT() do { %s } while (0)\n' + '#define RP_IN() do { %s } while (0)\n' % (' '.join(out_parts), ' '.join(in_parts))) + pre += to_local + post = '\n' + to_global + '#undef RP_OUT\n#undef RP_IN\n' + new = pre + head + decl + inner + ' RP_OUT();\n' + tail + post + return new, 'ok (%d calls, %d returns, xmm %s)' % (ncalls, nret, ''.join(xmms) or '-') + + +def promote(code, name): + """Promote function NAME inside a translated chunk of text. + + Returns (text, why); text is unchanged when the function is left alone.""" + m = re.search(r'^void %s\(void\)\n\{\n' % re.escape(name), code, re.M) + if not m: + return code, 'not found' + end = code.find('\n}\n', m.end()) + if end < 0: + return code, 'no end' + end += 3 + new, why = promote_body(name, code[m.start():end]) + if new is None: + return code, why + return code[:m.start()] + new + code[end:], why diff --git a/tools/recomp/test_fprem_completion.py b/tools/recomp/test_fprem_completion.py index 8491c2c46..81d6fd744 100644 --- a/tools/recomp/test_fprem_completion.py +++ b/tools/recomp/test_fprem_completion.py @@ -1,5 +1,6 @@ """Completed host reduction must clear the x87 C2 polling bit.""" from pathlib import Path +import os import shutil import subprocess import tempfile @@ -17,7 +18,8 @@ def build_run(source): c.write_text(source, encoding='utf-8') args = ([cc, '/nologo', '/W0', '/O2', str(c), '/Fe:'+str(exe)] if Path(cc).stem.lower() == 'cl' else - [cc, '-O2', str(c), '-o', str(exe), '-lm']) + [cc, '-O2', str(c), '-o', str(exe)] + + ([] if os.name == 'nt' else ['-lm'])) built = subprocess.run(args, cwd=tmp, capture_output=True, text=True) assert built.returncode == 0, built.stdout+built.stderr return subprocess.run([str(exe)], cwd=tmp, capture_output=True, text=True, timeout=5) diff --git a/tools/recomp/test_lifter_fpu_tbyte.py b/tools/recomp/test_lifter_fpu_tbyte.py index da2050d2e..ee3b4af1c 100644 --- a/tools/recomp/test_lifter_fpu_tbyte.py +++ b/tools/recomp/test_lifter_fpu_tbyte.py @@ -113,7 +113,8 @@ def test_helpers_match_the_80_bit_format(self): cc = shutil.which("clang") or shutil.which("gcc") or shutil.which("cc") if cc: exe = os.path.join(d, "f80") - built = subprocess.run([cc, "-O2", c, "-o", exe, "-lm"], + built = subprocess.run([cc, "-O2", c, "-o", exe] + + ([] if os.name == "nt" else ["-lm"]), capture_output=True, text=True) else: from tools.conformance import __main__ as runner diff --git a/tools/recomp/test_lifter_frndint.py b/tools/recomp/test_lifter_frndint.py index f5e1ec6e5..c672d6ad6 100644 --- a/tools/recomp/test_lifter_frndint.py +++ b/tools/recomp/test_lifter_frndint.py @@ -35,7 +35,10 @@ def test_frndint_reads_the_guest_control_word(self): class FrndintRuntimeTest(unittest.TestCase): def test_each_rounding_control_rounds_its_own_way(self): - cc = next(filter(None, map(shutil.which, ("cc", "gcc", "clang"))), None) + # Match the Windows project's Clang/MSVC toolchain before a MinGW + # compiler installed elsewhere on PATH. Keep the Unix preference. + names = ("clang", "cc", "gcc") if os.name == "nt" else ("cc", "gcc", "clang") + cc = next(filter(None, map(shutil.which, names)), None) if not cc: self.skipTest("no C compiler available") source = r''' diff --git a/tools/recomp/test_lifter_host_semantics.py b/tools/recomp/test_lifter_host_semantics.py index af3753a39..d04ff5017 100644 --- a/tools/recomp/test_lifter_host_semantics.py +++ b/tools/recomp/test_lifter_host_semantics.py @@ -18,6 +18,7 @@ Intel SDM results. On an x86 host the run exercises the SSE instructions; on AArch64 it exercises the portable fallbacks -- the half that matters there. """ +import os import shutil import subprocess import tempfile @@ -263,7 +264,8 @@ def test_helpers_and_lowerings_match_x86_on_this_host(): built = subprocess.run( [cc, "-std=gnu11", "-O2", "-Wall", "-Wno-unused-function", "-Wno-unused-variable", "-Wno-unused-but-set-variable", - "-I", str(_ROOT / "templates" / "runtime"), str(src), "-o", str(exe), "-lm"], + "-I", str(_ROOT / "templates" / "runtime"), str(src), "-o", str(exe)] + + ([] if os.name == "nt" else ["-lm"]), capture_output=True, text=True) assert built.returncode == 0, built.stderr[-4000:] ran = subprocess.run([str(exe)], capture_output=True, text=True) diff --git a/tools/recomp/test_regpromote.py b/tools/recomp/test_regpromote.py new file mode 100644 index 000000000..10e61da4f --- /dev/null +++ b/tools/recomp/test_regpromote.py @@ -0,0 +1,125 @@ +"""regpromote: guest registers in C locals must behave as the globals did. + +The text checks pin the shape (sync around every call and return, the global +names back for the call itself); the run check compiles a promoted function +against a stand-in register file and compares what it leaves in memory and +in g_cpu with the unpromoted original. +""" +import os +import shutil +import subprocess +import tempfile + +from . import regpromote + +BODY = """void sub_00001000(void) +{ + esp = esp - 8; + MEM32(esp) = eax; + eax = eax + MEM32(esp + 0xC); + PUSH32(esp, 0x00001010u); RECOMP_ABI_CALL_POP(0x00002000u, sub_00002000, 0u); /* call */ + ecx = eax * 3; + { uint32_t _icall_esp = g_esp; + { uint32_t _icall_target = MEM32(0x40); PUSH32(esp, 0x00001020u); RECOMP_ICALL_SAFE_AT(_icall_target, _icall_esp, 0x0000101Bu); } /* indirect call */ + } + MEM32(0x44) = ecx + eax; + esp = esp + 8; + esp += 4; return; /* ret */ +} +""" + +CALLEE = """void sub_00002000(void) +{ + eax = eax ^ 0x55u; + esp += 4; return; +} +""" + +HEADER = r""" +#include +#include +#include +static uint8_t g_ram[0x10000]; +#define XBOX_PTR(a) ((uintptr_t)g_ram + (uint32_t)(a)) +#define MEM32(a) (*(volatile uint32_t *)XBOX_PTR(a)) +struct recomp_cpu { uint32_t r_eax, r_ecx, r_edx, r_esp, r_ebx, r_esi, r_edi; }; +static struct recomp_cpu g_cpu; +typedef struct { float f[4]; } RecompXmm; +#define g_eax g_cpu.r_eax +#define g_ecx g_cpu.r_ecx +#define g_edx g_cpu.r_edx +#define g_esp g_cpu.r_esp +#define g_ebx g_cpu.r_ebx +#define g_esi g_cpu.r_esi +#define g_edi g_cpu.r_edi +#define eax g_eax +#define ecx g_ecx +#define edx g_edx +#define esp g_esp +#define ebx g_ebx +#define esi g_esi +#define edi g_edi +#define PUSH32(sp, v) do { uint32_t _pv = (uint32_t)(v); (sp) -= 4; MEM32(sp) = _pv; } while (0) +#define RECOMP_ABI_CALL_POP(va, fn, pop) (fn)() +typedef void (*recomp_func_t)(void); +void sub_00002000(void); +static void sub_00003000(void) { g_eax += 7; g_esp += 4; } +#define RECOMP_ICALL_SAFE_AT(t, saved, site) do { \ + if ((t) == 0x3000u) sub_00003000(); else { g_esp = (saved); eax = 0; } } while (0) +""" + +MAIN = r""" +int main(void) { + uint32_t out[2][4]; + for (int k = 0; k < 2; k++) { + for (int arm = 0; arm < 2; arm++) { + memset(g_ram, 0, sizeof g_ram); memset(&g_cpu, 0, sizeof g_cpu); + g_esp = 0x8000; g_eax = 5; MEM32(0x8004) = 11; MEM32(0x40) = arm ? 0x3000u : 0x9999u; + if (k == 0) sub_00001000_orig(); else sub_00001000(); + out[k][arm * 2] = MEM32(0x44); out[k][arm * 2 + 1] = g_eax ^ (g_esp << 8) ^ (g_ecx << 16); + } + } + printf("%u %u %u %u | %u %u %u %u\n", out[0][0], out[0][1], out[0][2], out[0][3], + out[1][0], out[1][1], out[1][2], out[1][3]); + return memcmp(out[0], out[1], sizeof out[0]) != 0; +} +""" + + +def test_shape(): + new, why = regpromote.promote(BODY, "sub_00001000") + assert why.startswith("ok (2 calls, 1 returns"), why + assert "_icall_esp = esp;" in new and "_icall_esp = g_esp;" not in new + assert new.count("RP_OUT();") == 4 # two calls, one return, the end + assert "#define eax g_eax\nRECOMP_ABI_CALL_POP" not in new + assert "#define edi g_edi\nRECOMP_ICALL_SAFE_AT" in new + + +def test_refuses(): + for bad in (" g_eax = 1;\n", " mm0 = mm1;\n", " foo(eax);\n"): + body = BODY.replace(" ecx = eax * 3;\n", bad) + new, why = regpromote.promote(body, "sub_00001000") + assert new == body and not why.startswith("ok"), why + + +def test_runs_the_same(): + cc = shutil.which("cc") or shutil.which("clang") or shutil.which("gcc") + if not cc: + return + promoted, why = regpromote.promote(BODY, "sub_00001000") + assert why.startswith("ok"), why + orig = BODY.replace("sub_00001000(void)", "sub_00001000_orig(void)") + src = HEADER + CALLEE + orig + promoted + MAIN + with tempfile.TemporaryDirectory() as d: + c, exe = os.path.join(d, "t.c"), os.path.join(d, "t") + open(c, "w").write(src) + subprocess.run([cc, "-O2", "-o", exe, c], check=True) + r = subprocess.run([exe], capture_output=True, text=True) + assert r.returncode == 0, r.stdout + + +if __name__ == "__main__": + test_shape() + test_refuses() + test_runs_the_same() + print("ok") diff --git a/tools/recomp/translator.py b/tools/recomp/translator.py index 92787ee00..37a6d1683 100644 --- a/tools/recomp/translator.py +++ b/tools/recomp/translator.py @@ -551,6 +551,9 @@ def __init__(self, xbe_data, func_db, label_db=None, classification_db=None, self.classification_db = classification_db or {} self.abi_db = abi_db or {} self.trace_functions = set(trace_functions or ()) + # Functions whose guest registers live in C locals (regpromote.py): + # a set of addresses, "all", or None. Set by tools.recomp --regpromote. + self.regpromote = None # Emit an entry hook in *every* function, so the profiler can report # how many distinct functions a run reached. That count is the only # frontier measure here that rises monotonically with progress: kernel @@ -1299,6 +1302,11 @@ def translate_function(self, func_addr, func_info): self._edge_needs_cf = False code = self._translate_function_once(func_addr, func_info, force_cf=True) + promote = getattr(self, 'regpromote', None) + if code and promote and (promote == 'all' or func_addr in promote): + from . import regpromote + name = _func_ident(func_addr, func_info.get("name", f"sub_{func_addr:08X}")) + code, _why = regpromote.promote(code, name) return code def translate_isolated(self, func_addr, func_info):