跳转至

Maxwell / NVN 行为:移植必须复现

用于诊断图形 bug 的参考。每一项说明:Maxwell(GM20B,3D class B197,compute B1C0)与 NVN 的行为、Vulkan / AMD(RDNA4,our GPU0)的替代行为、模拟器和移植项目如何处理,以及来源。条目按导致可见伪影的可能性排序。“Ours:”标记我们的运行时所做的处理(grep 级检查,见第 7 节;我们的代码按文件和函数引用)。许可:yuzu/suyu 为 GPL,仅引用其行为。

来源缩写:

1. 渲染目标

# Maxwell / NVN Vulkan / AMD 模拟器 / 移植项目 来源
1.1 片段着色器未写入的颜色目标保留其旧纹素,即使启用了混合。 写入/混合的是未定义值。 NXN:对未写入的输出使用零写入掩码且不混合(reflection colors)。这里已经修复。 NXN;MNG games/totk/tests/metal_semantics/probe.m:5
1.2 下一阶段将未写入的顶点输出读作 (0, 0, 0, 1),包括部分写入输出的未写入分量(VS 写入 xyz,FS 除以 w)。 FS 输入没有 VS 输出:接口不匹配,值未定义;已写入输出中的未写入分量未定义(AMD:取决于寄存器内容,通常为 0)。 Ryujinx 只初始化下一阶段读取的分量,但仅在阶段已链接时(Translation/Translator.cs:212-227、NextInputAttributesComponents);位置从 (0,0,0,1) 开始(:259)。NXN 的 Link's Awakening:太阳阴影查找除以 w = 0,场景变暗,没有阴影。Ours:缺失的输出用 (0, 0, 0, 1) 填充,未链接的输出由 translator patch 0006 初始化(第 7 节,检查 2)。 NXN
1.3 清除操作遵守剪刀矩形以及清除自身的通道/模板写入掩码,并且可以出现在 pass 内的绘制调用之间。SET_CLEAR_SURFACE_CONTROL (0x10f8) 包含 RESPECT_STENCIL_MASK、USE_CLEAR_RECT、USE_SCISSOR0、USE_VIEWPORT_CLIP0。 vkCmdClearAttachments 接受一个矩形,但忽略颜色写入掩码;load-op 清除是针对整个 attachment 的。 NXN/MNG 用清除的掩码绘制一个三角形(clear_color_tri、clear_ds_tri);TotK 使用 mid-pass、部分掩码和部分剪刀清除(NXN)。yuzu:“red dot”是清除命中了错误的 RT 索引(YZ-2305,PR 10243)。硬件做法(DK3D):CLEAR_BUFFERS(0x674 = 0x19d0)在一个字中携带颜色通道掩码(位 2-5)、深度(0)、模板(1)、目标 id(6-9)和层(10-20);ClearBufferFlags(0x43E = 0x10f8)一次性设置为 StencilMask | Scissor(maxwell/gpu_3d_base.cpp:69),因此每次清除都会被 scissor 0 裁剪,其模板位也会被模板正面写入掩码寄存器裁剪,并且 DK3D 的颜色清除不会编程 ColorWriteMask(0x680)。模板清除会在清除前紧挨着写入 StencilFrontMask(0x4E7)和 ClearStencil(0x368),之后恢复被遮蔽的状态(gpu_3d_base.cpp:497-510)。颜色清除前后会关闭多重采样光栅化(:459, :465)。数组:一个 MME 宏会为每一层重复清除(maxwell/clear_buffers.mme:5-22)。没有 pass 概念:清除操作位于绘制调用之间。Ours:带掩码的颜色和模板清除会被跳过(检查 3)。 OGD 0x10f8;NXN;DK3D maxwell/gpu_3d_base.cpp:444-512、engine_3d.def:305, 644
1.4 条件渲染(SET_RENDER_ENABLE_A..C 0x1550-0x1558,NVN SetRenderEnableConditional)在 GPU 上按命令顺序比较 addr 和 addr+16 处的两个 64 位计数器。TotK 几乎把每个清除都包在其中。TIMESTAMP 报告会向计数器字段写入 0。 VK_EXT_conditional_rendering 将一个 32 位字与零比较。 NXN:遵守该行为修复了黑色 3D 场景(光体积深度从未被清除)。deko3d 完全没有条件渲染(engine_3d.def 中没有 SET_RENDER_ENABLE),因此无法确定比较规则。Ours:在记录时于 CPU 上求值;来自未完成提交的报告计为 0(检查 5)。 OGD;NXN
1.5 深度模式是运行时状态:nvnDeviceSetDepthMode 的 near = -w(GL,NVN 默认)或 0。寄存器 SET_VIEWPORT_Z_CLIP(0x0d7c)为 NEGATIVE_W_TO_POSITIVE_W / ZERO_TO_POSITIVE_W。 Vulkan 会裁剪 z < 0,除非使用 VK_EXT_depth_clip_control 的 negativeOneToOne。 Ryujinx 从视口变换推断模式,若无法推断则回退到寄存器(GetDepthMode,RYU-SU)。NXN:错误的模式要么裁剪一切(黑窗口),要么把所有深度放到 0.5..1(景深模糊整个屏幕)。Ours:从 NVN 状态启用 negativeOneToOne。DK3D 会编程两部分:SetDepthMode(0x35F = 0xd7c)选择变换前的裁剪范围,视口变换本身把 z 映射到 [near, far]:对于 -1..1,z 缩放 = 0.5 * (far - near)、z 偏移 = 0.5 * (near + far);对于 0..1,缩放 = far - near、偏移 = near(maxwell/gpu_3d_base.cpp:143-147, 336-343)。这与 Vulkan negativeOneToOne 的数学相同。 OGD 0x0d7c;NXN-64,NXN;DK3D maxwell/gpu_3d_base.cpp:136-147, 321-362
1.6 深度钳制 / 裁剪:SET_VIEWPORT_CLIP_CONTROL (0x193c) 具有独立的 PIXEL_MIN_Z/PIXEL_MAX_Z 裁剪或钳制模式,以及几何裁剪模式。 depthClampEnable 也会禁用 z 裁剪(除非使用 VK_EXT_depth_clip_enable)。RADV 曾有一个动态深度钳制驱动缺陷,导致 TotK 顶点爆炸。 Ryujinx:clamp = !(clip & DepthClampDisabled)(RYU-SU UpdateDepthClampState)。yuzu 在受影响的 RADV 上禁用了动态深度钳制(YZ-2305, PR 10262)。DK3D 只有一个开关 depthClampEnable,它写入 VIEW_VOLUME_CLIP_CONTROL(0x64F = 0x193c),开启时值为 0x101A,关闭时值为 0x181D(初始化时默认是 0x181D);它把这些位标记为“待办:弄清楚这到底做什么”。位 3-4(DepthClampNear/Far)在两个值中都被设置,位 0-2 和 11 不同。没有单独的深度裁剪开关。视口近/远寄存器(Viewport 数组第 2-3 个词)携带钳制范围,当 far < near 时交换(:349-350)。Vulkan 钳制到视口深度的 [min, max] 与之匹配。 OGD 0x193c;DK3D maxwell/gpu_3d_state.cpp:42、gpu_3d_base.cpp:135-139, 349-350、engine_3d.def:621
1.7 深度格式:D24S8 是原生格式;D24 在 0..1 范围内统一量化为 2^-24。对于 D24 目标,多边形偏移单位为 D24 步长;SET_DEPTH_BIAS_CONTROL (0x1110) DEPTH_FORMAT_DEPENDENT。 RX 9070 没有 D24S8 附件;D32F 在接近 0 时有更细的步长,并采用基于每图元指数的 r。EQUAL / LEQUAL 测试在预通道与后续通道之间,以及贴花偏移上的表现会不同。 yuzu 在将 D24 模拟为 D32F 时会重新缩放深度偏移单位;Ryujinx 传入 units / 2(寄存器值是 API 值的两倍)(RYU-SU UpdateDepthBiasState)。MNG:D16 加载清除 0.5 → 32767,而片段 0.5 → 32768,导致 Ultrahand/Recall 分类瓦片可见。我们:D24S8 → D32_SFLOAT_S8,偏移未重新缩放(检查 4)。DK3D 将 PolygonOffsetUnits(0x56F = 0x15bc)写为 constantFactor * 2.0f,clamp 写入 0x61F,factor 写入 0x55B(maxwell/gpu_3d_state.cpp:148-157),证实了 x2 寄存器。 OGD 0x1110, 0x15bc, 0x156c;MNG docs/how-it-works.md:57-61;DK3D maxwell/gpu_3d_state.cpp:154
1.8 位置不变性:相同的顶点位置运算在不同程序中产生相同的深度(预通道后使用 EQUAL / LEQUAL)。 仅对于声明为 Invariant 的输出。 Ryujinx 为顶点位置添加 Invariant 装饰(CodeGen/Spirv/Declarations.cs:571),并将浮点运算标记为 NoContraction,除非 QueryHostReducedPrecision 返回真(CodeGen/Spirv/Instructions.cs:2047)。NXN:Odyssey 的地面在缺少该装饰时出现 z-fighting;LA 的 foliage 在预通道失败时,使用 EQUAL 深度测试绘制的 foliage 会消失。我们:翻译器默认(全部 NoContraction)。 NXN
1.9 对同时作为绑定渲染目标的纹理进行采样:Maxwell 的纹理缓存与 ROP 写入并不一致;NVN 要求在写入与采样之间使用 nvnCommandBufferBarrier(INVALIDATE_TEXTURE)(方法 INVALIDATE_TEXTURE_DATA_CACHE 0x1338、..._NO_WFI 0x1288)。读取不同的 mip 级别/层,或在测试时以只读方式读取深度,则没有问题。 在同一渲染 pass 中,同一图像既作为附件又被采样会形成反馈循环:除非使用 VK_EXT_attachment_feedback_loop_layout,否则结果未定义;在 AMD 上,DCC/HTILE 压缩数据不经解压缩无法被采样器读取。 yuzu:通过在附件反馈循环上添加屏障,修复了 Ultrahand 辉光的像素化问题(YZ-2305, PR 10402)。GCN 上 BotW 的黑色纹理通过 AMD_DEBUG=nohyperz(关闭 HTILE)绕过(https://git.openpunk.com/OpenPunk/yuzu/wiki/commit/bd401539420182cbbf8bc638b042536a7e690c3f)。我们:一条 NVN 屏障会结束当前 pass 并插入完整内存屏障;不会检测“采样纹理同时是当前附件”的情况(检查 10)。DK3D 的屏障配方(maxwell/gpu_base.cpp:49-115):排序部分与缓存部分是分开的。排序:Tiles = TiledCacheBarrier(0x3DF),Fragments = FragmentBarrier(0x378),Primitives = WaitForIdle(0x44),Full = gpfifo SetReference 加上带 NoPrefetch 的新 gpfifo 条目,这样后续命令和间接参数获取会在所有事情完成后才发生。Tiles 和 Fragments 在缓存失效前需要隐式 WFI。缓存(各自可选):纹理数据 InvalidateTextureDataCache(0x3DD,WFI)或 ..NoWfi(0x4A2);着色器 InvalidateShaderCaches(0x369:指令、全局数据、常量);描述符 InvalidateTextureHeaderCacheNoWfi(0x50A)+ InvalidateSamplerCacheNoWfi(0x509);zcull 0x4B2;L2 gpfifo MemOpB L2FlushDirty + L2SysmemInvalidate。纹理缓存失效是单独的标志;仅靠排序不会刷新它。每次队列刷新都以图像 + 着色器 + 描述符 + L2 失效结束,“以确保 CPU 更新在两次刷新之间可见”(:28-47):CPU 写入到纹理和描述符的数据会在下一次提交边界到达 GPU,而不是在提交中途(2.2)。 OGD;DK3D maxwell/gpu_base.cpp:28-115
1.10 重复附件:同一纹理绑定到两个颜色槽,其中一个的写入掩码为 0 时不会相互干扰(ROP 只写入已启用的通道)。 基于瓦片或压缩的附件可能会把未写入的副本覆盖到已写入的副本上。 NXN BotW G-buffer(c1 = c2,c3 = c4):将重复槽位置为不绑定(rt_dup_slot)。 NXN
1.11 每目标写入掩码:RtColorMask[i];当 RtColorMaskShared 时,条目 0 应用于所有目标。SET_SINGLE_CT_WRITE_CONTROL (0x0f90)。格式为 0 的颜色目标会被禁用。 每个附件使用 colorWriteMask。 RYU-SU UpdateRtColorMask、IsRtEnabled(“将格式写成 0 可禁用颜色”)。 OGD 0x0f90
1.12 sRGB 渲染目标视图:当视图格式是 sRGB 且底层纹理为线性时,NVN 在写入时进行编码。Maxwell 也有一个全局 SET_SRGB_WRITE (0x15b8)。 由附件视图的格式决定;需要 MUTABLE_FORMAT。 NXN Mario:绑定基础纹理导致存储线性值,画面更暗。我们:RT 视图采用视图格式(view_format)。 OGD 0x15b8;NXN
1.13 压缩与 3D 目标:agl 的 GPU BC4 编码器通过 RGBA16UI 视图渲染块,每个 pass 渲染一个 32×32×32 BC4 3D 纹理的平面。NVN 逐片(slice)渲染 3D 纹理。 需要 BLOCK_TEXEL_VIEW_COMPATIBLE + EXTENDED_USAGE、2D_ARRAY_COMPATIBLE。 NXN 阴影纹理(bcrt_attach)。yuzu:3D 片上的错误块深度导致地形出现 Gloom 纹理(YZ-2305, PR 10433)。这里已修复。 NXN
1.14 无渲染目标绘制(片段着色器仅写入一个 SSBO,1×1 视口)正常运行。 需要一个无附件(attachment-less)pass,并带有显式渲染区域。 NXN bare_dims:洞穴环境光恢复。我们这边:渲染区域为视口 0(vk_exec.c resolve_rt)。 NXN
1.15 窗口原点:NVN 默认左下角(GL);TotK 切换为左上角。Y 翻转与裁剪矩形翻转随之改变。 Vulkan 左上角;使用负视口高度进行翻转。 Ryujinx 在启用 Y 取反时按屏幕高度翻转裁剪矩形(RYU-SU UpdateScissorState)。 NXN
1.16 多视口 / 多裁剪矩形(16 个),由 ViewportIndex 输出选择。 需要 multiViewport 和 shaderOutputViewportIndex。 我们这边:一个视口、一个裁剪矩形;不同的裁剪矩形会被记录日志(nvn_api_cmd.c)。 OGD 0x192c
1.17 片段输出饱和度(逐目标):SET_PS_SATURATE(0x13a8)在固定功能中将输出 n 钳制到 0..1 或 -1..1。NVN 不暴露它(GLSLC 在代码中生成 .SAT)。 无。 Ryujinx 未模拟;列出此项是为了避免在错误的地方排查钳制差异。 OGD 0x13a8
1.18 新内存读作零(新池页);反馈效果从不清除其状态纹理。 新图像内容未定义。 NXN tex_zero_fill。 NXN
1.19 MSAA:nvnCommandBufferDownsample 执行解析;深度-模板降采样是游戏自行完成的复制。 vkCmdResolveImage 仅支持颜色;整数格式选取一个样本。 NXN LA:半尺寸深度副本中丢失模板,没有 AO。 NXN

2. 纹理与采样器

# Maxwell / NVN Vulkan / AMD 模拟器 / 移植 来源
2.1 自动块高:NVN 在选取 GOB 的 2 的幂之前截断 height_in_blocks * 1.5 / 8(deko3d 会向上取整)。错误的 H 会读取填充数据:160×160 BC3 warp 填充半黑。分配大小相同,因此大小检查无法发现。Mip 级别在级别尺寸能容纳于块高一半时缩小块高。 不透明最优平铺(opaque optimal tiling)。 MNG 已修复(40 个 SDK 实测案例,games/totk/tests/texture_auto_layout/probe.c)。我们这边:已经是截断形式(runtime/nvn_tex.c,块高选择)。DK3D 的读取:高度(按块行,向上取整)x 1.5,然后 (h + 7) / 8 向上取整,再取 <= 该值的最大 2 的幂,上限为 16 GOB(dk_image.cpp:444-447、pickTileSize :13-20);3D 直接用 depth + depth / 2 作为 GOB 数量(:451-454);视频图像固定为 2 GOB(:438)。每一级,当 unit << (shift - 1) >= level dimension 时 shift 递减,其中 unit 为 8 行 / 1 slice(adjustTileSize :23-37,使用于 :89-91),宽度 tile 始终为 0。数组层填充到已缩减为完整高度/深度的块(alignLayerSize :57-72)。所有这些都与 nvn_tex.c 一致,除了取整方式。 MNG docs/how-it-works.md:47-53;DK3D dk_image.cpp:13-37, 57-72, 80-103, 436-455
2.2 CPU 填充且没有 NVN flush 的纹理(TotK 地形:两个 121 层 ASTC 8×8 数组,每个 42 MB;BC4 材质纹理通过 ReadTexels 读回并复制到 5120×5120 粗糙度图集中)。CPU 与 GPU 共享内存,因此在 Switch 上无需 flush。 宿主机图像是副本;必须检测 CPU 写入。 NXN 对纹理页做写保护(tex_dirty_upload);ASTC 零值解码为品红色,BC 零值解码为黑色。yuzu:对 GPU 已使用页面的小型 CPU 写入未失效,导致雨/雪粒子缓慢和传送粒子损坏(YZ-2306、PR 10942)。我们这边:GetWriteWatch 世代(vk_memory.c、mem_written_pages)。 NXN-175
2.3 相同存储,每帧新纹理对象(瞬态目标、延迟句柄)。相同存储配合不同描述符会重新解释相同的 block-linear 字节。 两个 VkImage 在同一分配上使用最优平铺时,不共享已定义的布局(AMD 的平铺和 DCC 元数据是按图像独立存在的);新图像以 UNDEFINED 开始。 NXN 通过 (pool, offset, description) 共享宿主纹理;MNG 保留“地址处活动纹理”的重定向。yuzu:相机变化时,纹理缓存别名/重叠顺序导致整片区域黑一帧(YZ-2305、PR 10418)。我们这边:仅 GPU 使用的池纹理是绑定到池内存上的 VkImage(检查 7)。 NXN;MNG docs/how-it-works.md:67-71
2.4 延迟纹理句柄(BindTexturesDeferred):GPU 在命令执行时从内存读取句柄;BotW 只录制一次命令块,稍后再写入句柄。 不适用(n/a)。 NXN BotW:录制时复制的句柄为零,3D 场景变黑。 NXN
2.5 边框颜色:任意 RGBA(TSC 字 4-7,加上 sRGB 边框字段)。TotK/BotW 的草地交互图钳制到 (0.5, 0.5, 0.5, 0.5),即其“未接触”值。 三个固定颜色,除非 VK_EXT_custom_border_color(+ customBorderColorWithoutFormat);整数格式需要 INT_CUSTOM。 MNG 在着色器中模拟;使用透明黑时,远离玩家的草平铺在地面上且没有切割痕迹(MNG issue #49)。我们这边:支持时使用自定义边框,否则透明黑(检查 11)。 G80TEX;MNG games/totk/third_party/d2r_gfx/gfx/nvn/nvn_mtl.m:5041-5045
2.6 sRGB 解码是一个与格式无关的 TIC 位(G80_TIC_2_SRGB_CONVERSION);TSC 有自己的 SRGB_CONVERSION(字 0 位 13)。NVN 将 sRGB 作为格式暴露。 由格式决定。 在 NVN 层按格式翻译即可。 G80TEX:197, :377
2.7 Swizzle(分量映射):逐纹理和逐视图(TextureBuilderSetSwizzle、TextureViewSetSwizzle);BotW 布局捕捉会在相同存储上重建纹理,使用 (1, 1, 1, R) 或 alpha 1。默认按格式的映射 R8 → (R, 0, 0, 1)。 VkComponentMapping 应用于视图。 NXN:没有它时字体图集出现白框,HUD 出现暗框。我们这边:已映射(vk_exec_draw.c get_view)。 NXN
2.8 采样维度来自指令:对数组纹理的非数组采样读取第 0 层;1,512 个 BotW 着色器将立方体作为立方体数组采样。 视图类型必须与声明类型匹配。 NXN:将 2D 数组绑定为 2D 得到单一淡色(场景泛白);立方体以 6 层数组创建,并带 cube / cube-array 视图。我们这边:按资源类型提供视图(get_view)。 NXN
2.9 LOD:TSC 最小/最大 LOD 钳制、LOD 偏移、TWO_D_NO_MIPMAP 纹理类型,无 mip 时的缩小过滤器停留在基础层级;TIC 自带最大各向异性与各向异性质量字段。 minLod/maxLod、mipLodBias 受 maxSamplerLodBias 限制。 yuzu:为强制三线性过滤的模组提供各向异性启发式处理(YZ-2306, PR 10744)。我们:非 mip 过滤器 → maxLod = minLod + 0.25,偏移被钳制(vk_texture.c make_sampler)。 G80TEX
2.10 纹理采集舍入:Maxwell 的亚纹素精度与 AMD/Intel 不同;TLD4 选取的边缘纹素可能相差一个。 由实现定义的 subTexelPrecisionBits。 yuzu 在非 NVIDIA 主机上为采集操作添加亚像素偏移:修复了 BotW 在物体边缘的阴影伪影(“shader_recompiler: Add subpixel offset for correct rounding at ImageGather”,https://git.openpunk.com/OpenPunk/yuzu/src/commit/780240e6979b198e7bd10feaad5399b8b4b63762,仅标题)。 —
2.11 采集偏移:TLD4.PTP 接受四个逐纹素偏移,可能来自寄存器。 ConstOffsets 必须是常量。 我们:QueryHostSupportsNonConstantTextureOffset() => false 使 Ryujinx 模拟寄存器偏移(tools/xlate/Program.cs)。 RYU-SH Translation/Optimizations (TexturePass)
2.12 深度比较(TSC 深度比较 + TIC_4_DEPTH_TEXTURE),最小/最大归约过滤。 compareEnable;归约模式需要 samplerFilterMinmax,且不能与比较同时使用。 我们:当比较开启时丢弃归约(vk_texture.c make_sampler)。 G80TEX
2.13 ASTC / BC:原生 ASTC;全零 ASTC 块在主机上显示为错误颜色。 AMD 和 NVIDIA 桌面 GPU 不支持 ASTC:解码为 RGBA8。yuzu 为低 VRAM 提供 ASTC 重新压缩为 BC1/BC3 的选项(YZ-2305, PR 10398)。 我们:在设备支持采样 ASTC 的地方使用原生上传,否则使用计算着色器解码(vk_astc.c, TOTK_ASTC),CPU 解码(tex_astc.c)作为后备。 runtime/vk_astc.c
2.14 线性 CPU 写入纹理(FLAGS_LINEAR、小渐变、字形缓存):CPU 写入可直接对 GPU 可见。 必须重新上传,或在宿主内存上使用线性图像。 NXN Mario:没有其 64×1 RGBA16F 渐变时天空变黑。 NXN

3. 着色器数学

# Maxwell Vulkan / AMD 模拟器 / 移植 来源
3.1 FMNMX 和带 NaN 的 .SAT 返回另一个操作数 / 0(IEEE minNum)。游戏依赖这一点阻止 NaN 扩散(一个 NaN 像素被 bloom 或时间性通道放大后会导致闪烁或填满屏幕)。 GLSL.std.450 FMin/FMax/FClamp:当某个操作数为 NaN 时结果未定义(NMin/NMax 是丢弃 NaN 的形式)。AMD 硬件上的 min/max 通常会丢弃 NaN;但编译器仍可能假设没有 NaN。 Ryujinx 生成 FMin/FMax/FClamp(CodeGen/Spirv/Instructions.cs:336, 956, 972;saturate 为 FClamp 0..1,Instructions/InstEmitAluHelper.cs:419-423)。NXN 移植的着色器使用 isnan 选择;MNG 测得 Metal 的 min/max/saturate 会丢弃 NaN(probe.m:1-4)。 GLSL450; NXN
3.2 FMZ 乘法模式(FMUL/FFMA/... 的字段 Fmz, .FMZ):0 × 任何值 = 0,包括 Inf/NaN(DX9 规则)。 IEEE:0 × Inf = NaN。 Ryujinx 解码 Fmz(Decoders/InstDecoders.cs:306-309, 1901),但没有发射器读取它。我们:相同。需要普查(检查 13)。 RYU-SH
3.3 非规格化数:每个指令的 .FTZ。 AMD 默认将 fp32 非规格化数刷新为零;VK_KHR_shader_float_controls 按着色器设置此行为。 Ryujinx 忽略 .FTZ。风险较低。 RYU-SH (no Ftz use in Instructions/)
3.4 Warp 大小 32:SHFL、VOTE、S2R LANEID、投票(ballot)都是 32 位。 AMD RDNA 每个着色器运行 wave32 或 wave64(由驱动选择;RADV 片段默认 wave64);VK_EXT_subgroup_size_control 的 requiredSubgroupSize = 32 可修复。 Ryujinx 将 lane 掩码适配到 QueryHostSubgroupSize()(默认 32,IGpuAccessor.cs:218;Instructions/InstEmitWarp.cs:72, 106, InstEmitMove.cs:175-185)。我们:未覆盖,无要求的大小(检查 6)。 https://docs.mesa3d.org/envvars.html (RADV_PERFTEST=pswave32)
3.5 丢弃(KIL) 保持 quad 的导数有效(demote 语义)。 OpKill 会终止;之后在 quad 中的导数未定义。 Ryujinx 生成 OpKill(CodeGen/Spirv/Instructions.cs:508)。 —
3.6 发散控制流中的隐式 LOD 采样在 Maxwell 上正常工作(TEX 单元使用 quad 邻居)。 在非一致控制流中未定义;AMD 的 whole-quad 模式通常可用。 — VKSPEC (derivatives)
3.7 整数除法不存在(编译为 XMAD/IMAD 序列);MUFU.RCP(0) = +Inf,LG2(0) = -Inf。 通过 1/x、Log2 得到相同的 IEEE 边界情况。 风险较低。 —
3.8 FP64 出现在一些 TotK 过场动画着色器中。 某些主机没有 FP64。 yuzu 将 FP64 → FP32(YZ-2306, PR 10693);我们:softfloat64 任务标志(tools/xlate/Program.cs QueryHostSupportsShaderFloat64)。 —
3.9 整数顶点输入按原始位读取(骨骼索引 R8UI)。 声明为浮点的输入会转换整数。 NXN Mario:角色拉伸;改用 uint 输入重新翻译。我们:属性类型传递给 QueryGraphicsState(Program.cs)。 NXN

4. 内存模型

# Maxwell / Tegra Vulkan / AMD 独立显卡 模拟器 / 移植 来源
4.1 统一内存:CPU 在栅栏之后读取 GPU 写入的缓冲区和纹理;GPU 在 FlushMappedRange 之后看到 CPU 写入,或者实际上立即看到(TotK 会跳过 flush)。块线性布局是已定义的,因此将缓冲区复制到纹理存储即是一次纹理上传。 图像位于 VRAM 中,采用私有 tiling;复制到图像内存的缓冲区字节并不是纹素。 yuzu PR 10942(小型 CPU 写入);MNG 在一次分配上的两个缓冲区之间进行复制时使用别名可见性屏障。我们:CPU 可见池中由 GPU 写入的范围在栅栏完成时标记;仅 GPU 的池不标记(检查 8)。 MNG docs/how-it-works.md:67-71
4.2 计数器报告(ReportCounter, SET_REPORT_SEMAPHORE_A..D 0x1b00):{counter, timestamp};TIMESTAMP 将 0 放入计数器;SAMPLES_PASSED 统计可见样本;ZCULL 统计。 遮挡查询是独立对象;结果复制到缓冲区。 NXN TotK:时间戳计数器为 0 对条件清除很重要。我们:每条报告写入 {0, time};遮挡计数始终为 0(检查 5)。DK3D 写入 SET_REPORT_SEMAPHORE_OFFSET(0x6C0 = 0x1b00),载荷为 0、OPERATION=Counter、STRUCTURE_SIZE=FourWords(dk_variable.cpp:193-240):TIMESTAMP = 单元 Crop,计数器 Payload,在 ROP 已完成所有更早的绘制后写入;SAMPLES_PASSED = 单元 Crop,计数器 0x15;ZCULL 统计 = 四个单字报告,位于 +0、+4、+8、+12。TIMESTAMP_TOP 不是 3D 报告:它是 gpfifo Semaphore 释放 16 字节,带 ReleaseWfiDisable,当命令处理器到达它时写入,无需等待更早的工作(:246-253)。因此某个区间两侧的 TIMESTAMP - TIMESTAMP_TOP 就是其 GPU 耗时。布局:两个 64 位字,先计数器后时间戳(Primer.md:578)。每条报告后面跟着一次平铺缓存刷新(:256)。ResetCounter(0x54C = 0x1530)将 SAMPLES_PASSED 清零;它同时也是时间戳类型的重置(:282-300)。单位:dkTimestampToNs(ts) = ts * 625 / 384,即 614.4 MHz GPU 时钟周期(1.63 ns),而不是 19.2 MHz 系统时钟(include/deko3d.h:1367-1373)(检查 21)。 OGD 0x1b00; NXN; DK3D dk_variable.cpp:183-300, include/deko3d.h:1367
4.3 信号量 / 事件 由 GPU 按顺序写入;CPU 轮询它们。 在栅栏(fence)之后的完成线程上写入。 我们的实现:vk_completion_add,提交顺序。 —
4.4 内联统一缓冲区更新(UpdateUniformBuffer)按命令顺序在绘制之间发生;在录制时读取缓冲区会得到最后一个值。 必须快照。 NXN:在编码时快照;当两次写入重排时,Mario 的水波模拟出现了分歧。 NXN
4.5 GPU 写入的索引缓冲区(同一提交中稍早写入的 8 位索引)。 没有 VK_EXT_index_type_uint8 则没有 uint8 索引类型;CPU 加宽会读到过期的字节。 NXN 在 GPU 上加宽(wide_run)。我们的实现:在录制时进行 CPU 加宽;设备本地字节索引被跳过(检查 9)。 NXN
4.6 计算共享内存:GM20B 上每块 48 KiB。 AMD 64 KiB。 我们的实现:从任务(Program.cs)查询 QueryComputeSharedMemorySize。 OGD clb1c0.h
4.7 虚拟内存池 映射物理池页;同一字节可通过两个 GPU 地址访问。 两个 VkBuffer 共享同一个分配需要能感知别名的屏障。 MNG MTL4VisibilityOptionResourceAlias. MNG docs/how-it-works.md:67-71
4.8 命令列表调用:CallCommands / CallList 不是 GPU 子例程。DK3D dkCmdBufCallList 存储一个 Call 控制条目(dk_cmdbuf.cpp:327-338);dkQueueSubmitCommands 在 CPU 上遍历这些条目,并在提交时递归进入被调用的列表(dk_queue.cpp:287-296),发出指向命令字的 gpfifo 条目,GPU 稍后再获取这些命令字(每个条目都可以关闭预取,:176)。因此列表结构在提交时固定,命令字在获取时被读取。 n/a 我们的实现:OP_CALL 在重放时遍历,CallCommands 在录制时内联记录(vk_exec.c walk,nvn_api_cmd.c inline_recording)。对控制流是等价的;延迟绑定的句柄是稍后读取的数据(2.4)。 DK3D dk_cmdbuf.cpp:327,dk_queue.cpp:280-298

5. 固定功能

# Maxwell / Tegra Vulkan / AMD 独立显卡 模拟器 / 移植 来源
5.1 顶点激发:最后一个(GL 约定,NVN 默认;SET_PROVOKING_VERTEX 0x1684,NVN 55 SetProvokingVertexMode)。平面插值输出(实例/粒子 ID、材质索引、平面法线)来自最后一个顶点。 第一个顶点,除非 VK_EXT_provoking_vertex 设置了 provokingVertexMode = LAST_VERTEX。 我们的实现:在 provokingVertexLast 可用时采用最后顶点模式(vk_pipeline.c create_graphics;检查 1)。DK3D:ProvokingVertexLast(0x5A1)在队列设置时以及根据 DkProvokingVertex 的每个光栅化器状态中被设为 1(maxwell/gpu_3d_base.cpp:76,gpu_3d_state.cpp:67);DkRasterizerState 默认为 Last(include/deko3d.h:823)。这证实了硬件侧默认是最后一个。 OGD 0x1684;runtime/nvn_ext.c;DK3D include/deko3d.h:794-823
5.2 图元重启:可以使用任意索引值,与任何索引大小的原始索引进行比较;可应用于列表(SET_DA_PRIMITIVE_RESTART_TOPOLOGY_CHANGE)和非索引绘制(..._VERTEX_ARRAY 0x0de8)。 仅在索引类型全 1 时重启;列表需要 primitiveTopologyListRestart。 Ryujinx 仅在 GL 上为 draw-arrays 启用它(RYU-SU 注释)。NXN:TotK 地形条带使用 0xffff。我们的实现:非全 1 的重启不重放,仅记录日志。DK3D:PrimitiveRestartEnable(0x591 = 0x1644)+ PrimitiveRestartIndex(0x592 = 0x1648),仅在启用时写入索引;可以是任意值;PrimitiveRestartWithDrawArrays(0x37A = 0x0de8)在设置时被强制置 0(maxwell/gpu_3d_ibo.cpp:21-30,gpu_3d_base.cpp:73)。除此之外,列表与条带之间没有更多区别。 OGD 0x1644, 0x1648;DK3D maxwell/gpu_3d_ibo.cpp:21-30
5.3 点大小:当着色器不写入时使用 SET_POINT_SIZE(0x1518);GLSLC 将写入的大小钳制到驱动常量。 点大小必须由着色器写入。 Ryujinx 在 VS 开始时从图形状态写入 PointSize(Translation/EmitterContext.cs:84-88);我们的实现从支持缓冲区读取 SetPointSize 状态(translator patch 0008)。 NXN
5.4 线宽、平滑线(SET_LINE_WIDTH_FLOAT 0x13b0,SET_ANTI_ALIASED_LINE)。 wideLines、VK_EXT_line_rasterization。 我们的实现:1 px,记录日志。 OGD
5.5 多边形偏移单位是寄存器中 API 值的两倍;NVN API 采用 GL 单位。 depthBiasConstantFactor 以格式相关的 r 为单位。 Ryujinx 从寄存器中读取 units / 2f(RYU-SU)。我们的实现采用 NVN API 单位(正确),但使用 D32F 而非 D24(1.7)。DK3D constantFactor * 2.0f(1.7)。 OGD 0x15bc, 0x187c;DK3D maxwell/gpu_3d_state.cpp:154
5.6 双源混合:SRC1 因子读取着色器的第二个输出。 需要片元输出 Location 0, Index 1。 Ryujinx 仅在 DualSourceBlendEnable 时声明 Index(Translation/Translator.cs:196,CodeGen/Spirv/Declarations.cs:608-617)。我们的实现:SRC1 因子已映射(vk_pipeline.c blend_factor),但转译器始终收到 false(Program.cs QueryGraphicsState)。(检查 12) —
5.7 Alpha 到覆盖率 带抖动(SET_ALPHA_TO_COVERAGE_DITHER_CONTROL 0x12e0),alpha 测试(Maxwell 上的固定功能)。 A2C 无抖动;无 alpha 测试。 Ryujinx 从图形状态在着色器中模拟 alpha 测试和 A2C 抖动;我们的实现向转译器传递 AlphaTestOp.Always、A2C false(Program.cs QueryGraphicsState)。 OGD 0x12e0, 0x16b4
5.8 逻辑运算作用于颜色目标。 logicOp 特性;在浮点目标上被忽略。 我们的实现:在管线键中。 OGD 0x19c4
5.9 顶点格式:*SCALED 8/16 位,RGB10A2 SNORM/SCALED,3 分量格式,32 位 scaled。 没有 32 位 SCALED;3 分量格式通常不受支持。 我们的实现:32 位 scaled 按整数读取(记录日志),3 分量加宽(vk_pipeline.c 顶点格式表)。 —
5.10 变换反馈、光栅化器丢弃。 VK_EXT_transform_feedback。 我们的实现:没有 XFB 的光栅化器丢弃 = 不绘制(nvn_api_cmd.c)。 OGD 0x0744

Maxwell / NVN 与 Vulkan / AMD 的行为对比

# Maxwell / NVN Vulkan / AMD 模拟器 / 移植版 来源
5.11 深度界限、采样掩码、保守光栅化。 可选特性。 我们的:忽略并记录日志(nvn_ext.c)。 OGD 0x0fa4
5.12 无 alpha 目标上的混合因子(RGBX 模拟为 RGBA)。 目标 alpha 读取存储值。 Ryujinx 在无 alpha 目标上替换 DST_ALPHA 因子(FilterBlendFactor,RYU-SU)。 RYU-SU

6. 其他项目中的已知 BotW / TotK 问题

游戏 症状 原因 修复 / 链接
TotK 3D 场景变黑 条件性清除被忽略 NXN
TotK 洞穴品红 / 黑色 CPU 填充的 ASTC/BC 在未 flush 时永远不会上传 NXN
TotK 火把无法照亮的石块 粗糙度图集由从未上传的 BC4 的 ReadTexels 构建 NXN
TotK BC4 3D 纹理绘制丢失 压缩渲染目标 NXN
TotK 缺少环境光、泛光光晕 无渲染目标的绘制被跳过 NXN
TotK 传送台一半黑 块高度取整 MNG docs/how-it-works.md:49-53
TotK 使用 Ultrahand / Recall 时出现彩色方块 D16 清除与深度 EQUAL 下的片段取整冲突 MNG docs/how-it-works.md:57-61
TotK/BotW 远离 Link 处草变平、无裁剪 自定义边框颜色(0.5) MNG nvn_mtl.m:5041-5045
TotK 云闪烁、地底缺失几何体 着色器重编译器缺陷 YZ-2305 PR 10234
TotK 屏幕上的红点 清除定向到错误的 RT 索引 YZ-2305 PR 10243
TotK 2× 分辨率下 Link/地形花屏 同步期间缓冲缓存被清除 YZ-2305 PR 10249
TotK 顶点爆炸(RADV) 驱动动态深度钳制缺陷 YZ-2305 PR 10262
TotK Ultrahand 发光像素化 无屏障的附件反馈循环 YZ-2305 PR 10402
TotK 相机切换时某些区域黑一帧 纹理缓存别名/重叠顺序 YZ-2305 PR 10418
TotK 地形上的瘴气纹理 3D 切片上的块深度调整 YZ-2305 PR 10433
TotK 远处地形间隙(地底) 着色器重编译器中的单字符错误 YZ-2305 PR 10459
TotK RDNA3 上的纹理损坏 未找到原因(AMD 测试用例) YZ-2305
BotW/TotK 传送时雨/雪变慢、粒子损坏 CPU 对 GPU 使用中的页面进行小写入未被失效 YZ-2306 PR 10942
TotK 超过 1008p 的模组中 AO 异常 缩放阈值 YZ-2306 PR 10675
TotK AMD 上出现“侦探模式”Link AMD EDS3 颜色混合驱动回归 YZ-2306 PR 10946
BotW 对象边缘的阴影瑕疵(AMD) gather 取整 yuzu commit 780240e(2.10)
BotW GCN 上的黑色纹理 HTILE(深度压缩) AMD_DEBUG=nohyperz wiki(1.9)
BotW 3D 场景变黑 延迟纹理句柄 NXN
BotW albedo 散落方块 重复的颜色附件 NXN
BotW HUD 暗色框、黑色耐力环 截取纹理 swizzle 重建 NXN

7. 针对我们运行时的检查

2026-10-22 的 grep 级别检查,状态已于 2026-10-26 更新;“saw(所看到的)”是 grep 和短读取显示的内容,不是测试结果。

  1. 激发顶点(Provoking vertex)。Vulkan 从第一个顶点绘制 flat 输出,NVN 从最后一个顶点。vk_pipeline.c 在 create_graphics 中启用 provokingVertexLast,并链入 VkPipelineRasterizationProvokingVertexStateCreateInfoEXT{LAST_VERTEX};若缺少该特性,它会记录日志说明 flat 输出来自第一个顶点。已修复。
  2. 未写入的顶点输出。spirv_add_outputs(vk_pipeline.c)会为 VS 缺少的 FS 输入添加初始化为 (0, 0, 0, 1) 的输出。我们的翻译器从不链接各个阶段,因此 Ryujinx 的 InitializeVertexOutputs 不会初始化任何用户输出;翻译器补丁 0006 会初始化这些未链接的输出。已修复。
  3. 带掩码的清除。vk_exec.c 会跳过带部分通道掩码的颜色清除(W_CLEAR_MASK);clear_ds 会跳过带部分掩码的模板清除(W_STENCIL_MASK)。TotK 会发出此类清除(NXN)。请检查日志中的 colour clear with channel mask 和 stencil clear with mask。DK3D(1.3)确认了重放必须做的操作:颜色清除 = scissor 0 + 通道掩码,模板清除 = value & mask 在前端写入掩码下合并;因此将 colorWriteMask / stencilWriteMask 设为清除掩码的绘制就是完全等效的操作。缺失。
  4. D24 → D32F。vk_formats.c 将 D24S8 映射到 D32_SFLOAT_S8_UINT;vk_exec_draw.c 的 record_fixed 会原样传递 NVN 单位而不缩放。深度 EQUAL 测试通过以及贴花偏移可能会发生 z-fighting。可查看:各 pass 的深度测试操作直方图、针对 D24 目标的 EQUAL 测试。对 TotK 不适用:对原野和战斗存档(2026-10-22)的普查只看到深度目标 0x34(DEPTH32F)和 0x32(DEPTH16);带偏置的绘制(单位 -5 和 1,因子 -0.5 和 -0.2)都在 DEPTH32F 上。
  5. 计数器和条件渲染。vk_exec_xfer.c(截至 2026-10-22):每个 report 都会写入 {0, time};SAMPLES_PASSED 始终为 0;条件会在记录时由 CPU 读取,未完成的 report 读取为 0。对 NVN 日志进行 grep,查找 TIMESTAMP 以外的计数器类型;任何由遮挡驱动的可见性(光晕、剔除)都是关闭的。2026-10-22 已修复:SAMPLES_PASSED 计数现在来自 RESET 与 REPORT 之间的 Vulkan 遮挡查询(vk_exec_xfer.c,原野中每帧约 4 次查询,战斗中约 21 次);游戏的 CPU 会读取它们(guest f_70010c3298)来剔除对象。
  6. 子组大小。翻译器假设为 32;RDNA4 可能以 wave64 运行片段着色器。vk_caps.c 和 vk_pipeline.c 中的 pin_subgroup32 在 subgroupSizeControl 允许时,用 VkPipelineShaderStageRequiredSubgroupSizeCreateInfo 将各阶段固定为 32 条 lane;否则会告知翻译器 GPU 自身的大小(TOTK_VK_SUBGROUP_PIN=0 强制该行为)。已修复。
  7. 图像别名到仅 GPU 的池内存上。vk_texture.c 的 bind_pool_memory 会将每个 VkImage 绑定到其池分配中;纹理按地址加描述符作为键,每个新图像都以 UNDEFINED 开始(init_image)。同一个存储上的两个描述符,或通过缓冲区写入的字节,在 AMD 上彼此看不到对方的数据。检查:统计每帧每个地址的不同键数量。风险(属于 yuzu PR 10418 一类)。
  8. GPU 写入 CPU 可见的池。note_gpu_write(vk_exec_xfer.c)会在 fence 完成时对页面做标记,因此同一提交中稍后采样的纹理仍保留其旧上传;GPU 专用的池会被跳过。MNG 在 TotK 中看到每帧最多一次缓冲区拷贝,因此优先级较低。风险。
  9. 索引缓冲区。vk_exec_draw.c 的 op_draw_idx:字节索引在记录时由 CPU 加宽,设备本地内存中则跳过;restart_for:非全 1 的重启索引会被记录并丢弃。普查(2026-10-22):每帧 130-170 次字节索引绘制在 CPU 上加宽,0 次设备本地,0 次非全 1 重启。OK;VK_KHR_index_type_uint8(已支持)可消除 CPU 开销。
  10. 反馈循环。普查(2026-10-22):每帧 425 次(原野)到 667 次(战斗),G-buffer 材质/贴花着色器对当前 open pass 的 1600x900 目标进行采样。地形 G-buffer pass 会读取其自身的 albedo、normal 和 R32F 目标,发生在片段所在像素;在 RX 9070 上,读取返回陈旧数据,表现为地面上约 16x14 px 的暗色矩形,Link 旁边的贤者之灵未绘制。vk_exec_draw.c(反馈循环)现在能检测此类绘制:在启用 VK_EXT_attachment_feedback_loop_layout 和 dynamicRenderingLocalRead 时,目标会切换到反馈布局,并在绘制前加入按区域(by-region)屏障;若未启用,pass 会在完整屏障后重新开始(TOTK_FEEDBACK)。已修复。
  11. 边框颜色。vk_texture.c 的 make_sampler:只有启用 customBorderColorWithoutFormat 时才使用自定义颜色,否则使用透明黑色;整数格式走 float 路径。确认 RX 9070 报告支持该特性(草贴图需要 0.5)。若支持则 OK。
  12. 双源混合。vk_pipeline.c 的 blend_factor 映射 SRC1 因子;Program.cs 的 QueryGraphicsState 始终传入 dualSourceBlendEnable = false,因此不存在 Index 1 输出。记录包含 SRC1 因子的管线。若使用则不匹配。
  13. FMZ / NaN。统计 build/v121/shaders/ 中设置了 Fmz 字段(Decoders/InstDecoders.cs:1901 的位位置)的 FMUL/FFMA/FMUL32I/FFMA32I 指令;FMin/FMax/FClamp 的 NaN 结果未定义(3.1)。普查(2026-10-22):12397 个着色器中有 1 条指令为 FMZ,12241 个着色器中有 FTZ,7457 个着色器中有 FMNMX。翻译器输出 GLSL.std.450 的 FMin/FMax(Ryujinx GenerateMinimum),而不是 NMin/NMax。FMZ:无需修复。FMNMX:OK(RX 9070 测试:FMin/FMax(NaN, 1) = 1,两种顺序都是;FClamp(NaN, 0, 1) = 0)。
  14. 点大小 / 线宽。普查:SetLineWidth 4.5 约每帧 1 次(2 次线绘制到 144x144 R8 目标),SetPointSize 5 约每帧 14 次;4 个点 VS 依赖它。点大小:翻译器补丁 0008 从支持缓冲区读取它,已修复。线宽:1 px,非默认值会记录日志(nvn_ext.c),不匹配,轻微问题。
  15. 块高度。nvn_tex.c 像 NVN 一样截断(2.1)。DK3D 向上取整(dk_image.cpp:446);其余每个步骤都一致。OK。
  16. 深度模式和钳制。vk_pipeline.c 的 create_graphics(negativeOneToOne 来自 nvn_depth_minus_w,depthClampEnable 来自 NVN 状态)。DK3D 在 z 数学(1.5)和钳制开关(1.6)上一致。OK。
  17. 深度偏置参数顺序。vk_exec_draw.c 的 record_fixed 使用(units、clamp、factor);DK3D 接受同样的三个参数,并为寄存器将 units 加倍(1.7)。OK。
  18. 采样器 LOD。vk_texture.c 的 make_sampler。OK。
  19. 视口 / 裁剪矩形数组。单个视口(vk_exec_draw.c 的 record_fixed),额外的裁剪矩形会记录日志。普查:0 个差异。OK。

  20. 计算着色器中的深度比较采样(VUID-vkCmdDispatch-None-06479):一个计算着色器通过一个开启比较的 R32_SFLOAT 视图,对由 CPU 写入的 4x4 R32F 占位符进行采样,约每帧 1 次。需要一份 D32 副本。次要。

  21. 报告时间戳:单位与时刻。 write_counter(vk_exec_xfer.c)在每次报告的栅栏完成时写入主机端 ns * 12 / 625(19.2 MHz 滴答);nvnDeviceGetTimestampInNanoseconds 使用相同系数换算回来(nvn_ext.c)。DK3D:GPU 滴答按 625/384 ns(614.4 MHz)换算,TIMESTAMP 在 ROP 已完成先前工作后写入,TIMESTAMP_TOP 在命令处理器到达它时写入(4.2)。这对引擎的 GPU 时间(动态分辨率和速度都跟随它)有两个影响: (a) 如果 1.2.1 的消费者使用自己的常量而不是 NVN 换算调用来缩放原始报告滴答,我们的 19.2 MHz 单位与硬件的相差 32 倍;在修复单位之前先找到那个消费者;(b) 差值需要在每个提交段中使用 vkCmdWriteTimestamp2,TIMESTAMP_TOP 用 TOP_OF_PIPE,TIMESTAMP 用 BOTTOM_OF_PIPE,再换算为 (a) 所示的任意单位。不匹配(时刻);单位未经核实。

8. 尚未阅读的来源

  • Mesa NVK(src/nouveau/vulkan,MIT):它如何将 Vulkan 的触发顶点、深度裁剪、重启、反馈循环映射到 B197 上。
  • envytools rnndb graph/gf100_3d.xml、tsc.xml、tic.xml(MIT):字段级别的 TIC/TSC 含义(LOD 吸附、三线性优化)。
  • Ryujinx Ryujinx.Graphics.Gpu/Image/TextureCache.cs、Texture.cs(MIT):别名/重叠规则、深度纹理 swizzle。
  • Ryujinx 的 TotK/BotW PR 和 issue(GitHub 仓库已消失;镜像在 2026-10-22 时被屏蔽或返回 404)。
  • yuzu 2023-08 至 2024-02 的进度报告,以及 suyu/sudachi 的跟踪器(仅行为层面)。
  • switchbrew wiki 的 NVN 和 GPU 页面。

本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 to/totk-pc-recomp