The short answer
Retain world update, culling, sorting, geometry preparation, and material data. Replace the renderer behind a small explicit graphics contract.
Do not plan to render most of a frame through translated OpenGL and peel individual passes into native Vulkan. Shared images and synchronization make that the hardest route.
On Linux, run the same viewer build and scene through native OpenGL and Mesa Zink. It is the cheapest way to test the driver-front-end hypothesis.
What the current frame actually does
The main frame coordinator performs scene work and rendering in one long path. Vulkan can reduce driver submission cost and enable parallel command recording, but it does not automatically parallelize culling, LOD, geometry rebuilds, sorting, or content processing.
Reusable side of the seam
The best existing boundary is after postSort() creates pass-indexed render maps and before draw pools issue commands. LLDrawInfo already collects geometry ranges, textures, transforms, skinning, and material data for a draw.
It is an input to the seam, not the final packet. It still contains GL-backed resources and mutable viewer objects, so a snapshot step must resolve it into immutable, frame-owned render data.
This leaves scene traversal, culling, batching, LOD, spatial grouping, and most CPU geometry logic available to both backends.
Coupled side of the seam
Draw pools choose shaders and mutate implicit state while drawing. Buffers, textures, render targets, and shaders expose GL object names. The deferred pipeline shares depth and G-buffer attachments through global FBO, shader, texture-unit, and vertex-buffer state.
That is why replacing an individual pass with native Vulkan is not a local change today.
Direct GL call sites
A static scan found about 670 direct gl* calls across 52 files after excluding project-local lookalikes. Roughly 274 are outside indra/llrender. Counts are directional, not a complexity estimate.
GLSL source files
The shader manager assembles source fragments, injects runtime defines, compiles and links through OpenGL, then discovers uniforms and blocks through GL reflection.
Explicit shader bindings found
The shader inventory found no explicit layout(binding=...) or layout(location=...) declarations. A Vulkan pipeline needs a stable resource and vertex-layout contract.
Active rendering thread
Submission is effectively main-threaded. There is one optional shared-context texture upload worker; the experimental VBO GL work queue is compiled out.
Observed in the repository snapshot. These facts explain migration cost, not the cause of a particular slow frame.
Four routes, only two worth carrying forward
A translation layer and a native Vulkan backend solve different problems. Treating them as consecutive layers hides the resource-ownership problem rather than reducing it.
| Route | What it proves or buys | Constraint | Recommendation |
|---|---|---|---|
| Improve native OpenGL | Fastest route to fixes if traces reveal query stalls, uploads, state churn, poor batching, or scene preparation. | Does not resolve macOS API deprecation or long-term driver investment. | Do now |
| Zink, GL over Vulkan | A controlled A/B of the broader GL driver and API path. It may also become an opt-in Linux runtime if it is stable and faster. | It can expose different extensions, limits, shader paths, and vendor strings. It still exposes GL objects and implicit state. Windows packaging is a separate support burden. macOS support is described as experimental and limited. | Experiment |
| Translated GL plus native Vulkan passes | Technically possible with external memory and semaphores on selected stacks. | Requires translation-layer internals or exportable resources, platform-specific handles, ownership transfers, barriers, and two debuggers. The viewer has no such seam. | Reject as plan |
| Explicit contract plus two backends | Lets one prepared frame feed OpenGL or Vulkan, preserves fallback, and makes whole passes independently portable after resources are neutral. | Requires deliberate shader, resource-lifetime, and state-model work before visible Vulkan coverage grows quickly. | Strategic route |
Why not ANGLE?
ANGLE translates OpenGL ES, not the desktop OpenGL 3.3 to 4.6 contract this viewer expects. Adopting it would first require a substantial GLES port, which duplicates much of the work needed for an explicit backend.
Where MoltenVK fits
MoltenVK maps Vulkan to Metal. It does not run the current OpenGL renderer. It becomes relevant once a native Vulkan backend exists, and its portability subset must be part of the first prototype rather than a late macOS check.
Build one small explicit contract
Do not wrap each GL call. Define the work a renderer needs in Vulkan-shaped terms, then make OpenGL honor that contract first.
Minimum contract
RenderDeviceand typed buffer, image, sampler, shader, and pipeline handlesFrameContextwith bounded frames in flight and deferred destructionPassEncoderorCommandListwith complete pipeline state- Explicit attachments, load/store behavior, reads, writes, layouts, and dependencies
- Descriptor arenas, pipeline caches, and an uploader with staging, residency, and ownership rules
Things that must not leak upward
GLuint,GLenum, FBO names, or Vulkan handlesglEnable-shaped state toggles and implicit current bindings- Backend-specific shader locations and reflection
- API calls from materials, culling, draw-packet construction, or UI layout
- Unbounded resource deletion or allocation in the hot draw loop
A gated migration, not a rewrite
Each stage produces something shippable or a decision. The expensive native work starts only after evidence shows where it can help.
Establish the frame-time truth
Check in a repeatable scene, camera path, warm-cache procedure, fixed settings, resolution, and present mode. Capture p50, p95, and p99 CPU and GPU frame time. Split scene preparation, submission, passes, present, uploads, readbacks, waits, and shader compilation.
Prove a small contract against both APIs
Choose three slices that stress different requirements: one full-screen pass, one indexed material draw, and one streaming texture upload. Define only the resources, pass access, commands, and lifetime needed by those slices. Run them through OpenGL and an isolated Vulkan replay or test mode.
Expand the contract and make shaders explicit
Translate LLDrawInfo into immutable packets with stable resource handles, copied constants, draw ranges, descriptor inputs, and pipeline keys. Define a shader manifest and variant key. Compile Vulkan variants to SPIR-V while either generating compatible legacy GLSL or retaining the existing GL compiler path during transition. Add staging rings, descriptor arenas, pipeline caches, and deferred destruction.
Build a whole-frame Vulkan vertical slice
Create the instance, device, surface, swapchain, upload path, resource lifetime, and a feature-reduced but complete frame. Exercise Windows, Linux, and MoltenVK from the start. Include resize, minimize, swapchain recreation, readback, and device-loss behavior. A triangle is a platform check, not the milestone.
Port complete logical passes
Migrate presentation and uploads, then opaque deferred geometry, lighting and shadows, post-deferred alpha/water/sky/avatar paths, probes, dynamic textures, media, snapshots, and UI. Preserve the numeric ordering semantics currently embedded in draw pools. Decide explicitly whether the OSMesa headless path stays GL-only.
Do not estimate the full program from static call counts. Estimate Stage 0 and the three-slice dual-backend spike first, then reforecast from measured pass cost, shader variants, platform coverage, and required feature parity. Full parity is a multi-quarter graphics program, not a small compatibility patch.
The first 30 days
The initial month should leave the team with a benchmark, a diagnosis, and one proven architectural seam. It should not end with a large Vulkan dependency graph and no better frame-time evidence.
Make the workload reproducible
- Build a suite: steady warm cache, cold streaming, avatar-heavy, draw-call and alpha-heavy, shadow/probe/fill-heavy, and UI-heavy.
- Capture or replay workload data where possible; control simulator updates, background services, cache state, and camera timing.
- Control VSync, frame caps, resolution, power mode, hybrid-GPU selection, thermal state, and shader-cache state.
- Record traces and visual output for every run.
Measure the right dimensions
- Render-thread, scene-prep, and GPU pass timings with p50, p95, p99, and 1 percent lows.
- Draws, batches, pipeline/shader changes, uploads, readbacks, waits, compilation, and memory residency.
- Per-core CPU use, not only process-wide percentage.
- Correlated CPU and GPU traces: time inside a GL call may be a wait on earlier GPU work, not submission cost.
- GPU/driver/OS/backend identity in every capture.
Run the discriminating A/B
- Same Linux viewer build, same scene, native vendor OpenGL versus Zink.
- Also test the existing core-profile option on non-macOS platforms.
- Capture extensions and limits; verify identical settings, shader variants, feature masks, and visual output.
- Confirm shared-context texture upload, queries, compressed formats, and sync objects.
Interpret the result
- Zink wins and submission dominates: the broader driver or API path is implicated, but not isolated.
- Both are similar and scene prep dominates: work on culling, rebuilds, sorting, batching, and threading first.
- GPU time dominates both: reduce shadows, overdraw, fill, and expensive passes.
- Waits or residency dominate: fix streaming, synchronization, and lifetime before changing APIs.
Act on
- A benchmark and hardware matrix before choosing the cure
- An explicit backend boundary with one API active per run
- Shader and resource contracts that both backends consume
- Early MoltenVK portability testing
- A dual-backend test slice before broad abstraction work
Consider
- Zink as an opt-in Linux runtime if it wins and remains stable
- Narrow GL/Vulkan interop only for isolated offscreen experiments
- Parallel Vulkan command recording after measurement identifies submission pressure
Keep in view
- macOS OpenGL deprecation creates a platform-longevity reason independent of current FPS
- Existing GL fixes may deliver value while the backend work proceeds
- UI is not an easy first pass because it relies heavily on immediate-mode emulation
Do not plan around
- Wrapping every
gl*call one for one - Peeling native Vulkan passes out of a Zink-backed frame
- ANGLE as a desktop OpenGL drop-in
- A big-bang renderer rewrite
Risks that decide whether this succeeds
Solving the wrong bottleneck
A native API cannot compensate for expensive scene preparation or content-driven geometry work.
A GL-shaped abstraction
A facade built from state toggles will make the Vulkan backend slow, complicated, and leaky.
Shader combinatorics
Runtime source injection and feature fragments can create a large pipeline-variant and compilation problem.
Lifetime and synchronization defects
Vulkan turns implicit driver behavior into application responsibility. Streaming content makes this a central design problem.
Late platform divergence
A design validated only on Windows can fail against Linux drivers or the MoltenVK portability subset.
False parity
Average FPS can improve while frame pacing, visual correctness, or memory behavior regresses.
Evidence and references
Repository evidence
Primary platform references
Confidence labels used implicitly in this assessment: repository observations are facts about snapshot 4ef9f8f; bottleneck explanations are hypotheses until captured on affected systems; staffing and schedule are planning estimates, not commitments.