Location
src/core/
viewport.c — Viewport lifecycle, scene, rendering, passes
viewport_internal.h — Internal struct definitions
light.c — Light management and light indicators
display.c — Display settings
overlay.c — Overlay system
overlay_builtin.c — Built-in overlay implementations (selection face tint, outline, grid)
theme.c — Default theme and accent color system
vertex_format.c — Flexible vertex format
subsystem.c — Subsystem vtable dispatch
Internal Structures
MopViewport
struct MopViewport {
const MopRhiBackend *rhi; /* Backend function table */
MopRhiDevice *device; /* Backend device instance */
MopRhiFramebuffer *framebuffer; /* Color+depth+ID targets */
MopBackendType backend_type; /* Active backend enum value */
int width, height; /* Framebuffer dimensions */
MopColor clear_color; /* Per-frame clear value */
MopRenderMode render_mode; /* Solid or wireframe */
MopShadingMode shading_mode; /* Flat or smooth */
/* Camera */
MopOrbitCamera camera; /* Orbit camera state */
MopVec3 cam_eye, cam_target, cam_up;
float cam_fov_radians, cam_near, cam_far;
MopMat4 view_matrix, projection_matrix;
/* Scene — flat array with active flags */
struct MopMesh meshes[MOP_MAX_MESHES];
uint32_t mesh_count;
/* Lights */
MopLight lights[MOP_MAX_LIGHTS]; /* Up to 8 lights */
uint32_t light_count;
MopMesh *light_indicators[MOP_MAX_LIGHTS]; /* Visual indicators */
/* Interaction */
MopGizmo *gizmo; /* TRS manipulation gizmo */
uint32_t selected_id; /* Currently selected object ID */
MopInteractState interact_state; /* State machine (idle, orbit..) */
MopGizmoAxis drag_axis; /* Active drag axis */
float click_start_x, click_start_y;
/* Event queue */
MopEvent events[MOP_MAX_EVENTS];
int event_head, event_tail;
/* Overlays */
MopOverlayEntry overlays[MOP_MAX_OVERLAYS];
uint32_t overlay_count;
/* Display settings */
MopDisplaySettings display;
/* Post-processing */
uint32_t post_effects; /* Bitmask of MopPostEffect */
/* Undo/redo stacks */
/* ... */
/* Subsystems (particles, water) */
/* ... */
/* Timing */
float time, last_time;
};
MopMesh (internal)
struct MopMesh {
MopRhiBuffer *vertex_buffer;
MopRhiBuffer *index_buffer;
uint32_t vertex_count, index_count;
uint32_t object_id;
MopMat4 transform;
MopVec3 position, rotation, scale_val;
bool use_trs;
MopColor base_color;
float opacity;
MopBlendMode blend_mode;
MopMaterial material;
bool has_material;
MopVertexFormat vertex_format;
MopMesh *parent;
bool active;
};
Scene Management
Meshes are stored in a pointer-stable pool: vp->meshes is an array of MopMesh*, each struct heap-allocated once per add_mesh. Growing the pointer array via realloc does not invalidate any MopMesh* handle a host may be holding. Add / remove are O(1) via a free-list of released slot indices. Instanced meshes, lights, and camera objects use the same scheme. No upper bound on mesh count.
Each MopMesh carries a slot_index (for O(1) removal) and a viewport back-pointer (so setters that only take a mesh pointer — mop_mesh_set_position etc. — can auto-acquire the scene lock). MopLight and MopCameraObject follow the same pattern.
Rendering Orchestration
mop_viewport_render orchestrates multiple passes:
- Frame begin — clear color, depth, and object ID buffers (reverse-Z: depth cleared to 0.0 on Vulkan)
- Light indicator update — create/destroy/reposition indicator meshes for active lights
- Gizmo update — recompute handle positions and screen-space scale
- Pass: scene — draw all active meshes with
object_id < 0xFFFE0000(depth test on, backface cull on) - Pass: gizmo + indicators — draw gizmo handles (
object_id >= 0xFFFF0000) and light indicators (object_id >= 0xFFFE0000) without depth test or backface cull - Pass: overlays — wireframe, normals, bounds, selection face tint + outline, custom overlays
- Post-processing — gamma, tonemapping, vignette, fog
- Frame end — finalize and make framebuffer readable
Object ID Ranges
| Range | Usage |
|---|---|
0x00000001 – 0xFFFDFFFF | Scene meshes |
0xFFFE0000 – 0xFFFEFFFF | Light indicators (0xFFFE0000 + light_index) |
0xFFFF0000 – 0xFFFFFFFF | Gizmo handles |
Thread Safety
The viewport maintains an internal recursive pthread_mutex_t (the "scene mutex"). Every public mutation and reader in the MOP API acquires it automatically via MOP_VP_LOCK / MOP_VP_UNLOCK helpers. Recursive semantics mean nested internal calls don't deadlock, and hosts can safely call MOP APIs from inside their own mop_viewport_scene_lock / _unlock blocks.
/* Host explicit batch lock — used when you want multiple mutations
* to appear atomic relative to the render thread. */
void mop_viewport_scene_lock(MopViewport *vp);
void mop_viewport_scene_unlock(MopViewport *vp);
mop_viewport_render holds the lock for the entire frame, serializing the render pass against concurrent host mutations. Uncontended lock/unlock cost is ~20 ns, so single-threaded hosts pay a trivial overhead.
Coverage today: every lifecycle op (add_mesh, remove_mesh, add_light, add_camera, etc.), every hot setter (mop_mesh_set_*, mop_light_set_*, mop_camera_object_set_*), all config setters (set_camera, set_post_effects, set_environment, etc.), undo/redo, selection, input dispatch, texture pipeline. The interactive mop_mesh_edit_* ops are not yet auto-locked — if you call them from a worker thread, bracket with scene_lock.
tests/test_scene_threading.c exercises concurrent add/remove/mutate against an active render thread. The library is clean under make SANITIZE=tsan test.
Render-to-Texture (RTT)
Two paths for getting rendered pixels into a host-owned texture:
-
Blit-to-texture — render into MOP's internal framebuffer, then copy out:
MopRenderResult mop_viewport_render(MopViewport *vp); bool mop_viewport_present_to_texture(MopViewport *vp, MopTexture *target); /* or fused: */ MopRenderResult mop_viewport_render_to(MopViewport *vp, MopTexture *target);Present handles SSAA downsampling on the way out. Target may be presentation-sized (downsampled from internal) or internal-sized (1:1). CPU uses a box-filter; Vulkan uses
vkCmdBlitImagewithVK_FILTER_LINEAR. -
Wrap-host-target — MOP renders directly into the host's pixel buffer (zero-copy on CPU):
MopTexture *tex = mop_tex_create(vp, &(MopTextureDesc){...}); MopViewport *vp = mop_viewport_create(&(MopViewportDesc){ .width = W, .height = H, .render_target = tex, ... });CPU backend aliases the texture's buffer as the framebuffer color (via
mop_sw_framebuffer_alloc_wrapping). Vulkan / OpenGL return NULL from the wrap path and fall back transparently — deep wrapping of hostVkImage/GLuintis indocs/TODO.md.
Environment (HDRI / Procedural Sky)
mop_viewport_set_environment(vp, &(MopEnvironmentDesc){
.type = MOP_ENV_HDRI,
.hdr_path = "sunset.exr",
.intensity = 1.0f,
});
mop_viewport_set_environment_background(vp, true); /* show as skybox */
Supports .hdr and .exr. Precomputes diffuse irradiance + prefiltered specular + BRDF LUT for split-sum PBR IBL. Auto-exposure runs on the input HDR so scene brightness maps to ~0.18 middle gray after ACES.
GPU-Driven Rendering (scaffolding)
void mop_viewport_set_gpu_driven_rendering(MopViewport *vp, bool enabled);
Hooks into Vulkan's indirect-draw infrastructure (compute cull + Hi-Z + async compute are already built). Flipping this flag is safe today but currently a no-op on the draw path — per-pipeline-bucket vkCmdDrawIndexedIndirectCount dispatch is the next-round activation work (see docs/TODO.md).
Vertex Format Presets
MopVertexFormat mop_vertex_format_standard(void); /* pos + normal + color + uv */
MopVertexFormat mop_vertex_format_posonly(void); /* pos only */
MopVertexFormat mop_vertex_format_pos_normal(void); /* pos + normal */
MopVertexFormat mop_vertex_format_pos_normal_uv(void); /* pos + normal + uv */
MopMeshDesc has an optional vertex_format field — when non-NULL, the mesh stores raw vertex bytes in the caller's layout and the engine copies them unchanged. The legacy mop_viewport_add_mesh_ex still exists as an alias.