Overview
A single call to mop_viewport_render executes a complete frame. There is no retained command buffer or deferred execution — the frame is processed synchronously.
Frame Sequence
mop_viewport_render(viewport)
│
├── 1. rhi->frame_begin(device, framebuffer, clear_color)
│ Backend clears color, depth, and object-ID buffers.
│
├── 2. For each active mesh:
│ ├── Compute MVP = projection * view * model
│ ├── Build MopRhiDrawCall struct
│ └── rhi->draw(device, framebuffer, &draw_call)
│ Backend processes the draw call:
│ ├── CPU: transform vertices, clip, rasterize
│ ├── OpenGL: bind VAO, set uniforms, glDrawElements
│ └── Vulkan: bind pipeline, push constants, vkCmdDrawIndexed
│
└── 3. rhi->frame_end(device, framebuffer)
Backend finalizes the frame:
├── CPU: no-op (data is already in memory)
├── OpenGL: glFinish or fence sync
└── Vulkan: vkQueueSubmit, vkQueueWaitIdle
CPU Backend Detail
For each draw call, the CPU backend:
- Reads vertex and index data from the RHI buffer
- For each triangle (3 indices):
a. Transforms vertices to clip space via
mvpb. Transforms normals via the model matrix (upper 3x3) c. Clips the triangle against 6 frustum planes (Sutherland-Hodgman) d. For each clipped sub-triangle:- Perspective division → NDC
- Viewport transform (NDC → screen pixels, Y-flipped)
- Backface culling (screen-space signed area test)
- Flat shading (average face normal · light direction)
- Half-space rasterization (solid) or Bresenham (wireframe)
- Per-pixel: depth test → write color + depth + object_id
Post-Frame
After mop_viewport_render returns:
mop_viewport_read_colorreturns a pointer to the RGBA8 color buffermop_viewport_pickreads from the object-ID and depth buffers- Both are valid until the next
render,resize, ordestroy
Timing
The frame is fully synchronous. mop_viewport_render does not return until all pixels are written.