diff --git a/dm/gfx/nvn/dmsh_chain.h b/dm/gfx/nvn/dmsh_chain.h index 9a8bbaafbb..41b0bb1342 100644 --- a/dm/gfx/nvn/dmsh_chain.h +++ b/dm/gfx/nvn/dmsh_chain.h @@ -1,3 +1,3 @@ // Generated file (dm/gfx/tools/dmsh_chain.py --header): the B shader library translation chain version this source tree expects (default options), one for every game; // the host loads only /b-/. Run dmsh_chain.py --header after changing the chain's tools and commit this file with the change -#define DMSH_CHAIN "764aff238713" +#define DMSH_CHAIN "8a1b56655ae9" diff --git a/dm/gfx/tools/dmsh_build.py b/dm/gfx/tools/dmsh_build.py index e04fb6f782..9a9782cdea 100644 --- a/dm/gfx/tools/dmsh_build.py +++ b/dm/gfx/tools/dmsh_build.py @@ -56,7 +56,9 @@ def precise(msl): return re.sub(r'\bfast::(min|max|clamp)\(', r'precise::\1(', m # -O1: the front end's -O2 (CorrelatedValuePropagation, then SimplifyCFG) folds the translated shaders' repeated tests of one value into switches whose cases jump past their own join. # Apple's GPU backend then exceeds its control-flow ID budget ("Max Unstruct ID 758 ... exceeds supported limit", a note, not an error) and falls back to a slower lowering: # Odyssey's alRenderMaterialUber took 25 minutes and 17.3 MB of GPU code, 31 s and 1.3 MB at -O1. Ordinary shaders come out within a few percent either way (the backend optimizes again) -MAC_METAL = ['xcrun', '-sdk', 'macosx', 'metal', '-c', '-O1', '-fpreserve-invariance'] +# -fwrapv: signed integer overflow wraps, as SPIR-V's integer arithmetic and the Switch's GPUs do; in MSL it is undefined, and the front end folds around it +# (backport of seedds/nxnative e4e5ff6: Ocarina of Time's sign extension a = w * 65536, (a >> 31) << 16 | uint(a) >> 16 took its sign bits from bits 15-30 of w) +MAC_METAL = ['xcrun', '-sdk', 'macosx', 'metal', '-c', '-O1', '-fpreserve-invariance', '-fwrapv'] MAC_RECORD = ' '.join(MAC_METAL) + '; xcrun -sdk macosx metallib' # The outer shader and fetch-variant caches restore compiled libraries directly, # before the MSL compile cache can check their compiler. Keep override builds separate there too. @@ -670,6 +672,21 @@ def vs_init(src): k += len('main0_out out = {};') return src[:k] + ''.join(f'\n out.{n} = float4(0.0, 0.0, 0.0, 1.0);' for n in outs) + src[k:] +VP_BASE_INSTANCE = '(*spvDescriptorSet0.vp_c0)._m0[100].y' # c0 + 0x644, the driver's base-instance word (row 100, second component) + +def vs_base_instance(src): + """The draw's base instance from Metal, not from the driver segment. GLSLC adds c0 + 0x644 to Maxwell's instance ID where a shader needs the absolute instance (gl_InstanceID + + gl_BaseInstance); the host writes that word from the draw command (nvn_mtl.m encode_args), but an indirect draw's base instance is in its argument record, which the GPU may write + earlier in the same submission. Metal passes every draw's base instance, direct or indirect, as [[base_instance]], so a vertex function reads it there. Super Mario Party Jamboree's + GPU-driven instances (276504dc5c987ecf writes the records, 4b05556737e9535f draws them: record = instance + c0 + 0x644, 80 bytes each) all took the first records' transforms + with 0 there, and stray grey triangles crossed the board. The segment's rows are float4 holding raw bits, so the word becomes the float with the base instance's bits; a function without + the parameter gets it (backport of seedds/nxnative 8fa9b2d1)""" + if VP_BASE_INSTANCE not in src: return src + src = src.replace(VP_BASE_INSTANCE, 'as_type(gl_BaseInstance)') + def add(m): + return m.group(0) if '[[base_instance]]' in m.group(0) else m.group(0)[:-1] + ', uint gl_BaseInstance [[base_instance]])' + return re.sub(r'\bvertex\s+\w+\s+main0\s*\((?:[^()]|\([^()]*\))*\)', add, src) # the parameter list, whose attributes hold parentheses ([[buffer(0)]]) + def vs_layer(src): """A vertex shader that writes the layer (render_target_array_index) loses the point size output. The translator writes gl_PointSize = 1.0 in every vertex shader; Metal builds a pipeline whose vertex function writes the layer only with an input primitive class, and one that writes a point size only with the point class (or none), so with both no pipeline can be built. @@ -943,8 +960,8 @@ def finish(key, w, j, c1): except ValueError as e: return 'image atomic MSL failed', str(e), None src = precise(src); open(msl, 'w').write(src) # min / max / clamp with Maxwell's NaN results (see precise) pad = [] - if j['stage'] == 'vs': # outputs start as (0,0,0,1) (see vs_init); no point size beside a layer output (see vs_layer) - src = vs_layer(vs_init(src)); open(msl, 'w').write(src) + if j['stage'] == 'vs': # outputs start as (0,0,0,1) (see vs_init); no point size beside a layer output (see vs_layer); the base instance from Metal (see vs_base_instance) + src = vs_base_instance(vs_layer(vs_init(src))); open(msl, 'w').write(src) if j['stage'] == 'vs' and PAIRS.get(key): # outputs the pixel shader reads but the vertex shader does not write get (0,0,0,1) (see vs_pad) src, pad = vs_pad(key, src) if pad: open(msl, 'w').write(src) diff --git a/dm/gfx/tools/gl_shaders.py b/dm/gfx/tools/gl_shaders.py index 6d4e404107..0d262aec40 100644 --- a/dm/gfx/tools/gl_shaders.py +++ b/dm/gfx/tools/gl_shaders.py @@ -162,7 +162,7 @@ def metallib(msls, out, sdk, extra): os.makedirs(out, exist_ok=True) def comp(m): a = m[:-6] + f'.{sdk}.air' - r = subprocess.run(['xcrun', '-sdk', sdk, 'metal', '-c', '-fpreserve-invariance', *extra, m, '-o', a], capture_output=True, text=True) + r = subprocess.run(['xcrun', '-sdk', sdk, 'metal', '-c', '-fpreserve-invariance', '-fwrapv', *extra, m, '-o', a], capture_output=True, text=True) if r.returncode: red(f'{m}: metal compile failed: {r.stderr.strip()[:400]}') return a with cf.ThreadPoolExecutor(os.cpu_count()) as ex: airs = list(ex.map(comp, msls)) diff --git a/dm/gfx/tools/vk_shaders.py b/dm/gfx/tools/vk_shaders.py index 38e4155b47..7d72d34cc4 100644 --- a/dm/gfx/tools/vk_shaders.py +++ b/dm/gfx/tools/vk_shaders.py @@ -147,7 +147,7 @@ def metallib(msls, out, sdk, extra): airs = [] def comp(m): a = m[:-6] + f'.{sdk}.air' - r = subprocess.run(['xcrun', '-sdk', sdk, 'metal', '-c', '-fpreserve-invariance', *extra, m, '-o', a], capture_output=True, text=True) + r = subprocess.run(['xcrun', '-sdk', sdk, 'metal', '-c', '-fpreserve-invariance', '-fwrapv', *extra, m, '-o', a], capture_output=True, text=True) if r.returncode: red(f'{m}: metal compile failed: {r.stderr.strip()[:400]}') return a with cf.ThreadPoolExecutor(os.cpu_count()) as ex: airs = list(ex.map(comp, msls)) diff --git a/dm/ios/tools/ios_metal.py b/dm/ios/tools/ios_metal.py index f270b18a9a..2abb5b1a6a 100644 --- a/dm/ios/tools/ios_metal.py +++ b/dm/ios/tools/ios_metal.py @@ -5,7 +5,7 @@ # and the default fast mode lets each GPU's backend compiler treat them as always false; on the A20 Pro the opening course's odd frames had 727k NaN pixels in the main image (a few dozen with the same AIR on the M4 Pro), which became the black band / black-edged glow # -fpreserve-invariance: [[invariant]] position outputs only agree across shaders with this flag (as dmsh_build.py's MAC_METAL: a depth pre-pass followed by a less-or-equal G-buffer) # -O1: -O2's switch folding leaves a control-flow graph Apple's GPU backend can only lower slowly into very large code (as dmsh_build.py's MAC_METAL) -METAL = ['xcrun', '-sdk', 'iphoneos', 'metal', '-c', '-O1', '-mios-version-min=18.0', '-fmetal-math-mode=relaxed', '-fpreserve-invariance'] +METAL = ['xcrun', '-sdk', 'iphoneos', 'metal', '-c', '-O1', '-mios-version-min=18.0', '-fmetal-math-mode=relaxed', '-fpreserve-invariance', '-fwrapv'] # -fwrapv: see dmsh_build.py MAC_METAL METALLIB = ['xcrun', '-sdk', 'iphoneos', 'metallib'] RECORD = ' '.join(METAL) + '; ' + ' '.join(METALLIB) # Written into json chain.metal