Skip to content

[Platform] Define renderer capability tiers and validate every fallback #138

Open

Description

@proggeramlug

Parent: #126
Related existing platform issues: #18, #55, #74

Problem

Bloom targets Metal, DX12, Vulkan, WebGPU, Android, Apple mobile/spatial/watch platforms, but renderer feature behavior is not defined as one capability contract. Desktop defaults can exceed adapter limits (#55), some platforms contain stubs/no-ops, and CI does not execute every target. High-end work will worsen this unless fallbacks are explicit and testable.

Outcome

Data-driven renderer capability tiers selected from actual adapter features/limits, queryable by games, forceable in tests, and covered by a platform/fallback matrix.

Tier contract

Define names based on capabilities rather than marketing; an initial shape may be:

  • Baseline: conventional material bindings, CPU draws, reduced MRT/post-FX, conventional shadows, software/screen-space GI fallback.
  • Modern: storage-buffer scene data, indirect draws, texture-array/paged indirection, full temporal/post-FX path.
  • High-end: descriptor-indexed resources, visibility shading, virtual geometry/shadows, ray-query enhancements.

The implementation may choose different names/count, but each tier must list required features and limits plus per-feature fallback. Never infer high-end solely because the machine is “desktop.”

Capability report

At initialization, produce a structured report containing:

  • adapter identity, backend, driver where available;
  • all requested vs available features and relevant limits;
  • selected tier and reason;
  • per-system path: geometry, materials, shadows, GI, reflections, AA/upscale, texture format, path tracing;
  • disabled/limited features and their fallback;
  • shader compiler state on Windows (DXC/FXC and ray-query consequences).

Expose this read-only report through a Perry-safe TypeScript API and concise startup log. A requested unsupported feature returns a result/error; it may not silently no-op.

Device creation

  • Base required limits on adapter.limits() and chosen tier requirements.
  • Request only features actually used by the selected tier.
  • If preferred-tier device creation fails, retry a documented lower tier and record the cause.
  • Add BLOOM_FORCE_RENDER_TIER (or equivalent) for testing lower paths on capable hardware.

CI/qualification matrix

  • Compile every supported target that can be compiled in hosted CI.
  • Run headless/golden smoke at forced tiers on available desktop backends.
  • Run web in a browser with a real canvas/WebGPU adapter, not only cargo check.
  • Add scheduled/device-farm or documented release qualification for Android/iOS/tvOS/visionOS/watchOS where hosted runtime execution is unavailable.
  • Treat deliberate platform stubs as an explicit capability response and document scope.

Acceptance criteria

  • A single table in docs is generated or validated from the same tier definitions used by code.
  • Forced lower tiers pass the relevant quality corpus on a high-end adapter.
  • An adapter below a preferred limit starts on a lower tier instead of failing device creation.
  • TypeScript can query every material rendering feature needed to choose content/settings without parsing logs.
  • No public renderer setter silently succeeds when the active platform cannot implement it.
  • Platform workflow covers macOS/Metal, Windows/DX12, Linux/Vulkan, web/WebGPU, and compile/release checks for declared mobile/Apple targets.
  • Capability snapshots are uploaded with quality/performance results.

Likely files

  • device creation under native/*/src/lib.rs
  • new shared capability model under native/shared/src/renderer/
  • src/core/ public types/API
  • tools/validate-ffi.js
  • .github/workflows/test.yml, platform docs

Non-goals

  • Identical images/performance on every tier.
  • Requiring high-end features for engine startup.
  • Keeping a platform target “supported” through silent no-op parity.

Dependencies

Should land early; render-foundation issues consume this contract.

Activity

  1. proggeramlug commented on Jul 28, 2026

    @proggeramlug
    ContributorAuthor

    Capability/fallback finding from the realtime PT qualification added at 1041a06:

    A supported ray-query adapter currently cannot run PT independently when SSGI is disabled. The frame graph schedules accel_rebuild, card capture, SDF/card preparation, and related shared inputs only when FRAME_FEATURE_SSGI is present. pt_active() and the PT graph node depend only on the PT mode and ray-query support, so set_path_tracing(2) plus set_ssgi_enabled(false) reports PT as supported/active but never prepares the TLAS/geometry inputs; accumulation remains at zero and the request effectively becomes a silent no-op.

    The new #135 diagnostics test deliberately retains the established SSGI infrastructure gate and documents the coupling; it does not broaden that capture-only patch into a scheduler rewrite.

    This fits #138 acceptance directly. Recommended implementation scope:

    1. Model shared ray-scene preparation as an explicit capability/consumer (SSGI || PT, with only the resources each consumer needs), rather than ownership by the SSGI feature bit.
    2. Add forced matrix cases for PT-on/SSGI-off, PT-off/SSGI-on, both-on, and both-off.
    3. Require PT-on/SSGI-off to advance accumulation and produce finite nonzero HDR on a ray-query adapter.
    4. Require both-off to retain the current zero shared-preparation pass/cost contract.
    5. Report the selected acceleration owner/path in the capability snapshot so unsupported requests cannot silently succeed.
  2. proggeramlug commented on Jul 28, 2026

    @proggeramlug
    ContributorAuthor

    The PT-on/SSGI-off coupling reported above is fixed in draft PR #147 at 751be72.

    New ownership contract:

    • both off: no acceleration/card/SDF/cache preparation passes, unchanged
    • SSGI only: shared accel_rebuild + card_capture, then the established SDF/clipmap/WSRC/card-light chain
    • PT only: shared accel_rebuild + card_capture only
    • SSGI + PT: the complete established chain remains available
    • unsupported PT requests are normalized to PathTracingMode::Off in the frame-plan key, so they do not schedule ray-scene preparation

    Runtime qualification now reports ray_scene_preparation as disabled, ssgi, pt, or ssgi+pt.

    Net-positive evidence:

    • PT-only on M1 Max / Metal now advances and renders with SSGI explicitly disabled.
    • Its qualification result is identical to the prior SSGI-backed checkpoint: 10,984 accepted-history texels, 10,989 valid/accumulated texels, 0 non-finite HDR pixels, max luminance 0.3491.
    • The functional PT-only topology removes four SSGI-only passes (sdf_bake, scene_sdf_clipmap, wsrc_bake, card_light) compared with the previous required workaround of leaving SSGI enabled.
    • Graph tests cover both-off, PT-only, SSGI-only, and combined ownership and ordering.
    • Full validation: native library 296 passed / 1 ignored; release golden corpus 39 passed / 2 ignored; wasm32 check, formatting, whitespace, and file-size ratchet passed.

    This resolves the concrete silent-no-op finding, while #138 remains open for the broader public capability/result API and forced-tier platform matrix.

  3. proggeramlug commented on Jul 28, 2026

    @proggeramlug
    ContributorAuthor

    Implemented and pushed the first capability-tier slice in PR #147 at aa983c9.

    What landed:

    • Added baseline, modern, and high-end renderer tiers selected only from granted wgpu::Features and wgpu::Limits.
    • Made the numeric feature/limit requirements and every documented per-system path one shared Rust definition. docs/renderer-capability-tiers.md is byte-for-byte validated against it.
    • Added BLOOM_FORCE_RENDER_TIER=baseline|modern|high-end. A supported lower request constrains the active material tier, GPU-driven submission, hardware ray-query GI/reflections, and path tracing. Unsupported upward requests are rejected with a diagnostic.
    • Kept automatic optional-path selection independent: lacking ray query does not disable supported descriptor indexing/GPU submission, and lacking an unrelated optional feature does not remove a working fast path.
    • Replaced the ad-hoc quality adapter tier label with a structured capability snapshot containing detected/selected/requested tier, rejection reason, relevant granted features, and relevant granted limits. This is embedded in the existing quality report artifact.
    • Made the ray-query golden harness understand an intentionally forced lower tier, so excluded hardware-only cases report a capability skip instead of rendering the raster fallback and comparing it to a PT golden.

    Regression evidence:

    • ./scripts/ci-check.sh --quick: pass (303 shared unit tests, 44 golden/integration tests, 3 render-target tests, strict clippy, wasm check, FFI/contracts, quality governance).
    • BLOOM_FORCE_RENDER_TIER=baseline ... --test golden_render: 44 passed, 0 failed, 2 pre-existing ignored hardware diagnostics.
    • BLOOM_FORCE_RENDER_TIER=modern ... --test golden_render: 44 passed, 0 failed, 2 pre-existing ignored hardware diagnostics.
    • File-size ratchets and git diff --check: pass.

    The first two acceptance boxes are now complete. Remaining #138 work is intentionally still open: tier-aware device request/retry with recorded downgrade causes (including #55), full Perry/TypeScript capability querying, unsupported-setter results, and the complete hosted/mobile qualification matrix. The device negotiation/retry slice is next.

  4. proggeramlug commented on Jul 28, 2026

    @proggeramlug
    ContributorAuthor

    Second #138 slice landed in PR #147 at 21f73d8: tier-aware native device negotiation and recorded fallback causes.

    Implemented:

    • Shared native and direct Linux startup now build device requests from Bloom's actual active shader-layout contract rather than wgpu::Limits::default() or the adapter's maximum values.
    • The native contract explicitly requests 5 bind groups, 4 MRT color attachments, 19 sampled textures, 16 samplers, 8 storage buffers, and 64 KiB uniform bindings. PT requests 9 storage buffers.
    • Tier A requests a bounded working set (4,096 textures + 64 samplers + fallback entries) instead of granting the adapter's potentially huge descriptor maximum.
    • Preferred startup requests only supported optional features. If the backend rejects it, startup retries the core contract with optional features removed; the preferred error is retained as the fallback cause.
    • The selected request, requested limits/features, preferred/selected tier, and fallback cause are logged as JSON and embedded in quality artifacts at adapter.device_negotiation.
    • Adapters below the currently implemented native shader-layout contract now get an explicit list of insufficient limits instead of a generic backend request failure.

    Regression/qualification evidence:

    • A production smoke creates the complete renderer with the negotiated device rather than merely checking request_device. This found and fixed two initially under-specified requirements (64 KiB joint uniforms and the combined texture+sampler binding-array budget).
    • Production construction passes on Apple M1 Max in automatic high-end mode, forced baseline, and forced modern.
    • The simulated Fedora Asahi shape from aarch64 Linux build + runtime: XLookupString mismatch, max_color_attachments limit, lib name #55 (max_color_attachments=5) deterministically requests 4, not 8.
    • Local complete quick lane: 306 unit tests passed (1 ignored), negotiated-device integration passed, 44 renderer goldens passed (2 ignored), 3 render-target tests passed, plus strict lint/wasm/contracts/quality governance.
    • Hosted PR run 30349837571: Linux release build passed, Ubuntu shared tests passed, and macOS/Windows builds + tests, web build, real Chrome WebGPU smoke, FFI, lint, and quality contract all passed.

    I am leaving the “adapter below preferred limit starts” acceptance box unchecked until the exact Asahi-class runtime (or equivalent lower-limit hardware runner) executes the renderer, rather than treating a pure plan simulation as runtime proof. Windows' direct window path and remaining mobile-specific device creation paths also still need migration/qualification.

  5. proggeramlug commented on Jul 28, 2026

    @proggeramlug
    ContributorAuthor

    Windows direct-window migration landed at ddb3e6f.

    • init_engine_for_hwnd now uses the same bounded, fallback-capable device planner as shared native and Linux startup.
    • Existing Windows policy is preserved: Dynamic DXC setup and ray-capable adapter enumeration remain unchanged, and hardware GI/ray query remains opt-in through BLOOM_HW_GI, BLOOM_PT, or --pt (and can still be disabled by BLOOM_FORCE_SW_GI).
    • The negotiation report is handed to the renderer and therefore appears in the quality adapter snapshot.
    • Hosted Windows release build 90245812396 passed, and Windows shared tests 90245812375 passed in run 30350261839.

    The remaining device-creation migration is now the platform-specific mobile/Apple profiles; those cannot blindly reuse the 5-bind-group/4-MRT desktop contract because Android/WebGPU intentionally compile folded/lean layouts.

  6. proggeramlug commented on Jul 28, 2026

    @proggeramlug
    ContributorAuthor

    Android/device-profile checkpoint: 4ab5419

    • Generalized native device negotiation into explicit native-full and folded-mobile profiles selected from the compiled shader layout, not platform marketing or adapter maxima.
    • Migrated both Android startup routes (direct window startup and shared attach) to folded-mobile: 4 bind groups, 4 color attachments, 19 sampled textures/stage, 16 samplers/stage, 4 storage buffers/stage, and a 64 KiB uniform binding.
    • Preserved Android’s compiled fold_scene_inputs, lean_mrt, and mobile_lights behavior, optional BC/timestamps/Tier A/GPU-driven/ray-query policy, and BLOOM_FORCE_SW_GI; this changes device creation only, not render-pass selection or pixels.
    • Added the same featureless retry and structured adapter.device_negotiation evidence used by desktop.
    • Added unit coverage proving a 4-bind-group/4-storage-buffer mobile adapter is accepted while retaining the established four-attachment safety envelope.
    • Cross-compiled the default Android crate successfully for aarch64-linux-android with NDK 30.
    • Full local quick lane is green: 307 unit tests + negotiated complete-renderer integration, 44 goldens, 3 render-target tests, strict lint/contracts/WASM/quality governance.

    Hosted checks for the pushed SHA are running. The below-preferred-limit acceptance item remains open until a real constrained adapter demonstrates the retry at runtime; plan/unit simulation alone is not being counted as closure evidence.

  7. proggeramlug commented on Jul 28, 2026

    @proggeramlug
    ContributorAuthor

    iOS checkpoint: ed4757c

    • Migrated both independent UIKit startup paths to the shared bounded native-full negotiation profile.
    • Preserved the established iPhone/A18 behavior where advertised ray query may be rejected by Metal device creation: the same featureless retry now uses Bloom’s active shader-layout limits instead of requesting every adapter limit.
    • Both paths emit and embed the structured device-negotiation report.
    • aarch64-apple-ios and aarch64-apple-ios-sim compile successfully with default features and --locked; FFI/schema/file-size contracts pass.
    • Corrected the iOS lockfile’s stale bloom-shared dependency list for its already-enabled models3d/serde_json feature.

    No render passes, shaders, formats, surface configuration, or per-frame work changed.

  8. proggeramlug commented on Jul 28, 2026

    @proggeramlug
    ContributorAuthor

    Apple TV/spatial checkpoints: 8c17a76 (tvOS), 7944b5a (visionOS)

    • Migrated both direct startup paths in each platform crate to bounded native-full negotiation.
    • Removed 250 lines of duplicated feature/default-limit request code while preserving surfaces, formats, presentation, compositor/input setup, BLOOM_FORCE_SW_GI, and optional rendering features.
    • Device-negotiation JSON is emitted and embedded in quality snapshots on both platforms.
    • Default-feature aarch64-apple-tvos, aarch64-apple-tvos-sim, aarch64-apple-visionos, and aarch64-apple-visionos-sim checks pass; device builds revalidated with --locked.
    • FFI/schema/file-size contracts pass. Existing platform warnings are unchanged.

    Together with the prior macOS/Linux/Windows/Android/iOS work, every non-stub native platform startup path now delegates device creation to the same capability planner. Hosted checks are running on the pushed head.

  9. proggeramlug commented on Jul 28, 2026

    @proggeramlug
    ContributorAuthor

    Permanent mobile compile coverage: 491806c

    • Added the canonical cross / target-check CI lane with locked Cargo checks and deterministic Android NDK compiler selection.
    • PR matrix now compiles 12 declared Rust targets: Android arm64/armv7/x86_64; iOS arm64 device/arm64 simulator/x64 simulator; tvOS device/simulator; visionOS device/simulator; watchOS device/simulator.
    • Renderer/model/image platform glue is enabled in these checks; mobile Jolt stays split into the existing release matrix, which builds all published mobile C++ archives from source.
    • Every matrix command passed locally through the same script. actionlint, CI command-inventory governance, FFI/schema parity, and file-size contracts pass.

    I will check the platform-workflow acceptance box only after the new hosted matrix completes successfully on GitHub’s Linux/macOS runner images.

  10. proggeramlug commented on Jul 28, 2026

    @proggeramlug
    ContributorAuthor

    Hosted PR qualification now covers the declared platform matrix at commit 491806c: macOS/Metal, Windows/DX12, Linux/Vulkan, the real-browser WebGPU known-frame smoke, plus successful compile checks for Android (aarch64/armv7/x86_64), iOS (device/arm64 simulator/x64 simulator), tvOS (device/simulator), visionOS (device/simulator), and watchOS (device/simulator). All target jobs passed in PR #147. This completes the platform-workflow acceptance item; runtime proof on constrained mobile adapters remains separate.

  11. proggeramlug commented on Jul 28, 2026

    @proggeramlug
    ContributorAuthor

    Renderer capability/result API checkpoint is complete in draft PR #147 through 7b07a39.

    Public query contract:

    • getRendererCapabilities() now returns adapter/backend/driver identity, detected/selected/requested tier, granted features and limits, per-system material/geometry/shadow/GI/reflection/AA/texture/PT paths, the selected device request and fallback cause, live material binding capacities/residency, hardware ray-query and path-tracing availability, GPU-driven status, imported-refraction path, and supported transparency modes.
    • Native and web return live reports. watchOS returns a typed explicit availability: "unavailable" result rather than inventing desktop capabilities.
    • The full report is Perry-safe JSON and the TypeScript interface is compile-checked by all 20 canonical examples.

    Unsupported-setting contract:

    • 79 public renderer/scene controls now return boolean: true only when the active backend applied the request, false for unsupported/rejected paths.
    • Explicit false paths include web native-filesystem HDR loading, non-FIFO browser present modes, web/watch path tracing, Perry-WASM pointer-only LOD/transform calls, watch shadow controls, unavailable outline post-FX, feature-gated native paths, invalid material scratch payloads, and caught native FFI failures.
    • Existing callers remain source-compatible because they may ignore the new result; there is no second capability query or allocation in setter hot paths.
    • tools/validate-ffi.js now hard-gates the 79-function status contract across the manifest, shared native macros, web, watchOS, and every TypeScript declaration. Web parity is down to five documented hard gaps.
    • The watch implementation also fixed a real fidelity bug where already-normalized material colors were divided by 255 a second time.

    Qualification on exact commit 7b07a39:

    • Full local quick lane: 308/309 unit tests passed (one pre-existing ignored), negotiated headless renderer passed, 44/46 renderer goldens passed (two hardware-only ignored), all render-target/shader/quality negative-control checks passed, and strict correctness/performance clippy passed.
    • Real Chrome WebGPU known-frame smoke passed with unchanged RGB means (99, 177, 241).
    • watchOS simulator compile passed.
    • All 20 canonical TypeScript examples compiled.
    • Hosted PR Rendering quality roadmap: layered PBR and renderer foundations #147: all 24 applicable checks passed, including macOS/Windows/Linux builds and shared tests, web build + browser smoke, FFI/lint/contracts, and all 12 Android/iOS/tvOS/visionOS/watchOS target jobs.

    This completes the TypeScript material-capability query and no-silent-renderer-setter acceptance items. The constrained physical-adapter runtime item remains deliberately unchecked.

  12. proggeramlug commented on Jul 28, 2026

    @proggeramlug
    ContributorAuthor

    Capability snapshot propagation is complete in draft PR #147 at cce6721.

    What changed:

    • Every quality run now writes a named top-level capabilities.json, and result.json references it as artifacts.capability_snapshot.
    • The snapshot records the engine revision, selected machine class, adapter/driver/backend identity, detected/selected/forced tier, granted features and relevant limits, every selected renderer path, the exact device request, and any fallback cause.
    • Existing quality telemetry still embeds the same data and rejects missing or inconsistent snapshots before accepting image or performance evidence.
    • The hardware workflows already upload the complete output directory with if: always(), so the named snapshot is retained on both success and failure.
    • bloom-render-perf now uses the production bounded device-negotiation/fallback path rather than requesting the adapter's maximum limits, and embeds the complete validated snapshot under adapter in both timing and upload reports.
    • The fixed raster benchmark explicitly does not request unused ray-query support, avoiding the known possibility that merely enabling it changes backend scheduling.
    • wgpu tracing remains qualification-only; ordinary engine startup still calls the trace-off path.

    Real Apple M1 Max / Metal smoke evidence:

    • untraced report: availability=reported, selected tier high-end, selected request bloom_device_preferred, bounded 5 bind groups / 4 MRT attachments, ray_query=false
    • the report passed the quality runner's complete capability_snapshot_failures validator with zero failures
    • traced upload report retained the same capability evidence, reported zero texture-upload bytes, and correctly set timing_includes_trace_io=true

    Regression evidence:

    • cargo fmt --manifest-path tools/render-perf/Cargo.toml -- --check
    • cargo check --manifest-path tools/render-perf/Cargo.toml
    • 12/12 quality orchestration tests, including named-artifact JSON round trip
    • ./scripts/ci-check.sh --quick: pass (316 unit tests, negotiated-device production smoke, 49 golden/integration tests, render-target tests, strict clippy, wasm, FFI parity, quality governance/negative controls, and examples)

    Relevant code:

    The only unchecked #138 criterion now is runtime proof on a genuinely constrained physical adapter; simulated plan tests are intentionally not being presented as hardware evidence.

  13. proggeramlug commented on Aug 9, 2026

    @proggeramlug
    ContributorAuthor

    Capability-tier checkpoint in 44d53ec: virtual visibility now has an explicit bounded submission backend for adapters that support primitive index + indirect first instance but lack MULTI_DRAW_INDIRECT_COUNT (verified on Apple M1 Max / Metal). Count-capable adapters retain the existing path. The fallback uses GPU binning plus 22 fixed indirect draws, with no CPU readback or per-cluster CPU loop; its extra buffers and three compute passes exist only on that capability branch. Full virtual-geometry filter passes 29/29.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions