21 FEB 2026

rahulmnavneeth

viewport core

homedocs

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:

  1. Frame begin — clear color, depth, and object ID buffers (reverse-Z: depth cleared to 0.0 on Vulkan)
  2. Light indicator update — create/destroy/reposition indicator meshes for active lights
  3. Gizmo update — recompute handle positions and screen-space scale
  4. Pass: scene — draw all active meshes with object_id < 0xFFFE0000 (depth test on, backface cull on)
  5. Pass: gizmo + indicators — draw gizmo handles (object_id >= 0xFFFF0000) and light indicators (object_id >= 0xFFFE0000) without depth test or backface cull
  6. Pass: overlays — wireframe, normals, bounds, selection face tint + outline, custom overlays
  7. Post-processing — gamma, tonemapping, vignette, fog
  8. Frame end — finalize and make framebuffer readable

Object ID Ranges

RangeUsage
0x000000010xFFFDFFFFScene meshes
0xFFFE00000xFFFEFFFFLight indicators (0xFFFE0000 + light_index)
0xFFFF00000xFFFFFFFFGizmo 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:

  1. 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 vkCmdBlitImage with VK_FILTER_LINEAR.

  2. 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 host VkImage/GLuint is in docs/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.