本文档记录项目的总体架构、设计思路与模块约束。具体 crate 入口、文件职责和运行命令请查看对应模块 README。
项目目标是保持无环依赖:上层可以依赖下层,下层不反向依赖上层业务。
flowchart TB
L6["L6 truvis-winit-app<br/>winit 事件循环、窗口生命周期、渲染线程启动"]
L5["L5 truvis-app<br/>demo app、GuiPlugin、overlay plugin、render pipeline plugin、RenderGraph 编排<br/><br/>L5 truvis-frame-runtime<br/>RenderAppShell 帧骨架 + render-loop 适配层<br/><br/>L5 truvis-frame-api<br/>RenderApp / RenderAppHooks / Plugin 契约与 Plugin Ctx"]
L4["L4 truvis-render-backend<br/>RenderBackend:World + RenderWorld + swapchain/present/cmd/sync 生命周期"]
L3["L3 truvis-render-graph / truvis-scene / truvis-asset / truvis-gui-backend<br/>按帧同步辅助、CPU 场景、资产加载、底层 ImGui Vulkan 录制"]
L2["L2 truvis-render-interface<br/>RenderWorld、BindlessManager、GpuScene、FrameCounter、CmdAllocator、FifBuffers"]
L1["L1 truvis-gfx<br/>Vulkan RHI 封装"]
L0["L0 truvis-utils / truvis-logs / truvis-path / descriptor-layout"]
L6 --> L5 --> L4 --> L3 --> L2 --> L1 --> L0
GUI 的 RenderGraph 适配位于 truvis-app::gui_plugin,底层 truvis-gui-backend 只保留 GuiMesh / GuiPass 等 Vulkan 后端能力,不依赖 render graph 或 frame runtime。
启动入口唯一:平台层创建窗口和渲染线程,渲染线程只通过 Box<dyn RenderApp> 驱动 App。
启动流程:
flowchart TD
RunApp["WinitApp::run_app"] --> InitEnv["init_env"]
InitEnv --> CreateWindow["create Window + SharedState"]
CreateWindow --> SpawnThread["spawn RenderThread"]
SpawnThread --> AppFactory["app_factory() -> Box<dyn RenderApp><br/>通常是 RenderAppShell<ConcreteApp>"]
AppFactory --> InitAfterWindow["app.init_after_window(raw handles, scale_factor, initial_size)"]
每帧流程:
flowchart TD
RenderLoop["render_loop"] --> DrainInput["drain channel InputEvent"]
DrainInput --> PushInput["app.push_input_event(event)"]
PushInput --> ReadSize["read latest window size"]
ReadSize --> RecreateSwapchain["app.recreate_swapchain_if_needed(size)"]
RecreateSwapchain --> TimeToRender["app.time_to_render()"]
TimeToRender --> RunFrame["app.run_frame()"]
RenderApp 通常由 RenderAppShell<A> 实现。RenderAppShell 持有 RenderBackend、待处理输入队列与具体 App hooks,因此具体 App 不需要持有 backend 或手写转发生命周期方法。
RenderAppShell::run_frame 的固定顺序:
flowchart TD
BeginFrame["render_backend.begin_frame()"] --> Input["drain input events<br/>app.on_input(events)"]
Input --> Update["update_ctx = render_backend.update_phase()<br/>app.update(&mut update_ctx)"]
Update --> PluginUpdate["app.visit_plugins_mut<br/>Plugin::update"]
PluginUpdate --> Prepare["render_backend.prepare(app.camera())"]
Prepare --> Render["render_ctx = render_backend.render_phase()<br/>app.render(&render_ctx)"]
Render --> Present["render_backend.present()"]
Present --> EndFrame["render_backend.end_frame()"]
关闭流程:
- 渲染线程观察到退出信号后调用
RenderApp::shutdown(&mut self)。 RenderAppShell先调用 App hooks 的shutdown(),再通过 App 提供的 shutdown visitor 调用 Plugin shutdown,最后销毁 RenderBackend。RenderBackend拥有Gfxroot owner;backend 销毁时先等待 GPU idle,释放所有子资源,最后销毁Gfx。- 主线程等待渲染线程完成后再 drop
Window。
RenderBackend 持有渲染后端核心状态:
RenderBackend
-> Gfx Vulkan root owner + typed Ctx factory
-> World CPU scene + assets
-> RenderWorld GPU resources + frame state
-> RenderPresent swapchain/present resources
RenderAppShell 只持有:
RenderBackend- 待处理
InputEvent队列 - 具体 App hooks
RenderAppShell 不持有 GUI、Camera、Overlay、InputState 或任何具体 render pipeline plugin。
具体 App state 持有:
GuiPluginCameraController/InputManagerDebugInfoOverlay/PipelineControlsOverlayTrianglePlugin、ShaderToyPlugin或RtPipeline等具体渲染能力
RenderBackend 通过 lifecycle Ctx 借出内部字段:
RenderBackendInitCtxRenderBackendUpdateCtxRenderBackendRenderCtxRenderBackendResizeCtxRenderBackendShutdownCtx
RenderAppShell 从 RenderBackend Ctx 裁剪标准生命周期需要的 Plugin Ctx,App 在 render hook 中为特有 render 能力裁剪 PluginRenderCtx:
PluginInitCtxPluginUpdateCtxPluginRenderCtxPluginResizeCtxPluginShutdownCtx
这些 Ctx 携带 phase-appropriate 的 typed Gfx Ctx(如 device、resource、queue、surface、immediate、device-info),调用点只获得当前阶段需要的能力,不持有完整 &Gfx。
GUI draw data 不进入通用 Ctx。GuiPlugin 自行持有 imgui context、draw data、GUI mesh buffer、font texture map,并通过 prepare_render_data 和 contribute_passes 接入 render hook。
RenderApp 是 render loop 的外部契约:
init_after_windowrun_framepush_input_eventrecreate_swapchain_if_neededtime_to_rendershutdown
RenderAppShell<A> 是适配层:它实现 RenderApp,持有 RenderBackend、输入事件队列和 A: RenderAppHooks,把 render loop 的外部生命周期转发到 backend 与具体 App hooks。
RenderAppHooks 是 RenderAppShell 回调具体 App 的 hook 契约:
initvisit_plugins_mutvisit_plugins_mut_revon_inputupdaterendercameraon_resizeshutdown
RenderAppShell 使用 visit_plugins_mut 批量调用 Plugin::init、Plugin::update 和 Plugin::on_resize,使用 visit_plugins_mut_rev 调用 Plugin::shutdown。输入事件目前仍由 App hooks 显式处理,因为 GUI 事件消费和 App 自有 InputManager 之间存在 App 级策略。
Plugin 是可复用能力单元的标准生命周期:
initon_inputupdateon_resizeshutdown
Plugin 的特有能力不放进统一 trait。例如:
GuiPlugin::begin_frame/ui/end_frame/prepare_render_data/contribute_passesTrianglePlugin::contribute_passesShaderToyPlugin::contribute_passesRtPipeline::contribute_compute_passes/contribute_present_passes
App 通过持有具体类型来组合这些能力,并通过 visitor 暴露标准生命周期 Plugin,不使用 downcast、注册表或消息总线。
CPU 语义数据从 World 进入 RenderBackend prepare 阶段,再同步到 GPU 可见资源:
flowchart LR
Assets["AssetHub / SceneManager"] --> Prepare["RenderBackend::prepare(camera)"]
Prepare --> GpuResources["GpuScene / BindlessManager / GlobalDescriptorSets"]
GpuResources --> BuildGraph["App render hook builds RenderGraph"]
BuildGraph --> RecordCommands["pass command recording"]
RecordCommands --> Submit["queue submit"]
Submit --> Present["swapchain present"]
RenderGraph 规则:
- App 在
RenderAppHooks::render中创建 RenderGraph。 - 渲染管线 Plugin 只贡献自己的 pass,不决定整个 App 的完整执行顺序。
- App 显式决定 GUI pass 与渲染管线 pass 的添加顺序,RenderGraph 按该顺序录制,不做自动重排。
- pass 必须声明 image 读写状态,让 RenderGraph 在线性序列中推导同步与 layout transition。
Triangle / ShaderToy 使用单个 present graph。RT demo 使用 compute graph 与 present graph:App 先让 RtPipeline 贡献 compute passes,再在 present graph 中先 resolve,最后调用 GuiPlugin::contribute_passes 叠加 GUI。
线程模型:
flowchart LR
subgraph MainThread["main thread"]
MainOwns["owns EventLoop and Window"]
MainInput["sends InputEvent through channel"]
MainSize["writes latest size to AtomicU64"]
MainExit["sets exit flag"]
MainOwns --> MainInput
MainOwns --> MainSize
MainOwns --> MainExit
end
subgraph RenderThread["render thread"]
RenderApp["owns Box<dyn RenderApp>"]
RenderBackend["owns RenderBackend through RenderAppShell"]
VulkanObjects["creates, uses, destroys all Vulkan objects"]
RenderApp --> RenderBackend --> VulkanObjects
end
MainInput -- "InputEvent channel" --> RenderApp
MainSize -- "latest size AtomicU64" --> RenderApp
MainExit -- "exit flag" --> RenderApp
约束:
- 主线程不调用 Vulkan、
ash或truvis-gfxAPI。 - 所有 Vulkan 对象在渲染线程创建、使用和销毁。
- resize 通过 latest-size 模式合并连续事件。
- GPU 同步优先通过 RenderGraph、binary semaphore 和 frame timeline 表达。
生命周期契约以显式 owner 为边界:
Gfx是 Vulkan root owner,由RenderBackend持有并在所有子资源之后销毁。- 叶子 Vulkan/VMA/WSI wrapper 通过
destroy(self, ctx, reason)或destroy_mut(&mut self, ctx, reason)释放,释放所需依赖由 owner 在调用点传入 typedGfxCtx。 Drop不调用 Vulkan/VMA/WSI release API,只通过 debug assertion 暴露遗漏的显式销毁。RenderWorld、manager、plugin 字段和长期资源 wrapper 不保存 typedGfxCtx、&Gfx、&GfxDevice或&VMemAllocator引用。
GPU 资源按用途分类:
- Persistent:pipeline、sampler、descriptor layout、shader binding
- Frame:command buffer、per-frame buffer、FIF resources
- Swapchain:swapchain image/view、present semaphore、window-sized targets
- Asset:texture、mesh buffer、material-related GPU resources
- GUI:imgui font texture、per-frame GUI mesh buffer、texture map
- RenderGraph:按帧导入的 image 状态引用与同步计划;图内 transient image/buffer 是未来能力,不作为当前资源生命周期类别
创建路径:
RenderBackend::new初始化Gfx,创建World/RenderWorld。RenderBackend::init_after_window创建 surface、swapchain 和RenderPresent。RenderAppShell创建RenderBackend并把RenderBackendInitCtx包装为RenderAppInitCtx交给 App hooks。- App state 从
RenderAppInitCtx中的 RenderBackend Ctx 构造PluginInitCtx,依次初始化自己持有的 Plugin。
重建路径:
- render loop 调用
RenderApp::recreate_swapchain_if_needed(size)。 RenderAppShell调用RenderBackend::handle_resize(size)。- RenderBackend 只有实际重建时返回
Some(RenderBackendResizeCtx)。 RenderAppShell把返回值包装为RenderAppResizeCtx交给 App hooks,App state 构造PluginResizeCtx并通知需要 resize 的 Plugin。
销毁路径:
RenderApp::shutdown(&mut self):RenderAppShell等待 GPU idle 后,先用RenderAppShutdownCtx调用 App hooks shutdown,再用PluginShutdownCtx反向遍历 Plugin shutdown。- App / Plugin shutdown 必须在
RenderBackend::destroy()释放 backend 子资源之前释放自己持有的 GPU 资源;需要 manager 访问时通过 shutdown context 使用RenderWorld。 - manager-owned image/view 只能通过
GfxResourceManager释放,manager 负责 image-view-before-image、延迟销毁队列与DestroyReason诊断。 - backend destroy:
gfx.wait_idel()-> release present/FIF/assets/GPU scene/cmd/backend resources ->gfx.destroy()。 gfx.destroy()开始后,剩余 App / Plugin 字段的Drop不得再调用 Vulkan/VMA/WSI 销毁 API。