Rendering architecture assessment

Vulkan is a direction, not the diagnosis.

An incremental port is realistic. A GL-over-Vulkan layer is useful as an experiment and perhaps a Linux fallback, but it is the wrong chassis for replacing renderer components one at a time.

Second Life viewer Repository snapshot 4ef9f8f 26 August 2026 Architecture recommendation, not an implementation plan

The short answer

Yes, port incrementally

Retain world update, culling, sorting, geometry preparation, and material data. Replace the renderer behind a small explicit graphics contract.

No, do not mix by default

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.

Test Zink immediately

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.

The premise needs measurement. Low total CPU and GPU utilization does not establish an OpenGL compatibility problem. This viewer prepares and submits much of a frame on one thread. One saturated core on a 16-core CPU can appear as roughly 6 percent aggregate CPU use, while the GPU waits for work.

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.

FrameInput, idle work, coroutines
GeometryCreate objects, partition queue, rebuild
VisibilityCull regions and spatial partitions
PrepareState sort and build render maps
SceneShadows, G-buffer, lighting, alpha
FinalizeHDR, SSR, tone map, AA, UI
PresentPlatform OpenGL buffer swap

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.

670

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.

219

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.

0

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.

1

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.

Shared viewer coreWorld update · geometry rebuild · culling · LOD · state sort · material preparation
Immutable frame snapshot and renderer contractDraw packets · pipeline keys · declared reads/writes · typed handles · frame context · upload requests
↓ select one backend before window creation ↓
OpenGL backendCompatibility fallback and reference output
Vulkan backendExplicit lifetime, barriers, descriptors, command buffers
Null/test backendOptional validation of packets and lifetime without a GPU
WGL / SDL GL / CGLExisting platform path
Vulkan surfaceWindows · SDL3 on Linux · MoltenVK on macOS
Capture toolsValidation · RenderDoc · screenshot comparison

Minimum contract

  • RenderDevice and typed buffer, image, sampler, shader, and pipeline handles
  • FrameContext with bounded frames in flight and deferred destruction
  • PassEncoder or CommandList with 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 handles
  • glEnable-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
Operational rule: one graphics API owns the window and frame in a process. Pass-by-pass migration happens in source behind the common contract, not by transferring live render targets between GL and Vulkan every frame.

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.

Stage 0

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.

Exit gateA named bottleneck explains a material part of bad p95 frames on a supported hardware matrix. Native GL versus Zink results are understood.
Stage 1

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.

Exit gateThe same immutable inputs render correctly through both APIs. The contract expresses attachments, reads, writes, uploads, dynamic state, and lifetime without GL-shaped toggles. Migration cost is measured.
Stage 2

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.

Exit gateCI compiles every declared shader variant, validates reflected layouts, and tests cold and warm caches. A growing pass allowlist uses the contract, and a lint rule prevents new direct GL calls outside the existing allowlist.
Stage 3

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.

Exit gateA fixed scene presents through Vulkan on all target platforms with clean validation, stable memory, capture support, and measured CPU/GPU timing.
Stage 4

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.

Exit gateFeature parity is tracked per pass. Direct GL calls are finally confined to the GL backend and platform layer. Vulkan graduates from hidden opt-in to beta and then per-GPU default only where performance and stability justify it.

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.

Control: Require phase-level p95 CPU and GPU evidence before approving the Vulkan build-out.

A GL-shaped abstraction

A facade built from state toggles will make the Vulkan backend slow, complicated, and leaky.

Control: Require complete pipeline/pass descriptions and typed resources. Let Vulkan constraints shape the contract.

Shader combinatorics

Runtime source injection and feature fragments can create a large pipeline-variant and compilation problem.

Control: Inventory variants, make bindings explicit, compile offline, cache pipeline artifacts, and cap variant dimensions.

Lifetime and synchronization defects

Vulkan turns implicit driver behavior into application responsibility. Streaming content makes this a central design problem.

Control: Define frames in flight, staging ownership, fences, deferred destruction, residency budgets, and device-loss behavior before broad pass migration.

Late platform divergence

A design validated only on Windows can fail against Linux drivers or the MoltenVK portability subset.

Control: Run every vertical slice on all three targets and keep a conservative capability profile until evidence permits specialization.

False parity

Average FPS can improve while frame pacing, visual correctness, or memory behavior regresses.

Control: Gate on p95/p99 time, worst frames, 1 percent lows, input latency, per-test screenshot tolerances, validation output, VRAM, and long-session stability.
Default-on gate: Vulkan should become the default only on hardware classes where it materially improves the diagnosed bottleneck, has clean validation, meets visual parity, and shows no meaningful frame-pacing, memory, or stability regression. Exercise resize, fullscreen, multi-monitor, DPI changes, alt-tab, suspend/resume, region crossings, device loss, screenshots, cube maps, media, probes, and sustained texture churn. Keep OpenGL selectable until the support matrix makes removal an evidence-backed decision.

Evidence and 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.