Repository
lib-viewport
Kinogaki's 3D viewport framework, shared by every geometry domain.
- GitHub
- kinogaki/lib-viewport (private)
- Folder
libs/lib-viewport/- Crates
- 3
- Rust lines
- 13,264 in src, 2,672 in tests
Crates
| Crate | What it is | Files | Lines | Test lines |
|---|---|---|---|---|
kinogaki-viewport-core | Kinogaki viewport core: what a 3D viewport shows, independent of any GPU API or UI toolkit. | 16 | 4,768 | 391 |
kinogaki-viewport-gpu | Kinogaki's wgpu viewport renderer: pipelines, retained GPU buffers, offscreen multisampled targets and owned WGSL shaders. | 27 | 7,366 | 2,219 |
kinogaki-viewport-ui | Kinogaki viewport widgets for Kinogaki UI (lib-ui): one/four-view layout, camera navigation, the six-axis view cube, transform gizmo handles and point number labels. | 8 | 1,130 | 62 |
README
lib-viewport
Kinogaki's 3D viewport framework, shared by every geometry domain. Mesh, CAD, SDF and other libraries turn their geometry into viewport packets; this library renders them and provides the viewport widgets. It never depends on a geometry library or on Studio.
| Crate | Responsibility |
|---|---|
crates/core (kinogaki-viewport-core) | No GPU or UI API. Orbit Camera (perspective/orthographic, projection, rays, framing, grid spacing, roll, and a scene camera's Lens while a view looks through one), Ray; the SurfaceSource packet trait (shared point positions, vertex shading records, triangles, edges, point markers, sparse DrawUpdate ranges, element counts, object hover faces, selection outlines); ElementKind/ElementSet selections; PreviewUpdate patches and GPU motions for live transforms (its own: a weighted motion is the SDK's kinogaki-edit deform); OverlayDraw/GuideDraw layers; world-space SceneLines (camera and light glyphs) |
crates/gpu (kinogaki-viewport-gpu) | The wgpu renderer: one texture per view, retained buffers with sparse uploads, selection masks and outlines, object silhouettes, grid (its lines through the origin in the application's axis colors, set_axis_colors), Mesh Check/guide overlays, surface analysis shading (zebra, isophotes, curvature, draft), surface colors linear (RGB9E5, pack_linear) interpolated then shown through the host's display transform per pixel (set_display, WGSL the host gives), times the images polygons show (SurfaceTextures, array textures with mip levels, each image uploaded once, read smooth or nearest and wrapped as each corner says); owned WGSL shaders |
crates/ui (kinogaki-viewport-ui) | widgets on Kinogaki UI (lib-ui): one/four-view layout, camera navigation (Alt-drag orbit/pan/dolly, wheel, trackpad, pinch), the six-axis view cube, transform gizmo handle geometry, hit testing and painting |
A domain library implements SurfaceSource for its packet type and keeps its own picking structures; an application passes Scene { surface, selection, preview, findings, guides } (the selection an ElementSelection: one set per drawn kind) to ViewportRenderer::render_scene and draws the texture inside its viewport widget. See docs/ARCHITECTURE.md.
Sibling checkouts
kinogaki-viewport-ui uses lib-ui (Kinogaki's UI framework and theme) from ../lib-ui, and the crates reach the SDK's packet, geometry and edit contracts at ../../sdk. In the Kinogaki Studio tree every library sits at libs/<name> beside the others and the SDK at ../../sdk; clone the root repository (git clone kinogaki/studio kinogaki-studio) and run its tools/setup.py.
Checks
GPU rendering regressions run through Studio's editor harness (the root's python3 tools/check_all.py --changed and --full run them) with the packets its standard values make. Commit and push here before updating the root's libs/lib-viewport pin; the root's python3 tools/check_all.py checks all repositories together. Source ownership remains private; dependencies retain their own licenses (THIRD_PARTY_NOTICES.md).
Its documents
The repository's architecture document as written there (code blocks and links left out), and the opening of each other document.
lib-viewport architecture (libs/lib-viewport/docs/ARCHITECTURE.md)
lib-viewport architecture
Packets
A domain library owns its geometry and builds an immutable packet per displayed revision, shared as Arc<dyn SurfaceSource>. The allocation is the packet's identity: renderers compare packets by address and keep GPU buffers while topology_key is unchanged. Columns (positions as [f32; 3], vertices as Vertex) are read at column granularity through column_bytes, so paged copy-on-write storage uploads without flattening. A packet derived from another records DrawUpdate ranges and the parent's identity token, plus up to DrawUpdate::ANCESTORS older tokens with the union of the changes since each (inherit). The geometry worker can finish a packet the UI never displays when a newer edit arrives first; the next packet still patches whichever recent packet the GPU holds instead of re-uploading the mesh. The renderer applies the ranges for the packet it holds when no preview dirtied the buffers, otherwise it uploads complete streams. face_count, lines and points define element counts for selections; object_faces and selection_segments answer the two queries that need domain topology (object hover silhouettes and edit-overlay outlines).
A packet whose points all move differently (a whole-mesh deformer step) can return a ShadingPlan from derived_shading: its vertex records' normals are then stale, the update rewrites positions only, and after each position upload (or preview restore) the renderer recomputes every face normal and vertex shaded normal on the GPU (shading.wgsl; two compute passes, the vertex buffer bound in ranges within the storage binding limit). Face normals come from (p[i] - p[0]) × (p[i+1] - p[0]). The plan's Smoothing picks the vertex rule: Angle sums the faces around the point within 60° of the vertex's own face; Edges walks the vertex's fan in both directions from its own face, crossing each link that is smooth, or unmarked and within 60° (hard edges, borders and edges of more than two faces stop it; PointFans packs a fan start bit and the two link bits above each vertex id); Authored stores the domain library's normalized normals (per vertex, point or face) as they are, a zero normal taking the face's. Sums are angle-weighted. Each part of a plan (FaceVertices, PointFans, AuthoredNormals) uploads once and stays while the next plan shares its allocation; only the current plan's parts are kept, and each layout binds at most eight storage buffers. Scene objects derive once per packet. A fresh upload of a derived packet draws the same pixels as a patched one. Patch updates keep up to BufferRanges::MOST spans (8,192). More than 64 spans, or more than 32 MB, go through one mapped upload buffer with a copy per span. A shorter stream drops the ranges past its end.
Line classes. A packet's segments are drawn and picked in classes (SurfaceSource::line_classes, LineClass): a mesh's edges are one class (LineStyle::EDGES), a CAD packet adds hulls, curves, an isoparm grid and sketch strokes. A class owns a contiguous run of segments and the ids first_id..first_id + segments, so every class's ids share one packet-wide space: one edge ElementSet can hold lines of several classes, and a pick turns a packet id back into the class's own id (LineClass::local). A class selects at one level (LineClass::selects): edges, or faces for lines drawing a primitive without triangles (a curve as its samples), where line local is face local; a face class's lines pick, box and hover as their faces (the renderer draws the line of DisplayOptions::hover_face in its class's hover look), and merged packets offset a face class's ids as their faces. A LineStyle has four looks (plain, hover, preview, selected; color and width each, selected wider than hovered), an optional dash, a depth bias and the wire flag: a wire class's plain segments take the view's wire color (dark over shaded faces, light in wireframe), follow the wire toggle and the density cutoff. DisplayOptions::hidden_line_classes hides classes by id bit (a hull toggle). The renderer keeps the classes as up to 32 records in a uniform buffer (binding 5), written only when they change; class lines have their own varyings (LineOut), so surfaces carry none. The plain pass draws every segment once with a fragment shader that never discards; dashed classes' plain segments draw in a second pass of their own, since a discarding shader slows every fragment it draws (on the M4 Max the edges of a 65,536-face grid cost 0.05–0.1 ms more per frame through one); a one-class packet skips the class search. the hovered and previewed segments (Scene::highlight, a LineHighlight of packet ids: the line under the pointer and what a second click would add) draw from a small buffer built when the highlight changes, and selected lines draw over them from the selection outline buffer. Scene objects' wires keep their own shader (object_wire_vs): a shaded object's take the view's wire color, a wireframe object's (lines alone among them, a printer's plate) the light one, as no faces of their own lie under them (crates/gpu/tests/gpu_bare_wires.rs).
Point markers. A packet's markers (SurfaceSource::points) all draw alike: dark squares five points across, selected ones seven across in the accent (the shader's own constants). They are opaque and write depth, drawn last in the main pass, so a nearer marker hides the ones it covers and their fragments are rejected early. Each is drawn toward the eye from its point by some pixels' worth of world length there: a plain one by twice its half size, so a surface sloping away under it up to 63° never cuts it; a selected one further, so it shows over plain ones it overlaps (Blender's rule). A marker's component is its point id, which point picks and point selections use; markers at the vertex level (one a vertex record) draw the same way.
Picking. pick::Picker holds a packet's positions, triangles, segments and markers, each kind in a bounding-box tree. It is a cache of its packet, kept with it (SurfaceSource::derived, the packet's Identity): Picker::follow makes it from the picker kept with an earlier packet by what the packet's DrawUpdate says changed since (positions and triangles' corners only: a few moved positions refit the leaves holding their items and the nodes above them, copying only the column cells those nodes are in; past an eighth of them every node is refit on the workers), and Picker::of builds one (columns read and boxes made on the workers, the three trees at once, their top six levels split across threads); Picker::of_until builds unless asked to stop, checked as each subtree starts, and keeps nothing when stopped. Every query widens the boxes by the picker's reach: the f32 rounding of its largest coordinate, so a host testing its own f64 positions finds every item its boxes hold. The host follows where it prepares a packet and builds where it rests, off the UI thread. Besides the hits below, the picker lists its candidates for a host's own tests: triangles_along a ray, and triangles_in, segments_in and points_in a screen square (geometry-view's object picks, boxes and snapping, worked out on the value's f64 positions). pick_all lists what lies under the pointer for a PickQuery (triangles, line classes, points, screen radii, select through): points within their radius, then lines, then every triangle the ray crosses (the stacked-candidates list); pick takes the first. PickTarget holds ids and coordinates only: Triangle { face, triangle, bary }, Line { class, id, t } with the segment's object-space parameter, Point { id } with the marker's component (a CV id, not a position index). A box (pick_box) takes every element it touches that shows, each by its first piece, scanned in blocks on the workers; a ray from each touching triangle's middle decides whether it shows, unless the box sees through. Over what the GPU drew (pick_box_seen), no ray is cast: ViewportRenderer::faces_seen draws the edited surface's faces (their ids and depth, single-sampled, as the view culls and sections them) for the box's view and reads back the box; a face shows where a pixel inside the box was drawn of it, or, smaller than a pixel and wholly inside, where its middle is no deeper than what was drawn there; points and lines show by their occlusion slack against that depth, a pixel more for the pixel grid.
Analysis. Zebra, isophotes, curvature and draft read each vertex record's shading normal and curvature (k1 ≥ k2) as the packet gives them: a mesh estimates them, a CAD tessellation writes its exact normals and principal curvatures, and nothing is recomputed. DisplayRanges (98th percentile in 4096 bins of the largest value per measure, updated locally from changed values) is the one place a packet's curvature display ranges come from.
DrawUpdate also carries line-record and marker ranges: a CV drag rewrites its positions, the hull segments that changed and the CV markers in place (line_patch_bytes counts them), with no new allocation. PlainSurface is a packet made of plain arrays for libraries with no paged storage (and tests).
Merged surfaces. MergedSurface shows several packets as one edited surface (an object made of several packets: a CAD object's bodies, each at each of its placements). Each Part is a packet and the transform placing it; streams are concatenated with point, vertex, face, line and marker ids offset, positions placed and normals turned by the transform. Line classes merge by class id, a merged class holding every part's segments and ids of that class in part order; markers follow part after part, each part's marker ids offset by the markers before it. part_of and merged map ids both ways; split divides a selection among the parts, whose own selection_segments outline their shares (a CAD packet outlines selected faces by their boundary edges); each part is one object for hover outlines. A new merged surface uploads whole; one whose parts moved in place (a CV drag: each moved part's packet a position and shading DrawUpdate over its old one) comes from MergedSurface::moved, which rewrites only those ranges, offset into the merged streams, and carries them as its own DrawUpdate, so the renderer patches its buffers (the curvature display ranges stay the old surface's). Each Part has a face look (FaceLook::Shaded, or SeeThrough { tint, opacity } for a finished sketch's profiles beside the bodies); see-through parts become the merged packet's face runs (SurfaceSource::face_runs, neighbours of one look joined), and face outlines carry merged face ids.
ElementSet holds sorted ids of one ElementKind plus an all flag. Each constructed set has a fresh revision: equal revisions compare equal immediately, so an application that rebuilds the set only when its selection changes pays nothing per frame. Different revisions compare members. PreviewUpdate patches positions, vertices and triangles during a drag without touching the packet; restoring writes the packet's own ranges back. Rigid moves (rotation, translation, uniform scale) go to the GPU instead: affine moves every point, and motion moves the points flagged in a MotionMask (two bits per point: moved, and rigid shading for points whose faces all move). The renderer uploads the mask once per gesture into binding 3 and drops its reference when the preview ends. The shaders apply the preview matrix to masked positions in object space, before model, and turn rigid vertices' normals by preview_normal, dividing their curvature by the scale. Only the faces stretched between moved and fixed points, and their points' vertices, arrive as patches, already final. Mesh finding layers follow the motion. Tool guides own their positions and ignore it. OverlayDraw layers reference the packet's point ids; GuideDraw carries its own positions. Both name their base packet and are rejected for any other. SceneLines belong to no packet: world-space line layers (camera and light glyphs, a context's plate, curvature combs), each with its own colored layers, drawn with or without a surface, against the scene's depth; a view takes any number of them (Scene::lines), each uploaded once per allocation. Overlay layers hold their ids in shared slices: a later packet of the same topology takes the same layers without a copy, and the renderer keeps their upload while the allocations and the positions buffer stay.
ElementSelection is what the edited surface shows selected: at most one set per drawn family, faces (Face, or Object alone), edges and points (Point or Vertex), so levels that combine (a sketch's curves and points) show together, each in its kind's selected look. A present slot is a level shown even when empty (a present points draws the markers, a present edges the wires); ElementSelection::one is the common case. The renderer lays the slots' bitsets one after another at the front of the selection buffer (u.picks: first word, words, all), so one set costs what it did; it keeps an owned copy (SelectedSets) and uploads the mask only when a slot's set changes. Face boundaries and selected lines are two outline buffers, each rebuilt only when its slot changes, drawn by selected_boundary_vs and selected_edge_vs.
Renderer
ViewportRenderer keeps one shared geometry upload and a cache per view (target, camera, options, selection, preview and overlay identities); an unchanged view re-renders nothing. Selection and hover masks are bitsets uploaded only when the selection, hover or packet changes. Ordinary wires follow the reference two-pixels-per-edge density cutoff; explicit wireframe keeps all wires. Object selection and hover use a coverage pass plus screen-space dilation. Upload counters (mesh_uploads, selection_mask_uploads, selection_line_uploads, byte totals) exist for regression tests and benchmarks. GPU limits are checked before replacing the scene. Display options add Houdini-style overlays: points draws every point at any selection level (picked only at the point and vertex levels); normals draws each vertex record's shading normal as an instanced line from its point (no CPU work; rigid previews turn it); face_normals draws a FaceNormals set (per face center and normal, named by its base packet like the other overlays, uploaded once per set); normal_length is in object space.
See-through faces. A packet's face runs (FaceRun: a range of whole triangles in its indices and a non-shaded look) draw after every opaque surface and scene object: the shaded triangles draw in the gaps between the runs with the surface pipeline (a packet without runs draws in one call, as before), then each run draws with see_through_fs, blended, depth-tested against the opaque depth and writing none, its look read from binding 6 by the draw's instance. They are picked, hovered and selected per face like any face. Runs are not sorted among themselves; overlapping see-through runs blend in run order. The looks buffer is written only when a run's look changes (face_look_uploads).
Scene objects draw in batches (gpu/batches.rs). Each object is a record in one storage buffer (placement, normal matrix, flags, edge count), rewritten only when an object changes, so an orbit writes nothing but the view uniform. Packets placed once merge into batches of up to 262,144 vertex records: streams are concatenated with point and vertex ids offset, and a per-vertex slot names the record. Packets placed several times draw instanced from their own buffers. An object may carry its own placements (SceneObject::placements, an instancer's column of Xforms): it draws instanced at model · p for each, its records made only when the placements, the model or its flags change (ViewportRenderer::placement_records counts them), so an orbit or a steady frame over 100,000 instances makes none; drawn one by one (a ghost) it draws once a placement (crates/gpu/tests/gpu_placements.rs). Each batch draws its surfaces and its wires in one draw apiece (merged_* and instanced_* in mesh.wgsl), and the shaders apply the wire density cutoff per record. A draw costs every vertex it names whether its records show or not, so each batch also keeps what its records can show (faces, wireframe members, the fewest edges of a shaded member) and the CPU skips a draw that would show nothing in the view: a million-face object whose wires are past the cutoff took about 2 ms of GPU a frame drawing hidden wires. A batch keeps its buffers while any member stays in the scene or is held (Scene::held: packets not drawn now that show again soon, a value's pieces its context hides for a while); an object drawn one by one keeps its buffers the same way, and a packet neither drawn nor held lets go of them after 16 renders (crates/gpu/tests/gpu_held.rs). Absent members draw nothing, so hiding or removing objects rebuilds nothing. Ghost and tinted objects, and packets whose shading is derived, keep the per-object path. A batched object draws the same surface pixels as the same packet drawn as the edited surface under that placement. An object drawn one by one whose packet updates one the layer holds (update_from names it, same topology, not placed in this render) takes over that packet's buffers and writes only the changed ranges, then derives its shading again; the last shading plan stays uploaded. A smooth subdivision surface following a cage drag thus writes 12 bytes a point per step (object_patches counts them). SceneObject::edges false keeps a shaded object's edges off in shaded views (a smooth surface whose cage shows the wires). DisplayOptions::surface_faces and surface_wires turn off the edited surface's faces and its ordinary wires (a subdivision cage beside its smooth surface: Both keeps the wires, drawn light, Smooth neither); selection feedback, points and marks still draw.
Widgets
layout::quad splits a viewport into four views with two-point gutters. navigation maps a press (Alt with any button, or the middle button) to the move its whole drag makes (Move::of_press), and the wheel, trackpad and pinch to camera motion; the application decides when navigation applies and keeps a press's move while its drag lasts. ViewCube paints the six axis balls, orbits on drag and snaps to orthographic views on click, returning the perspective camera it left. gizmo holds the transform handle frame, drag deltas, angle/step snapping, hit testing and painting for move/rotate/scale; applying a drag to geometry belongs to the application's editing context. labels::point_labels places point numbers (and edge and polygon numbers, from their middles): points in view, one per screen cell, visited in a scattered order that stops after a long run of taken cells, so a million points cost about 0.1–6 ms (see Studio's PERFORMANCE.md); the application paints them. Visual proportions come from lib-ui's Theme.
Looking through a scene camera
Camera::look_through takes a scene camera's world placement (looking down -Z, +Y up), a distance to orbit about and a Lens: the tangent of half the film's vertical angle (or half its height, orthographic), the film's aspect and the clipping planes. The view then projects with that lens, fitting the whole film inside the view, and keeps the camera's roll. Navigation works as usual, and Camera::placement gives the eye's placement back for writing to the scene camera. An orthographic scene camera keeps its projection when orbited and zooms by narrowing its film.
Images. A surface's polygons can show images over their colors (Scene::textures, SceneObject::textures: a SurfaceTextures, a TextureCorner a polygon vertex and the images they name). gpu/textures.rs keeps one table for every surface of every view: an Rgba8UnormSrgb array texture of each of SurfaceImage::SIDES (256, 512, 1024, 2048), an image a layer, read at group 3 by a word (array and layer, plus one) with the derivatives taken in the entry point, through one of two repeating samplers: trilinear and 16× anisotropic, or the nearest texel of level 0 alone (Filter::Nearest, so pixel art's texels stay themselves from afar). Above the array's bits the word holds how the corner reads it (its Sampling): each axis's wrap, which the fragment applies before sampling (clamp and mirror staying a footprint inside the edges, so the far edge never blends in at a coarser level; black outside 0–1). So every path draws the same way, a merged batch of many objects in one call whatever images they show. Corners become a table of three words a polygon vertex (u, v, the image's word with the corner's sampling), read by the vertex's component as per-vertex colors are: binding 8 of the edited surface and of objects drawn one by one (one record naming none without images), and one table of every batched object's (group 1, binding 5), whose records name where theirs begin (above the flags in flags.x). The fragment multiplies the linear color by the image before the display transform. An image uploads once (image_uploads, image_upload_bytes), however many surfaces show it; its layer stays while anything else holds it, and a layer nothing holds goes to the next image. Arrays double, their layers copied on the GPU; past the device's layers an image shows as none. A table is made when a surface's corners or images change allocation: a new image costs its surfaces one table, a color edit, a camera or a drag nothing. A SurfaceImage holds its own mip levels, made on the host's worker, so nothing here filters or resizes; corners made for another packet are not drawn (crates/gpu/tests/gpu_textures.rs).
Scalar shading and the section plane. Shading::Scalar colors the edited surface by a host's value per position (Scene::scalars, one f32 a position of the packet; a deviation, a thickness) through the same diverging map as Curvature, from AnalysisSettings::scalar_min (blue) through green to scalar_max (red); values whose count is not the packet's are not drawn, and without values the surface shows the clay. They live in their own storage buffer (binding 7), written over the last ones while the count stays (a drag's step) and replaced otherwise; scene objects bind none and show the clay. DisplayOptions::section is a world plane [n, d] (unit n): the surface's and scene objects' faces on its positive side are discarded, their edges clipped to it in the vertex stage and their points dropped; the grid and line layers stay (crates/gpu/tests/gpu_scalar_section.rs).
Silhouettes. Selected and hovered objects show a coverage silhouette (gpu/object_outline.rs). The opaque surfaces write the coverage in the main pass, as a second target beside the picture (Shaded in mesh.wgsl: in object mode, whether the fragment belongs to a selected or a hovered object; the nearest surface's wins, so whatever is nearer hides it), resolved only in frames that draw a silhouette. Small passes then draw the outline over the resolved picture: each 8 × 8 tile's largest coverage (along each row's runs of 8 pixels, then down 8 of them: two passes of many fragments of eight reads, since one fragment a tile reading all 64 left most of the GPU idle, about 0.2 ms at the 1784 × 846 pixel view), and the stroke, which reads a pixel's neighbourhood only where a tile within the stroke's reach covers more than the pixel itself. Nothing is drawn again, and most of a view (inside a silhouette or far from one) costs a few texel reads.
