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

Design-language Chrome

The viewport drives MOP's first-party chrome — corner axis navigator, transform gizmo, selection outline, HUD/label text — through a couple of pieces of state.

grid_plane_axis

int grid_plane_axis;   /* 0 = X-normal (YZ), 1 = Y-normal (XZ),
                          2 = Z-normal (XY).  Default 1 (XZ ground). */

Which world axis is normal to the analytical grid. Only changes on a navigator-axis click; free-orbit preserves whatever plane was last set, so the grid never disappears mid-drag.

Navigator clickgrid_plane_axisPlane
Top / Bottom (+Y)1XZ ground
Front / Back (+Z)2XY wall
Left / Right (+X)0YZ wall

Frame-on-axis-click

Clicking an axis ball in the corner navigator does both an orientation snap and a frame-all centered on world origin:

  1. mop_orbit_camera_snap_to_view sets yaw / pitch.
  2. vp->camera.target(0, 0, 0) so the grid (laid out around origin) sits in the middle of the viewport — 3DS Max / Maya feel.
  3. vp->camera.distance is sized from the farthest mesh corner from origin (not the scene centroid) with a 4-unit floor and a 1.30× margin, so meshes stay visible without recentering.

After a snap the user can free-orbit normally; the next click re-anchors.

Per-frame text queue

The viewport carries a heap-allocated MopTextPrim *text_prims queue populated by mop_text_draw_2d / mop_text_draw_label. The post-readback composite path rasterizes it on top of the overlay prims, then drains the queue. See Text for submission semantics; the lifecycle is:

  1. Host calls mop_text_draw_* zero or more times.
  2. mop_viewport_render walks the queue, projects label anchors, runs greedy push-up stacking, blits glyphs.
  3. Queue resets to empty for the next frame.

Strings are owned by the queue (malloc copies on submission), freed at frame end and on viewport destroy.

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.