Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ concurrency:
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
# sdl2-sys bundled build ships an older SDL2 CMakeLists.txt that uses
# cmake_minimum_required(VERSION 3.1.3). Newer CMake versions (4.0+)
# removed compatibility with policies < 3.5; this env var restores it.
CMAKE_POLICY_VERSION_MINIMUM: "3.5"

jobs:
# Fast sanity checks to fail quickly before heavy matrix jobs
Expand Down
2 changes: 2 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ The `RenderBackend` trait abstracts the GPU backend. Both 2D (SpriteBatch) and 3
| `rapier3d` | `rapier3d` |
| `headless` | (empty) |
| `xbox-gdk` | `wgpu-backend` |
| `sdl-window` | `wgpu-backend`, `sdl2` |
| `switch-vulkan` | `wgpu-backend` |
<!-- /gen:feature-flags -->

### SpriteBatch (2D)
Expand Down
32 changes: 32 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

129 changes: 129 additions & 0 deletions docs/findings/switch-vulkan-poc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# Nintendo Switch Vulkan Feasibility PoC -- Findings Report

**Issue**: #341 (parent: #135 Console Strategy)
**Date**: 2026-04-02
**Status**: Architecture validated, Nintendo SDK access required for runtime testing

## Executive Summary

GoudEngine's wgpu-based rendering architecture is structurally compatible with
Nintendo Switch's Vulkan 1.1 support. The engine's trait abstraction and runtime
backend selection support plugging in a Switch platform without modifying higher
layers. A `switch-vulkan` feature flag enables Switch-specific paths.

**Verdict**: Conditionally feasible. wgpu's Vulkan backend aligns with Switch's
Vulkan 1.1 support, but significant blockers exist around the proprietary SDK,
custom Rust target, and NVN-to-Vulkan translation layer performance.

## Architecture Integration

The following components were added under the `switch-vulkan` feature flag:

| Component | File | Purpose |
|-----------|------|---------|
| `SwitchVulkanPlatform` | `libs/platform/switch_vulkan_platform.rs` | `PlatformBackend` impl for Switch `nn::vi` windowing |
| `SwitchWindowHandle` | `libs/graphics/backend/wgpu_backend/switch_surface.rs` | `raw-window-handle` wrapper for wgpu surface creation |
| `switch_init` | `libs/graphics/backend/wgpu_backend/switch_init.rs` | wgpu Vulkan initialization from Switch handle |
| `WindowBackendKind::SwitchVulkan` | `libs/platform/mod.rs` | Enum variant for runtime backend selection |
| native_runtime arms | `libs/platform/native_runtime.rs` | Detection, validation, construction for Switch pair |

All components compile and type-check on macOS (x86_64 and aarch64). The
constructors return `BackendNotSupported` on non-aarch64 targets, allowing
cross-platform CI to verify the code without a Switch devkit.

## wgpu Vulkan on Nintendo Switch

### Compatibility

Nintendo Switch supports Vulkan 1.1 via a translation layer over NVN (Nintendo's
proprietary low-level API). wgpu's Vulkan backend targets Vulkan 1.0+ and should
work, but:
- The Switch's Vulkan driver has known conformance gaps
- Some Vulkan extensions used by wgpu may not be available
- Performance overhead from NVN translation is unknown

### Surface Creation

Switch uses `nn::vi` for display/layer management. The Vulkan surface is created
from an `nn::vi::Layer` handle, not from a standard window handle. This requires
a custom `raw-window-handle` implementation.

Risk: Medium. No standard windowing model -- requires manual `RawWindowHandle`
construction from `nn::vi` native window pointers.

## Shader Pipeline

GoudEngine uses naga for shader translation (GLSL -> WGSL -> SPIR-V). The Switch
Vulkan driver accepts SPIR-V, so the existing pipeline should work without
modification. However:
- naga's SPIR-V output should be tested against Switch's Vulkan conformance
- Some SPIR-V features may not be supported by the Switch driver
- Shader compilation performance on Switch hardware is unknown

## Blocking Issues

| # | Issue | Severity | Mitigation |
|---|-------|----------|------------|
| 1 | No official Rust target for Switch (`aarch64-nintendo-switch-none`) | High | Custom target JSON + tier 3 LLVM support |
| 2 | NintendoSDK is NDA-protected, not publicly available | High | Requires Nintendo developer program membership |
| 3 | Vulkan loader: Switch uses `nn::gfx` loader, not standard `libvulkan.so` | High | Custom Vulkan loader integration in wgpu |
| 4 | Audio: `nn::audio` API, not ALSA/PulseAudio | High | Custom audio backend via FFI |
| 5 | Input: `nn::hid` for Joy-Con/Pro Controller | Medium | Extend InputManager with Switch mappings |
| 6 | Performance: NVN-to-Vulkan translation overhead unknown | Medium | Benchmark once SDK available |
| 7 | File I/O: `nn::fs` for ROM/save data access | Medium | Custom asset loader path |

## Audio/Input/Filesystem Gaps

### Audio
The Switch uses `nn::audio` for sound output. GoudEngine's current audio backend
(rodio on desktop) would need a Switch-specific implementation wrapping `nn::audio`
via FFI. The `AudioManager` trait abstraction exists but has no Switch backend.

### Input
Switch input uses `nn::hid` for Joy-Con and Pro Controller support. The engine's
`InputManager` would need:
- Joy-Con button mapping (including gyro/accelerometer)
- Handheld vs. docked controller detection
- Touch screen support in handheld mode

### Filesystem
Switch uses `nn::fs` for ROM access and save data. The engine's `AssetServer`
would need a custom loader path that reads from the ROM filesystem via `nn::fs`
instead of standard `std::fs` operations.

## Build Toolchain

- Target: `aarch64-nintendo-switch-none` (custom target JSON)
- SDK: NintendoSDK (requires Nintendo developer program membership)
- Linker: Nintendo toolchain (part of SDK)
- Sysroot: SDK sysroot for aarch64
- Rust: Nightly required for custom target support

## Certification Requirements

Nintendo Lot Check requirements affecting engine:
- Must handle HOME button suspend/resume correctly
- Must support handheld/docked mode switching (720p to 1080p resolution change)
- Must meet performance requirements (30fps minimum)
- Must handle controller disconnect/reconnect gracefully
- Must support save data management via `nn::account` + `nn::fs`

## Estimated Development Effort

| Phase | Work | Weeks |
|-------|------|-------|
| SDK setup + custom Rust target | | 2-3 |
| Vulkan surface + basic rendering | | 3-4 |
| Audio + input integration | | 2-3 |
| File I/O + asset loading | | 1-2 |
| Performance optimization | | 2-3 |
| Lot Check compliance | | 2-3 |
| **Total** | | **12-18** |

## Recommended Next Steps

1. Join Nintendo developer program and obtain NintendoSDK
2. Create custom Rust target JSON for `aarch64-nintendo-switch`
3. Test wgpu Vulkan backend on Switch devkit
4. If Vulkan works: plan audio, input, filesystem as separate issues
5. If Vulkan blocked: evaluate NVN backend as alternative (requires writing custom wgpu backend)
3 changes: 3 additions & 0 deletions goud_engine/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ rapier2d = ["dep:rapier2d"]
rapier3d = ["dep:rapier3d"]
headless = []
xbox-gdk = ["wgpu-backend"]
sdl-window = ["wgpu-backend", "dep:sdl2"]
switch-vulkan = ["wgpu-backend"]

# TODO: https://github.com/aram-devdocs/GoudEngine/issues/8
[dependencies]
Expand Down Expand Up @@ -83,6 +85,7 @@ web-sys = { version = "0.3", optional = true, features = [
"AudioDestinationNode", "BaseAudioContext",
"Blob", "Url",
] }
sdl2 = { version = "0.38", optional = true, features = ["bundled", "raw-window-handle"] }
bumpalo = "3.20.2"
smallvec = "1.15.1"

Expand Down
11 changes: 9 additions & 2 deletions goud_engine/src/libs/graphics/backend/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,12 @@

pub mod blend;
pub mod capabilities;
#[cfg(any(feature = "native", feature = "xbox-gdk"))]
#[cfg(any(
feature = "native",
feature = "xbox-gdk",
feature = "sdl-window",
feature = "switch-vulkan"
))]
pub mod native_backend;
pub mod null;
#[cfg(feature = "legacy-glfw-opengl")]
Expand All @@ -32,7 +37,9 @@ pub mod render_backend;
pub mod types;
#[cfg(any(
all(feature = "native", feature = "wgpu-backend"),
feature = "xbox-gdk"
feature = "xbox-gdk",
feature = "sdl-window",
feature = "switch-vulkan"
))]
pub mod wgpu_backend;

Expand Down
28 changes: 21 additions & 7 deletions goud_engine/src/libs/graphics/backend/native_backend/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ use super::render_backend::RenderBackend;
use super::render_backend::StateOps;
#[cfg(any(
all(feature = "native", feature = "wgpu-backend"),
feature = "xbox-gdk"
feature = "xbox-gdk",
feature = "sdl-window",
feature = "switch-vulkan"
))]
use super::wgpu_backend::WgpuBackend;

Expand All @@ -30,7 +32,9 @@ pub enum NativeRenderBackend {
OpenGlLegacy(Box<OpenGLBackend>),
#[cfg(any(
all(feature = "native", feature = "wgpu-backend"),
feature = "xbox-gdk"
feature = "xbox-gdk",
feature = "sdl-window",
feature = "switch-vulkan"
))]
/// Default wgpu backend used by the native runtime.
Wgpu(Box<WgpuBackend>),
Expand All @@ -43,7 +47,9 @@ impl NativeRenderBackend {
Self::OpenGlLegacy(backend) => backend.info(),
#[cfg(any(
all(feature = "native", feature = "wgpu-backend"),
feature = "xbox-gdk"
feature = "xbox-gdk",
feature = "sdl-window",
feature = "switch-vulkan"
))]
Self::Wgpu(backend) => backend.info(),
}
Expand All @@ -59,7 +65,9 @@ impl NativeRenderBackend {
Self::OpenGlLegacy(backend) => backend.bind_texture_by_index(index, unit),
#[cfg(any(
all(feature = "native", feature = "wgpu-backend"),
feature = "xbox-gdk"
feature = "xbox-gdk",
feature = "sdl-window",
feature = "switch-vulkan"
))]
Self::Wgpu(backend) => backend.bind_texture_by_index(index, unit),
}
Expand All @@ -71,7 +79,9 @@ impl NativeRenderBackend {
Self::OpenGlLegacy(backend) => backend.set_viewport(0, 0, width, height),
#[cfg(any(
all(feature = "native", feature = "wgpu-backend"),
feature = "xbox-gdk"
feature = "xbox-gdk",
feature = "sdl-window",
feature = "switch-vulkan"
))]
Self::Wgpu(backend) => backend.resize(width, height),
}
Expand All @@ -84,7 +94,9 @@ impl NativeRenderBackend {
Self::OpenGlLegacy(_) => {}
#[cfg(any(
all(feature = "native", feature = "wgpu-backend"),
feature = "xbox-gdk"
feature = "xbox-gdk",
feature = "sdl-window",
feature = "switch-vulkan"
))]
Self::Wgpu(backend) => backend.drop_surface(),
}
Expand All @@ -97,7 +109,9 @@ impl NativeRenderBackend {
Self::OpenGlLegacy(_) => Ok(()),
#[cfg(any(
all(feature = "native", feature = "wgpu-backend"),
feature = "xbox-gdk"
feature = "xbox-gdk",
feature = "sdl-window",
feature = "switch-vulkan"
))]
Self::Wgpu(backend) => backend.recreate_surface(),
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ impl DrawOps for NativeRenderBackend {
Self::OpenGlLegacy(backend) => backend.set_vertex_attributes(layout),
#[cfg(any(
all(feature = "native", feature = "wgpu-backend"),
feature = "xbox-gdk"
feature = "xbox-gdk",
feature = "sdl-window",
feature = "switch-vulkan"
))]
Self::Wgpu(backend) => backend.set_vertex_attributes(layout),
}
Expand All @@ -23,7 +25,9 @@ impl DrawOps for NativeRenderBackend {
Self::OpenGlLegacy(backend) => backend.set_vertex_bindings(bindings),
#[cfg(any(
all(feature = "native", feature = "wgpu-backend"),
feature = "xbox-gdk"
feature = "xbox-gdk",
feature = "sdl-window",
feature = "switch-vulkan"
))]
Self::Wgpu(backend) => backend.set_vertex_bindings(bindings),
}
Expand All @@ -40,7 +44,9 @@ impl DrawOps for NativeRenderBackend {
Self::OpenGlLegacy(backend) => backend.draw_arrays(topology, first, count),
#[cfg(any(
all(feature = "native", feature = "wgpu-backend"),
feature = "xbox-gdk"
feature = "xbox-gdk",
feature = "sdl-window",
feature = "switch-vulkan"
))]
Self::Wgpu(backend) => backend.draw_arrays(topology, first, count),
}
Expand All @@ -57,7 +63,9 @@ impl DrawOps for NativeRenderBackend {
Self::OpenGlLegacy(backend) => backend.draw_indexed(topology, count, offset),
#[cfg(any(
all(feature = "native", feature = "wgpu-backend"),
feature = "xbox-gdk"
feature = "xbox-gdk",
feature = "sdl-window",
feature = "switch-vulkan"
))]
Self::Wgpu(backend) => backend.draw_indexed(topology, count, offset),
}
Expand All @@ -74,7 +82,9 @@ impl DrawOps for NativeRenderBackend {
Self::OpenGlLegacy(backend) => backend.draw_indexed_u16(topology, count, offset),
#[cfg(any(
all(feature = "native", feature = "wgpu-backend"),
feature = "xbox-gdk"
feature = "xbox-gdk",
feature = "sdl-window",
feature = "switch-vulkan"
))]
Self::Wgpu(backend) => backend.draw_indexed_u16(topology, count, offset),
}
Expand All @@ -94,7 +104,9 @@ impl DrawOps for NativeRenderBackend {
}
#[cfg(any(
all(feature = "native", feature = "wgpu-backend"),
feature = "xbox-gdk"
feature = "xbox-gdk",
feature = "sdl-window",
feature = "switch-vulkan"
))]
Self::Wgpu(backend) => {
backend.draw_arrays_instanced(topology, first, count, instance_count)
Expand All @@ -116,7 +128,9 @@ impl DrawOps for NativeRenderBackend {
}
#[cfg(any(
all(feature = "native", feature = "wgpu-backend"),
feature = "xbox-gdk"
feature = "xbox-gdk",
feature = "sdl-window",
feature = "switch-vulkan"
))]
Self::Wgpu(backend) => {
backend.draw_indexed_instanced(topology, count, offset, instance_count)
Expand Down
Loading
Loading