// TOTK Vulkan adaptation of MacNativeGames games/totk/tools/totk_xlate/Program.cs (MIT, see LICENSE; MNG commit b75be8cf). // Provenance: that file is based on D2R Mobile dm/gfx/tools/dmsh_xlate/Program.cs. Changes here: SPIR-V for Vulkan is the final // output (no MSL service, no VertexCounterLowering, no NEAR_IS_MINUS_W rewrite), depth clip control reported as supported, // non-constant texture offsets reported as unsupported, --disasm dropped. // Protocol (stdout carries replies only; diagnostics go to stderr): // totk_xlate --serve one request per stdin line: {"job": {...}, "out": ""}; one reply line per request: // "ok " or "fail "; a malformed line gets "fail ? " and the loop continues. // totk_xlate batch mode: a JSON array of jobs, prints a summary, exit 1 if any failed. // job: { "key", "stage": "vs|ps|cs", "code": path (graphics = 0x50-byte SPH + instructions, compute = instructions), "c1": hex, // "tex": {slot: type}, "img": {slot: {type, fmt}}, "attr": {loc: float|sint|uint}, "fout": {idx: float|sint|uint}, // "local": [x, y, z], "shared": bytes, "debug": bool, "reduced": bool, "softfloat64": bool, "zero_locals": bool, // "sbuf_align": int, "subgroup": int } // Output: /.spv and /.res.json (bindings, queried slots, notes). using Ryujinx.Common.Memory; using Ryujinx.Graphics.Shader; using Ryujinx.Graphics.Shader.Translation; using System; using System.Collections.Generic; using System.Linq; using System.IO; using System.Runtime.InteropServices; using System.Text.Json; using System.Text.Json.Nodes; static class SwshXlate { const int TexBase = 0x168, SampBase = 0x568; // NVN 分离贴图 / 采样器句柄在 c[2] 的字节基址(每槽 8 字节;dm/gfx/tools/maxwell_refs.py 全量实测) | byte base of NVN separate texture / sampler handles in c[2] (8 bytes per slot; full measurement in dm/gfx/tools/maxwell_refs.py) sealed class Accessor : IGpuAccessor { readonly byte[] _code; readonly JsonObject _job; int _binding = 1; public readonly List Notes = new(); public readonly SortedSet TexQueried = new(), ImgQueried = new(); readonly byte[] _c1; public Accessor(byte[] code, JsonObject job) { _code = code; _job = job; _c1 = job["c1"] is JsonNode h ? Convert.FromHexString((string)h) : Array.Empty(); } // c1 常数段(着色器包控制段前 0x100 字节):间接跳转 BRX 的跳转表在这里(LDC c[1]);不提供时翻译器解不出跳转目标、控制流出错(粒子着色器死循环) // c1 constant segment (the 0x100 bytes before the control segment in the shader package): the jump table of indirect jump BRX lives here (LDC c[1]); without it the translator cannot resolve jump targets and control flow goes wrong (particle shaders loop forever) public uint ConstantBuffer1Read(int offset) { if (offset >= 0 && offset + 4 <= _c1.Length) return BitConverter.ToUInt32(_c1, offset); Notes.Add($"c1 读越界 +{offset:X}(c1 长 {_c1.Length})"); return 0; } // 全部资源放进第 0 组,各占不同绑定号(spirv-cross 出成一个参数缓冲);常量段号 index 的缓冲由翻译器给 binding = index+1 的惯例不依赖 // All resources go into set 0, each with a distinct binding number (spirv-cross emits them as one argument buffer); the translator's convention of giving the buffer of constant segment number index binding = index+1 is not relied on public SetBindingPair CreateConstantBufferBinding(int index) => new(0, _binding++); public SetBindingPair CreateImageBinding(int count, bool isBuffer) { int b = _binding; _binding += count; return new(0, b); } public SetBindingPair CreateStorageBufferBinding(int index) => new(0, _binding++); public SetBindingPair CreateTextureBinding(int count, bool isBuffer) { int b = _binding; _binding += count; return new(0, b); } public ReadOnlySpan GetCode(ulong address, int minimumSize) => MemoryMarshal.Cast(new ReadOnlySpan(_code)[(int)address..]); public int QuerySamplerArrayLengthFromPool() => 32; public int QueryTextureArrayLengthFromBuffer(int slot) => 32; public int QueryTextureArrayLengthFromPool() => 32; string Get(string table, int slot) => _job[table] is JsonObject t && t[slot.ToString()] is JsonNode n ? (n is JsonObject o ? (string)o["type"] : (string)n) : null; int TexSlot(int handle, int cbufSlot) { (int off0, _, _) = TextureHandle.UnpackOffsets(handle); int bytes = off0 * 4; return (cbufSlot < 0 || (cbufSlot & 0xffff) == 2) && bytes >= TexBase && bytes < SampBase && (bytes - TexBase) % 8 == 0 ? (bytes - TexBase) / 8 : -1; } public SamplerType QuerySamplerType(int handle, int cbufSlot = -1) { int slot = TexSlot(handle, cbufSlot); if (slot >= 0) TexQueried.Add(slot); string t = slot >= 0 ? Get("tex", slot) ?? Get("img", slot) : null; if (t == null) { Notes.Add($"贴图句柄 {handle:X}(常量段 {cbufSlot},槽 {slot})无 PC 类型,按 2D"); return SamplerType.Texture2D; } if (t.EndsWith("_depth")) t = t.Substring(0, t.Length - 6); // depth-format texture: same sampler type here; the host binds a depth view return t switch { "1d" => SamplerType.Texture1D, "1darray" => SamplerType.Texture1D | SamplerType.Array, "buffer" => SamplerType.TextureBuffer, "2d" => SamplerType.Texture2D, "2darray" => SamplerType.Texture2D | SamplerType.Array, "2dms" => SamplerType.Texture2D | SamplerType.Multisample, "2dmsarray" => SamplerType.Texture2D | SamplerType.Multisample | SamplerType.Array, "3d" => SamplerType.Texture3D, "cube" => SamplerType.TextureCube, "cubearray" => SamplerType.TextureCube | SamplerType.Array, _ => SamplerType.Texture2D, }; } public TextureFormat QueryTextureFormat(int handle, int cbufSlot = -1) { int slot = TexSlot(handle, cbufSlot); if (slot >= 0) ImgQueried.Add(slot); string f = _job["img"] is JsonObject t && slot >= 0 && t[slot.ToString()] is JsonObject o ? (string)o["fmt"] : null; if (f == null) { Notes.Add($"图像句柄 {handle:X} 槽 {slot} 无 PC 格式,按 R8G8B8A8Unorm"); return TextureFormat.R8G8B8A8Unorm; } return Enum.TryParse(f, true, out TextureFormat tf) ? tf : TextureFormat.R8G8B8A8Unorm; } int Local(int i) => _job["local"] is JsonArray a && a.Count > i ? (int)a[i] : 1; public int QueryComputeLocalSizeX() => Local(0); public int QueryComputeLocalSizeY() => Local(1); public int QueryComputeLocalSizeZ() => Local(2); public int QueryComputeSharedMemorySize() => _job["shared"] is JsonNode n ? (int)n : 0; // 配对 PC 的 groupshared 声明之和 | sum of the paired PC shader's groupshared declarations static AttributeType Attr(string s) => s switch { "sint" => AttributeType.Sint, "uint" => AttributeType.Uint, _ => AttributeType.Float }; public GpuGraphicsState QueryGraphicsState() { Array32 attr = default; Array8 fout = default; if (_job["attr"] is JsonObject a) foreach (var kv in a) attr[int.Parse(kv.Key)] = Attr((string)kv.Value); if (_job["fout"] is JsonObject f) foreach (var kv in f) fout[int.Parse(kv.Key)] = Attr((string)kv.Value); // Vulkan: VK_EXT_depth_clip_control handles NEAR_IS_MINUS_W, so the shader is never rewritten ("depth_minus_w" in a job is ignored). const bool depthMinusW = false; // "early_fragment_tests": the fragment shader's NVN control byte +0x718 (the driver's early-Z-force, Ryujinx's // EarlyZForce); the SPIR-V then declares EarlyFragmentTests (MacNativeGames ae8f4f4). bool earlyZ = _job["early_fragment_tests"] is JsonValue ez && ez.TryGetValue(out bool eb) && eb; return new GpuGraphicsState(earlyZ, InputTopology.Triangles, false, TessPatchType.Triangles, TessSpacing.EqualSpacing, false, false, false, depthMinusW, false, 1f, AlphaTestOp.Always, 0f, attr, false, fout, false, false /* 不做 Y 取反 | no Y flip */, true /* 原点左上 | origin upper-left */, false); } public bool QueryHostSupportsDepthClipControl() => true; // Vulkan: the ConstOffset / ConstOffsets image operands Ryujinx emits must be constants. A TLD4.PTP whose offsets // come from registers (agl_technique_pfx compute b940aa86, dfe7662f) otherwise becomes an OpConstantComposite over // computed ids, which spirv-val rejects; false makes TexturePass emulate only non-constant offsets. public bool QueryHostSupportsNonConstantTextureOffset() => false; // "softfloat64": true lowers FP64 operations to FP32 (Ryujinx's DoubleToFloat pass), for a host without shaderFloat64. public bool QueryHostSupportsShaderFloat64() => _job["softfloat64"] is not JsonValue v || !(v.TryGetValue(out bool b) && b); public bool QueryHostSupportsTextureShadowLod() => true; // Sub-texel precision bits of a host whose gathers round differently from Maxwell (AMD, Intel): Ryujinx then // biases gather coordinates so the same texels are picked (TexturePass.InsertCoordGatherBias). 0: no bias. public int QueryHostGatherBiasPrecision() => _job["gather_bias"] is JsonNode n ? (int)n : 0; // 作业 reduced = true:放宽精度——翻译器只给强制精确的运算打 NoContraction(缺省全部打:保持 Maxwell 分开舍入的逐位结果,代价是 MSL 里大量禁优化的 spvFMul 等) // Job reduced = true: relaxed precision - the translator marks NoContraction only on operations that must be exact (the default marks all: keeps the bit-exact results of Maxwell's separate rounding, at the cost of many optimization-disabled spvFMul etc. in the MSL) public bool QueryHostReducedPrecision() => _job["reduced"] is JsonNode n && (bool)n; // Host answers for GPUs other than the RX 9070 (runtime/vk_caps.c); absent from the job they keep Ryujinx's defaults. // "sbuf_align": n: the host binds each storage buffer at its guest address rounded down to n (minStorageBufferOffsetAlignment // above 4), so storage offsets count from there (Ryujinx's unaligned path; the multi-target path uses n too). public bool QueryHasUnalignedStorageBuffer() => _job["sbuf_align"] is JsonNode; public int QueryHostStorageBufferOffsetAlignment() => _job["sbuf_align"] is JsonNode n ? (int)n : 16; // "subgroup": n: lanes per subgroup when the host cannot pin 32 (AMD GCN/Vega run 64): ballots and votes span n lanes. public int QueryHostSubgroupSize() => _job["subgroup"] is JsonNode n ? (int)n : 32; // "mvprev": true: the vertex shader also writes last frame's position, computed from slots bank + 15 in place of // the user banks c3-c16 (patch 0015, runtime/vk_mvprev.c); res.json's "mvprev" names the output location (-1: // none) and "mvprev_banks" the banks the position depends on (bit n: cn). public bool QueryMotionPrevious() => _job["mvprev"] is JsonValue v && v.TryGetValue(out bool b) && b; // "zero_locals": true: function-local variables start at 0 (patch 0016, runtime/vk_caps.c decides per driver). public bool QueryZeroLocals() => _job["zero_locals"] is JsonValue v && v.TryGetValue(out bool b) && b; public int MotionPreviousLocation = -1, MotionPreviousBanks; public void ReportMotionPrevious(int location, int banks) => (MotionPreviousLocation, MotionPreviousBanks) = (location, banks); } internal static string Translate(JsonObject job, string outDir) // also called by the --service mode (Service.cs) { string key = (string)job["key"]; try { byte[] code = File.ReadAllBytes((string)job["code"]); var acc = new Accessor(code, job); // 作业 debug = false:关 DebugMode(DebugMode 会跳过 AST 优化器,产物满是临时变量;不改语义) // Job debug = false: turn off DebugMode (DebugMode skips the AST optimizer and the output is full of temporaries; semantics unchanged) var flags = (job["debug"] is JsonNode dbg && !(bool)dbg ? 0 : TranslationFlags.DebugMode) | ((string)job["stage"] == "cs" ? TranslationFlags.Compute : 0); var ctx = Translator.CreateContext(0, acc, new TranslationOptions(TargetLanguage.Spirv, TargetApi.Vulkan, flags)); ShaderProgram p = ctx.Translate(); File.WriteAllBytes(Path.Combine(outDir, key + ".spv"), p.BinaryCode); var res = new JsonObject { ["key"] = key, ["stage"] = (string)job["stage"], ["notes"] = new JsonArray(acc.Notes.ConvertAll(n => (JsonNode)n).ToArray()), ["cbufs"] = new JsonArray(), ["sbufs"] = new JsonArray(), ["textures"] = new JsonArray(), ["images"] = new JsonArray(), ["uses_frag_coord"] = p.Info.UsesFragCoord, ["frag_out_map"] = p.Info.FragmentOutputMap, ["tex_queried"] = new JsonArray(acc.TexQueried.Select(x => (JsonNode)x).ToArray()), ["img_queried"] = new JsonArray(acc.ImgQueried.Select(x => (JsonNode)x).ToArray()), }; if (acc.QueryMotionPrevious()) (res["mvprev"], res["mvprev_banks"]) = (acc.MotionPreviousLocation, acc.MotionPreviousBanks); foreach (var b in p.Info.CBuffers) res["cbufs"].AsArray().Add((JsonNode)new JsonObject { ["binding"] = b.Binding, ["slot"] = b.Slot }); foreach (var b in p.Info.SBuffers) res["sbufs"].AsArray().Add((JsonNode)new JsonObject { ["binding"] = b.Binding, ["slot"] = b.Slot, ["cb_slot"] = b.SbCbSlot, ["cb_offset"] = b.SbCbOffset, ["flags"] = (int)b.Flags }); foreach (var (list, name) in new[] { (p.Info.Textures, "textures"), (p.Info.Images, "images") }) foreach (var t in list) res[name].AsArray().Add((JsonNode)new JsonObject { ["binding"] = t.Binding, ["type"] = (int)t.Type, ["format"] = t.Format.ToString(), ["cbuf_slot"] = t.CbufSlot, ["handle"] = t.HandleIndex, ["array"] = t.ArrayLength, ["separate"] = t.Separate, ["flags"] = (int)t.Flags }); File.WriteAllText(Path.Combine(outDir, key + ".res.json"), res.ToJsonString(new JsonSerializerOptions { WriteIndented = true })); return null; } catch (Exception e) { if (Environment.GetEnvironmentVariable("DMSH_STACK") != null) Console.Error.WriteLine(e.StackTrace); return $"{e.GetType().Name}: {e.Message.Split('\n')[0]}"; } } static int Main(string[] args) { if (args.Length == 1 && args[0] == "--serve") { // One request per line: {"job": {...}, "out": ""}; one reply line "ok " / "fail ". string line; while ((line = Console.ReadLine()) != null) { if (line.Length == 0) continue; string key = "?", err; try { var req = JsonNode.Parse(line).AsObject(); var job = req["job"].AsObject(); string o = (string)req["out"]; key = (string)job["key"]; Directory.CreateDirectory(o); err = Translate(job, o); } catch (Exception e) { err = $"{e.GetType().Name}: {e.Message.Split((char)10)[0]}"; } Console.WriteLine(err == null ? $"ok {key}" : $"fail {key} {err}"); Console.Out.Flush(); } return 0; } var jobs = JsonNode.Parse(File.ReadAllText(args[0])).AsArray(); string outDir = args[1]; Directory.CreateDirectory(outDir); int ok = 0, bad = 0; foreach (JsonObject job in jobs) { string err = Translate(job, outDir); if (err == null) ok++; else { bad++; Console.Error.WriteLine($"【红】{(string)job["key"]}:{err}"); } } Console.WriteLine($"翻译 {ok} 成功,{bad} 失败"); return bad == 0 ? 0 : 1; } }