Feature/graphic/render queue - #75
Open
YuanSang0512 wants to merge 18 commits into
Open
Conversation
- Add RenderState.hpp with DepthState/BlendState/CullFaceState/StencilState moved as frontend value types (was nested in opengl StateManager) - RenderState combines the four states with operator==/!= for sorting keys and incremental state application (rendering queue design Step 1) - StateManager migration to reference these frontend types happens in Step 2
- RenderDevice: add virtual apply_state(const RenderState&) so the frontend executor/backend apply state through the abstract device only - StateManager: drop Singleton inheritance, become a plain class owned by opengl::Device; replace set_* + apply() with incremental apply(RenderState) that only applies changed components (depth/blend/cull/stencil) - StateManager uses the frontend RenderState types (Step 1); removes nested state structs, dirty flags, and get_* accessors (no external users) - opengl::Device holds a StateManager member and forwards apply_state Rendering queue design Step 2
- UniformBuffer.hpp now defines the type-erased UniformValue variant (int/float/Vector3/Vector4/Matrix3/Matrix4), the simple-path UniformData (name->value list) and the batch-path UboBlock (struct reference + binding) - UniformBuffer class stays a placeholder pending backend impl - Rendering queue design Step 3
- RenderCommand: value-type draw command carrying target/va/ib/shader/ RenderState/UniformData/UboBlock/texture slots/transparent/instance_count (references resources, does not own); MAX_TEXTURE_SLOTS = 8 - RenderQueue: submit() collects commands, flush() applies state via RenderDevice::apply_state and draws via device.draw/draw_instance - Basic flush executes in submission order; sorting lands in Step 5 Rendering queue design Step 4
- RenderQueue::flush sorts commands: opaque front-to-back, transparent back-to-front, grouped by state/transparency to reduce state switches - Apply simple-path uniforms (UniformData) via std::visit dispatch to the shader's set_uniform_* (int/float/Vector3/Vector4/Matrix3/Matrix4) - RenderCommand::shader is now non-const (uniforms are mutated during exec) - UBO batch upload left as TODO pending UniformBuffer backend Rendering queue design Step 5
- Renderer now enqueues RenderCommand instead of drawing immediately - add Renderer::flush() that executes the queued commands via RenderQueue - draw/draw_instance take non-const Shader& (uniforms mutated on execute) - test_window: build commands with target/textures/uniforms and flush each frame (post-processing pipeline now expressed as queued commands) Rendering queue design Step 6
- RenderObject: geometry + material + state as a reusable draw unit (fields mirror RenderCommand, to_command() builds a command per frame) - Renderer::draw(const RenderObject&) enqueues via to_command() - Upper-layer draw interface evolution (design §7.1, direction 3) Rendering queue design Step 7
- include/gkit/graphic/resource/: GPU resource abstractions (Buffer, VertexBuffer, IndexBuffer, VertexArray, Shader, Texture, FrameBuffer, RenderBuffer, StorageBuffer, UniformBuffer) - include/gkit/graphic/render/: render pipeline types (Renderer, RenderDevice, RenderQueue, RenderCommand, RenderState, RenderObject) - graphic root keeps only config.hpp and VertexBufferLayout.hpp - update all includes across src/include/test to new paths
- Add Viewport value type; RenderCommand/RenderObject carry a per-target viewport so FBO commands use FBO size and screen commands use window size (GL viewport is global state, must be set per command) - RenderDevice::set_viewport(Viewport) abstract interface; opengl Device implements via glViewport; RenderQueue sets it before each command - Renderer: keep only draw(const RenderObject&); remove simplified draw(va,ib,shader) and draw_instance (covered by RenderObject) - test_window: FBO is half the window (400x400), each RenderObject carries its viewport; drop manual fbo->set_viewport in the loop - RenderQueue sorts FBO-targeted commands before screen commands and unbinds the previous FBO when switching targets
- RenderCommand/RenderObject gain clear + clear_flags: a command can request the target (FBO or screen) be cleared before drawing - RenderQueue::flush clears the bound target when cmd.clear is set - test_window: triangle_to_fbo clears the FBO color/depth attachment before drawing, so the post-processing quad samples a clean framebuffer
- New render/Material.hpp: shader + texture slots + UniformData + UboBlock (resources held by pointer reference, not owned, shareable across objects) - Move MAX_TEXTURE_SLOTS from RenderCommand to graphic/config.hpp so both Material and RenderCommand use it without a dependency between them - Remove the duplicate definition from RenderCommand RenderObject refactor Step 1
- RenderObject now owns CPU vertex/index data and a Material; GPU resources (VBO/IBO/VAO) are lazily created and cached on first draw via ensure_uploaded - RenderCommand holds only RenderObject* + per-draw controls (target, viewport, clear, sorting metadata); geometry/material/state read from the object - Renderer::draw(RenderObject&, target, viewport) enqueues the command - RenderQueue executes: switch target, set viewport, clear, apply state, lazily upload geometry, bind material shader/textures, apply uniforms, draw - Material moved to graphic/resource/ (it references shader/texture resources) - MAX_TEXTURE_SLOTS moved to config.hpp (shared by Material and RenderCommand) - test_window: builds objects from vertex/index arrays + Material, no manual VAO/VBO/IBO/shader RenderObject refactor (design doc RenderObject重构方案)
- draw(RenderObject&, target = nullptr, viewport = full window) - allows draw(obj) to render to the screen at full window by default
- Rename test/graphic/test_window.cpp to test_render.cpp (GLOB picks it up, produces test_render.exe) - Use new RenderObject API with default target/viewport: renderer.draw(obj) for screen full-window, explicit target for FBO draws
- Size FBO color textures from the FBO, not the global SCR_WIDTH/SCR_HEIGHT - Default per-command viewport to the render target size (optional viewport) - Snapshot RenderState into RenderCommand so per-draw state is self-contained - Force GL state sync on Device construction (initial shadow-state divergence) - Reject draw commands with missing or invalid shaders at enqueue time - Clear the screen once per frame in the render test
- Add a translucent triangle (alpha shader + SrcAlpha blend, 1:1 blend at u_alpha=0.8), offset 50px to the bottom-left, depth-tested between the quad and the comparison triangle - Enable depth testing on the opaque objects so screen depth is real - Add a stencil-mask pass in the FBO: a stencil triangle offset 50px up-right writes stencil=1 (Always/Replace), the FBO color is then cleared (keeping stencil), and the next triangle draws with NotEqual(1) so the masked region is left empty
…re/graphic/render_queue
Contributor
Author
gkit::graphic::RenderObject 用户手册1. 它是什么
对比旧 API:以前你要手动 头文件:RenderObject.hpp 2. 所有权模型(务必先看)
3. 构造RenderObject obj(
const std::vector<float>& vertices, // 交错排列的顶点数据
const std::vector<uint32_t>& indices, // 索引
const VertexBufferLayout& layout, // 顶点属性布局
const Material& material // 材质(shader + 纹理槽 + uniforms)
);
VertexBufferLayout layout;
layout.push<float>(3); // position:vec3
layout.push<float>(2); // uv:vec2
layout.push<float>(4); // color:vec4
// 每顶点跨距 = (3+2+4) × 4 = 36 字节布局类型只支持 4. 成员详解4.1
|
| 类别 | 绘制顺序 | depth_key 含义 |
|---|---|---|
不透明(transparent=false) |
值小的先画 | 越小越近,近优先(front-to-back) |
透明(transparent=true) |
值小的后画 | 越小越远,远优先(back-to-front) |
为什么透明要反过来? 透明物体靠混合叠加,必须先画远处、再画近处,后画的近物才能正确地叠加在远物之上。所以透明物体要设一个比不透明物体更小的 depth_key 让它排最后。
整体排序优先级(由队列保证):
- FBO 目标命令先画,屏幕命令后画(这样后处理才能采样 FBO 结果)
- 不混合 → 混合
- 不透明 → 透明
- 组内再按 depth_key
4.4 绘制控制 — instance_count / clear / clear_flags
obj.instance_count = 1; // >1 时走实例化绘制
obj.clear = false; // 绘制前是否清目标
obj.clear_flags = ClearFlags::All; // 清什么:Color / Depth / Stencil / ColorDepth / All注意 clear 是逐命令的:一次 draw() 前的清屏只对该命令生效,不会影响其他 draw。
5. 与 Renderer 配合
auto& renderer = gkit::graphic::Renderer::instance();
renderer.init(); // 默认 OpenGL 后端
// 定义一次
gkit::graphic::RenderObject triangle_obj(vertices, indices, layout, material);
// 每帧:入队 → 执行
renderer.draw(triangle_obj); // 目标=屏幕,视口=全窗口
renderer.draw(triangle_obj, fbo.get()); // 渲染到 FBO
renderer.draw(triangle_obj, nullptr, viewport); // 自定义视口
renderer.flush(); // 排序 + 应用状态 + 真正绘制三个关键点:
draw()只入队,flush()才真正画。每帧在 swap buffer 前调一次flush()。draw()接收非 const 引用:执行时可能懒上传 GPU 资源。- 每次
draw()都会把对象的state快照进命令。所以同一个 RenderObject 可以用不同状态多次 draw——例如先关深度画一次、再开深度画一次。
6. 坑与注意
- shader 必须有效:
draw()会拒绝无 shader 或编译/链接失败的对象,并打 error 日志(见 Renderer.cpp)。 - 默认视口是全窗口(SCR 500×500),不是目标尺寸。渲染到尺寸不同的离屏 FBO 时,务必显式传匹配 FBO 的
Viewport,否则 GL 全局视口会错位。 - 透明物体要自己管理深度:如果目标上先画了不透明物体,记得给透明物体开深度测试(
state.depth.enabled = true),否则会渲染错层;深度关闭时不写深度缓冲。 - FBO 必须先于屏幕命令:队列已强制排序,但如果你手写多遍后处理链,注意每个 FBO 都要用独立的命令。
- 对象的生命周期:对象须存活到
flush()执行完;传入命令的是对象指针。
7. 完整示例
一个混合 + 深度测试的最小场景(完整可运行版见 test_render.cpp):
// —— 初始化 ——
auto& renderer = gkit::graphic::Renderer::instance();
renderer.init();
auto& device = renderer.get_device();
// —— 几何:彩色三角形(位置 + 颜色)——
std::vector<float> vertices = { /* x,y,z, r,g,b ×3 */ };
std::vector<uint32_t> indices = {0, 1, 2};
gkit::graphic::VertexBufferLayout layout;
layout.push<float>(3); layout.push<float>(3);
// —— 材质 ——
gkit::graphic::Material tri_material;
tri_material.shader = device.create_shader("color_triangle.shader").get();
// 若 shader 带 uniform:
tri_material.uniforms.values.push_back({"u_alpha", 0.8f});
// —— 定义对象 ——
gkit::graphic::RenderObject tri_obj(vertices, indices, layout, tri_material);
tri_obj.state.depth.enabled = true; // 开深度测试
tri_obj.transparent = true; // 透明类,排后面
tri_obj.depth_key = 0.0f; // 最近,最后画
tri_obj.state.blend.enabled = true; // 开混合
tri_obj.state.blend.src_rgb = gkit::graphic::BlendFunc::SrcAlpha;
tri_obj.state.blend.dst_rgb = gkit::graphic::BlendFunc::OneMinusSrcAlpha;
// —— 渲染循环 ——
while (running) {
renderer.clear(gkit::graphic::ClearFlags::ColorDepth);
renderer.draw(tri_obj); // 入队
renderer.flush(); // 执行
SDL_GL_SwapWindow(window);
}
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PR: feat(graphic): 落地渲染队列(排序 + 批量执行 + RenderObject)
描述
概述
在 graphic 模块落地
.vscode/渲染队列设计方案.md中的**渲染队列(RenderQueue)**设计:Renderer::draw不再立即执行,而是把一条RenderCommand记录进RenderQueue,由flush()统一排序、并经前端RenderDevice抽象批量执行。配套落地了可复用的RenderObject绘制单元(几何 + 材质 + 状态,隐藏 VAO/VBO/IBO)、Material资源结构、前端RenderState/UniformValue值类型,以及resource/+render/头文件拆分。最后的健壮性修复补齐了 FBO 尺寸、按目标 viewport、命令状态快照与入队期 shader 校验。改动内容
1. 前端值类型(
include/gkit/graphic/render/RenderState.hpp、include/gkit/graphic/resource/UniformBuffer.hpp)RenderState快照类型,把深度/混合/剔除/模板打包成一个值(排序键 + 命令自携带状态)。UniformValue(std::variant<int, float, Vector3, Vector4, Matrix3, Matrix4>)+UniformData(按名逐条赋值)与UboBlock(批量路径引用)三种 uniform 载体。MAX_TEXTURE_SLOTS移入config.hpp,作为引擎级纹理槽上限。2. 渲染队列(
RenderCommand.hpp、RenderQueue.hpp/cpp)RenderCommand值类型:引用目标(FrameBuffer*)与绘制单元,携带optional<Viewport>、RenderState快照、经材质引用的纹理/uniform 源,以及排序元数据(transparent、depth_key)和逐命令清屏。RenderQueue::submit()入队;flush()稳定排序(FBO 目标排在屏幕之前,保证后处理能采样 FBO;blend/透明分组;不透明 front-to-back、透明 back-to-front)后逐条执行:切换目标 → 按目标 viewport → 清屏 → 增量应用状态 → 绑定 shader/纹理/uniform →device.draw(_instance)。3. RenderObject 绘制单元(
RenderObject.hpp/cpp)vertices+indices+VertexBufferLayout+Material,VAO/VBO/IBO 首次绘制时懒创建并缓存(ensure_uploaded),对调用方隐藏。4. Material 资源(
resource/Material.hpp)Material结构:shader 指针 + 纹理槽 +UniformData+UboBlock,按引用共享、由资源系统持有。5. StateManager → 后端内部增量应用器(
backend/opengl/StateManager.*)StateManager成为opengl::Device的私有成员。RenderDevice新增apply_state(RenderState),Device转发给状态管理器的 dirty-flag 增量应用器;前端执行器完全看不到该类型。6. 后端资源工厂接线(
RenderDevice.hpp、backend/opengl/Device.*、create_device.cpp)RenderDevice新增set_viewport/draw/draw_instance/clear执行入口。Device构造时强制全量状态同步,保证 shadow 状态启动即与 GL 一致、永不漂移。7. Renderer 集成(
Renderer.hpp/cpp)draw(RenderObject&, target, viewport)把对象快照成命令并入队(不再立即绘制);flush()执行队列。draw提供默认target/viewport参数(屏幕目标 / 全屏)。8. FBO 与 viewport 正确性(
FrameBuffer.*、Texture.*)FrameBuffer暴露width()/height();attach_color_texture把帧缓冲纹理调整为 FBO 尺寸,不再写死全局SCR_WIDTH/SCR_HEIGHT。9. 头文件布局(
include/gkit/graphic/)resource/(缓冲、纹理、shader、帧缓冲、材质…)与render/(Renderer、RenderDevice、RenderObject、RenderQueue、RenderCommand、RenderState)。gkit.hpp与CMakeLists.txt同步更新。10. 测试(
test/graphic/test_render.cpp,新增 shader)test_window改名为test_render:彩色三角形渲染到 FBO,后处理四边形采样 FBO 纹理(反色)上屏,再加一个半尺寸三角形做对比——覆盖按目标 viewport、逐命令清屏与目标切换。color_triangle.shader、post_process.shader与alpha_triangle.shader(带u_alphauniform);每帧清屏。SrcAlpha/OneMinusSrcAlpha混合(u_alpha=0.8),深度测试开启;给不透明物体也启用深度测试,让屏幕深度真实参与分层。Always/Replace写stencil=1;只清 FBO 颜色(保留模板);随后三角形NotEqual(1)绘制,模板区域内被遮住留空。绘制工作流
test_render单帧的实际流程(所有draw()只入队,flush()时排序 + 批量执行):排序后的实际执行顺序:
Draw1 → Draw2(FBO 目标在前)→Draw3 → Draw4 → Draw5(屏幕目标在后,透明最后)。这与入队顺序一致,因为排序按 target 分组。单次绘制的完整调用链
以
renderer.draw(triangle_obj, fbo.get())为例,从用户侧到最终呈现的每一层:关键点:
draw()只入队,真正的 GPU 调用全在flush()。同一RenderObject可在多帧复用(GPU 资源缓存)。draw()时的obj.state拷贝,之后改obj.state不影响已入队命令。draw()就被拒,不会拖到flush()崩溃。注意事项
SDL_GL_DEPTH_SIZE=16(有深度)但SDL_GL_STENCIL_SIZE=0(无模板)。模板测试目前只能在 FBO 上用(其 RenderBuffer 是GL_DEPTH24_STENCIL8)。若要在屏幕做模板遮罩,需先SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8)。depth.enabled = true。glClear(Color)保留模板:清屏不受模板测试影响,且ClearFlags::Color不清模板——这是"清画面但不清模板标记"能实现的前提。stencil.enabled=false,StateManager 增量应用会在切到屏幕命令时自动关闭模板,模板状态不会泄漏到后续屏幕绘制。sort_key把 FBO 目标整体排在屏幕前,无法表达"屏幕命令夹在两次 FBO 绘制之间"的多趟(如 shadow map)场景——这是当前单队列模型的设计局限。sort_key未纳入 shader:排序只按 target/blend/透明分组,同目标内 shader 不参与排序,可能出现 shader 来回切换、无法批处理。Commits
9125886a9f72e8566854e1a65498e742184e49f93e3827ab043a01373432b06fea1e14ee6940ae3d83dd2e4ba19a03cba8248b03cf1fdd95设计说明
RenderCommand入队时快照自己的RenderState,同一RenderObject的两条命令可携带不同状态。renderer.draw(object, target, viewport),VAO/VBO/IBO 生命周期在内部。StorageBuffer/UniformBuffer后端、draw_instance的逐实例缓冲接线、纹理资源模块集成。验证
gcc(Debug preset,启用测试)构建通过:gkit_graphic+test_render。test_render.exe完整跑完帧循环(FBO 模板遮罩 → 后处理四边形 → 对比三角形 → 透明三角形 → 屏幕)无崩溃、无 GL 错误。