From 156f2fb5b032494af6d22f324ede0da04f664b18 Mon Sep 17 00:00:00 2001 From: phobos665 <5970062+phobos665@users.noreply.github.com> Date: Mon, 2 Nov 2026 09:22:42 +0000 Subject: [PATCH] feat(hle): the title's gamma ramp is applied at present D3DDevice_SetGammaRamp was lifted and did nothing: the console applies the ramp in the DAC as each frame is scanned out, so nothing drawn ever carries it, and no replacement put it anywhere. It is now replaced, and the ramp (the Xbox's three 256-byte tables) is looked up per channel in the display resolve (d3d8_display_set_gamma), as a 256x1 texture read only when the ramp is not the identity. Constantine sets identity ramps at its default brightness, so its picture is unchanged there; its Brightness option now takes effect. Not yet seen on screen with a non-identity ramp: MoltenVK's swap chain cannot be read back to check it on a Mac. --- src/d3d/d3d8_display.c | 78 ++++++++++++++++++++++++++++++++++++++++-- src/d3d/d3d8_display.h | 5 +++ src/hle/hle_d3d8.c | 30 ++++++++++++++++ 3 files changed, 110 insertions(+), 3 deletions(-) diff --git a/src/d3d/d3d8_display.c b/src/d3d/d3d8_display.c index e5d40b6e9..b37f3cacb 100644 --- a/src/d3d/d3d8_display.c +++ b/src/d3d/d3d8_display.c @@ -256,7 +256,7 @@ D3D8DisplayFit d3d8_display_fit(UINT shape_w, UINT shape_h, UINT bb_w, UINT bb_h * that makes the supersampling exact. */ typedef struct { float origin_x, origin_y, ratio_x, ratio_y; - float texel_x, texel_y, inv_taps, pad; + float texel_x, texel_y, inv_taps, gamma; /* gamma: 1 to look up the ramp */ } ResolveConstants; static const char kSource[] = @@ -269,6 +269,7 @@ static const char kSource[] = " return o;\n" "}\n" "Texture2D scene : register(t9);\n" + "Texture2D ramp : register(t10);\n" "SamplerState smp : register(s9);\n" "float4 ps_main(VSOut i) : SV_Target {\n" /* The footprint's corner: SV_Position is the pixel's centre (x + 0.5), @@ -284,7 +285,15 @@ static const char kSource[] = " [unroll] for (int x = 0; x < TAPS; x++)\n" " sum += scene.SampleLevel(smp,\n" " (base + (float2(x, y) + 0.5) * step) * q.xy, 0);\n" - " return sum * q.z;\n" + " float4 c = sum * q.z;\n" + /* The title's gamma ramp (d3d8_display_set_gamma), as the console's DAC + * applies it at scan-out: each channel looked up in its own column. */ + " if (q.w > 0.5) {\n" + " int3 k = int3(saturate(c.rgb) * 255.0 + 0.5);\n" + " c.rgb = float3(ramp.Load(int3(k.r, 0, 0)).r, ramp.Load(int3(k.g, 0, 0)).g,\n" + " ramp.Load(int3(k.b, 0, 0)).b);\n" + " }\n" + " return c;\n" "}\n"; static struct { @@ -295,10 +304,69 @@ static struct { RhiDepthState *depth; RhiRasterState *raster; RhiSampler *sampler; + RhiImage *ramp_img; /* 256x1: the gamma ramp, R G B in place */ + RhiView *ramp_view; UINT taps; /* what the shaders were built for */ int tried, failed; } g; +/* The title's gamma ramp (D3DDevice_SetGammaRamp, hle_d3d8.c). The console + * applies it in the DAC as each frame is scanned out, so nothing drawn + * shows it and only the present can; an identity ramp costs nothing. */ +static struct { + uint8_t rgba[256 * 4]; + int on, dirty; +} g_gamma; + +void d3d8_display_set_gamma(const uint8_t red[256], const uint8_t green[256], + const uint8_t blue[256]) +{ + int i, identity = 1; + + for (i = 0; i < 256; i++) { + g_gamma.rgba[i * 4 + 0] = red[i]; + g_gamma.rgba[i * 4 + 1] = green[i]; + g_gamma.rgba[i * 4 + 2] = blue[i]; + g_gamma.rgba[i * 4 + 3] = 0xFF; + if (red[i] != i || green[i] != i || blue[i] != i) + identity = 0; + } + g_gamma.on = !identity; + g_gamma.dirty = 1; +} + +/* The ramp as a texture the resolve reads, made and refilled here, on the + * thread that presents. 1 when it should be applied. */ +static int gamma_ready(void) +{ + if (!g_gamma.on) + return 0; + if (!g.ramp_img) { + RhiImageDesc d; + memset(&d, 0, sizeof d); + d.type = RHI_IMAGE_2D; + d.width = 256; + d.height = 1; + 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_SAMPLED; + if (!(g.ramp_img = rhi_image_create(&d, NULL)) || + !(g.ramp_view = rhi_view_create(g.ramp_img, RHI_VIEW_SAMPLED, NULL))) { + g_gamma.on = 0; + return 0; + } + g_gamma.dirty = 1; + } + if (g_gamma.dirty) { + rhi_image_update(g.ramp_img, 0, NULL, g_gamma.rgba, 256 * 4, 256 * 4); + g_gamma.dirty = 0; + } + return 1; +} + static void fail(const char *what) { g.failed = 1; @@ -431,7 +499,7 @@ HRESULT d3d8_display_resolve(RhiView *scene, UINT scene_w, UINT scene_h, c.texel_x = 1.0f / (float)scene_w; c.texel_y = 1.0f / (float)scene_h; c.inv_taps = 1.0f / (float)(taps * taps); - c.pad = 0.0f; + c.gamma = gamma_ready() ? 1.0f : 0.0f; memcpy(mapped, &c, sizeof c); rhi_buffer_unmap(g.cb); } @@ -456,6 +524,7 @@ HRESULT d3d8_display_resolve(RhiView *scene, UINT scene_w, UINT scene_h, rhi_set_shader(RHI_STAGE_PIXEL, g.ps); rhi_set_uniform_buffers(RHI_STAGE_PIXEL, 7, 1, &g.cb); rhi_set_textures(9, 1, &scene); + rhi_set_textures(10, 1, g_gamma.on ? &g.ramp_view : &scene); /* bound either way */ rhi_set_samplers(9, 1, &g.sampler); rhi_set_blend_state(g.blend, blend_factor, 0xFFFFFFFF); rhi_set_depth_state(g.depth, 0); @@ -467,6 +536,7 @@ HRESULT d3d8_display_resolve(RhiView *scene, UINT scene_w, UINT scene_h, { RhiView *none = NULL; rhi_set_textures(9, 1, &none); + rhi_set_textures(10, 1, &none); } rhi_output_restore(&saved); return S_OK; @@ -481,6 +551,8 @@ void d3d8_display_shutdown(void) rhi_depth_state_destroy(g.depth); g.depth = NULL; rhi_raster_state_destroy(g.raster); g.raster = NULL; rhi_sampler_destroy(g.sampler); g.sampler = NULL; + rhi_view_destroy(g.ramp_view); g.ramp_view = NULL; + rhi_image_destroy(g.ramp_img); g.ramp_img = NULL; g.tried = 0; g.failed = 0; } diff --git a/src/d3d/d3d8_display.h b/src/d3d/d3d8_display.h index 07c23f093..c1c8647ba 100644 --- a/src/d3d/d3d8_display.h +++ b/src/d3d/d3d8_display.h @@ -87,6 +87,11 @@ void d3d8_display_output_shape(UINT scene_w, UINT scene_h, HRESULT d3d8_display_resolve(RhiView *scene, UINT scene_w, UINT scene_h, RhiView *out, D3D8DisplayFit fit); +/* The title's gamma ramp, 0..255 to 0..255 per channel, applied at the + * resolve as the console's DAC applies it at scan-out. */ +void d3d8_display_set_gamma(const uint8_t red[256], const uint8_t green[256], + const uint8_t blue[256]); + void d3d8_display_shutdown(void); diff --git a/src/hle/hle_d3d8.c b/src/hle/hle_d3d8.c index 9efabbddf..053f19dea 100644 --- a/src/hle/hle_d3d8.c +++ b/src/hle/hle_d3d8.c @@ -5093,6 +5093,36 @@ HLE_EXPORT(D3DDevice_SetViewport) } } +/* void D3DDevice_SetGammaRamp(DWORD dwFlags, const D3DGAMMARAMP *pRamp) + * + * The Xbox's ramp is BYTE red[256], green[256], blue[256], and the console + * applies it in the DAC as each frame is scanned out (at the next vblank, or + * at once with D3DSGR_IMMEDIATE), so nothing drawn ever carries it. Here it + * is applied at the present's resolve (d3d8_display_set_gamma). Constantine + * sets one from its Brightness option, and without it every frame was the + * darker, uncorrected picture. */ +void d3d8_display_set_gamma(const uint8_t red[256], const uint8_t green[256], + const uint8_t blue[256]); +HLE_ORIGINAL(D3DDevice_SetGammaRamp); +HLE_EXPORT(D3DDevice_SetGammaRamp) +{ + static int seen; + static unsigned long calls; + uint32_t flags = HLE_ARG(0), ramp = HLE_ARG(1); + + first_call(&seen, "D3DDevice_SetGammaRamp", flags); + if (original_missing(hle_original_D3DDevice_SetGammaRamp, "D3DDevice_SetGammaRamp")) + return; + if (ramp) { + const uint8_t *r = (const uint8_t *)HLE_PTR(ramp); + d3d8_display_set_gamma(r, r + 256, r + 512); + if (calls++ < 4) + fprintf(stderr, "[HLE-D3D8] gamma ramp (flags 0x%X): red 0->%u 64->%u 128->%u " + "192->%u 255->%u\n", flags, r[0], r[64], r[128], r[192], r[255]); + } + HLE_CALL_ORIGINAL(D3DDevice_SetGammaRamp); +} + /* void D3DDevice_SetScissors(DWORD Count, BOOL Exclusive, const D3DRECT *pRects) * Xbox-only: clip drawing to (or, with Exclusive, outside) up to eight * rectangles of the render target; Count 0 turns it off. TimeSplitters 2