Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ jobs:
make GAME="$GAME" -j3 t_perf_order
./t_perf_order --cpu 2>&1 | tee ../../../logs/t_perf_order.log
make GAME="$GAME" -j3 perf-order-tsan 2>&1 | tee ../../../logs/t_perf_order-tsan.log
tests="t_gfx_suspend t_capture t_dirty_slots t_dirty_race t_tex_layout t_pace t_fstep t_link_ticks t_cpu_tex t_hmap t_cover t_pso_skey t_refl t_rt_range_ro t_present_rescue t_gpuprof t_sparse_layout t_timestamp t_rt_depth_ro t_rt_range_shape t_ub t_pso_pending t_vk_desc"
tests="t_gfx_suspend t_capture t_dirty_slots t_dirty_race t_tex_layout t_pace t_fstep t_link_ticks t_edr_policy t_cpu_tex t_hmap t_cover t_pso_skey t_refl t_rt_range_ro t_present_rescue t_gpuprof t_sparse_layout t_timestamp t_rt_depth_ro t_rt_range_shape t_ub t_pso_pending t_vk_desc"
make GAME="$GAME" -j3 $tests 2>&1 | tee ../../../logs/gfx-build.log
failed=0
for t in $tests; do
Expand Down
22 changes: 22 additions & 0 deletions dm/gfx/nvn/nvn_edr.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Experimental SDR-to-EDR presentation policy. This expands an SDR picture's
// highlights; it does not recover a game's original scene-referred HDR signal.
#pragma once
#include <math.h>
#include <stdlib.h>
#include <string.h>

static inline int nvn_edr_requested(void) {
const char *value = getenv("DM_PRESENT_EDR");
return value && !strcmp(value, "1");
}
static inline float nvn_edr_headroom(int requested, int active, float current, float potential) {
if (!requested || !active || !isfinite(current) || !isfinite(potential)) return 1;
return fmaxf(1, fminf(2, fminf(current, potential)));
}

// The input is the same stored SDR bytes the stock present pass samples.
// Decode once to extended-linear sRGB, keep shadows/midtones, and expand only
// luminance above 0.5. A common RGB gain preserves the highlight's chromaticity.
#define NVN_EDR_MSL \
"float3 edr_decode(float3 x) { x=clamp(x,0.0f,1.0f); return select(pow((x+0.055f)/1.055f,float3(2.4f)),x/12.92f,x<=0.04045f); }\n" \
"float3 edr_map(float3 rgb,float headroom,bool alreadyLinear) { float3 l=alreadyLinear?clamp(rgb,0.0f,1.0f):edr_decode(rgb); float y=dot(l,float3(0.2126f,0.7152f,0.0722f)); float w=smoothstep(0.5f,1.0f,y); return l*(1.0f+(headroom-1.0f)*w); }\n"
43 changes: 32 additions & 11 deletions dm/gfx/nvn/nvn_mtl.m
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
#include "dmsh_chain.h" // DMSH_CHAIN: the translation chain version of the B shader library expected by this source tree, one for every game (beside this file, tools/dmsh_chain.py --header)
#include "game_profile.h" // game profile constants (profile id, the profile's switches)
#include "../../host/front.h"
#include "nvn_edr.h"
// Texture slot table per stage: 0-127 the combined handles (nvnCommandBufferBindTexture), 128-255 the separate texture binds (NVN_SEPTEX_SLOT0, nvn_obj.h)
#define NTEX 256
// The NVN driver's constant segments c0 and c2 (byte offsets, slot counts) and the binding kinds of the shader reflection have one definition, which the shader build reads too (dmsh_build.py through dmsh_abi.py)
Expand Down Expand Up @@ -454,7 +455,7 @@ void mtl_init(void) {
g_present_args = [g_dev newArgumentTableWithDescriptor:pad error:&ate];
if (!g_present_args) MDIE("failed to create the present argument table: %s", ate.localizedDescription.UTF8String);
for (int i = 0; i < 3; i++) {
g_present_uv[i] = [g_dev newBufferWithLength:sizeof(float) * 8 options:MTLResourceStorageModeShared];
g_present_uv[i] = [g_dev newBufferWithLength:sizeof(float) * 12 options:MTLResourceStorageModeShared];
if (!g_present_uv[i]) MDIE("failed to create present UV buffer[%d]", i);
res_add(g_present_uv[i]);
}
Expand Down Expand Up @@ -10060,7 +10061,7 @@ void mtl_exec(const uint8_t *p) {
}

// ---------- Present: draw the swapchain texture to the CAMetalLayer; optional screenshot ----------
static id<MTLRenderPipelineState> g_blit_pso; static MTLPixelFormat g_blit_fmt;
static id<MTLRenderPipelineState> g_blit_pso; static MTLPixelFormat g_blit_fmt; static int g_blit_edr;
static id<MTLSamplerState> g_blit_samp;
static uint64_t g_present_n;
uint64_t mtl_present_count(void) { return g_present_n; } // Actual present count (heat attribution DM_HEAT computes the present frame rate)
Expand Down Expand Up @@ -10397,23 +10398,34 @@ int mtl_display_hz(void) {
static _Atomic uint64_t g_present_stall_ns; // For unit tests: an extra pause before each present on the present thread (simulating a drawable that stays unavailable)
static CAMetalLayer *g_present_layer_resident;
void mtl_present_test_stall(uint64_t ns) { g_present_stall_ns = ns; }
__attribute__((weak)) float front_present_edr_headroom(void) { return 1; } // iOS supplies scene headroom; Mac output remains SDR
static int g_edr_mode = -1;
static int present_edr(MTLPixelFormat format) {
if (g_edr_mode < 0) g_edr_mode = nvn_edr_requested();
return g_edr_mode && format == MTLPixelFormatRGBA16Float;
}
static void present_pipeline(MTLPixelFormat format) {
if (!g_blit_pso || g_blit_fmt != format) {
int edr = present_edr(format);
if (!g_blit_pso || g_blit_fmt != format || g_blit_edr != edr) {
static const char *src_msl =
Comment on lines +10415 to 10418

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Run the required game-route regression checks

This changes the shared presentation shader and its constant-buffer layout even when EDR is disabled, but the commit's validation explicitly reports no five-game route and only a Silksong smoke test rather than a gameplay route. Synthetic GPU readbacks do not exercise integration with the other games' swapchains and presentation paths. Before merging, run scripts/regress.py check --games for the five local games most likely to be affected, or document why a smaller selection establishes a narrow reach, and attach the named-game report required by AGENTS.md.

AGENTS.md reference: AGENTS.md:L30-L30

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that the shared uniform/shader layout changes even with EDR off. The PR description now includes a named eight-game coverage table and the narrower final-presentation report: current GPU checks cover both presentation implementations, SDR byte parity, crop/bars, conversion/headroom and MetalFX screenshots, with separate native layer checks. The only real-game evidence is the explicitly dated short Silksong off/on smoke, not a gameplay route. Only BotW/TotK currently have prepared Mac hosts, routes and saves; five routes have not been run. Normal Mac BGRA8 routes would test shared SDR integration, not the opt-in iOS float output. Multi-game gameplay acceptance remains pending before production/default adoption; I am leaving this validation thread open rather than presenting the synthetic checks as named-game passes.

"#include <metal_stdlib>\nusing namespace metal;\n"
"struct V { float4 p [[position]]; float2 uv; };\n"
"vertex V vs(uint id [[vertex_id]]) { float2 uv = float2((id << 1) & 2, id & 2); V o; o.p = float4(uv * float2(2, -2) + float2(-1, 1), 0, 1); o.uv = uv; return o; }\n"
"struct Crop { float4 rect; float4 edge; };\n"
"fragment float4 fs(V in [[stage_in]], texture2d<float> t [[texture(0)]], sampler s [[sampler(0)]], constant Crop& c [[buffer(0)]]) { return t.sample(s, clamp(in.uv * c.rect.zw + c.rect.xy, c.edge.xy, c.edge.zw)); }\n";
"struct Crop { float4 rect; float4 edge; float4 edr; };\n"
NVN_EDR_MSL
"fragment float4 fs(V in [[stage_in]], texture2d<float> t [[texture(0)]], sampler s [[sampler(0)]], constant Crop& c [[buffer(0)]]) { float4 color=t.sample(s, clamp(in.uv * c.rect.zw + c.rect.xy, c.edge.xy, c.edge.zw));\n"
"#if EDR_PRESENT\n color.rgb=edr_map(color.rgb,c.edr.x,c.edr.y>0.0f);\n#endif\n return color; }\n";
NSError *e = nil;
id<MTLLibrary> lib = [g_dev newLibraryWithSource:@(src_msl) options:nil error:&e];
MTLCompileOptions *options = [MTLCompileOptions new]; options.preprocessorMacros = @{@"EDR_PRESENT":@(edr)};
id<MTLLibrary> lib = [g_dev newLibraryWithSource:@(src_msl) options:options error:&e];
if (!lib) MDIE("present shader compile failed %s", e.localizedDescription.UTF8String);
MTLRenderPipelineDescriptor *pd = [MTLRenderPipelineDescriptor new];
pd.vertexFunction = [lib newFunctionWithName:@"vs"]; pd.fragmentFunction = [lib newFunctionWithName:@"fs"];
pd.colorAttachments[0].pixelFormat = format;
g_blit_pso = [g_dev newRenderPipelineStateWithDescriptor:pd error:&e];
if (!g_blit_pso) MDIE("present pipeline failed %s", e.localizedDescription.UTF8String);
g_blit_fmt = format;
g_blit_edr = edr;
MTLSamplerDescriptor *sd = [MTLSamplerDescriptor new]; sd.minFilter = sd.magFilter = MTLSamplerMinMagFilterLinear; sd.supportArgumentBuffers = YES;
g_blit_samp = [g_dev newSamplerStateWithDescriptor:sd];
}
Expand Down Expand Up @@ -10473,9 +10485,12 @@ static int present_encode(id<MTLCommandBuffer> cb, id<MTLTexture> src, id<MTLTex
rp.colorAttachments[0].storeAction = MTLStoreActionStore;
id<MTLRenderCommandEncoder> en = [cb renderCommandEncoderWithDescriptor:rp];
[en setViewport:vp];
[en setRenderPipelineState:g_blit_pso]; [en setFragmentTexture:present_raw_src(src) atIndex:0]; [en setFragmentSamplerState:g_blit_samp atIndex:0];
float uv[8] = {(float)cx / src.width, (float)cy / src.height, (float)cw / src.width, (float)ch / src.height,
(cx + 0.5f) / src.width, (cy + 0.5f) / src.height, (cx + cw - 0.5f) / src.width, (cy + ch - 0.5f) / src.height};
id<MTLTexture> raw = present_raw_src(src);
[en setRenderPipelineState:g_blit_pso]; [en setFragmentTexture:raw atIndex:0]; [en setFragmentSamplerState:g_blit_samp atIndex:0];
float uv[12] = {(float)cx / src.width, (float)cy / src.height, (float)cw / src.width, (float)ch / src.height,
(cx + 0.5f) / src.width, (cy + 0.5f) / src.height, (cx + cw - 0.5f) / src.width, (cy + ch - 0.5f) / src.height,
present_edr(dst.pixelFormat) ? front_present_edr_headroom() : 1,
raw.pixelFormat == MTLPixelFormatRGBA8Unorm_sRGB || raw.pixelFormat == MTLPixelFormatBGRA8Unorm_sRGB, 0, 0};
if (atomic_load_explicit(&g_origin_ll, memory_order_relaxed)) { uv[1] += uv[3]; uv[3] = -uv[3]; } // lower-left origin: the window image is stored bottom-up
[en setFragmentBytes:uv length:sizeof uv atIndex:0];
[en drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:3];
Expand Down Expand Up @@ -10557,7 +10572,11 @@ static int present_encode_m4(id<MTL4CommandBuffer> cb, id<MTLTexture> src, id<MT
double dw = dst.width, dh = dst.height;
MTLViewport vp = present_viewport(r[2], r[3], dw, dh);
int full = vp.originX == 0 && vp.originY == 0 && vp.width == dw && vp.height == dh;
id<MTLTexture> tex = present_raw_src(src), up = fx ? fx_encode(fx, cb, tex, r, vp, dst.pixelFormat, fx == &g_fx_present) : nil;
// MetalFX continues to process SDR perceptual bytes. The final present pass
// decodes/expands once; screenshots target BGRA8 and stay SDR even when EDR
// is requested. Do not feed linear EDR values into the perceptual scaler.
MTLPixelFormat upscale_format = present_edr(dst.pixelFormat) ? MTLPixelFormatBGRA8Unorm : dst.pixelFormat;
id<MTLTexture> tex = present_raw_src(src), up = fx ? fx_encode(fx, cb, tex, r, vp, upscale_format, fx == &g_fx_present) : nil;
if (up) { tex = up; r[0] = r[1] = 0; r[2] = up.width; r[3] = up.height; } // the scaled picture, the viewport's size: drawn 1:1
MTL4RenderPassDescriptor *rp = [MTL4RenderPassDescriptor new];
rp.colorAttachments[0].texture = dst;
Expand All @@ -10573,6 +10592,8 @@ static int present_encode_m4(id<MTL4CommandBuffer> cb, id<MTLTexture> src, id<MT
float *uv = uvb.contents; NSUInteger tw = tex.width, th = tex.height, cx = r[0], cy = r[1], cw = r[2], ch = r[3];
uv[0] = (float)cx / tw; uv[1] = (float)cy / th; uv[2] = (float)cw / tw; uv[3] = (float)ch / th;
uv[4] = (cx + 0.5f) / tw; uv[5] = (cy + 0.5f) / th; uv[6] = (cx + cw - 0.5f) / tw; uv[7] = (cy + ch - 0.5f) / th;
uv[8] = present_edr(dst.pixelFormat) ? front_present_edr_headroom() : 1;
uv[9] = tex.pixelFormat == MTLPixelFormatRGBA8Unorm_sRGB || tex.pixelFormat == MTLPixelFormatBGRA8Unorm_sRGB; uv[10] = uv[11] = 0;
if (atomic_load_explicit(&g_origin_ll, memory_order_relaxed)) { uv[1] += uv[3]; uv[3] = -uv[3]; } // lower-left origin: the window image is stored bottom-up
[g_present_args setAddress:uvb.gpuAddress atIndex:0];
[en setViewport:vp]; [en setRenderPipelineState:g_blit_pso];
Expand All @@ -10584,7 +10605,7 @@ static int present_encode_m4(id<MTL4CommandBuffer> cb, id<MTLTexture> src, id<MT
// returns whether it was submitted. Completion reports the encoding kind, or PRESENT_FAILED on a GPU error. Resources remain resident and retained until GPU completion.
static int present_offscreen_m4(id<MTLTexture> src, const int crop[4], id<MTLTexture> out, void (^done)(int)) {
id<MTL4CommandAllocator> al = [g_dev newCommandAllocator]; id<MTL4CommandBuffer> cb = [g_dev newCommandBuffer];
id<MTLBuffer> uvb = [g_dev newBufferWithLength:sizeof(float) * 8 options:MTLResourceStorageModeShared];
id<MTLBuffer> uvb = [g_dev newBufferWithLength:sizeof(float) * 12 options:MTLResourceStorageModeShared];
if (!al || !cb || !uvb) { MLOG("[RED] offscreen present: no command allocator or buffer"); return 0; }
id<MTLTexture> raw = present_raw_src(src);
fx_state fx = {0};
Expand Down
4 changes: 4 additions & 0 deletions dm/gfx/tests/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ check_t_perf_order: t_perf_order
DM_NVN_FAST_WAKE=0 DM_NVN_SLICE_MERGE=0 DM_NVN_CS_HOIST=0 MTL_DEBUG_LAYER=1 ./t_perf_order
t_link_ticks: t_link_ticks.m $(MTL_T_DEP)
clang $(CFLAGS) -fobjc-arc -o $@ t_link_ticks.m $(filter-out ../nvn/hle_nvn.c,$(MTL_T_SRC)) $(FW)
t_edr_policy: t_edr_policy.c ../nvn/nvn_edr.h
clang $(CFLAGS) -o $@ t_edr_policy.c
t_edr_present: t_edr_present.m edr_probe_host.c ../nvn/nvn_edr.h $(MTL_T_DEP)
clang $(CFLAGS) -fobjc-arc -o $@ t_edr_present.m edr_probe_host.c $(MTL_T_SRC) $(FW)
t_cpu_tex: t_cpu_tex.m $(MTL_T_DEP)
clang $(CFLAGS) -fobjc-arc -o $@ t_cpu_tex.m $(filter-out ../nvn/hle_nvn.c,$(MTL_T_SRC)) $(FW)
.PHONY: check_t_cpu_tex
Expand Down
4 changes: 4 additions & 0 deletions dm/gfx/tests/edr_probe_host.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Synthetic headroom for the GPU color-transform test, never a display claim.
#include <stdatomic.h>
_Atomic float edr_probe_headroom = 2;
float front_present_edr_headroom(void) { return atomic_load(&edr_probe_headroom); }
20 changes: 20 additions & 0 deletions dm/gfx/tests/t_edr_policy.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#include "../nvn/nvn_edr.h"
#include <stdio.h>
static int checks, failed;
#define CHECK(name, ok) do { checks++; int pass = !!(ok); failed += !pass; printf("%s: %s\n", name, pass ? "PASS" : "FAIL"); } while (0)
int main(void) {
unsetenv("DM_PRESENT_EDR"); CHECK("EDR is off by default", !nvn_edr_requested());
setenv("DM_PRESENT_EDR", "0", 1); CHECK("zero keeps SDR", !nvn_edr_requested());
setenv("DM_PRESENT_EDR", "1", 1); CHECK("one explicitly opts in", nvn_edr_requested());
setenv("DM_PRESENT_EDR", "true", 1); CHECK("other values do not accidentally opt in", !nvn_edr_requested());
CHECK("off keeps SDR headroom", nvn_edr_headroom(0, 1, 8, 8) == 1);
CHECK("an inactive scene keeps SDR", nvn_edr_headroom(1, 0, 8, 8) == 1);
CHECK("SDR-only display keeps SDR", nvn_edr_headroom(1, 1, 1, 1) == 1);
CHECK("current headroom limits output", fabsf(nvn_edr_headroom(1, 1, 1.2f, 8) - 1.2f) < 0.00001f);
CHECK("potential headroom also limits output", nvn_edr_headroom(1, 1, 8, 1.5f) == 1.5f);
CHECK("experimental boost is capped at two", nvn_edr_headroom(1, 1, 8, 8) == 2);
CHECK("invalid headroom falls back to SDR", nvn_edr_headroom(1, 1, NAN, 8) == 1);
CHECK("invalid potential falls back to SDR", nvn_edr_headroom(1, 1, 2, INFINITY) == 1);
printf("edr policy: %d checks, %d failed\n", checks, failed);
return failed != 0;
}
Loading