[shad-PS4] Vulkan渲染器 | 着色器_重新编译器 | SPIR-V 格式

发布于:2025-07-08 ⋅ 阅读:(23) ⋅ 点赞:(0)

在这里插入图片描述

第六章:Vulkan渲染器

欢迎回来!

前一章 第5章:输入处理器中,我们了解了shadPS4如何将键盘、鼠标或手柄输入转化为模拟PS4游戏能理解的信号。

现在游戏能够接收我们的输入指令后,它需要一种方式向我们展示正在发生的事情——它需要将图形绘制到屏幕上

这正是Vulkan渲染器组件发挥作用的地方。我们可以将其视为模拟器的艺术家放映师

PS4游戏(具体来说是模拟的AMD"Liverpool"芯片GPU)会生成详细的图形指令,描述_要绘制什么_(3D模型、纹理、视觉效果)和_如何绘制_。这些指令专为PS4硬件设计,使用的是与我们PC显卡不兼容的图形语言

Vulkan渲染器的核心职责包括:

  1. 接收指令:获取模拟PS4 GPU生成的图形命令
  2. 翻译转换:将这些PS4专用绘图指令转换为PC显卡能理解的Vulkan图形指令
  3. 资源管理:在PC GPU上处理图形内存,分配纹理(图像)、缓冲区(顶点数据、uniform数据)和渲染目标(GPU绘制的图像)等资源
  4. 指令执行:将转换后的指令发送至显卡执行
  5. 画面呈现:将最终绘制完成的图像显示到模拟器窗口

若没有Vulkan渲染器,游戏将如同盲人运行——我们无法看到任何视觉输出。


应用场景:显示游戏帧

渲染器的核心任务是将PS4 GPU生成的完整图像显示到屏幕。游戏通常每秒需要绘制数十帧新画面。

以下是模拟PS4 GPU完成帧渲染并准备显示时的简化流程:

  1. 模拟PS4 GPU发出信号,表示已完成将帧渲染到特定内存区域
  2. Vulkan渲染器的**呈现器(Presenter)**收到新帧就绪通知及对应内存地址
  3. 呈现器通过Vulkan从**交换链(Swapchain)**获取可用图像槽位(由操作系统管理并与模拟器窗口关联)
  4. 使用Vulkan指令将完成的帧图像从模拟内存复制/绘制到交换链图像,可能需要进行缩放或定位以适应窗口
  5. 可能在游戏帧上叠加绘制调试信息、菜单等模拟器UI元素(使用ImGui或Qt GUI
  6. 最终通过Vulkan"呈现"命令显示交换链图像

流程可视化如下:

将呈现器的指令,通过pc gpu(Vulkan)执行,在交换链上初步显示图像
在这里插入图片描述

导图展现了 呈现器如何作为游戏帧与PC显示之间的桥梁,协调Vulkan指令完成画面输出。

Vulkan渲染器核心概念

Vulkan渲染器由多个与Vulkan API交互的组件构成:

  1. Vulkan API现代显卡直接通信的低级图形接口。shadPS4使用C++封装库(vulkan.hpp)和内存管理库(VMA)简化操作
  2. 实例与物理设备:通过vk::Instance连接Vulkan运行时,枚举vk::PhysicalDevice选择显卡,创建vk::LogicalDevice作为主交互接口
  3. 交换链:连接渲染过程与窗口系统的图像集合,管理渲染输出与窗口显示的同步
  4. 光栅化器:将PS4 GPU绘图指令(如"绘制三角形列表")转换为Vulkan指令(vkCmdDraw)的核心引擎
  5. 流水线缓存:缓存已创建的图形流水线配置(顶点格式、着色器等),避免重复创建开销
  6. 纹理缓存:管理GPU纹理内存,处理纹理数据加载与格式转换
  7. 缓冲区缓存:管理顶点、索引等数据缓冲区的GPU内存分配
  8. 呈现器:管理最终输出图像,处理后期特效(如FSR超分辨率)和UI叠加
  9. 调度器:管理Vulkan指令缓冲的提交,使用不同调度器处理绘制、呈现等任务
Vulkan初始化(实例与设备)

Vulkan::Instance类处理显卡连接:

// 创建Vulkan实例
instance{CreateInstance(/*窗口类型、验证层等参数*/)},
// 枚举物理设备
physical_devices{EnumeratePhysicalDevices(instance)} {

// 选择物理设备(优先独立显卡)
physical_device = physical_devices[0];
// 创建设备队列(图形/呈现)
CreateDevice();
// 初始化VMA内存分配器
CreateAllocator();
}
呈现器(帧显示)

Vulkan::Presenter类协调最终显示:

Presenter::Presenter(/*窗口、GPU状态等*/) 
    : instance{窗口, 配置参数},
      swapchain{instance, 窗口},
      rasterizer{创建光栅化器} {

    // 初始化帧缓冲池
    present_frames.resize(swapchain.GetImageCount());
    // 创建同步栅栏
    frame.present_done = device.createFence(...);
}

帧准备与呈现流程:

Frame* Presenter::PrepareFrame(/*属性、内存地址等*/) {
    // 从纹理缓存获取游戏帧图像
    auto& image = texture_cache.GetImage(image_id);
    // 转换图像布局为可读模式
    image.Transit(vk::ImageLayout::eShaderReadOnlyOptimal);
    // 执行后期处理/直接拷贝
    cmdbuf.blitImage(/*源图像→帧缓冲图像*/);
}

void Presenter::Present(Frame* frame) {
    // 获取交换链图像
    swapchain.AcquireNextImage();
    // 绘制UI叠加层
    ImGui::Core::Render(cmdbuf, swapchain_image_view);
    // 提交指令缓冲
    scheduler.Flush(/*同步信号*/);
    // 呈现图像
    swapchain.Present();
}
光栅化器(指令转换)

Vulkan::Rasterizer处理PS4 GPU指令转换:

void Rasterizer::Draw(bool is_indexed) {
    // 获取图形流水线
    const auto* pipeline = pipeline_cache.GetGraphicsPipeline();
    // 准备渲染状态(颜色/深度附件)
    auto state = PrepareRenderState();
    // 绑定资源(缓冲区/纹理)
    BindResources(pipeline);
    // 绑定顶点/索引缓冲
    buffer_cache.BindVertexBuffers();
    // 开始渲染流程
    BeginRendering(*pipeline, state);
    // 记录Vulkan绘制指令
    cmdbuf.drawIndexed(/*顶点数、实例数等*/);
}

资源绑定过程:

void BindBuffers(/*着色器阶段、绑定点等*/) {
    // 获取Vulkan缓冲对象
    auto [vk_buffer, offset] = buffer_cache.ObtainBuffer();
    // 创建描述符信息
    buffer_infos.emplace_back(vk_buffer->Handle(), offset, size);
    // 记录描述符写入操作
    set_writes.push_back(/*绑定到着色器槽位*/);
}

总结

Vulkan渲染器是shadPS4的核心图形模块,承担着:

  • 初始化Vulkan运行时环境
  • 转换PS4 GPU指令至Vulkan API调用
  • 通过交换链管理窗口输出
  • 协调纹理/缓冲区缓存实现高效资源管理
  • 使用调度器优化指令提交效率

其复杂的光栅化流程和呈现机制,成功架起了PS4专用图形硬件PC GPU之间的桥梁。

而要处理PS4特有的着色器格式,则需要接下来的关键组件——着色器重编译器

下一章:着色器重编译器


第七章:着色器_重新编译器

前一章 第6章:Vulkan渲染器中,我们学习了 shadPS4 如何使用 Vulkan 图形 API 识别驱动,将模拟的 PS4 GPU 生成的输出绘制到我们 PC 的屏幕上。

渲染器需要知道要绘制什么以及如何绘制,这些信息来自于运行在显卡上的程序——着色器

  • PS4 游戏使用专门为 PS4 的 AMD "Liverpool" GPU 编写的着色器。这些着色器就像用特殊语言编写的指令,只有 PS4 的图形芯片能够理解。

  • 我们的 PC 显卡(无论是 NVIDIA、AMD 还是 Intel)使用不同的语言,通常需要以 HLSLGLSL 等语言编写着色器,或编译成 SPIR-V(Vulkan 使用的中间格式)等中间格式。

这就是着色器重新编译器发挥作用的地方。

我们可以将其视为专门处理图形程序的**语言学家和翻译器**。

它的工作是将游戏原始的着色器(为 PS4 编写)转换为我们的 PC 显卡能通过 Vulkan 理解并执行的等效着色器。这至关重要,因为如果没有正确的翻译,图形将出现故障、缺失或完全错误。

应用场景:游戏需要绘制纹理

假设游戏需要在屏幕上绘制一个角色。

这涉及在 GPU 上运行一个着色器程序,该程序计算角色模型的每个点(顶点)在屏幕上的位置,以及每个像素应具有的颜色(基于角色的纹理图像)。

PS4 游戏向模拟器提供该着色器程序的原始二进制数据,这些数据使用 PS4 原生 GPU 指令集(AMD 的 GCN/RDN ISA)编写。Vulkan 渲染器无法直接将这些数据传递给我们的 PC 显卡。

以下是着色器重新编译器处理此问题的流程:

  1. 游戏控制的模拟 PS4 GPU 向Vulkan 渲染器表明需要使用特定的着色器程序,提供其在模拟内存中的位置
  2. Vulkan 渲染器检查是否已翻译过该着色器。若未翻译,则将原始 PS4 着色器代码传递给着色器重新编译器
  3. 着色器重新编译器逐条解析原始 PS4 着色器指令
  4. 将这些指令转换为中间表示(IR),这是重新编译器使用的通用内部语言。可以将 IR 视为着色器逻辑的详细蓝图,但不绑定到任何特定硬件
  5. 重新编译器IR 执行各种优化和转换通道以简化逻辑,处理 PS4 特有功能并适配目标硬件(PC 显卡)
  6. 最后将优化后的 IR 转换为 SPIR-V 格式的最终着色器代码,这是Vulkan 渲染器可使用的语言
  7. 重新编译器同时生成着色器的元数据如所需访问的纹理和缓冲区等资源信息)。这些信息与翻译后的 SPIR-V 一并返回
  8. 💡Vulkan 渲染器接收到翻译后的 SPIR-V元数据。使用元数据设置着色器的资源访问方式,并将 SPIR-V 编译为 GPU 可执行程序
  9. Vulkan 渲染器现在可以在绘制角色时使用此编译后的着色器

核心翻译流程可视化:
在这里插入图片描述
(IR优化后 再转换,转换后命令+元数据–>vulkan渲染)

导图展现了 将原始 PS4 着色器代码通过重新编译器内部阶段(前端、IR、转换通道)转换为 PC 兼容的 SPIR-V 输出的过程。

着色器重新编译器

着色器重新编译器是模拟器中最复杂的组件之一,需要深入了解源(PS4 GPU ISA)和目标(Vulkan/SPIR-V)图形架构。以下是其核心组件和概念:

  1. 前端(解码器 & CFG 构建器):读取原始二进制 PS4 着色器代码。逐条解码指令并构建控制流图(CFG),映射着色器程序所有可能执行路径(分支、循环等)。这是理解着色器结构的第一步
  2. 中间表示(IR):重新编译器首先将着色器转换为抽象 IR,而非直接从 PS4 指令翻译到 SPIR-V。IR 使用通用操作(如"加法运算"、“内存读取”、“数值比较”)表示着色器逻辑,不依赖特定硬件。这使得分析和适配任何目标后端更加容易。IR::Program 结构体保存此表示
  3. 优化与转换通道着色器转换为 IR 形式后,会经过多个"通道"处理
    • 优化通道:简化代码(如移除冗余指令、替换常量计算),提升着色器执行效率
    • 转换通道:处理 PS4 GPU 特有行为或 Vulkan/SPIR-V 没有直接对应的功能,将其转换为通用 IR 操作序列。例如处理特殊内存访问模式或 PS4 特有的插值模式
  4. 资源追踪:翻译过程中,重新编译器分析着色器访问的资源(纹理、缓冲区、用户数据寄存器)。这些信息存储在 Shader::Info 结构体中,对Vulkan 渲染器正确设置 Vulkan 描述符集至关重要
  5. 后端(SPIR-V 生成):完成所有通道处理后,后端将优化后的 IR 转换为目标格式(Vulkan 使用 SPIR-V)。这涉及将 IR 操作和资源映射到对应的 SPIR-V 等效形式
  6. 运行时信息(RuntimeInfo:着色器的某些行为可能取决于 PS4 GPU 硬件寄存器的实时状态(如渲染目标布局、曲面细分细节或计算工作组大小)。RuntimeInfo 捕获这些设置,确保当设置变化时重新编译器能生成正确的着色器变体

核心代码组件:

主翻译函数(TranslateProgram

该函数驱动整个翻译流程

接收原始 PS4 着色器代码和其他必要输入

协调各阶段处理,最终返回 IR 表示收集的信息

#include "shader_recompiler/frontend/decode.h" // PS4指令解码
#include "shader_recompiler/frontend/control_flow_graph.h" // CFG构建
#include "shader_recompiler/frontend/structured_control_flow.h" // IR结构构建
#include "shader_recompiler/ir/passes/ir_passes.h" // 优化/转换通道
#include "shader_recompiler/recompiler.h"
#include "shader_recompiler/runtime_info.h" // 运行时上下文

namespace Shader {

IR::Program TranslateProgram(std::span<const u32> code, Pools& pools, Info& info,
                             RuntimeInfo& runtime_info, const Profile& profile) {
    // 1. 解码PS4指令
    Gcn::GcnCodeSlice slice(code.data(), code.data() + code.size());
    Gcn::GcnDecodeContext decoder;
    IR::Program program{info}; // 保存IR的对象
    program.ins_list.reserve(code.size());
    while (!slice.atEnd()) {
        program.ins_list.emplace_back(decoder.decodeInstruction(slice)); // 解码每条指令
    }

    // 清空对象池以供本次翻译使用
    pools.ReleaseContents();

    // 2. 构建控制流图(CFG)
    Common::ObjectPool<Gcn::Block> gcn_block_pool{64}; // CFG块对象池
    Gcn::CFG cfg{gcn_block_pool, program.ins_list}; // 从指令构建CFG

    // 3. 结构化CFG并构建中间表示(IR)
    program.syntax_list = Shader::Gcn::BuildASL(pools.inst_pool, pools.block_pool, cfg,
                                                program.info, runtime_info, profile);
    // syntax_list包含结构化的IR块

    // 从结构化列表提取IR块
    program.blocks = GenerateBlocks(program.syntax_list);
    // 获取后序排列的块(某些通道需要)
    program.post_order_blocks = Shader::IR::PostOrder(program.syntax_list.front());

    // 4. 运行优化和转换通道
    // 示例通道:
    if (!profile.support_float64) { // 目标显卡不支持FP64时适配
        Shader::Optimization::LowerFp64ToFp32(program);
    }
    Shader::Optimization::SsaRewritePass(program.post_order_blocks); // 标准优化
    Shader::Optimization::ConstantPropagationPass(program.post_order_blocks); // 常量评估
    // ... 其他处理PS4特性的通道 ...
    Shader::Optimization::ResourceTrackingPass(program); // 分析资源使用
    Shader::Optimization::CollectShaderInfoPass(program, profile); // 收集信息到program.info

    // 调试时可选:导出最终IR
    Shader::IR::DumpProgram(program, info);

    return program; // 返回翻译后的程序(IR)
}

// ... GenerateBlocks等辅助函数 ...
} // namespace Shader

代码功能

将PS4游戏机的着色器代码(GPU专用程序)转换为中间表示(IR)的核心函数,属于编译器前端的一部分。整个过程分为四个主要阶段。

指令解码阶段

  • 从输入的PS4机器码(32位整数数组)逐条解码成内部指令表示。

  • GcnCodeSlice管理代码读取位置,GcnDecodeContext负责解析指令类型和参数。

  • 解码结果存入program.ins_list

控制流分析阶段

  • 构建控制流图(CFG)来展示指令间的跳转关系,比如条件分支和循环。

  • 使用对象池gcn_block_pool管理内存,避免频繁分配释放内存块。

中间表示生成阶段

将线性的指令列表转换为结构化的IR(类似高级编程语言的抽象表示)。

BuildASL函数生成带层次结构的语法树,GenerateBlocks将其转换为更适合优化的基本块形式,PostOrder排序用于后续优化。

优化与适配阶段

根据目标GPU硬件特性进行代码转换:

  • 若显卡不支持双精度浮点(FP64),自动降级为单精度(FP32)
  • 进行静态单赋值(SSA)重构、常量传播等标准优化
  • 分析资源使用情况并收集着色器元数据
  • 调试时可输出最终IR用于验证

最终返回的IR::Program包含优化后的中间代码和提取的硬件特性信息,供后续编译阶段使用。整个过程通过对象池管理内存,提升性能。


中间表示(IR::Program, IR::Block, IR::Inst

IR 是重新编译器处理的核心数据结构,由基本块(IR::Block)和指令(IR::Inst)组成:

struct Program {
    explicit Program(Info& info_) : info{info_} {}

    AbstractSyntaxList syntax_list; // CFG结构化表示
    BlockList blocks; // 基础块平面列表
    BlockList post_order_blocks; // 后序遍历的块
    std::vector<Gcn::GcnInst> ins_list; // 原始解码指令
    Info& info; // 收集的着色器信息
};
优化与转换通道

ir_passes.h 定义了各种优化和转换函数:

namespace Shader::Optimization {

void SsaRewritePass(IR::BlockList& program); // 简化SSA形式
void DeadCodeEliminationPass(IR::Program& program); // 移除无用指令
void ConstantPropagationPass(IR::BlockList& program); // 编译时评估常量表达式
void ResourceTrackingPass(IR::Program& program); // 分析资源使用
void CollectShaderInfoPass(IR::Program& program, const Profile& profile); // 收集最终信息
void LowerBufferFormatToRaw(IR::Program& program); // 转换PS4缓冲区格式访问
void LowerFp64ToFp32(IR::Program& program); // FP64转FP32适配
void SharedMemoryToStoragePass(IR::Program& program, const RuntimeInfo& runtime_info,
                               const Profile& profile); // 处理PS4共享内存操作
着色器信息(Shader::Info

该结构体存储元数据,供Vulkan 渲染器创建图形流水线:

struct Info {
    UserDataMask ud_mask{}; // 使用的用户数据寄存器掩码
    BufferResourceList buffers; // 访问的缓冲区列表
    ImageResourceList images; // 访问的图像列表
    SamplerResourceList samplers; // 使用的采样器列表
    Stage stage; // 着色器阶段(顶点/片段/计算等)
    // ... 其他标志位和元数据 ...
};

总结

着色器重新编译器是 shadPS4 不可或缺的组件。

通过解码 PS4 GPU 指令、构建中间表示、应用优化转换通道以及追踪资源使用,它有效地将 PS4 专用硬件设计的着色器转换为 PC 显卡通过 Vulkan 可执行的 SPIR-V 格式。

这一复杂过程受到目标硬件配置文件和运行时上下文的指导,使游戏图形能正确呈现在我们的屏幕上,让Vulkan 渲染器得以正常运行。

当游戏的视觉效果成功呈现后,我们将转向模拟器的用户界面部分——图形用户界面!

下一章:Qt图形界面


网站公告

今日签到

点亮在社区的每一天
去签到