ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Axmol RHI重构:GPU Compute与Graphics Pipeline并行架构落地

Axmol RHI重构:GPU Compute与Graphics Pipeline并行架构落地 1. 这不是一次“小更新”而是 Axmol 渲染架构的底层重写如果你最近打开 Axmol 的 GitHub 仓库翻到include/axmol/renderer目录下那几份被彻底重命名、重组织的头文件——RHI.h、RHIDevice.h、RHICommandList.h、RHIComputePipelineState.h——你大概率会愣一下这不像以往的 patch倒像把引擎的“血管”和“神经”重新布了一遍。我从去年底开始跟进这个 RHIRender Hardware Interface重构分支从最初只支持 OpenGL ES 的单后端抽象层到现在能同时调度 Vulkan、Metal、Direct3D 12 和 WebGPU 四大原生 API并让 Compute Shader 真正成为与 Graphics Pipeline 并列的一等公民整个过程不是“加功能”而是对 Axmol 渲染管线哲学的一次校准。核心关键词Axmol、RHI、GPU Compute、Compute Shader、GraphicsPipeline这五个词串起来讲的其实是一个老问题的新解法游戏引擎如何摆脱“只为画图服务”的思维定式让 GPU 不再只是“显卡”而是一台可编程的通用并行协处理器。过去三年里我用 Axmol 做过粒子系统爆炸模拟、实时布料碰撞、HDR 色调映射预处理、甚至轻量级 AI 推理后处理比如超分降噪所有这些需求都卡在同一个瓶颈上——CPU 端做不了传统渲染管线又绕不开光栅化阶段的开销。这次 RHI 升级就是把那个“绕不开”的墙亲手拆掉。它适合谁不是只适合图形程序员。如果你是用 Axmol 做教育类互动应用的开发者想让粒子随麦克风输入实时响应如果你是做 AR 工具链的工程师需要在 iOS 上用 Metal Compute 快速完成图像畸变校正如果你是独立游戏作者想用 GPU 加速物理约束求解来撑起 5000 个可交互 NPC——那么这次升级不是“锦上添花”而是你项目能否落地的关键支点。它不改变你写 C 的方式但彻底改变了你“能做什么”和“多快做完”的边界。我实测过一个典型场景用旧版 Axmol 在 iPad Pro 上跑 10 万粒子的力场模拟帧率稳定在 28 FPS升级 RHI 后同一逻辑改写为 Compute Shader帧率跃升至 59 FPSCPU 占用从 92% 降到 34%GPU 利用率从 41% 提升到 87%。这不是参数调优的结果是计算模型切换带来的质变。2. 为什么必须重写 RHI旧架构的三个硬伤与新设计的底层逻辑2.1 旧 RHI 的“胶水层”本质抽象 ≠ 解耦Axmol 早期的 RHI 设计本质上是 OpenGL ES 的“翻译器”。它把drawCall()、setUniform()、bindTexture()这些操作封装成跨平台接口但背后所有状态管理、资源生命周期、命令提交逻辑都深度耦合在 OpenGL ES 的上下文模型里。举个具体例子旧版Texture2D::initWithImage()方法内部会直接调用glGenTextures()glBindTexture()glTexImage2D()然后把生成的GLuint存进对象成员变量。这种写法在 Android 上跑得稳但在 Metal 上就出问题——Metal 的纹理对象MTLTexture不是整数 ID而是一个 Objective-C 对象指针且必须由MTLDevice创建不能跨设备共享。旧架构强行用void*指针去“兼容”结果是iOS 构建时编译通过运行时崩溃在texture-getNativeTexture()返回空指针。更致命的是资源同步机制。OpenGL ES 依赖隐式同步driver 自动插入 barrier而 Vulkan/Metal 要求显式内存屏障vkCmdPipelineBarrier/encoder.memoryBarrier()。旧 RHI 完全没提供 barrier 控制能力导致在 Vulkan 后端上Compute Shader 写入的纹理Graphics Pipeline 读取时经常拿到脏数据——不是 bug是设计缺失。2.2 Compute Shader 的“寄生”困境没有独立管线就没有真正并行旧版 Axmol 把 Compute Shader 当作 Graphics Pipeline 的“附属品”。你得先创建一个GLSLProgram再手动调用glDispatchCompute()所有 dispatch 参数group count X/Y/Z都得自己算、自己传而且无法与RenderState或Camera等高层对象联动。最麻烦的是资源绑定Graphics Pipeline 用glBindBufferBase()绑定 SSBOCompute Shader 却要用glBindBufferRange()两套绑定逻辑互不兼容。我曾试图在一个SpriteBatch渲染循环里插一段 Compute Shader 做顶点变形结果发现每次 dispatch 后OpenGL 状态机被污染后续 draw call 全部错位必须手动glUseProgram(0)glBindVertexArray(0)glBindBuffer(GL_ARRAY_BUFFER, 0)重置——这不是开发体验是状态擦屁股大赛。根本原因在于旧 RHI 没有定义RHIComputePipelineState这个概念。它只有RHIGraphicsPipelineState而 Compute Shader 被塞进RHIProgram里当一个特殊 flag 处理。这就像给汽车设计发动机时只考虑“驱动轮子”却没预留“驱动发电机”的接口——不是不能发电是每次发电都得拆掉变速箱临时接线。2.3 新 RHI 的三大设计原则从“适配”走向“驾驭”新 RHI 的重构不是推倒重来而是基于 Vulkan 的设计理念反向工程。它确立了三个不可妥协的原则第一资源所有权归设备。RHIDevice是唯一能创建RHIResourceTexture、Buffer、Sampler的对象。RHICommandList只负责记录命令不持有资源。这意味着你在 iOS 上创建的RHIResource不能直接传给 Android 的RHICommandList使用——听起来是限制实则是解放。它强制你思考资源生命周期什么时候创建在哪个线程创建是否需要跨帧复用我见过太多团队在旧架构下把纹理创建写在init()里结果在 Vulkan 后端因VkDevice尚未初始化而 crash新架构下这类错误在编译期就被拦截。第二管线状态完全分离。RHIGraphicsPipelineState和RHIComputePipelineState是两个完全独立的类各自拥有专属的DescriptorSetLayout、ShaderStage、PipelineLayout。它们共用RHIProgram着色器二进制但绑定方式、参数验证、缓存策略全部解耦。这意味着你可以用同一个 compute shader 二进制在不同帧里分别绑定不同的RWStructuredBuffer和Texture2D而 graphics pipeline 完全不受影响。我测试过一个案例用 compute shader 实时生成噪声纹理同时 graphics pipeline 用该纹理渲染角色皮肤两者完全异步无锁竞争。第三命令提交原子化。RHICommandList::submit()不再是“提交一批命令”而是“提交一个 command buffer”。每个RHICommandList实例对应一个VkCommandBuffer/MTLCommandBuffer提交后自动 reset。这消除了旧版中常见的“command list 重复 submit 导致 GPU hang”问题。更重要的是它天然支持多线程录制你可以让一个线程跑 graphics commands另一个线程跑 compute commands最后统一 submit——这是旧架构靠std::mutex死锁都搞不定的并发模型。3. 核心细节解析RHIComputePipelineState 如何真正落地3.1 从着色器源码到可执行管线编译、布局、验证三步闭环新 RHI 的 Compute Shader 支持不是“加个 API 就完事”而是一整套工具链协同。以一个典型的粒子速度场更新 shader 为例.comp文件#version 450 layout(local_size_x 64, local_size_y 1, local_size_z 1) in; layout(set 0, binding 0) buffer ParticleBuffer { vec3 positions[]; vec3 velocities[]; }; layout(set 0, binding 1) uniform Params { float deltaTime; vec3 gravity; }; void main() { uint idx gl_GlobalInvocationID.x; if (idx uint(positions.length())) return; velocities[idx] gravity * deltaTime; positions[idx] velocities[idx] * deltaTime; }旧版 Axmol 需要你手动解析#version、提取local_size、硬编码 binding index。新版则通过RHIProgram::createComputeProgram()自动完成三件事编译阶段调用glslangValidatorVulkan、metaliOS、dxcWindows或ANGLEWeb进行前端编译生成 SPIR-V 或平台原生 bytecode。关键点在于它会扫描layout(local_size_*)并提取为RHIComputePipelineDesc::threadGroupSize成员而不是丢给用户去 parse 字符串。布局推导阶段分析layout(set*, binding*)自动生成DescriptorSetLayout。注意set0不代表“第一个 set”而是逻辑 descriptor set indexbinding0 和 binding1 分别对应ParticleBuffer和Params。新 RHI 会检查 buffer 的bufferTypeStorageBuffervsUniformBuffer是否匹配RHIResource::getType()并在创建RHIComputePipelineState时做类型校验——如果ParticleBuffer实际是RHIResource::Type::Texture2D创建直接失败报错信息明确指出 “binding 0 expects StorageBuffer, got Texture2D”。验证阶段在RHIDevice::createComputePipelineState()中不仅验证 shader 二进制合法性还检查threadGroupSize是否符合硬件限制。例如Vulkan 规范要求maxComputeWorkGroupSize[0]至少为 64但某些低端 Mali GPU 实际只支持 32。新 RHI 会在创建时查询VkPhysicalDeviceLimits::maxComputeWorkGroupSize[0]若64 limit则抛出RHIError::InvalidThreadGroupSize并附带当前设备实测值。我踩过的坑是在 Pixel 3 上测试local_size_x128通过但部署到三星 Galaxy A51 就 crash就是因为没做这层硬件适配校验。3.2 资源绑定DescriptorSet 的两级缓存与零拷贝优化旧版资源绑定是“每次 draw call 都重绑”新 RHI 引入RHIDescriptorSet作为中间层实现真正的状态缓存。它的设计分两级一级缓存DescriptorSetLayout 缓存。RHIDevice维护一个std::unordered_mapDescriptorSetLayoutKey, std::shared_ptrRHIDescriptorSetLayout。DescriptorSetLayoutKey由 binding 数量、每个 binding 的 type/visibility/stage 组成哈希值。这意味着只要 shader 的 binding layout 完全一致哪怕 shader 代码不同就复用同一 layout 对象避免重复vkCreateDescriptorSetLayout。二级缓存DescriptorSet 实例池。每个RHIDescriptorSetLayout关联一个std::vectorstd::shared_ptrRHIDescriptorSet池。RHICommandList::bindDescriptorSet()时优先从池中取空闲实例绑定完成后放回池中。关键优化在于RHIDescriptorSet::update()不做 memcpy而是用vkUpdateDescriptorSets的pImageInfo/pBufferInfo直接指向RHIResource的 native handle。这意味着你 update 一个 texture binding实际只传一个VkDescriptorImageInfo*指针而非复制整个 texture 数据。我实测过一个高频绑定场景每帧更新 200 个粒子系统的 transform buffer。旧版每次setUniformBuffer()触发 200 次glBindBufferBase()glBufferData()CPU 开销 1.8ms新版用RHIDescriptorSet批量 updateCPU 开销降至 0.23ms且 GPU driver 不再频繁 flush cache。提示RHIDescriptorSet的 lifetime 必须严格遵循RHICommandList的 submit 周期。我在早期测试中曾把DescriptorSet创建在Scene类里长期持有结果在 Vulkan 后端出现VK_ERROR_DEVICE_LOST——因为DescriptorSet依赖的VkDescriptorPool在 frame end 时被 reset而Scene对象还拿着失效 handle。正确做法是DescriptorSet与RHICommandList同生命周期或使用RHIDevice::allocateDescriptorSet()按帧分配。3.3 线程安全与多队列Compute Queue 的独立调度能力新 RHI 最颠覆性的变化是让 Compute Shader 拥有了独立于 Graphics Queue 的执行队列。RHIDevice提供getComputeQueue()方法返回RHIQueue*其submit()行为与getGraphicsQueue()-submit()完全隔离。这意味着你可以实现真正的异步计算主线程构建 graphics command list渲染 UI 和角色工作线程 A构建 compute command list更新物理模拟工作线程 B构建 compute command list处理音频频谱分析所有 command list 提交到各自 queueGPU 硬件自动调度关键在于RHIQueue::submit()的 barrier 语义。RHIQueue::submit()默认不等待完成但提供RHIQueue::waitIdle()和RHIQueue::submitAndWait()两种模式。我推荐的生产模式是graphics queue 用submit()异步提交compute queue 用submitAndWait()保证关键计算结果就绪后再触发 graphics render——比如布料模拟必须完成才能渲染下一帧角色。Vulkan 下这依赖VkQueueSubmit的pWaitSemaphores和pSignalSemaphores。新 RHI 将其封装为RHICommandList::addWaitSemaphore()和RHICommandList::addSignalSemaphore()。例如让 compute queue 的输出纹理 ready 后再让 graphics queue 开始绘制// Compute queue side auto computeCmdList device-createCommandList(); computeCmdList-dispatch(1024, 1, 1); computeCmdList-addSignalSemaphore(textureReadySemaphore); // signal when done computeQueue-submit(computeCmdList); // Graphics queue side auto graphicsCmdList device-createCommandList(); graphicsCmdList-addWaitSemaphore(textureReadySemaphore); // wait for compute graphicsCmdList-draw(...); // now safe to use the texture graphicsQueue-submit(graphicsCmdList);这套机制在 Metal 上通过MTLCommandBuffer的addCompletedHandler实现在 D3D12 上通过ID3D12Fence实现。新 RHI 的价值在于你不用关心底层 fence handle只需操作RHIQueue和RHISemaphore抽象。4. 实操过程从零搭建一个 GPU 物理粒子系统4.1 环境准备与最小可行验证第一步不是写 shader而是验证 RHI Compute 是否真正可用。我建议从最简 case 入手用 compute shader 把一个 buffer 的所有元素加 1。确认构建配置确保 CMakeLists.txt 中启用了AXMOL_RHI_BACKEND_VULKANAndroid/Linux或AXMOL_RHI_BACKEND_METALiOS/macOS。对于 Windows需启用AXMOL_RHI_BACKEND_D3D12并安装 Windows SDK 10.0.19041。Web 平台需启用AXMOL_RHI_BACKEND_WEBGPU并确认浏览器支持Chrome 113。创建 compute program// 加载 shader 二进制SPIR-V 或 .air auto shaderCode FileUtils::getInstance()-getDataFromFile(shaders/increment.comp.spv); auto program RHIProgram::createComputeProgram(shaderCode.getBytes(), shaderCode.getSize()); if (!program) { CC_LOG_ERROR(Failed to create compute program); return; }创建 compute pipeline stateRHIComputePipelineDesc desc; desc.program program; desc.threadGroupSize {64, 1, 1}; // 匹配 shader 的 local_size auto pipelineState device-createComputePipelineState(desc); if (!pipelineState) { CC_LOG_ERROR(Failed to create compute pipeline state); return; }创建 storage bufferRHIBufferDesc bufferDesc; bufferDesc.size 1024 * sizeof(uint32_t); bufferDesc.usage RHIResource::Usage::StorageBuffer; bufferDesc.cpuAccess RHIResource::CPUAccess::None; auto buffer device-createBuffer(bufferDesc); // 初始化 buffer 数据CPU 端 uint32_t* data new uint32_t[1024]; for (int i 0; i 1024; i) data[i] i; device-updateBuffer(buffer, data, 0, bufferDesc.size); delete[] data;构建 command list 并 dispatchauto cmdList device-createCommandList(); cmdList-begin(); cmdList-bindPipelineState(pipelineState); cmdList-bindBuffer(buffer, 0, 0); // set0, binding0 cmdList-dispatch(1024 / 64, 1, 1); // 1024 elements, 64 per group cmdList-end(); // 提交到 compute queue device-getComputeQueue()-submit(cmdList); // 等待完成 device-getComputeQueue()-waitIdle(); // 验证结果 uint32_t* result new uint32_t[1024]; device-readBuffer(buffer, result, 0, bufferDesc.size); for (int i 0; i 10; i) { CC_LOG_INFO(result[%d] %u, i, result[i]); // 应输出 1,2,3... } delete[] result;这个验证流程必须 100% 通过才能进入下一步。我遇到过三次失败第一次是 shader 编译失败忘了加-fspv-target-envvulkan1.2参数第二次是threadGroupSize超限误设为{128,1,1}第三次是readBuffer时没 waitIdle读到脏数据。每一次失败都暴露了底层硬件或 driver 的真实限制比任何文档都管用。4.2 构建粒子系统结构化缓冲区与多 pass dispatch真实粒子系统需要更多数据。我们定义Particle结构体struct Particle { glm::vec3 position; glm::vec3 velocity; float life; float mass; };对应 shader 的 storage bufferlayout(set 0, binding 0) buffer ParticleBuffer { Particle particles[]; }; layout(set 0, binding 1) buffer ParticleCount { uint particleCount; }; layout(set 0, binding 2) uniform Params { float deltaTime; vec3 gravity; vec3 wind; };关键实操点Buffer 创建particleCount必须是RHIResource::Usage::StorageBuffer且cpuAccess RHIResource::CPUAccess::Write因为 CPU 需要动态修改粒子数量。particlesbuffer 则设为cpuAccess RHIResource::CPUAccess::None纯 GPU 访问。DescriptorSet 绑定particles和particleCount是两个独立 binding必须在DescriptorSetLayout中明确定义。我曾把它们塞进同一个 binding结果 shader 编译失败——SPIR-V 验证器报错 “buffer type mismatch”。Dispatch 计算particleCount的值由 CPU 写入但 shader 里particles.length()不可用SPIR-V 不支持 runtime array length。必须用particleCount作为循环上限void main() { uint idx gl_GlobalInvocationID.x; if (idx particleCount) return; // 显式检查 // ... update logic }多 pass 调度一个完整粒子 cycle 需要 3 个 compute passUpdatePass整合物理力更新 position/velocityCollisionPass与场景几何体碰撞检测需额外 binding 地形 bufferEmitPass根据 emitter rate 生成新粒子需 atomic add每个 pass 创建独立RHIComputePipelineState但共享同一DescriptorSetbinding 不同。EmitPass的 shader 需用atomicAdd更新particleCount这要求particleCountbuffer 的memoryModel设为RHIResource::MemoryModel::Relaxed。4.3 与 Graphics Pipeline 无缝集成从 compute 输出到 sprite 渲染粒子系统最终要画出来。新 RHI 的优势在于compute 输出的particlesbuffer 可直接作为 vertex buffer 输入 graphics pipeline。创建 vertex buffer viewRHIResource::getView()方法可将 storage buffer 转为 vertex buffer viewRHIBufferViewDesc viewDesc; viewDesc.offset 0; viewDesc.size particleCount * sizeof(Particle); viewDesc.format RHIFormat::R32G32B32_SFLOAT; // position only auto vertexView particlesBuffer-getView(viewDesc);Graphics pipeline 绑定在RHIGraphicsPipelineState的vertexInputState中指定vertexView为VertexBuffer并设置stride sizeof(Particle)offset offsetof(Particle, position)。Instanced rendering用drawInstanced(particleCount, 1)渲染所有粒子。shader 中gl_InstanceID对应粒子索引可从particlesbuffer 中 fetch 数据// vertex shader layout(location 0) in vec3 a_position; uniform samplerBuffer u_particles; void main() { vec3 pos texelFetch(u_particles, gl_InstanceID).xyz; gl_Position u_mvp * vec4(pos, 1.0); }这里的关键技巧是samplerBuffer在 OpenGL ES 中性能较差但新 RHI 在 Vulkan/Metal 后端会自动将samplerBuffer降级为storageBuffer访问规避性能陷阱。我对比过同样 10 万粒子用samplerBuffer在 Adreno 640 上 32 FPS用storageBuffer提升到 54 FPS。注意texelFetch的索引必须是int不能是uint。SPIR-V 验证器对类型极其严格uint会导致OpImageFetchoperand type mismatch。这是我在 Metal 后端踩过的坑——Metal shader 用uint没问题但 SPIR-V 编译器要求int。5. 常见问题与排查技巧实录5.1 Vulkan 后端常见问题速查表现象可能原因排查命令解决方案vkCreateComputePipelines返回VK_ERROR_INVALID_SHADER_NVshader 编译未启用SPV_KHR_shader_ballot扩展spirv-val --target-env spv1.2 shader.spv添加-fspv-extSPV_KHR_shader_ballot编译参数vkQueueSubmit后 GPU hangmissingvkCmdPipelineBarrierbetween compute and graphicsvktrace查看 command buffer 内容在 compute dispatch 后、graphics draw 前插入vkCmdPipelineBarrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_VERTEX_SHADER_BIT, ...)vkAllocateDescriptorSets失败VkDescriptorPoolsize 不足vkGetPhysicalDeviceProperties(device, props)查maxPerStageDescriptorStorageBuffers增加VkDescriptorPoolCreateInfo::maxSets或按需创建多个 poolvkCmdDispatch无效果threadGroupSize超出VkPhysicalDeviceLimits::maxComputeWorkGroupSizevkGetPhysicalDeviceProperties(device, props)动态计算workGroupCountX ceil(particleCount / threadGroupSizeX)5.2 Metal 后端独有问题与绕过方案Metal 对 compute shader 的限制比 Vulkan 更严格threadgroup_memory大小限制Metal 要求threadgroup_memory总大小 ≤ 32KBA12 及以上或 16KBA11 及以下。如果你的 shader 声明shared float data[1024]在 A11 设备上会编译失败。解决方案用devicememory 替代threadgroup或动态调整数组大小。atomic_uint不支持atomic_fetch_add_explicitMetal 的atomic_uint只支持atomic_fetch_add隐式 sequential consistency不支持显式 memory order。SPIR-V to MSL 转换器会报错。绕过方案在 shader 中去掉memory_order_relaxed参数或改用atomic_fetch_add。MTLCommandBuffer完成回调延迟addCompletedHandler可能延迟 2-3 帧。如果你依赖 compute 结果立即渲染会看到一帧延迟。解决方案用MTLFence替代 handler或在 graphics command list 中addWaitEventiOS 15。5.3 WebGPU 后端调试技巧WebGPU 的 error handling 比 native API 更友好但调试工具链不成熟启用 validation layer在GPURequestAdapterOptions中设置powerPreference: high-performance并开启GPUDeviceDescriptor::requiredFeatures [timestamp-query]获取精确 timing。Shader debug 输出WebGPU 不支持printf但可通过storage buffer写入 debug 数据然后mapAsync()读回。我写了一个DebugBuffer工具类每帧写入vec4(debugValue, frameIndex, 0, 0)用 Python 脚本解析二进制 dump。跨域纹理加载限制WebGPU 的GPUTexture不能直接从跨域 image 加载。必须用createImageBitmap()transferToImageBitmap()中转。否则writeTexture()失败且无明确 error。5.4 性能调优实战心得Dispatch size 不是越大越好threadGroupSize {256,1,1}在高端 GPU 上可能比{64,1,1}慢。原因warp/wavefront 利用率下降寄存器压力增大。我的经验是移动端优先{64,1,1}桌面端尝试{128,1,1}用vkCmdWriteTimestamp测量实际 dispatch 时间。Buffer mapping 策略RHIResource::CPUAccess::Write的 buffer不要每帧map()/unmap()。改为RHIResource::CPUAccess::Write | RHIResource::CPUAccess::Coherent用invalidateRange()替代unmap()减少 driver overhead。DescriptorSet 复用陷阱RHIDescriptorSet::update()时如果 binding 的 resource 是nullptrVulkan 会 crash。必须确保所有 binding 都有有效 resource或用RHIResource::nullTexture()/RHIResource::nullBuffer()占位。Compute 与 Graphics 的 bandwidth 竞争当 compute shader 大量读写 global memorygraphics pipeline 的 texture fetch 会变慢。解决方案在 compute pass 中用coherentqualifier 声明 buffer或在 graphics pipeline 中降低 texture resolution。6. 从技术升级到工程落地我们团队的真实迁移路径我们团队用三个月完成了 Axmol 2.3 项目的 RHI Compute 迁移。不是一次性切换而是分四步走第一周验证与探路目标不是功能而是建立信心。我们只做了两件事1跑通 increment shader 验证环境2用 compute shader 实现一个 100x100 的 Conways Game of Life纯 GPU 运算CPU 只负责提交 dispatch。这让我们确认了 driver 兼容性、shader 编译链、debug 工具链全部可用。第二周核心模块替换选择粒子系统作为首个迁移模块。旧版用CCParticleSystemQuad CPU 更新帧率 32 FPS。新版用 compute shader instanced rendering帧率 58 FPS。关键收获我们发现了particleCount的 atomic update 竞争问题——多个 emitter 同时 emit 时atomicAdd返回值被覆盖。解决方案引入RHIQueue::submitAndWait()串行化 emit pass牺牲一点并发性换取确定性。第三周架构适配修改Scene类增加onComputeUpdate()生命周期方法与onUpdate()并列。所有 compute 相关逻辑dispatch、resource update集中在此。好处是1逻辑清晰2便于 profiling用CCProfilingTimer分离 compute time3为 future 的 job system 预留接口。第四周回归与压测用自动化脚本跑 1000 个测试用例覆盖 Android/iOS/Web 三大平台。重点监控1内存泄漏RHIResource的 ref count 是否归零2GPU hang连续 3 帧queue-waitIdle()超时3视觉一致性compute 输出与旧版 CPU 计算结果误差 0.001。最终发现 2 个 Vulkan 驱动 bugAdreno 630 的VK_ERROR_DEVICE_LOST通过降级threadGroupSize规避。迁移后我们项目的 APK 体积增加了 1.2MB主要是 SPIR-V shader 二进制但 runtime 内存下降 18%CPU 占用从 72% 降到 41%GPU 利用率从 55% 提升到 89%。最意外的收益是原来需要 3 个CCAction组合实现的“粒子跟随鼠标”效果现在用一行 compute shader 就搞定——position lerp(oldPosition, mousePosition, 0.1)且平滑度远超 CPU 插值。我个人在实际迁移中最大的体会是RHI Compute 不是“更快的 CPU”而是“不同的计算范式”。你不能再想着“把 for loop 搬到 GPU”而要思考“如何用并行思维重构算法”。比如旧版粒子碰撞用 nested loop O(n²)新版改用 spatial hash grid compute shader 的 bucket sort复杂度降到 O(n log n)。这个思维转变比学会写 shader 重要十倍。
返回列表