Compare commits

..

10 Commits

Author SHA1 Message Date
enricobuehler eb8a659319 fix(client): unused 'decoder label under default features + box the PyroWave backend variant
ci / web (push) Successful in 56s
ci / docs-site (push) Successful in 1m7s
decky / build-publish (push) Successful in 17s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 9s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 8s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 8s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 8s
ci / bench (push) Successful in 6m20s
android / android (push) Successful in 14m39s
arch / build-publish (push) Successful in 12m29s
docker / deploy-docs (push) Successful in 22s
flatpak / build-publish (push) Successful in 6m17s
deb / build-publish (push) Successful in 11m58s
ci / rust (push) Successful in 18m3s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 14m21s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 13m58s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 4m3s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m54s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 4m58s
apple / swift (push) Successful in 4m52s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 5m38s
apple / screenshots (push) Successful in 23m0s
ci.yml's -D warnings clippy (default features) flagged the labeled block
whose only break lives behind the pyrowave cfg — restructured as cfg'd
let-bindings, no label. Also boxed Backend::PyroWave (the decoder's
pinned create-info hold + plane ring dwarfed the other variants —
clippy::large_enum_variant under the feature).

Both configs strict-clippy clean on .21; 26 tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 02:18:45 +02:00
enricobuehler fa4df1de9e feat(client): PyroWave session wiring — advertisement, opt-in, decoder selection (Phase 2b, part 3)
ci / web (push) Successful in 1m21s
ci / docs-site (push) Successful in 1m19s
ci / rust (push) Failing after 6m16s
decky / build-publish (push) Successful in 16s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 8s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 8s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 7s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 9s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 7s
arch / build-publish (push) Successful in 10m49s
ci / bench (push) Successful in 5m37s
docker / deploy-docs (push) Successful in 11s
flatpak / build-publish (push) Failing after 8m5s
deb / build-publish (push) Successful in 11m49s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 13m36s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 13m38s
android / android (push) Successful in 13m45s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m45s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m51s
apple / swift (push) Successful in 4m43s
windows / build (aarch64-pc-windows-msvc) (push) Failing after 4m52s
windows / build (x86_64-pc-windows-msvc) (push) Failing after 5m24s
apple / screenshots (push) Successful in 23m53s
The pump now advertises decodable_codecs_for(presenter device) — the
CODEC_PYROWAVE bit rides only when the device passed the compute-feature
probe — and PUNKTFUNK_PREFER_PYROWAVE=1 is the Phase-2 lab opt-in that
names the codec in preferred_codec (the only route resolve_codec will
take it, plan §3; a Settings toggle is Phase-3 productization). A
negotiated PyroWave session builds Decoder::new_pyrowave on the
presenter's device instead of an FFmpeg decoder. clients/session grows
the `pyrowave` feature forwarding both crate features.

With this the Phase-2 client chain is code-complete:
Hello bit → preference → Welcome::codec → pyrowave decode on the
presenter device → planar CSC → present. On-glass .21 run +
latency-probe/loss-harness numbers vs HEVC remain owed (plan Phase-2
exit criteria).

Validated on .21: session client + all crates compile with and without
the features, clippy clean, 26 + 308 tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 01:48:36 +02:00
enricobuehler ef862454b0 chore(core): regenerate the C ABI header + lockfile for PUNKTFUNK_CODEC_PYROWAVE
audit / bun-audit (push) Successful in 11s
ci / web (push) Successful in 54s
audit / cargo-audit (push) Successful in 2m22s
ci / docs-site (push) Successful in 1m6s
decky / build-publish (push) Successful in 21s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 8s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 9s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 7s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 7s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 8s
ci / bench (push) Successful in 5m17s
android / android (push) Successful in 11m29s
flatpak / build-publish (push) Successful in 5m58s
arch / build-publish (push) Successful in 13m47s
deb / build-publish (push) Successful in 16m4s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 13m13s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 16m58s
docker / deploy-docs (push) Successful in 9s
ci / rust (push) Successful in 23m46s
apple / swift (push) Successful in 4m50s
windows-host / package (push) Successful in 14m8s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m39s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 4m31s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 4m55s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 5m39s
release / apple (push) Successful in 25m19s
apple / screenshots (push) Successful in 22m8s
ci.yml's header-freshness gate caught the stale include/punktfunk_core.h
(the ABI constant landed without the regenerated header); the lockfile
records pf-client-core's new optional deps (ash, pyrowave-sys).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 01:44:29 +02:00
enricobuehler f77eec1299 feat(client): PyroWave planar present path + Linux NVENC match-arm fix (Phase 2b, part 2)
android / android (push) Failing after 45s
ci / web (push) Successful in 51s
ci / docs-site (push) Successful in 56s
decky / build-publish (push) Successful in 17s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 8s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 8s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 7s
arch / build-publish (push) Failing after 4m15s
ci / rust (push) Failing after 3m59s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 59s
deb / build-publish (push) Failing after 4m7s
ci / bench (push) Successful in 5m28s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Failing after 3m46s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 3m58s
flatpak / build-publish (push) Failing after 8m3s
docker / deploy-docs (push) Successful in 22s
windows-host / package (push) Successful in 14m21s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m31s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m35s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 4m57s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 5m46s
apple / swift (push) Successful in 4m59s
apple / screenshots (push) Successful in 21m40s
The arch package job (--features nvenc) tripped the same class of
Codec::PyroWave non-exhaustive matches as windows-host had, in
nvenc_cuda.rs (6 sites) — dispatch-guarded unreachable!() arms, plus
the vk_util-extraction leftover unused imports in vulkan_video.rs.
All Linux host feature combos (none / pyrowave / nvenc,vulkan-encode /
all three) now compile clean on .21.

Presenter: planar_csc.frag (+ committed .spv) — the 3-plane variant of
nv12_csc.frag (separate Cb/Cr R8 planes, same push-constant CSC-row
contract, siting correction self-disables at full-res chroma).
CscPass grows a shared builder + new_planar()/bind_planes_planar()
(GENERAL-layout descriptors — pyrowave planes stay GENERAL); the Vk
presenter builds the planar pass when the device passed the pyrowave
probe, FrameInput::PyroWave rides present_frame (no acquire barrier
needed: the decoder fence-completed and barriered the planes on the
same queue), and run.rs presents it with no demote rung (only device
loss ends the session).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 01:42:15 +02:00
enricobuehler 575975687c feat(client): PyroWave decode backend on the presenter's device (Phase 2b, part 1)
ci / web (push) Successful in 55s
ci / docs-site (push) Successful in 1m2s
ci / rust (push) Failing after 4m1s
decky / build-publish (push) Successful in 18s
arch / build-publish (push) Failing after 4m45s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 9s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 9s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 8s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 7s
deb / build-publish (push) Failing after 4m46s
ci / bench (push) Successful in 5m12s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Failing after 3m47s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 4m39s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m41s
flatpak / build-publish (push) Successful in 6m20s
android / android (push) Successful in 13m47s
docker / deploy-docs (push) Successful in 28s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m59s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 4m55s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 6m50s
apple / swift (push) Successful in 4m45s
apple / screenshots (push) Successful in 20m33s
The presenter's device creation now probes + enables the PyroWave
compute feature set alongside the Vulkan Video probe (shaderInt16,
storageBuffer8BitAccess, subgroup size control — gated on support,
harmless when unused) and exports the facts through VulkanDecodeDevice
(pyrowave_decode capability + feature bools + apiVersion + the queue-
family shape).

pf-client-core (feature `pyrowave`, Linux): video_pyrowave.rs — the
decoder runs pyrowave compute on the PRESENTER's own VkDevice, zero
interop (plan §4.5): pinned content-equivalent create-info
reconstruction satisfies pyrowave 0.4.0's lifetime rule without
refactoring the presenter's creation; queue access rides the existing
device-wide QueueLock (the FFmpeg/Skia contract); decode records into
our command buffer, fence-synchronous (sub-ms), into a 4-deep ring of
3xR8 plane sets (decode REQUIRES storage usage + identity swizzles, so
the encoder's RG8 trick doesn't apply). Backend::PyroWave +
DecodedImage::PyroWave + Decoder::new_pyrowave + decodable_codecs_for
(advertisement gated on the device probe) wired through the decode
dispatch; no demote ladder (nothing else decodes it — fallback is
session renegotiation, plan §4.6).

Still to come for a live session: the presenter's planar-CSC render
path for the new variant, pump/shell opt-in (preferred_codec) wiring,
and the on-glass .21 run.

Validated on .21: pf-client-core + pf-presenter compile with and
without the feature, clippy clean, 26 client-core tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 01:33:38 +02:00
enricobuehler 49ba1cd11b fix(host): cover Codec::PyroWave in the Windows NVENC/AMF match arms
ci / web (push) Successful in 1m12s
ci / docs-site (push) Successful in 1m45s
decky / build-publish (push) Successful in 17s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 8s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 31s
arch / build-publish (push) Failing after 8m9s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 7s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 9s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 9s
ci / bench (push) Successful in 5m57s
android / android (push) Successful in 12m13s
ci / rust (push) Failing after 13m5s
deb / build-publish (push) Failing after 7m32s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 7m30s
windows-host / package (push) Successful in 14m18s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Failing after 9m37s
docker / deploy-docs (push) Successful in 25s
apple / swift (push) Successful in 4m51s
apple / screenshots (push) Successful in 20m29s
The nine non-exhaustive matches windows-host CI tripped on (run 9917) —
all inside encoder objects a PyroWave session can never open (the
open_video dispatch routes PyroWave to its own backend on Linux and
bails on Windows), so the arms are dispatch-guarded unreachable!().
Verified: cargo check -p punktfunk-host --features nvenc,amf-qsv
--release green on the windows-amd64 runner.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 01:20:56 +02:00
enricobuehler e71cb9b7bd feat(core,host): CODEC_PYROWAVE negotiation — opt-in only, host dispatch wired
audit / bun-audit (push) Successful in 11s
windows-host / package (push) Failing after 48s
ci / docs-site (push) Successful in 50s
ci / web (push) Successful in 52s
android / android (push) Failing after 58s
decky / build-publish (push) Successful in 32s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 38s
audit / cargo-audit (push) Successful in 2m15s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 1m43s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m18s
ci / bench (push) Successful in 6m37s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 10s
deb / build-publish (push) Failing after 8m23s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 4m59s
arch / build-publish (push) Failing after 12m6s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10m10s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 12m54s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 11m49s
flatpak / build-publish (push) Failing after 8m16s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 5m46s
ci / rust (push) Failing after 19m32s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 11m33s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Failing after 10m56s
docker / deploy-docs (push) Successful in 21s
apple / swift (push) Successful in 4m53s
release / apple (push) Successful in 25m50s
apple / screenshots (push) Successful in 20m55s
Phase 2a+2c of design/pyrowave-codec-plan.md.

Core: CODEC_PYROWAVE = 0x08 on Hello::video_codecs/Welcome::codec.
Deliberately absent from resolve_codec's precedence ladder (plan §3 —
a 100-400 Mbps codec must never win a negotiation by mere mutual
support): reachable exclusively through the client's explicit
preferred_codec. Invariant tests cover never-auto-selected (even as the
only shared codec), preferred-path selection, and graceful fallback.
ABI mirror PUNKTFUNK_CODEC_PYROWAVE + lockstep assert for the
Apple/Android embedders.

Host: Codec::PyroWave variant threaded through the wire mappings; a
negotiated PyroWave session routes straight to the backend ahead of the
PUNKTFUNK_ENCODER pref dispatch (which stays a lab override). The
advertisement bit rides host_wire_caps only when the capture side would
actually deliver ingestible frames — linux_zero_copy_is_vaapi(), i.e.
AMD/Intel auto or an explicit operator pref on NVIDIA; per-session
raw-dmabuf OutputFormat plumbing is recorded as the Phase-3 item. The
libavcodec name helpers are dispatch-guarded unreachable; the web
console gains ApiCodec::PyroWave (api/openapi.json regenerated).

Validated on .21: 308 host tests green with and without the feature,
145 core tests green with quic, clippy clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 01:14:04 +02:00
enricobuehler 9724fb4a4e fix(pyrowave-sys): link user32 on Windows (Granite breadcrumbs MessageBoxA)
MSVC leg of the Phase-0 build gate verified on the windows-amd64 runner
(.133): full vendored C++ set compiles under MSVC, static link resolves,
API-version pin test green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 01:02:19 +02:00
enricobuehler 767f028bdf feat(host): PyroWave encoder — Phase 1 of the LAN low-latency codec plan
PyroWaveEncoder behind --features pyrowave + an explicit
PUNKTFUNK_ENCODER=pyrowave (loud EXPERIMENTAL warning: no client can
decode the stream until CODEC_PYROWAVE negotiation lands, plan Phase 2).

Design (plan §4.3): a private ash Vulkan-1.3 device shared with pyrowave
via pyrowave_create_device — DeviceHold pins the instance/device
create-infos the 0.4.0 API requires alive for the device's lifetime.
Capture dmabufs pass straight through on ANY vendor
(linux_zero_copy_is_vaapi → true for pyrowave; NVIDIA dmabuf→Vulkan
import validated by upstream's interop test on .21) with the same
per-buffer import cache as the Vulkan Video backend; the shared
rgb2yuv.comp BT.709-limited CSC writes R8+RG8 images pyrowave samples
directly (R/G view swizzles synthesize Cb/Cr — no NV12 copy). Encode
records into OUR command buffer (pyrowave_device_set_command_buffer), so
ingest + CSC + encode are one submission with a sub-ms fence wait; the
AU is exactly one pyrowave packet, keyframe=true on every frame.
reconfigure_bitrate is a free in-place budget change (Phase 3 pins the
session rate); reset() recreates only the pyrowave encoder object.

Shared ash leaf helpers (dmabuf import, image/memory utils) extracted
from vulkan_video.rs into encode/linux/vk_util.rs — vulkan-encode
builds unchanged.

Validated on .21 (RTX 5070 Ti): pyrowave_smoke green — encodes CPU
fills through the full open→CSC→GPU-encode→packetize path, decodes
every AU with upstream's own decoder, checks BT.709 plane means ±3;
rate retarget + rebuild covered. clippy clean, 308 host tests green
with the feature on.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 00:58:02 +02:00
enricobuehler 4c3b11445c feat(host): vendor PyroWave + minimal Granite subset as crates/pyrowave-sys
Phase 0 of design/pyrowave-codec-plan.md — the opt-in wired-LAN ultra-low-
latency codec. Vendored at upstream 509e4f88 (API 0.4.0, Granite 44362775,
volk + vulkan-headers pins in PUNKTFUNK-VENDOR.txt), pruned to the 6.6 MB
the standalone no-renderer build needs; scripts/vendor-pyrowave.sh
reproduces the tree (a pin bump is protocol-affecting, plan §4.2).

build.rs drives the wrapper CMakeLists (static archives incl. a static
C-API lib upstream only ships shared) + bindgen over pyrowave.h; Linux and
Windows only, empty stub elsewhere (Apple gets a native Metal port, §4.7).
Offline-safe by construction: no network, no system lib, vendored Vulkan
headers — same model as the opus dep (flatpak builder has no network).

Phase-0 validation on .21 (RTX 5070 Ti, driver 610.43.03):
- upstream pyrowave-c-test + interop test (incl. dmabuf/DRM-modifier
  Vulkan<->Vulkan) pass, from the pristine AND the pruned tree
- GPU kernel times at ~1.6 bpp noise: encode/decode 0.090/0.042 ms @800p,
  0.146/0.067 @1080p, 0.226/0.103 @1440p, 0.477/0.201 @4K — order of
  magnitude under NVENC's 1-2 ms retrieve, CBR lands within ~100 B of
  target
- cargo test -p pyrowave-sys green (static link + API-version pin check)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 00:35:10 +02:00
424 changed files with 142620 additions and 246 deletions
Generated
+11
View File
@@ -2759,6 +2759,7 @@ name = "pf-client-core"
version = "0.11.0"
dependencies = [
"anyhow",
"ash",
"async-channel",
"ffmpeg-next",
"mdns-sd",
@@ -2766,6 +2767,7 @@ dependencies = [
"pf-ffvk",
"pipewire",
"punktfunk-core",
"pyrowave-sys",
"rustls",
"sdl3",
"serde",
@@ -3120,6 +3122,7 @@ dependencies = [
"pf-driver-proto",
"pipewire",
"punktfunk-core",
"pyrowave-sys",
"quinn",
"rand 0.8.6",
"rcgen",
@@ -3190,6 +3193,14 @@ dependencies = [
"winresource",
]
[[package]]
name = "pyrowave-sys"
version = "0.11.0"
dependencies = [
"bindgen",
"cmake",
]
[[package]]
name = "quick-error"
version = "1.2.3"
+1
View File
@@ -10,6 +10,7 @@ members = [
"crates/pf-console-ui",
"crates/pf-ffvk",
"crates/pf-driver-proto",
"crates/pyrowave-sys",
"clients/probe",
"clients/linux",
"clients/session",
+3 -2
View File
@@ -10,7 +10,7 @@
"name": "MIT OR Apache-2.0",
"identifier": "MIT OR Apache-2.0"
},
"version": "0.9.1"
"version": "0.11.0"
},
"paths": {
"/api/v1/clients": {
@@ -2047,7 +2047,8 @@
"enum": [
"h264",
"h265",
"av1"
"av1",
"pyrowave"
]
},
"ApiDisplayInfo": {
+3
View File
@@ -14,6 +14,9 @@ path = "src/main.rs"
[features]
default = ["ui"]
# PyroWave client decode (the opt-in wired-LAN wavelet codec) — enables the decode
# backend + the planar present path; runtime opt-in stays PUNKTFUNK_PREFER_PYROWAVE=1.
pyrowave = ["pf-client-core/pyrowave", "pf-presenter/pyrowave"]
# The Skia console UI (stats OSD, capture HUD, later the gamepad library). Dropping it
# (`--no-default-features`) is the ~15 MB-smaller power-user build: same streaming,
# stats on stdout only.
+10
View File
@@ -40,6 +40,11 @@ tracing = "0.1"
[target.'cfg(target_os = "linux")'.dependencies]
pipewire = "0.9"
sdl3 = { version = "0.18", features = ["hidapi"] }
# PyroWave decode (the opt-in wired-LAN wavelet codec, design/pyrowave-codec-plan.md
# §4.5) — pure Vulkan compute on the presenter's shared device. `ash` only wraps the
# presenter's existing raw handles (same pinned version as pf-presenter).
pyrowave-sys = { path = "../pyrowave-sys", optional = true }
ash = { version = "0.38", optional = true }
[target.'cfg(windows)'.dependencies]
wasapi = "0.23"
@@ -57,3 +62,8 @@ windows = { git = "https://github.com/microsoft/windows-rs", rev = "a4f7b2cb7c63
# method itself is feature-gated behind this.
"Win32_Security",
] }
[features]
# PyroWave client decode — OFF by default (the flatpak/default builds stay unchanged);
# the Linux session client turns it on together with the host-side feature.
pyrowave = ["dep:pyrowave-sys", "dep:ash"]
+4
View File
@@ -33,7 +33,11 @@ pub mod session;
pub mod trust;
#[cfg(any(target_os = "linux", windows))]
pub mod video;
// PyroWave decode — Linux + `pyrowave` feature only (plan §4.5; the Windows client's
// present-path decision and the Apple Metal port are their own phases).
#[cfg(windows)]
pub mod video_d3d11;
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
pub mod video_pyrowave;
pub mod wol;
+40 -3
View File
@@ -211,6 +211,21 @@ fn pump(
frame_tx: async_channel::Sender<DecodedFrame>,
stop: Arc<AtomicBool>,
) {
// PUNKTFUNK_PREFER_PYROWAVE=1 — the Phase-2 lab opt-in for the wired-LAN wavelet codec
// (a Settings toggle is the Phase-3 productization). Riding `preferred_codec` is exactly
// the plan-§3 contract: the host only ever picks PyroWave when the client names it.
#[allow(unused_mut)]
let mut preferred = params.preferred_codec;
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
if std::env::var("PUNKTFUNK_PREFER_PYROWAVE").as_deref() == Ok("1") {
if params.vulkan.as_ref().is_some_and(|v| v.pyrowave_decode) {
preferred = punktfunk_core::quic::CODEC_PYROWAVE;
} else {
tracing::warn!(
"PUNKTFUNK_PREFER_PYROWAVE=1 but the presenter device failed the pyrowave probe — keeping the normal codec preference"
);
}
}
let connector = match NativeClient::connect(
&params.host,
params.port,
@@ -220,8 +235,9 @@ fn pump(
params.bitrate_kbps,
params.video_caps,
params.audio_channels,
crate::video::decodable_codecs(), // codecs FFmpeg can decode (HEVC/H.264/AV1)
params.preferred_codec, // the user's soft codec preference (0 = auto)
// FFmpeg's codecs plus CODEC_PYROWAVE when the presenter device passed the probe.
crate::video::decodable_codecs_for(params.vulkan.as_ref()),
preferred, // the user's soft codec preference (0 = auto; see the pyrowave opt-in above)
// This display's HDR volume → the host's virtual-display EDID. The env hatch wins so an
// A/B run can pin an exact peak (PUNKTFUNK_CLIENT_PEAK_NITS=600).
punktfunk_core::client::display_hdr_env_override().or(params.display_hdr),
@@ -262,7 +278,24 @@ fn pump(
welcome_codec = connector.codec,
"negotiated video codec"
);
let mut decoder = match Decoder::new(codec_id, &params.decoder, params.vulkan.as_ref()) {
// A negotiated PyroWave session decodes on the presenter's device, no FFmpeg —
// reachable only through the explicit preference above (resolve_codec never
// auto-picks the bit), so failing loudly here is failing an opted-in experiment.
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
let built = if connector.codec == punktfunk_core::quic::CODEC_PYROWAVE {
let mode = connector.mode();
match params.vulkan.as_ref() {
Some(vk) => Decoder::new_pyrowave(vk, mode.width, mode.height),
None => Err(anyhow::anyhow!(
"pyrowave session without a presenter device"
)),
}
} else {
Decoder::new(codec_id, &params.decoder, params.vulkan.as_ref())
};
#[cfg(not(all(target_os = "linux", feature = "pyrowave")))]
let built = Decoder::new(codec_id, &params.decoder, params.vulkan.as_ref());
let mut decoder = match built {
Ok(d) => d,
Err(e) => {
let _ = ev_tx.send_blocking(SessionEvent::Ended(Some(format!("video decoder: {e}"))));
@@ -413,6 +446,8 @@ fn pump(
DecodedImage::VkFrame(_) => "vulkan",
#[cfg(windows)]
DecodedImage::D3d11(_) => "d3d11va",
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
DecodedImage::PyroWave(_) => "pyrowave",
};
if total_frames == 1 {
let (w, h, path) = match &image {
@@ -422,6 +457,8 @@ fn pump(
DecodedImage::VkFrame(v) => (v.width, v.height, "vulkan-video"),
#[cfg(windows)]
DecodedImage::D3d11(d) => (d.width, d.height, "d3d11va"),
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
DecodedImage::PyroWave(f) => (f.width, f.height, "pyrowave"),
};
tracing::info!(width = w, height = h, path, "first frame decoded");
}
+75
View File
@@ -68,6 +68,11 @@ pub enum DecodedImage {
/// (Intel's Windows driver foremost). See `crate::video_d3d11`.
#[cfg(windows)]
D3d11(crate::video_d3d11::D3d11Frame),
/// PyroWave planar output: three R8 plane views on the presenter's own device,
/// decode already fence-complete, GENERAL layout — the presenter's planar CSC
/// samples them directly (BT.709 limited, the codec's fixed colour contract).
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
PyroWave(crate::video_pyrowave::PyroWavePlanarFrame),
}
/// One Vulkan-decoded frame. The image lives on the presenter's own VkDevice (the
@@ -183,6 +188,8 @@ impl DecodedImage {
DecodedImage::VkFrame(f) => f.keyframe,
#[cfg(windows)]
DecodedImage::D3d11(f) => f.keyframe,
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
DecodedImage::PyroWave(f) => f.keyframe,
}
}
@@ -197,6 +204,8 @@ impl DecodedImage {
DecodedImage::VkFrame(f) => (f.width, f.height),
#[cfg(windows)]
DecodedImage::D3d11(f) => (f.width, f.height),
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
DecodedImage::PyroWave(f) => (f.width, f.height),
}
}
}
@@ -312,6 +321,11 @@ enum Backend {
Vaapi(VaapiDecoder),
#[cfg(windows)]
D3d11va(crate::video_d3d11::D3d11vaDecoder),
/// PyroWave (wired-LAN wavelet codec): pyrowave compute on the presenter's device,
/// no FFmpeg involvement. No demotion rung — there is no other decoder for it.
/// Boxed: the decoder (pinned create-info hold + plane ring) dwarfs the other variants.
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
PyroWave(Box<crate::video_pyrowave::PyroWaveDecoder>),
Software(SoftwareDecoder),
}
@@ -360,6 +374,21 @@ pub fn decodable_codecs() -> u8 {
bits
}
/// [`decodable_codecs`] plus the PyroWave bit when the presenter's device passed the
/// compute-feature probe. Advertisement-only: `resolve_codec` never auto-picks PyroWave —
/// the session must also name it `preferred_codec` (plan §3), which the client does only
/// under its explicit opt-in.
pub fn decodable_codecs_for(vk: Option<&VulkanDecodeDevice>) -> u8 {
let bits = decodable_codecs();
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
if vk.map(|v| v.pyrowave_decode).unwrap_or(false) {
return bits | punktfunk_core::quic::CODEC_PYROWAVE;
}
#[cfg(not(all(target_os = "linux", feature = "pyrowave")))]
let _ = vk;
bits
}
/// libavcodec logs reference-frame recovery to the process stderr very verbosely
/// (`First slice in a frame missing`, `Could not find ref with POC …`, `Error
/// constructing the frame RPS`) — normal chatter while the decoder waits for a keyframe
@@ -539,6 +568,21 @@ impl Decoder {
/// Drain the "please ask the host for an IDR" flag — the pump calls this each iteration
/// (throttled) so a demoted/erroring decoder can resynchronize under the infinite GOP.
/// Open a PyroWave decoder for a `CODEC_PYROWAVE` session (plan §4.5): pyrowave
/// compute on the presenter's device, no FFmpeg. `codec_id` is irrelevant (kept as
/// HEVC so an — impossible — demotion path stays well-formed).
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
pub fn new_pyrowave(vk: &VulkanDecodeDevice, width: u32, height: u32) -> Result<Decoder> {
Ok(Decoder {
backend: Backend::PyroWave(Box::new(crate::video_pyrowave::PyroWaveDecoder::new(
vk, width, height,
)?)),
codec_id: ffmpeg::codec::Id::HEVC,
vaapi_fails: 0,
want_keyframe: false,
})
}
pub fn take_keyframe_request(&mut self) -> bool {
std::mem::take(&mut self.want_keyframe)
}
@@ -572,6 +616,11 @@ impl Decoder {
Backend::Vaapi(v) => v.decode(au).map(|f| f.map(DecodedImage::Dmabuf)),
#[cfg(windows)]
Backend::D3d11va(d) => d.decode(au).map(|f| f.map(DecodedImage::D3d11)),
// No demote ladder below PyroWave (nothing else decodes it): propagate the
// error; the pump surfaces it and the session falls back to HEVC by
// renegotiation (plan §4.6), not by decoder swap.
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
Backend::PyroWave(p) => return Ok(p.decode(au)?.map(DecodedImage::PyroWave)),
Backend::Software(s) => return Ok(s.decode(au)?.map(DecodedImage::Cpu)),
};
match result {
@@ -1077,6 +1126,24 @@ pub struct VulkanDecodeDevice {
/// features). The bundle now exists even without it — Windows D3D11 interop rides the
/// same struct — so consumers gate the FFmpeg-Vulkan decoder on THIS, not on `Some`.
pub video_decode: bool,
/// PyroWave decode (the wired-LAN wavelet codec) is usable: Vulkan 1.3 + the compute
/// features its kernels need were present AND enabled at device creation
/// (`shaderInt16`, `storageBuffer8BitAccess`, subgroup size control). Gates the
/// `CODEC_PYROWAVE` advertisement and the pyrowave decoder backend.
pub pyrowave_decode: bool,
/// The feature facts + creation shape the pyrowave decoder's pinned create-info
/// reconstruction mirrors (pyrowave 0.4.0 requires the instance/device create infos —
/// content-accurate, kept alive — to share our VkDevice).
pub f_shader_int16: bool,
pub f_storage_buffer8: bool,
pub f_subgroup_size_control: bool,
pub f_compute_full_subgroups: bool,
pub f_shader_float16: bool,
/// `VkPhysicalDeviceProperties::apiVersion` of the presenter's device.
pub api_version: u32,
/// The queue families the device was created with (one `VkDeviceQueueCreateInfo` each,
/// one queue per family, priority 1.0) — mirrored by the reconstruction.
pub queue_families: Vec<u32>,
/// The presenter enabled `VK_KHR_external_memory_win32` + `VK_KHR_win32_keyed_mutex`:
/// D3D11 shared-texture frames can reach the screen. Always `false` off Windows.
pub d3d11_import: bool,
@@ -1598,6 +1665,14 @@ mod tests {
f_sampler_ycbcr: true,
f_timeline_semaphore: true,
f_synchronization2: true,
f_shader_int16: false,
f_storage_buffer8: false,
f_subgroup_size_control: false,
f_compute_full_subgroups: false,
f_shader_float16: false,
api_version: 0,
queue_families: Vec::new(),
pyrowave_decode: false,
video_decode: true,
d3d11_import: false,
adapter_luid: None,
+561
View File
@@ -0,0 +1,561 @@
//! PyroWave client decode (design/pyrowave-codec-plan.md §4.5) — the wired-LAN wavelet
//! codec's decoder, running as plain Vulkan compute on the PRESENTER's own VkDevice (the
//! whole point: decode + CSC + present on one device, zero interop). Bypasses FFmpeg
//! entirely: the AU is one self-delimiting pyrowave packet; `push_packet` → ready →
//! `decode_gpu_buffer` recorded into OUR command buffer, submitted on the shared graphics
//! queue under the device's [`QueueLock`], fence-waited (sub-ms — Phase-0 measured
//! 0.067 ms GPU at 1080p on the RTX 5070 Ti).
//!
//! Output: three separate R8 planes (Y full-res, Cb/Cr half-res) — the decode path
//! requires STORAGE usage and IDENTITY/R swizzles, so the encoder's two-component
//! RG8 trick is not allowed here (pyrowave.h validation). The presenter samples them
//! with its planar CSC variant (BT.709 limited — the codec's fixed colour contract,
//! there is no VUI). A small ring of plane-sets keeps a decode from overwriting the set
//! the presenter is still sampling; the synchronous fence bounds decode-side reuse and
//! the ring depth covers present-side latency (≤ 12 frames in this pipeline).
//!
//! pyrowave 0.4.0 requires the instance/device create-infos to stay alive on the shared
//! device — the presenter doesn't pin its originals, so [`Hold`] reconstructs
//! content-equivalent ones from [`VulkanDecodeDevice`]'s exported extension lists,
//! feature facts and queue-family shape (pyrowave reads them for extension/feature
//! detection; pointer identity is not required).
use crate::video::{ColorDesc, VulkanDecodeDevice};
use anyhow::{bail, Context as _, Result};
use ash::vk;
use ash::vk::Handle as _;
use pyrowave_sys as pw;
use std::ffi::{c_char, c_void, CString};
use std::sync::Arc;
/// Plane-set ring depth: decode writes slot N while the presenter may still sample
/// N-1/N-2 (its own submission raced ahead under the shared queue's FIFO order, so
/// same-queue execution ordering already serializes writes vs. reads per slot; the ring
/// keeps LOGICAL reuse far enough behind).
const RING: usize = 4;
fn pw_check(r: pw::pyrowave_result, what: &str) -> Result<()> {
if r == pw::pyrowave_result_PYROWAVE_SUCCESS {
Ok(())
} else {
bail!("pyrowave {what} failed: result {r}")
}
}
/// Content-equivalent reconstruction of the presenter device's create-infos, pinned for
/// the lifetime of the `pyrowave_device` (heap boxes; moving `Hold` moves only pointers).
struct Hold {
_inst_ext_names: Vec<CString>,
_inst_ext_ptrs: Vec<*const c_char>,
_dev_ext_names: Vec<CString>,
_dev_ext_ptrs: Vec<*const c_char>,
_app_info: Box<vk::ApplicationInfo<'static>>,
instance_ci: Box<vk::InstanceCreateInfo<'static>>,
_queue_prio: Box<[f32; 1]>,
_queue_cis: Vec<vk::DeviceQueueCreateInfo<'static>>,
_feat2: Box<vk::PhysicalDeviceFeatures2<'static>>,
_v11: Box<vk::PhysicalDeviceVulkan11Features<'static>>,
_v12: Box<vk::PhysicalDeviceVulkan12Features<'static>>,
_v13: Box<vk::PhysicalDeviceVulkan13Features<'static>>,
device_ci: Box<vk::DeviceCreateInfo<'static>>,
}
impl Hold {
fn build(vkd: &VulkanDecodeDevice) -> Hold {
let inst_ext_names = vkd.instance_extensions.clone();
let inst_ext_ptrs: Vec<*const c_char> = inst_ext_names.iter().map(|c| c.as_ptr()).collect();
let dev_ext_names = vkd.device_extensions.clone();
let dev_ext_ptrs: Vec<*const c_char> = dev_ext_names.iter().map(|c| c.as_ptr()).collect();
let mut app_info =
Box::new(vk::ApplicationInfo::default().api_version(vk::API_VERSION_1_3));
let mut instance_ci = Box::new(vk::InstanceCreateInfo::default());
instance_ci.p_application_info = &mut *app_info;
instance_ci.enabled_extension_count = inst_ext_ptrs.len() as u32;
instance_ci.pp_enabled_extension_names = if inst_ext_ptrs.is_empty() {
std::ptr::null()
} else {
inst_ext_ptrs.as_ptr()
};
let queue_prio = Box::new([1.0f32]);
let mut queue_cis: Vec<vk::DeviceQueueCreateInfo<'static>> = vkd
.queue_families
.iter()
.map(|&fam| {
let mut ci = vk::DeviceQueueCreateInfo::default().queue_family_index(fam);
ci.queue_count = 1;
ci
})
.collect();
for ci in &mut queue_cis {
ci.p_queue_priorities = queue_prio.as_ptr();
}
// The feature facts the presenter enabled (VulkanDecodeDevice reports exactly
// what device creation turned on — pyrowave keys its paths off these).
let mut feat2 = Box::new(vk::PhysicalDeviceFeatures2::default());
feat2.features.shader_int16 = vkd.f_shader_int16 as u32;
let mut v11 = Box::new(
vk::PhysicalDeviceVulkan11Features::default()
.sampler_ycbcr_conversion(vkd.f_sampler_ycbcr),
);
let mut v12 = Box::new(
vk::PhysicalDeviceVulkan12Features::default()
.timeline_semaphore(vkd.f_timeline_semaphore)
.storage_buffer8_bit_access(vkd.f_storage_buffer8)
.shader_float16(vkd.f_shader_float16),
);
let mut v13 = Box::new(
vk::PhysicalDeviceVulkan13Features::default()
.synchronization2(vkd.f_synchronization2)
.subgroup_size_control(vkd.f_subgroup_size_control)
.compute_full_subgroups(vkd.f_compute_full_subgroups),
);
feat2.p_next = &mut *v11 as *mut _ as *mut c_void;
v11.p_next = &mut *v12 as *mut _ as *mut c_void;
v12.p_next = &mut *v13 as *mut _ as *mut c_void;
let mut device_ci = Box::new(vk::DeviceCreateInfo::default());
device_ci.p_next = &*feat2 as *const _ as *const c_void;
device_ci.queue_create_info_count = queue_cis.len() as u32;
device_ci.p_queue_create_infos = queue_cis.as_ptr();
device_ci.enabled_extension_count = dev_ext_ptrs.len() as u32;
device_ci.pp_enabled_extension_names = dev_ext_ptrs.as_ptr();
Hold {
_inst_ext_names: inst_ext_names,
_inst_ext_ptrs: inst_ext_ptrs,
_dev_ext_names: dev_ext_names,
_dev_ext_ptrs: dev_ext_ptrs,
_app_info: app_info,
instance_ci,
_queue_prio: queue_prio,
_queue_cis: queue_cis,
_feat2: feat2,
_v11: v11,
_v12: v12,
_v13: v13,
device_ci,
}
}
}
/// The queue-lock trampolines pyrowave calls around any internal queue use. `userdata`
/// is a raw pointer to the [`crate::video::QueueLock`] kept alive by the decoder's Arc.
unsafe extern "C" fn queue_lock_cb(ud: *mut c_void) {
// SAFETY: `ud` is the QueueLock the decoder's Arc pins; pyrowave only calls this
// while the decoder (and thus the Arc) lives.
unsafe { (*(ud as *const crate::video::QueueLock)).lock() }
}
unsafe extern "C" fn queue_unlock_cb(ud: *mut c_void) {
// SAFETY: as above.
unsafe { (*(ud as *const crate::video::QueueLock)).unlock() }
}
/// One decoded PyroWave frame: three R8 plane images on the presenter's device, GENERAL
/// layout, decode-complete (the decoder fence-waits before handing it over). `slot`
/// identifies the ring entry; the images/views live as long as the decoder.
pub struct PyroWavePlanarFrame {
/// Raw `VkImageView`s (Y, Cb, Cr) for the presenter's planar CSC sampling.
pub views: [u64; 3],
pub width: u32,
pub height: u32,
pub color: ColorDesc,
/// Every PyroWave frame is independently decodable — always a clean re-anchor.
pub keyframe: bool,
}
struct PlaneSet {
imgs: [vk::Image; 3],
mems: [vk::DeviceMemory; 3],
views: [vk::ImageView; 3],
/// First use transitions from UNDEFINED; afterwards GENERAL→GENERAL.
initialized: bool,
}
pub struct PyroWaveDecoder {
// ash wrappers reconstructed over the presenter's raw handles (not owned — the
// presenter outlives the decoder; Drop destroys only what this struct created).
device: ash::Device,
queue: vk::Queue,
_hold: Box<Hold>,
queue_lock: Arc<crate::video::QueueLock>,
pw_dev: pw::pyrowave_device,
pw_dec: pw::pyrowave_decoder,
ring: Vec<PlaneSet>,
next: usize,
cmd_pool: vk::CommandPool,
cmd: vk::CommandBuffer,
fence: vk::Fence,
width: u32,
height: u32,
}
// SAFETY: used only from the single decode thread; the shared-queue accesses go through
// QueueLock, matching the FFmpeg-Vulkan backend's threading contract.
unsafe impl Send for PyroWaveDecoder {}
impl PyroWaveDecoder {
pub fn new(vkd: &VulkanDecodeDevice, width: u32, height: u32) -> Result<PyroWaveDecoder> {
if !vkd.pyrowave_decode {
bail!("presenter device lacks the PyroWave compute feature set");
}
if width % 2 != 0 || height % 2 != 0 {
bail!("pyrowave 4:2:0 needs even dimensions (got {width}x{height})");
}
// SAFETY: the handles in `vkd` are the presenter's live instance/device (it
// outlives the decoder — same contract the FFmpeg Vulkan backend relies on);
// `Hold` pins the reconstructed create-infos for the pyrowave device's lifetime.
unsafe { Self::new_inner(vkd, width, height) }
}
unsafe fn new_inner(
vkd: &VulkanDecodeDevice,
width: u32,
height: u32,
) -> Result<PyroWaveDecoder> {
let static_fn = ash::StaticFn {
get_instance_proc_addr: std::mem::transmute::<usize, vk::PFN_vkGetInstanceProcAddr>(
vkd.get_instance_proc_addr,
),
};
let instance_h = vk::Instance::from_raw(vkd.instance as u64);
let device_h = vk::Device::from_raw(vkd.device as u64);
let entry = ash::Entry::from_static_fn(static_fn.clone());
let instance = ash::Instance::load(&static_fn, instance_h);
let device = ash::Device::load(instance.fp_v1_0(), device_h);
let queue = device.get_device_queue(vkd.graphics_qf, 0);
let _ = &entry;
let hold = Box::new(Hold::build(vkd));
let queue_lock = vkd.queue_lock.clone();
let mut queue_info = pw::pyrowave_device_create_queue_info {
queue: queue.as_raw() as usize as pw::VkQueue,
familyIndex: vkd.graphics_qf,
index: 0,
};
let create = pw::pyrowave_device_create_info {
// SAFETY(cast): re-labels the loader entry point between ash's and bindgen's
// identical C function-pointer types.
GetInstanceProcAddr: Some(std::mem::transmute::<
vk::PFN_vkGetInstanceProcAddr,
unsafe extern "C" fn(pw::VkInstance, *const c_char) -> pw::PFN_vkVoidFunction,
>(static_fn.get_instance_proc_addr)),
instance: vkd.instance as pw::VkInstance,
physical_device: vkd.physical_device as pw::VkPhysicalDevice,
device: vkd.device as pw::VkDevice,
instance_create_info: &*hold.instance_ci as *const vk::InstanceCreateInfo
as *const pw::VkInstanceCreateInfo,
device_create_info: &*hold.device_ci as *const vk::DeviceCreateInfo
as *const pw::VkDeviceCreateInfo,
queue_info: &mut queue_info,
queue_info_count: 1,
// The presenter/Skia/FFmpeg all serialize on this same lock.
queue_lock_callback: Some(queue_lock_cb),
queue_unlock_callback: Some(queue_unlock_cb),
userdata: Arc::as_ptr(&queue_lock) as *mut c_void,
};
let mut pw_dev: pw::pyrowave_device = std::ptr::null_mut();
pw_check(
pw::pyrowave_create_device(&create, &mut pw_dev),
"create_device (shared presenter device)",
)?;
let _ =
pw::pyrowave_device_set_queue_type(pw_dev, pw::VkQueueFlagBits_VK_QUEUE_COMPUTE_BIT);
let dinfo = pw::pyrowave_decoder_create_info {
device: pw_dev,
width: width as i32,
height: height as i32,
chroma: pw::pyrowave_chroma_subsampling_PYROWAVE_CHROMA_SUBSAMPLING_420,
// The fragment-iDWT path is for Mali/Adreno-class mobile GPUs only.
fragment_path: false,
};
let mut pw_dec: pw::pyrowave_decoder = std::ptr::null_mut();
if let Err(e) = pw_check(
pw::pyrowave_decoder_create(&dinfo, &mut pw_dec),
"decoder_create",
) {
pw::pyrowave_device_destroy(pw_dev);
return Err(e);
}
// Plane-set ring: 3 × R8, storage (decode writes) + sampled (presenter CSC).
let mem_props = instance.get_physical_device_memory_properties(
vk::PhysicalDevice::from_raw(vkd.physical_device as u64),
);
let make_plane = |w: u32, h: u32| -> Result<(vk::Image, vk::DeviceMemory, vk::ImageView)> {
let img = device.create_image(
&vk::ImageCreateInfo::default()
.image_type(vk::ImageType::TYPE_2D)
.format(vk::Format::R8_UNORM)
.extent(vk::Extent3D {
width: w,
height: h,
depth: 1,
})
.mip_levels(1)
.array_layers(1)
.samples(vk::SampleCountFlags::TYPE_1)
.tiling(vk::ImageTiling::OPTIMAL)
.usage(vk::ImageUsageFlags::STORAGE | vk::ImageUsageFlags::SAMPLED)
.initial_layout(vk::ImageLayout::UNDEFINED),
None,
)?;
let req = device.get_image_memory_requirements(img);
let ti = (0..mem_props.memory_type_count)
.find(|&i| {
(req.memory_type_bits & (1 << i)) != 0
&& mem_props.memory_types[i as usize]
.property_flags
.contains(vk::MemoryPropertyFlags::DEVICE_LOCAL)
})
.unwrap_or(0);
let mem = device.allocate_memory(
&vk::MemoryAllocateInfo::default()
.allocation_size(req.size)
.memory_type_index(ti),
None,
)?;
device.bind_image_memory(img, mem, 0)?;
let view = device.create_image_view(
&vk::ImageViewCreateInfo::default()
.image(img)
.view_type(vk::ImageViewType::TYPE_2D)
.format(vk::Format::R8_UNORM)
.subresource_range(vk::ImageSubresourceRange {
aspect_mask: vk::ImageAspectFlags::COLOR,
base_mip_level: 0,
level_count: 1,
base_array_layer: 0,
layer_count: 1,
}),
None,
)?;
Ok((img, mem, view))
};
let mut ring = Vec::with_capacity(RING);
for _ in 0..RING {
let (y, ym, yv) = make_plane(width, height)?;
let (cb, cbm, cbv) = make_plane(width / 2, height / 2)?;
let (cr, crm, crv) = make_plane(width / 2, height / 2)?;
ring.push(PlaneSet {
imgs: [y, cb, cr],
mems: [ym, cbm, crm],
views: [yv, cbv, crv],
initialized: false,
});
}
let cmd_pool = device.create_command_pool(
&vk::CommandPoolCreateInfo::default()
.queue_family_index(vkd.graphics_qf)
.flags(vk::CommandPoolCreateFlags::RESET_COMMAND_BUFFER),
None,
)?;
let cmd = device.allocate_command_buffers(
&vk::CommandBufferAllocateInfo::default()
.command_pool(cmd_pool)
.level(vk::CommandBufferLevel::PRIMARY)
.command_buffer_count(1),
)?[0];
let fence = device.create_fence(&vk::FenceCreateInfo::default(), None)?;
tracing::info!(
mode = %format!("{width}x{height}"),
"PyroWave decoder open on the presenter's device (compute iDWT, BT.709 limited)"
);
Ok(PyroWaveDecoder {
device,
queue,
_hold: hold,
queue_lock,
pw_dev,
pw_dec,
ring,
next: 0,
cmd_pool,
cmd,
fence,
width,
height,
})
}
/// One AU in → one frame out (the AU is a complete pyrowave frame: one packet).
pub fn decode(&mut self, au: &[u8]) -> Result<Option<PyroWavePlanarFrame>> {
// SAFETY: single decode thread; all handles owned/pinned by `self`; queue access
// serialized under the device-wide QueueLock; the fence bounds GPU completion
// before the frame is handed to the presenter.
unsafe { self.decode_inner(au) }
}
unsafe fn decode_inner(&mut self, au: &[u8]) -> Result<Option<PyroWavePlanarFrame>> {
pw_check(
pw::pyrowave_decoder_push_packet(self.pw_dec, au.as_ptr() as *const c_void, au.len()),
"push_packet",
)?;
// The reassembler delivers complete AUs only, so a frame is ready per push; a
// stale/duplicate packet (sequence rewind) simply isn't — skip, no error.
if !pw::pyrowave_decoder_decode_is_ready(self.pw_dec, false) {
return Ok(None);
}
let slot = self.next;
self.next = (self.next + 1) % RING;
let dev = self.device.clone();
dev.begin_command_buffer(
self.cmd,
&vk::CommandBufferBeginInfo::default()
.flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT),
)?;
let old_layout = if self.ring[slot].initialized {
vk::ImageLayout::GENERAL
} else {
vk::ImageLayout::UNDEFINED
};
let range = vk::ImageSubresourceRange {
aspect_mask: vk::ImageAspectFlags::COLOR,
base_mip_level: 0,
level_count: 1,
base_array_layer: 0,
layer_count: 1,
};
let to_write = |img| {
vk::ImageMemoryBarrier2::default()
// Order against the presenter's prior sampling of this slot (same queue).
.src_stage_mask(vk::PipelineStageFlags2::FRAGMENT_SHADER)
.src_access_mask(vk::AccessFlags2::NONE)
.dst_stage_mask(vk::PipelineStageFlags2::COMPUTE_SHADER)
.dst_access_mask(vk::AccessFlags2::SHADER_STORAGE_WRITE)
.old_layout(old_layout)
.new_layout(vk::ImageLayout::GENERAL)
.image(img)
.subresource_range(range)
};
let pre: Vec<_> = self.ring[slot].imgs.iter().map(|&i| to_write(i)).collect();
dev.cmd_pipeline_barrier2(
self.cmd,
&vk::DependencyInfo::default().image_memory_barriers(&pre),
);
let plane = |img: vk::Image, w: u32, h: u32| pw::pyrowave_image_view {
image: img.as_raw() as usize as pw::VkImage,
width: w,
height: h,
image_format: pw::VkFormat_VK_FORMAT_R8_UNORM,
view_format: pw::VkFormat_VK_FORMAT_R8_UNORM,
mip_level: 0,
layer: 0,
aspect: pw::VkImageAspectFlagBits_VK_IMAGE_ASPECT_COLOR_BIT,
swizzle: pw::VkComponentSwizzle_VK_COMPONENT_SWIZZLE_IDENTITY,
layout: pw::VkImageLayout_VK_IMAGE_LAYOUT_GENERAL,
};
let (w, h) = (self.width, self.height);
let buffers = pw::pyrowave_gpu_buffers {
planes: [
plane(self.ring[slot].imgs[0], w, h),
plane(self.ring[slot].imgs[1], w / 2, h / 2),
plane(self.ring[slot].imgs[2], w / 2, h / 2),
],
};
pw::pyrowave_device_set_command_buffer(
self.pw_dev,
self.cmd.as_raw() as usize as pw::VkCommandBuffer,
);
let dec_res = pw::pyrowave_decoder_decode_gpu_buffer(
self.pw_dec,
std::ptr::null(),
std::ptr::null(),
&buffers,
);
pw::pyrowave_device_set_command_buffer(self.pw_dev, std::ptr::null_mut());
pw_check(dec_res, "decode_gpu_buffer")?;
// Decode's storage writes → the presenter's fragment sampling (layout stays
// GENERAL: that is what the planar CSC descriptors use for this path).
let to_read = |img| {
vk::ImageMemoryBarrier2::default()
.src_stage_mask(vk::PipelineStageFlags2::COMPUTE_SHADER)
.src_access_mask(vk::AccessFlags2::SHADER_STORAGE_WRITE)
.dst_stage_mask(vk::PipelineStageFlags2::FRAGMENT_SHADER)
.dst_access_mask(vk::AccessFlags2::SHADER_SAMPLED_READ)
.old_layout(vk::ImageLayout::GENERAL)
.new_layout(vk::ImageLayout::GENERAL)
.image(img)
.subresource_range(range)
};
let post: Vec<_> = self.ring[slot].imgs.iter().map(|&i| to_read(i)).collect();
dev.cmd_pipeline_barrier2(
self.cmd,
&vk::DependencyInfo::default().image_memory_barriers(&post),
);
dev.end_command_buffer(self.cmd)?;
dev.reset_fences(&[self.fence])?;
{
let _guard = self.queue_lock.guard();
let cmds = [self.cmd];
dev.queue_submit(
self.queue,
&[vk::SubmitInfo::default().command_buffers(&cmds)],
self.fence,
)?;
}
dev.wait_for_fences(&[self.fence], true, 5_000_000_000)
.context("pyrowave decode fence")?;
self.ring[slot].initialized = true;
Ok(Some(PyroWavePlanarFrame {
views: [
self.ring[slot].views[0].as_raw(),
self.ring[slot].views[1].as_raw(),
self.ring[slot].views[2].as_raw(),
],
width: w,
height: h,
// No VUI in the bitstream: BT.709 limited is the fixed contract with the
// host's CSC (plan §4.7 CscRows note; sequence-header signaling is a
// follow-up once the C API exposes it).
color: ColorDesc {
primaries: 1,
transfer: 1,
matrix: 1,
full_range: false,
},
keyframe: true,
}))
}
}
impl Drop for PyroWaveDecoder {
fn drop(&mut self) {
// SAFETY: owned handles created by this struct on the presenter's device; the
// fence-synchronous decode means no work of OURS is in flight, and the presenter
// may still be sampling the last handed-over slot — idle the device's queue
// under the shared lock before destroying the plane images.
unsafe {
{
let _guard = self.queue_lock.guard();
let _ = self.device.queue_wait_idle(self.queue);
}
pw::pyrowave_decoder_destroy(self.pw_dec);
pw::pyrowave_device_destroy(self.pw_dev);
for set in &self.ring {
for v in set.views {
self.device.destroy_image_view(v, None);
}
for i in set.imgs {
self.device.destroy_image(i, None);
}
for m in set.mems {
self.device.free_memory(m, None);
}
}
self.device.destroy_fence(self.fence, None);
self.device.destroy_command_pool(self.cmd_pool, None);
// `self.device`/instance are the PRESENTER's — never destroyed here.
}
}
}
+5
View File
@@ -43,3 +43,8 @@ windows-sys = { version = "0.61", features = [
"Win32_UI_Shell",
"Win32_UI_WindowsAndMessaging",
] }
[features]
# PyroWave planar present path (the wired-LAN wavelet codec) — forwards to the decode
# backend in pf-client-core; OFF by default.
pyrowave = ["pf-client-core/pyrowave"]
@@ -0,0 +1,84 @@
// Planar 3-plane YCbCr → RGBA — the PyroWave variant of nv12_csc.frag (separate Cb and
// Cr R8 planes instead of an interleaved CbCr plane; design/pyrowave-codec-plan.md §4.5).
// Same push-constant contract (csc_rows precomputes the matrix + range expansion), same
// output modes — though PyroWave itself is 8-bit SDR BT.709 limited, keeping parity means
// one less divergence if the codec ever signals more. 4:4:4 needs no shader change: the
// chroma planes arrive full-res and the siting correction self-disables.
//
// Regenerate: shaders/build.sh (committed .spv, no build-time toolchain).
#version 450
layout(location = 0) in vec2 v_uv;
layout(location = 0) out vec4 frag;
layout(set = 0, binding = 0) uniform sampler2D u_y;
layout(set = 0, binding = 1) uniform sampler2D u_cb;
layout(set = 0, binding = 2) uniform sampler2D u_cr;
layout(push_constant) uniform Csc {
vec4 r0;
vec4 r1;
vec4 r2;
vec4 params; // x: mode, y: tonemap peak, z/w: reserved
} pc;
// SMPTE ST.2084 (PQ) EOTF: code value → display-referred linear, normalized to 1.0 =
// 10000 nits.
vec3 pq_eotf(vec3 e) {
const float m1 = 0.1593017578125; // 2610/16384
const float m2 = 78.84375; // 2523/4096 * 128
const float c1 = 0.8359375; // 3424/4096
const float c2 = 18.8515625; // 2413/4096 * 32
const float c3 = 18.6875; // 2392/4096 * 32
vec3 p = pow(max(e, vec3(0.0)), vec3(1.0 / m2));
return pow(max(p - c1, vec3(0.0)) / (c2 - c3 * p), vec3(1.0 / m1));
}
// BT.2020 → BT.709 primaries (linear light).
vec3 bt2020_to_709(vec3 c) {
return mat3(
1.6605, -0.1246, -0.0182,
-0.5876, 1.1329, -0.1006,
-0.0728, -0.0083, 1.1187
) * c;
}
// Linear → sRGB OETF.
vec3 srgb_oetf(vec3 c) {
c = clamp(c, 0.0, 1.0);
bvec3 lo = lessThanEqual(c, vec3(0.0031308));
vec3 hi = 1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055;
return mix(hi, c * 12.92, vec3(lo));
}
void main() {
// Left-cosited 4:2:0 chroma sampled at luma UV assumes CENTER siting — offset +0.25
// chroma texels to re-align (same correction as nv12_csc.frag; self-disables when the
// chroma plane is full-res).
vec2 cuv = v_uv;
int cw = textureSize(u_cb, 0).x;
if (cw < textureSize(u_y, 0).x) {
cuv.x += 0.25 / float(cw);
}
vec3 yuv = vec3(texture(u_y, v_uv).r, texture(u_cb, cuv).r, texture(u_cr, cuv).r);
vec3 rgb = vec3(
dot(pc.r0.xyz, yuv) + pc.r0.w,
dot(pc.r1.xyz, yuv) + pc.r1.w,
dot(pc.r2.xyz, yuv) + pc.r2.w
);
if (pc.params.x > 0.5) {
vec3 lin = pq_eotf(clamp(rgb, 0.0, 1.0)) * (10000.0 / 203.0);
lin = max(bt2020_to_709(lin), vec3(0.0));
float peak = max(pc.params.y, 1.0001);
float l = max(lin.r, max(lin.g, lin.b));
if (l > 1.0) {
float mapped = 1.0 + (l - 1.0) / (1.0 + (l - 1.0) / (peak - 1.0));
lin *= mapped / l;
}
rgb = srgb_oetf(lin);
} else {
rgb = clamp(rgb, 0.0, 1.0);
}
frag = vec4(rgb, 1.0);
}
Binary file not shown.
+58 -16
View File
@@ -32,6 +32,32 @@ impl CscPass {
/// `attachment_format` = the video image's format: R8G8B8A8 for SDR, a 10-bit
/// format when the pass writes PQ (8 bits would band the PQ curve visibly).
pub fn new(device: &ash::Device, attachment_format: vk::Format) -> Result<CscPass> {
Self::build(
device,
attachment_format,
2,
include_bytes!("../shaders/nv12_csc.frag.spv"),
)
}
/// The planar 3-plane variant (separate Cb/Cr R8 planes — the PyroWave decode
/// output, design/pyrowave-codec-plan.md §4.5). Same push-constant contract.
#[cfg(feature = "pyrowave")]
pub fn new_planar(device: &ash::Device, attachment_format: vk::Format) -> Result<CscPass> {
Self::build(
device,
attachment_format,
3,
include_bytes!("../shaders/planar_csc.frag.spv"),
)
}
fn build(
device: &ash::Device,
attachment_format: vk::Format,
plane_bindings: u32,
frag_spv: &[u8],
) -> Result<CscPass> {
// One color attachment: the presenter's video image. Content is fully
// overwritten (DONT_CARE load), and the pass ends in TRANSFER_SRC so the
// existing letterbox blit consumes it with no extra barrier.
@@ -89,20 +115,16 @@ impl CscPass {
}?;
let samplers = [sampler];
let bindings = [
vk::DescriptorSetLayoutBinding::default()
.binding(0)
.descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
.descriptor_count(1)
.stage_flags(vk::ShaderStageFlags::FRAGMENT)
.immutable_samplers(&samplers),
vk::DescriptorSetLayoutBinding::default()
.binding(1)
.descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
.descriptor_count(1)
.stage_flags(vk::ShaderStageFlags::FRAGMENT)
.immutable_samplers(&samplers),
];
let bindings: Vec<vk::DescriptorSetLayoutBinding> = (0..plane_bindings)
.map(|b| {
vk::DescriptorSetLayoutBinding::default()
.binding(b)
.descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
.descriptor_count(1)
.stage_flags(vk::ShaderStageFlags::FRAGMENT)
.immutable_samplers(&samplers)
})
.collect();
let set_layout = unsafe {
device.create_descriptor_set_layout(
&vk::DescriptorSetLayoutCreateInfo::default().bindings(&bindings),
@@ -124,7 +146,7 @@ impl CscPass {
let pool_sizes = [vk::DescriptorPoolSize::default()
.ty(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
.descriptor_count(2)];
.descriptor_count(plane_bindings)];
let desc_pool = unsafe {
device.create_descriptor_pool(
&vk::DescriptorPoolCreateInfo::default()
@@ -145,7 +167,7 @@ impl CscPass {
device,
render_pass,
pipeline_layout,
include_bytes!("../shaders/nv12_csc.frag.spv"),
frag_spv,
false, // opaque — the CSC output IS the video
)?;
@@ -184,6 +206,26 @@ impl CscPass {
unsafe { device.update_descriptor_sets(&writes, &[]) };
}
/// Planar variant of [`bind_planes`](Self::bind_planes): three single-component
/// plane views in GENERAL layout (the pyrowave decode leaves them there; same
/// fence-wait safety contract).
#[cfg(feature = "pyrowave")]
pub fn bind_planes_planar(&self, device: &ash::Device, planes: [vk::ImageView; 3]) {
let infos = planes.map(|view| {
[vk::DescriptorImageInfo::default()
.image_view(view)
.image_layout(vk::ImageLayout::GENERAL)]
});
let writes = [0u32, 1, 2].map(|b| {
vk::WriteDescriptorSet::default()
.dst_set(self.desc_set)
.dst_binding(b)
.descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
.image_info(&infos[b as usize])
});
unsafe { device.update_descriptor_sets(&writes, &[]) };
}
pub fn destroy(&self, device: &ash::Device) {
unsafe {
device.destroy_pipeline(self.pipeline, None);
+22
View File
@@ -968,6 +968,28 @@ fn run_inner(mut opts: SessionOpts, mut mode: ModeCtl) -> Result<Option<Outcome>
image,
} = f;
let did_present = match image {
// PyroWave planar frames: already on the presenter's device and
// fence-complete — a present failure has no demote rung (nothing
// else decodes the codec); only device loss ends the session.
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
DecodedImage::PyroWave(f) => {
st.hdr = false; // 8-bit SDR codec
match presenter.present(
&window,
FrameInput::PyroWave(f),
overlay_frame.as_ref(),
) {
Ok(p) => p,
Err(e) => {
if device_lost(&e) {
return Err(e)
.context("GPU device lost — the session cannot continue");
}
tracing::warn!(error = %format!("{e:#}"), "pyrowave present failed");
false
}
}
}
DecodedImage::Cpu(c) => {
st.hdr = c.color.is_pq();
presenter.present(&window, FrameInput::Cpu(&c), overlay_frame.as_ref())?
+168 -4
View File
@@ -40,6 +40,10 @@ pub enum FrameInput<'a> {
/// D3D11VA hand-off — a shareable NT-handle texture to import (`d3d11.rs`).
#[cfg(windows)]
D3d11(pf_client_core::video::D3d11Frame),
/// PyroWave planar output — three R8 plane views already on THIS device, decode
/// fence-complete, GENERAL layout (`pf_client_core::video_pyrowave`).
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
PyroWave(pf_client_core::video_pyrowave::PyroWavePlanarFrame),
}
/// The dmabuf/CSC machinery, present only when the device carries the import extensions.
@@ -321,6 +325,10 @@ pub struct Presenter {
#[cfg(windows)]
hw_win: Option<HwCtxWin>,
csc: CscPass,
/// The planar (3-plane) CSC variant for PyroWave frames; built only when the device
/// passed the pyrowave probe.
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
csc_planar: Option<CscPass>,
/// FFmpeg Vulkan Video decode handles — `None` when the stack can't do it.
video_export: Option<pf_client_core::video::VulkanDecodeDevice>,
/// The console-UI composite quad (§6.1's presenter half).
@@ -497,9 +505,23 @@ impl Presenter {
.push_next(&mut have_f12)
.push_next(&mut have_f13);
unsafe { instance.get_physical_device_features2(pdev, &mut have_f2) };
// Copy the one base-features fact out NOW: `have_f2` mutably borrows the 11/12/13
// structs through its pNext chain, so any later use of it would pin those borrows.
let have_shader_int16 = have_f2.features.shader_int16;
let features_ok = have_f11.sampler_ycbcr_conversion == vk::TRUE
&& have_f12.timeline_semaphore == vk::TRUE
&& have_f13.synchronization2 == vk::TRUE;
// PyroWave decode (the wired-LAN wavelet codec, design/pyrowave-codec-plan.md §4.5):
// plain Vulkan-1.3 compute on THIS device — no video extensions. Probed alongside so a
// capable device gets the features enabled below and advertises the codec; anything
// less simply never sets the CODEC_PYROWAVE bit.
let pyrowave_ok = dev_is_13
&& have_shader_int16 == vk::TRUE
&& have_f12.storage_buffer8_bit_access == vk::TRUE
&& have_f12.timeline_semaphore == vk::TRUE
&& have_f13.subgroup_size_control == vk::TRUE
&& have_f13.compute_full_subgroups == vk::TRUE
&& have_f13.synchronization2 == vk::TRUE;
// The decode queue family + which codec operations it can run.
let decode_family: Option<(u32, vk::VideoCodecOperationFlagsKHR)> = {
@@ -575,13 +597,18 @@ impl Presenter {
let mut en_f11 = vk::PhysicalDeviceVulkan11Features::default()
.sampler_ycbcr_conversion(have_f11.sampler_ycbcr_conversion == vk::TRUE);
let mut en_f12 = vk::PhysicalDeviceVulkan12Features::default()
.timeline_semaphore(have_f12.timeline_semaphore == vk::TRUE);
.timeline_semaphore(have_f12.timeline_semaphore == vk::TRUE)
.storage_buffer8_bit_access(pyrowave_ok)
.shader_float16(pyrowave_ok && have_f12.shader_float16 == vk::TRUE);
let mut en_f13 = vk::PhysicalDeviceVulkan13Features::default()
.synchronization2(have_f13.synchronization2 == vk::TRUE);
.synchronization2(have_f13.synchronization2 == vk::TRUE)
.subgroup_size_control(pyrowave_ok)
.compute_full_subgroups(pyrowave_ok);
let mut en_f2 = vk::PhysicalDeviceFeatures2::default()
.push_next(&mut en_f11)
.push_next(&mut en_f12)
.push_next(&mut en_f13);
en_f2.features.shader_int16 = if pyrowave_ok { vk::TRUE } else { vk::FALSE };
let priorities = [1.0f32];
let mut queue_info = vec![vk::DeviceQueueCreateInfo::default()
@@ -622,6 +649,13 @@ impl Presenter {
ext_mem_win32: ash::khr::external_memory_win32::Device::new(&instance, &device),
});
let csc = CscPass::new(&device, vk::Format::R8G8B8A8_UNORM)?;
// PyroWave is 8-bit SDR only, so the planar pass never needs the HDR10 rebuild.
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
let csc_planar = if pyrowave_ok {
Some(CscPass::new_planar(&device, vk::Format::R8G8B8A8_UNORM)?)
} else {
None
};
// The exported handle bundle: FFmpeg Vulkan Video handles when the device can
// decode, AND (Windows) the D3D11-interop facts — so it's built whenever EITHER
@@ -632,9 +666,9 @@ impl Presenter {
// all funnel their queue calls through it — see the `queue_lock` field docs).
let queue_lock = std::sync::Arc::new(pf_client_core::video::QueueLock::new());
#[cfg(windows)]
let export_worthy = video_ok || win_capable;
let export_worthy = video_ok || win_capable || pyrowave_ok;
#[cfg(not(windows))]
let export_worthy = video_ok;
let export_worthy = video_ok || pyrowave_ok;
let video_export = if export_worthy {
let qf_props = unsafe { instance.get_physical_device_queue_family_properties(pdev) };
let mut device_extensions: Vec<CString> =
@@ -678,6 +712,14 @@ impl Presenter {
f_sampler_ycbcr: have_f11.sampler_ycbcr_conversion == vk::TRUE,
f_timeline_semaphore: have_f12.timeline_semaphore == vk::TRUE,
f_synchronization2: have_f13.synchronization2 == vk::TRUE,
f_shader_int16: pyrowave_ok,
f_storage_buffer8: pyrowave_ok,
f_subgroup_size_control: pyrowave_ok,
f_compute_full_subgroups: pyrowave_ok,
f_shader_float16: pyrowave_ok && have_f12.shader_float16 == vk::TRUE,
api_version: dev_props.api_version,
queue_families: queue_info.iter().map(|q| q.queue_family_index).collect(),
pyrowave_decode: pyrowave_ok,
video_decode: video_ok,
#[cfg(windows)]
d3d11_import: win_capable,
@@ -742,6 +784,8 @@ impl Presenter {
#[cfg(windows)]
hw_win,
csc,
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
csc_planar,
video_export,
overlay_pipe,
retired_hw: None,
@@ -1017,6 +1061,10 @@ impl Presenter {
vk::Format::R8G8B8A8_UNORM
};
self.csc.destroy(&self.device); // fence-safe: only our cmd bufs reference it
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
if let Some(p) = &self.csc_planar {
p.destroy(&self.device);
}
self.csc = CscPass::new(&self.device, self.video_format)?;
if let Some(v) = self.video.take() {
unsafe {
@@ -1075,6 +1123,8 @@ impl Presenter {
FrameInput::VkFrame(v) => Some(v.color.is_pq()),
#[cfg(windows)]
FrameInput::D3d11(d) => Some(d.color.is_pq()),
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
FrameInput::PyroWave(f) => Some(f.color.is_pq()), // always SDR today
};
if let Some(pq) = frame_pq {
// A PQ stream we can only tone-map (no HDR10 surface) is the silent failure behind
@@ -1103,6 +1153,8 @@ impl Presenter {
#[cfg(windows)]
let mut win_frame: Option<crate::d3d11::HwFrame> = None;
let mut vk_frame: Option<(VkVideoFrame, [vk::ImageView; 2])> = None;
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
let mut pyro_frame: Option<pf_client_core::video_pyrowave::PyroWavePlanarFrame> = None;
let cpu_frame = match input {
FrameInput::Redraw => None,
FrameInput::Cpu(f) => Some(f),
@@ -1129,6 +1181,11 @@ impl Presenter {
vk_frame = Some((v, views));
None
}
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
FrameInput::PyroWave(f) => {
pyro_frame = Some(f);
None
}
};
// One frame in flight: the fence covers the command buffer, the staging buffer
@@ -1183,6 +1240,22 @@ impl Presenter {
}
self.csc.bind_planes(&self.device, views[0], views[1]);
}
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
if let Some(f) = &pyro_frame {
if self
.video
.as_ref()
.is_none_or(|v| v.width != f.width || v.height != f.height)
{
self.rebuild_video_image(f.width, f.height)?;
tracing::info!(width = f.width, height = f.height, "video image (re)built");
}
let planar = self
.csc_planar
.as_ref()
.context("PyroWave frame but the device failed the pyrowave probe")?;
planar.bind_planes_planar(&self.device, f.views.map(|v| vk::ImageView::from_raw(v)));
}
if let Some(o) = overlay {
// Point the composite at this overlay image (same fence-wait safety).
let infos = [vk::DescriptorImageInfo::default()
@@ -1325,6 +1398,18 @@ impl Presenter {
vk_sync = Some(sync);
}
// PyroWave frame: the planes are already on THIS device, decode
// fence-complete and barriered to fragment sampling (GENERAL) by the
// decoder — no acquire needed, just the planar CSC pass.
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
if let (Some(f), Some(v)) = (&pyro_frame, &self.video) {
let extent = vk::Extent2D {
width: v.width,
height: v.height,
};
self.record_csc_planar(v.framebuffer, extent, f.color);
}
// New frame: staging → video image (stride carried by buffer_row_length).
if let (Some(f), Some(v), Some(s)) = (cpu_frame, &self.video, &self.staging) {
barrier(
@@ -1681,6 +1766,81 @@ impl Presenter {
}
}
/// [`record_csc`] over the planar (PyroWave) pass — always 8-bit, no MSB packing.
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
unsafe fn record_csc_planar(
&self,
framebuffer: vk::Framebuffer,
extent: vk::Extent2D,
color: pf_client_core::video::ColorDesc,
) {
// The planar pass exists whenever a PyroWave frame reached us (checked at bind).
let Some(planar) = self.csc_planar.as_ref() else {
return;
};
unsafe {
self.device.cmd_begin_render_pass(
self.cmd_buf,
&vk::RenderPassBeginInfo::default()
.render_pass(planar.render_pass)
.framebuffer(framebuffer)
.render_area(vk::Rect2D {
offset: vk::Offset2D { x: 0, y: 0 },
extent,
}),
vk::SubpassContents::INLINE,
);
self.device.cmd_bind_pipeline(
self.cmd_buf,
vk::PipelineBindPoint::GRAPHICS,
planar.pipeline,
);
self.device.cmd_set_viewport(
self.cmd_buf,
0,
&[vk::Viewport {
x: 0.0,
y: 0.0,
width: extent.width as f32,
height: extent.height as f32,
min_depth: 0.0,
max_depth: 1.0,
}],
);
self.device.cmd_set_scissor(
self.cmd_buf,
0,
&[vk::Rect2D {
offset: vk::Offset2D { x: 0, y: 0 },
extent,
}],
);
self.device.cmd_bind_descriptor_sets(
self.cmd_buf,
vk::PipelineBindPoint::GRAPHICS,
planar.pipeline_layout,
0,
&[planar.desc_set],
&[],
);
let rows = csc_rows(color, 8, false);
let mut pc = [0f32; 16];
pc[..12].copy_from_slice(bytemuck_rows(&rows));
pc[12] = 0.0; // SDR passthrough — PyroWave has no PQ path
pc[13] = 0.0;
let bytes = std::slice::from_raw_parts(pc.as_ptr().cast::<u8>(), 64);
self.device.cmd_push_constants(
self.cmd_buf,
planar.pipeline_layout,
vk::ShaderStageFlags::FRAGMENT,
0,
bytes,
);
self.device.cmd_draw(self.cmd_buf, 3, 1, 0, 0);
self.device.cmd_end_render_pass(self.cmd_buf);
}
}
/// Per-plane views over a Vulkan-Video frame's multiplanar image — the CSC pass's
/// exact sampling contract (the frames pool was created MUTABLE_FORMAT for this).
/// 8-bit NV12 (R8 + R8G8) and 10-bit P010/X6 (R10X6 + R10X6G10X6).
@@ -1929,6 +2089,10 @@ impl Drop for Presenter {
#[cfg(target_os = "linux")]
self.hw.take();
self.csc.destroy(&self.device);
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
if let Some(p) = &self.csc_planar {
p.destroy(&self.device);
}
self.overlay_pipe.destroy(&self.device);
for s in self.render_sems.drain(..) {
self.device.destroy_semaphore(s, None);
+5
View File
@@ -928,6 +928,10 @@ pub const PUNKTFUNK_CODEC_H264: u8 = 0x01;
pub const PUNKTFUNK_CODEC_HEVC: u8 = 0x02;
/// Codec bit: AV1. (Mirrors `quic::CODEC_AV1`.)
pub const PUNKTFUNK_CODEC_AV1: u8 = 0x04;
/// Codec bit: PyroWave — the opt-in wired-LAN intra-only wavelet codec. Never auto-selected:
/// the host picks it ONLY when the client also passes it as `preferred_codec`
/// (design/pyrowave-codec-plan.md §3). (Mirrors `quic::CODEC_PYROWAVE`.)
pub const PUNKTFUNK_CODEC_PYROWAVE: u8 = 0x08;
// Keep the ABI cap bits in lockstep with the wire constants (compile-time guard against drift).
#[cfg(feature = "quic")]
@@ -938,6 +942,7 @@ const _: () = {
assert!(PUNKTFUNK_CODEC_H264 == crate::quic::CODEC_H264);
assert!(PUNKTFUNK_CODEC_HEVC == crate::quic::CODEC_HEVC);
assert!(PUNKTFUNK_CODEC_AV1 == crate::quic::CODEC_AV1);
assert!(PUNKTFUNK_CODEC_PYROWAVE == crate::quic::CODEC_PYROWAVE);
};
// Keep the ABI gamepad constants in lockstep with the wire enum (compile-time guard against drift).
+16 -3
View File
@@ -151,15 +151,28 @@ pub const CODEC_H264: u8 = 0x01;
pub const CODEC_HEVC: u8 = 0x02;
/// [`Hello::video_codecs`] bit: the client can decode AV1.
pub const CODEC_AV1: u8 = 0x04;
/// [`Hello::video_codecs`] bit: the client can decode **PyroWave** — the opt-in wired-LAN
/// intra-only wavelet codec (design/pyrowave-codec-plan.md; 100400 Mbps class, 8-bit SDR,
/// every frame independently decodable). Deliberately **absent from [`resolve_codec`]'s
/// precedence ladder**: it is selected only when the client also names it
/// [`Hello::preferred_codec`] (or the host operator forces the advertisement mask) — a codec
/// that needs a wired-LAN bitrate must never win a negotiation just because both ends support
/// it. The bit means "PyroWave bitstream as of the punktfunk-vendored pin"
/// (`crates/pyrowave-sys/vendor/pyrowave/PUNKTFUNK-VENDOR.txt`): upstream has no bitstream
/// version field, so a vendored bump that changes the bitstream bumps the punktfunk protocol
/// version instead (plan §4.2).
pub const CODEC_PYROWAVE: u8 = 0x08;
/// Resolve which single codec the host will emit, from the client's advertised [`Hello::video_codecs`]
/// bitfield (`0` = an older client, treated as HEVC-only) intersected with what the host's chosen
/// encoder can produce (`host_capable`, also a bitfield). `preferred` is the client's soft preference
/// ([`Hello::preferred_codec`], `0` = none): when it's in the shared set it wins; otherwise the tie is
/// broken by **HEVC > AV1 > H.264** (HEVC is the established, best-tested path; H.264 is the
/// compatibility / software floor). Returns the single-bit codec value, or `None` when client and host
/// share nothing — the caller then refuses the session with a clear error rather than emitting a
/// stream the client can't decode.
/// compatibility / software floor). [`CODEC_PYROWAVE`] is intentionally NOT in that ladder — it can
/// only be returned via the `preferred` path (plan §3: opt-in, pinned, honest). Returns the
/// single-bit codec value, or `None` when client and host share nothing the ladder may pick — the
/// caller then refuses the session with a clear error rather than emitting a stream the client
/// can't decode.
pub fn resolve_codec(client_codecs: u8, host_capable: u8, preferred: u8) -> Option<u8> {
// An older client (no codec byte) decodes HEVC — the only codec every pre-negotiation build sent.
let client = if client_codecs == 0 {
+24
View File
@@ -101,6 +101,30 @@ fn codec_negotiation_and_back_compat() {
// A preference the host can't emit still can't rescue a no-shared-codec case.
assert_eq!(resolve_codec(CODEC_HEVC, CODEC_H264, CODEC_HEVC), None);
// PyroWave is opt-in ONLY (plan §3): mutual support NEVER auto-selects it — the ladder
// ignores it entirely...
assert_eq!(
resolve_codec(CODEC_HEVC | CODEC_PYROWAVE, CODEC_HEVC | CODEC_PYROWAVE, 0),
Some(CODEC_HEVC)
);
// ...even when it is the ONLY shared codec (an all-intra 200 Mbps stream must never be a
// silent fallback)...
assert_eq!(resolve_codec(CODEC_PYROWAVE, CODEC_PYROWAVE, 0), None);
// ...it is reachable exclusively through the client's explicit preference.
assert_eq!(
resolve_codec(
CODEC_HEVC | CODEC_PYROWAVE,
CODEC_HEVC | CODEC_PYROWAVE,
CODEC_PYROWAVE
),
Some(CODEC_PYROWAVE)
);
// A pyrowave preference against a host without the backend falls back to the ladder.
assert_eq!(
resolve_codec(CODEC_HEVC | CODEC_PYROWAVE, CODEC_HEVC, CODEC_PYROWAVE),
Some(CODEC_HEVC)
);
// A Hello advertising codecs roundtrips, and the wire form of a codec-only Hello decodes on
// a build that ignores the trailing byte (back-compat: extra bytes are skipped).
let h = Hello {
+8
View File
@@ -80,6 +80,9 @@ log = "0.4"
# crate vendors libopus (cmake-built from source — no system lib, no vcpkg), so it builds on Windows
# MSVC too (needs CMake + NASM, both on the box). Both platforms that have an audio-capture backend.
[target.'cfg(any(target_os = "linux", target_os = "windows"))'.dependencies]
# PyroWave (opt-in wired-LAN wavelet codec) — vendored codec + bindgen'd C API, only compiled
# under `--features pyrowave`. Stub-empty on other targets, so the cfg here is belt-and-braces.
pyrowave-sys = { path = "../pyrowave-sys", optional = true }
opus = "0.3"
# Software H.264 encoder — the GPU-less encode path on both Linux and Windows (and a fallback when no
# hardware encoder is available). The default `source` feature statically compiles OpenH264 (BSD-2) —
@@ -265,6 +268,11 @@ amf-qsv = ["dep:ffmpeg-next"]
# bindings already carried for the dmabuf zero-copy bridge). Runtime-gated further by
# PUNKTFUNK_VULKAN_ENCODE (opt-in for now). Build the AMD/Intel RFI host with `--features vulkan-encode`.
vulkan-encode = []
# PyroWave — the opt-in wired-LAN intra-only wavelet codec (design/pyrowave-codec-plan.md).
# Builds the vendored codec from source (crates/pyrowave-sys, CMake + bindgen; Linux/Windows).
# OFF by default; runtime-gated further behind an explicit PUNKTFUNK_ENCODER=pyrowave.
# EXPERIMENTAL until CODEC_PYROWAVE negotiation lands (plan Phase 2).
pyrowave = ["dep:pyrowave-sys"]
# Build-time icon/version-info embedding (build.rs; Windows dev/CI hosts only — Linux packaging
# builds of this crate never execute the winresource block).
+148 -39
View File
@@ -37,6 +37,10 @@ pub enum Codec {
H264,
H265,
Av1,
/// PyroWave — the opt-in wired-LAN intra-only wavelet codec (design/pyrowave-codec-plan.md).
/// Only ever negotiated via the client's explicit `preferred_codec` (never the precedence
/// ladder) and only emitted by the `pyrowave`-feature backend; every AU is a keyframe.
PyroWave,
}
/// Chroma subsampling the encoder emits, negotiated with the client (the `PUNKTFUNK_444` gate + the
@@ -73,6 +77,7 @@ impl Codec {
match bit {
punktfunk_core::quic::CODEC_H264 => Codec::H264,
punktfunk_core::quic::CODEC_AV1 => Codec::Av1,
punktfunk_core::quic::CODEC_PYROWAVE => Codec::PyroWave,
_ => Codec::H265,
}
}
@@ -83,6 +88,7 @@ impl Codec {
Codec::H264 => punktfunk_core::quic::CODEC_H264,
Codec::H265 => punktfunk_core::quic::CODEC_HEVC,
Codec::Av1 => punktfunk_core::quic::CODEC_AV1,
Codec::PyroWave => punktfunk_core::quic::CODEC_PYROWAVE,
}
}
@@ -97,49 +103,74 @@ impl Codec {
/// still lands on HEVC for an auto client, exactly the pre-probe behaviour. Fed to
/// [`punktfunk_core::quic::resolve_codec`] against the client's advertised codecs.
pub fn host_wire_caps() -> u8 {
/// The static GPU superset (H.264 | HEVC | AV1) — mirrors the GameStream
/// `SERVER_CODEC_MODE_SUPPORT` advertisement for the unprobed backends.
const GPU_SUPERSET: u8 = punktfunk_core::quic::CODEC_H264
| punktfunk_core::quic::CODEC_HEVC
| punktfunk_core::quic::CODEC_AV1;
#[cfg(target_os = "linux")]
{
if matches!(
// PyroWave rides ON TOP of whatever H.26x set resolves below: feature-gated, Linux-only
// for now (the Windows host leg is blocked on the .173 D3D11 interop debt), and inert in
// negotiation unless the client explicitly prefers it (resolve_codec ignores the bit in
// its ladder). Advertised only when the capture side would actually deliver frames the
// backend ingests (raw-dmabuf passthrough / CPU RGB): `linux_zero_copy_is_vaapi()` —
// true on AMD/Intel auto and under an explicit PUNKTFUNK_ENCODER=pyrowave. On an NVIDIA
// host with `auto`, capture resolves to the EGL→CUDA import the backend can't consume,
// so the bit stays off until the OutputFormat plumbing carries a per-session
// raw-dmabuf decision (Phase 3); the operator opts in with the env instead (plan §3).
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
let pyro = if linux_zero_copy_is_vaapi()
&& !matches!(
crate::config::config().encoder_pref.as_str(),
// A software pref usually means a GPU-less box — no Vulkan device to open.
"software" | "sw" | "openh264"
) {
return punktfunk_core::quic::CODEC_H264;
punktfunk_core::quic::CODEC_PYROWAVE
} else {
0u8
};
#[cfg(not(all(target_os = "linux", feature = "pyrowave")))]
let pyro = 0u8;
let base = (|| {
/// The static GPU superset (H.264 | HEVC | AV1) — mirrors the GameStream
/// `SERVER_CODEC_MODE_SUPPORT` advertisement for the unprobed backends.
const GPU_SUPERSET: u8 = punktfunk_core::quic::CODEC_H264
| punktfunk_core::quic::CODEC_HEVC
| punktfunk_core::quic::CODEC_AV1;
#[cfg(target_os = "linux")]
{
if matches!(
crate::config::config().encoder_pref.as_str(),
"software" | "sw" | "openh264"
) {
return punktfunk_core::quic::CODEC_H264;
}
if linux_zero_copy_is_vaapi() {
if let Some(m) = vaapi_codec_support().wire_mask() {
return m;
}
}
// NVENC (static superset, like GameStream) — or an empty VAAPI probe (see above).
GPU_SUPERSET
}
if linux_zero_copy_is_vaapi() {
if let Some(m) = vaapi_codec_support().wire_mask() {
return m;
#[cfg(target_os = "windows")]
{
if windows_resolved_backend() == WindowsBackend::Software {
return punktfunk_core::quic::CODEC_H264;
}
if windows_backend_is_probed() {
if let Some(m) = windows_codec_support().wire_mask() {
return m;
}
}
// NVENC (static superset, like GameStream) — or an empty AMF/QSV probe (see above).
GPU_SUPERSET
}
// The macOS dev/test host has no GPU encode backend — keep the pre-probe advertisement.
#[cfg(not(any(target_os = "linux", target_os = "windows")))]
{
let _ = GPU_SUPERSET;
match crate::config::config().encoder_pref.as_str() {
"software" | "sw" | "openh264" => punktfunk_core::quic::CODEC_H264,
_ => punktfunk_core::quic::CODEC_HEVC,
}
}
// NVENC (static superset, like GameStream) — or an empty VAAPI probe (see above).
GPU_SUPERSET
}
#[cfg(target_os = "windows")]
{
if windows_resolved_backend() == WindowsBackend::Software {
return punktfunk_core::quic::CODEC_H264;
}
if windows_backend_is_probed() {
if let Some(m) = windows_codec_support().wire_mask() {
return m;
}
}
// NVENC (static superset, like GameStream) — or an empty AMF/QSV probe (see above).
GPU_SUPERSET
}
// The macOS dev/test host has no GPU encode backend — keep the pre-probe advertisement.
#[cfg(not(any(target_os = "linux", target_os = "windows")))]
{
let _ = GPU_SUPERSET;
match crate::config::config().encoder_pref.as_str() {
"software" | "sw" | "openh264" => punktfunk_core::quic::CODEC_H264,
_ => punktfunk_core::quic::CODEC_HEVC,
}
}
})();
base | pyro
}
/// Lowercase stats/console label (`"h264"` / `"hevc"` / `"av1"`) — the codec string seeded into
@@ -149,6 +180,7 @@ impl Codec {
Codec::H264 => "h264",
Codec::H265 => "hevc",
Codec::Av1 => "av1",
Codec::PyroWave => "pyrowave",
}
}
@@ -159,6 +191,9 @@ impl Codec {
Codec::H264 => "h264_nvenc",
Codec::H265 => "hevc_nvenc",
Codec::Av1 => "av1_nvenc",
// Guarded by the open_video dispatch: a PyroWave session never reaches a
// libavcodec backend.
Codec::PyroWave => unreachable!("PyroWave has no FFmpeg encoder"),
}
}
@@ -172,6 +207,9 @@ impl Codec {
Codec::H264 => "h264_vaapi",
Codec::H265 => "hevc_vaapi",
Codec::Av1 => "av1_vaapi",
// Guarded by the open_video dispatch: a PyroWave session never reaches a
// libavcodec backend.
Codec::PyroWave => unreachable!("PyroWave has no FFmpeg encoder"),
}
}
@@ -182,6 +220,9 @@ impl Codec {
Codec::H264 => "h264_amf",
Codec::H265 => "hevc_amf",
Codec::Av1 => "av1_amf",
// Guarded by the open_video dispatch: a PyroWave session never reaches a
// libavcodec backend.
Codec::PyroWave => unreachable!("PyroWave has no FFmpeg encoder"),
}
}
@@ -192,6 +233,9 @@ impl Codec {
Codec::H264 => "h264_qsv",
Codec::H265 => "hevc_qsv",
Codec::Av1 => "av1_qsv",
// Guarded by the open_video dispatch: a PyroWave session never reaches a
// libavcodec backend.
Codec::PyroWave => unreachable!("PyroWave has no FFmpeg encoder"),
}
}
}
@@ -324,7 +368,9 @@ impl Codec {
pub fn max_dimension(self) -> u32 {
match self {
Codec::H264 => 4096,
Codec::H265 | Codec::Av1 => 8192,
// PyroWave has no codec-level dimension cap (arbitrary even sizes); 8192 matches the
// buffer-math guard the other codecs get.
Codec::H265 | Codec::Av1 | Codec::PyroWave => 8192,
}
}
@@ -339,6 +385,9 @@ impl Codec {
Codec::H264 => 480_000_000,
Codec::H265 => 800_000_000,
Codec::Av1 => 1_200_000_000,
// No spec level/tier: the rate is a plain per-frame byte budget. Use the protocol's
// own bitrate clamp so the step-down probe logic never binds below it.
Codec::PyroWave => 8_000_000_000,
}
}
}
@@ -523,6 +572,20 @@ fn open_video_backend(
};
#[cfg(target_os = "linux")]
{
// A NEGOTIATED PyroWave session (client advertised + preferred it, plan §3) routes
// straight to that backend — the PUNKTFUNK_ENCODER pref below stays a lab override.
if codec == Codec::PyroWave {
#[cfg(feature = "pyrowave")]
{
return pyrowave::PyroWaveEncoder::open(width, height, fps, bitrate_bps)
.map(|e| (Box::new(e) as Box<dyn Encoder>, "pyrowave"));
}
#[cfg(not(feature = "pyrowave"))]
anyhow::bail!(
"session negotiated PyroWave but this host was built without --features \
punktfunk-host/pyrowave (the advertisement bit should not have been set)"
);
}
// Pick the GPU encode backend. NVIDIA → NVENC/CUDA (the original path, unchanged);
// AMD/Intel → VAAPI (one libavcodec backend for both). Auto-detect by default so a single
// Linux binary serves any GPU; `PUNKTFUNK_ENCODER` forces a specific backend (and surfaces
@@ -601,6 +664,30 @@ fn open_video_backend(
)
}
}
// PyroWave — the opt-in wired-LAN intra-only wavelet codec. Explicit-only, and
// EXPERIMENTAL until CODEC_PYROWAVE negotiation lands (plan Phase 2): no shipping
// client can decode the stream yet, so this arm exists for host-side bring-up and
// latency work only. Vendor-agnostic (any Vulkan 1.3 GPU); ignores the negotiated
// codec — every AU is an independently-decodable wavelet frame.
"pyrowave" => {
#[cfg(feature = "pyrowave")]
{
tracing::warn!(
?codec,
"PUNKTFUNK_ENCODER=pyrowave: EXPERIMENTAL all-intra wavelet stream — \
clients without a PyroWave decoder (all of them until CODEC_PYROWAVE \
lands) cannot display it"
);
pyrowave::PyroWaveEncoder::open(width, height, fps, bitrate_bps)
.map(|e| (Box::new(e) as Box<dyn Encoder>, "pyrowave"))
}
#[cfg(not(feature = "pyrowave"))]
{
anyhow::bail!(
"PUNKTFUNK_ENCODER=pyrowave requires a build with --features punktfunk-host/pyrowave"
)
}
}
// GPU-less software H.264 (openh264) — for a headless / GPU-lost box. Explicit-only:
// `auto` never picks it (a box with `/dev/nvidiactl` present but a dead driver would
// otherwise wrongly resolve to NVENC). Needs H.264 (openh264 emits only that) and a CPU
@@ -627,12 +714,17 @@ fn open_video_backend(
}
}
other => anyhow::bail!(
"unknown PUNKTFUNK_ENCODER={other:?} — use auto (default), nvenc, vaapi, vulkan, or software"
"unknown PUNKTFUNK_ENCODER={other:?} — use auto (default), nvenc, vaapi, vulkan, pyrowave, or software"
),
}
}
#[cfg(target_os = "windows")]
{
// The Windows host leg is blocked on the .173 D3D11-interop debt (plan Phase 0 §3);
// host_wire_caps never advertises the bit here, so this only guards a forged preference.
if codec == Codec::PyroWave {
anyhow::bail!("PyroWave host encode is not available on Windows yet");
}
let _ = cuda; // always false on Windows (no Cuda payload)
// NVIDIA → NVENC (direct SDK), AMD → AMF, Intel → QSV (both libavcodec), else → software
// H.264. `auto` (the default) resolves from the selected render adapter's vendor.
@@ -938,6 +1030,9 @@ pub fn linux_zero_copy_is_vaapi() -> bool {
match crate::config::config().encoder_pref.as_str() {
"nvenc" | "nvidia" | "cuda" => false,
"vaapi" | "amd" | "intel" => true,
// PyroWave ingests the raw capture dmabuf itself (Vulkan import + compute CSC) on ANY
// vendor — it must get the passthrough payload, never the EGL→CUDA import.
"pyrowave" => true,
_ => linux_auto_is_vaapi(),
}
}
@@ -1274,6 +1369,20 @@ mod vulkan_video;
#[cfg(all(target_os = "linux", feature = "vulkan-encode"))]
#[path = "encode/linux/vk_av1_encode.rs"]
mod vk_av1_encode;
// Small ash leaf helpers shared by the Linux Vulkan encode backends (dmabuf import, image/memory
// utilities) — extracted from `vulkan_video.rs` when the PyroWave backend arrived.
#[cfg(all(
target_os = "linux",
any(feature = "vulkan-encode", feature = "pyrowave")
))]
#[path = "encode/linux/vk_util.rs"]
mod vk_util;
// PyroWave — the opt-in wired-LAN intra-only wavelet codec (design/pyrowave-codec-plan.md §4.3):
// pure Vulkan compute via the vendored `pyrowave-sys`, sub-ms encode, every frame a keyframe.
// Explicit-only behind PUNKTFUNK_ENCODER=pyrowave; EXPERIMENTAL until CODEC_PYROWAVE lands.
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
#[path = "encode/linux/pyrowave.rs"]
mod pyrowave;
#[cfg(test)]
mod tests {
@@ -224,6 +224,8 @@ fn codec_guid(codec: Codec) -> nv::GUID {
Codec::H264 => nv::NV_ENC_CODEC_H264_GUID,
Codec::H265 => nv::NV_ENC_CODEC_HEVC_GUID,
Codec::Av1 => nv::NV_ENC_CODEC_AV1_GUID,
// Guarded by the open_video dispatch: a PyroWave session never reaches NVENC.
Codec::PyroWave => unreachable!("PyroWave never opens the direct-NVENC backend"),
}
}
@@ -522,6 +524,7 @@ impl NvencCudaEncoder {
}
Codec::Av1 => {}
Codec::H264 => {}
Codec::PyroWave => unreachable!("PyroWave never opens the direct-NVENC backend"),
}
// Chroma + bit depth. 4:4:4 (HEVC Range Extensions, chromaFormatIDC=3) engages on a YUV444
@@ -549,6 +552,7 @@ impl NvencCudaEncoder {
.set_inputPixelBitDepthMinus8(0);
}
Codec::H264 => {}
Codec::PyroWave => unreachable!("PyroWave never opens the direct-NVENC backend"),
}
}
@@ -596,6 +600,7 @@ impl NvencCudaEncoder {
av1.matrixCoefficients = mat;
av1.colorRange = 0;
}
Codec::PyroWave => unreachable!("PyroWave never opens the direct-NVENC backend"),
}
}
@@ -616,6 +621,7 @@ impl NvencCudaEncoder {
Codec::Av1 => {
cfg.encodeCodecConfig.av1Config.maxNumRefFramesInDPB = RFI_DPB;
}
Codec::PyroWave => unreachable!("PyroWave never opens the direct-NVENC backend"),
}
}
Ok(cfg)
@@ -1014,6 +1020,9 @@ impl Encoder for NvencCudaEncoder {
pic.codecPicParams.h264PicParams.seiPayloadArrayCnt = sei.len() as u32;
}
Codec::Av1 => {}
Codec::PyroWave => {
unreachable!("PyroWave never opens the direct-NVENC backend")
}
}
}
(api().encode_picture)(self.encoder, &mut pic)
File diff suppressed because it is too large Load Diff
@@ -88,6 +88,8 @@ fn lp_idx(codec: Codec) -> usize {
Codec::H264 => 0,
Codec::H265 => 1,
Codec::Av1 => 2,
// Guarded by the open_video dispatch: PyroWave never opens the VAAPI backend.
Codec::PyroWave => unreachable!("PyroWave has no VAAPI encoder"),
}
}
@@ -0,0 +1,199 @@
//! Small ash/Vulkan leaf helpers shared by the Linux Vulkan encode backends
//! (`vulkan_video.rs`, `pyrowave.rs`) — extracted verbatim from `vulkan_video.rs`
//! when the PyroWave backend arrived so the two don't fork copies.
// Every unsafe block carries a `// SAFETY:` proof (parent module enforces it).
use crate::capture::PixelFormat;
use anyhow::Result;
use ash::vk;
pub(crate) fn color_range(layer: u32) -> vk::ImageSubresourceRange {
vk::ImageSubresourceRange {
aspect_mask: vk::ImageAspectFlags::COLOR,
base_mip_level: 0,
level_count: 1,
base_array_layer: layer,
layer_count: 1,
}
}
pub(crate) unsafe fn find_mem(
mp: &vk::PhysicalDeviceMemoryProperties,
bits: u32,
want: vk::MemoryPropertyFlags,
) -> u32 {
for i in 0..mp.memory_type_count {
if (bits & (1 << i)) != 0 && mp.memory_types[i as usize].property_flags.contains(want) {
return i;
}
}
0
}
/// DRM fourcc -> the VkFormat whose *color* components match (Vulkan handles the byte swizzle).
pub(crate) fn fourcc_to_vk(fourcc: u32) -> Option<vk::Format> {
// fourcc_code(a,b,c,d) = a | b<<8 | c<<16 | d<<24
const XR24: u32 = 0x3432_5258; // XRGB8888
const AR24: u32 = 0x3432_5241; // ARGB8888
const XB24: u32 = 0x3432_4258; // XBGR8888
const AB24: u32 = 0x3432_4241; // ABGR8888
match fourcc {
XR24 | AR24 => Some(vk::Format::B8G8R8A8_UNORM),
XB24 | AB24 => Some(vk::Format::R8G8B8A8_UNORM),
_ => None,
}
}
pub(crate) fn pixel_to_vk(fmt: PixelFormat) -> Option<vk::Format> {
match fmt {
PixelFormat::Bgrx | PixelFormat::Bgra => Some(vk::Format::B8G8R8A8_UNORM),
PixelFormat::Rgbx | PixelFormat::Rgba => Some(vk::Format::R8G8B8A8_UNORM),
_ => None,
}
}
pub(crate) unsafe fn make_view(
device: &ash::Device,
image: vk::Image,
fmt: vk::Format,
layer: u32,
) -> Result<vk::ImageView> {
Ok(device.create_image_view(
&vk::ImageViewCreateInfo::default()
.image(image)
.view_type(vk::ImageViewType::TYPE_2D)
.format(fmt)
.subresource_range(color_range(layer)),
None,
)?)
}
/// Import a packed-RGB dmabuf as a SAMPLED VkImage (explicit DRM modifier). Caller destroys all
/// three returned handles. Extracted verbatim from `vulkan_video.rs`'s import path.
pub(crate) unsafe fn import_rgb_dmabuf(
device: &ash::Device,
ext_fd: &ash::khr::external_memory_fd::Device,
mem_props: &vk::PhysicalDeviceMemoryProperties,
d: &crate::capture::DmabufFrame,
cw: u32,
ch: u32,
) -> Result<(vk::Image, vk::DeviceMemory, vk::ImageView)> {
use anyhow::Context;
use std::os::fd::IntoRawFd;
let fmt = fourcc_to_vk(d.fourcc)
.with_context(|| format!("unsupported dmabuf fourcc {:#x}", d.fourcc))?;
let plane = [vk::SubresourceLayout::default()
.offset(d.offset as u64)
.row_pitch(d.stride as u64)];
let mut drm = vk::ImageDrmFormatModifierExplicitCreateInfoEXT::default()
.drm_format_modifier(d.modifier)
.plane_layouts(&plane);
let mut ext = vk::ExternalMemoryImageCreateInfo::default()
.handle_types(vk::ExternalMemoryHandleTypeFlags::DMA_BUF_EXT);
let img = device.create_image(
&vk::ImageCreateInfo::default()
.image_type(vk::ImageType::TYPE_2D)
.format(fmt)
.extent(vk::Extent3D {
width: cw,
height: ch,
depth: 1,
})
.mip_levels(1)
.array_layers(1)
.samples(vk::SampleCountFlags::TYPE_1)
.tiling(vk::ImageTiling::DRM_FORMAT_MODIFIER_EXT)
.usage(vk::ImageUsageFlags::SAMPLED)
.sharing_mode(vk::SharingMode::EXCLUSIVE)
.initial_layout(vk::ImageLayout::UNDEFINED)
.push_next(&mut ext)
.push_next(&mut drm),
None,
)?;
// dup the fd; Vulkan takes ownership of the dup on a successful import.
let dup = d.fd.try_clone().context("dup dmabuf fd")?.into_raw_fd();
let fd_props = {
let mut p = vk::MemoryFdPropertiesKHR::default();
let _ = (ext_fd.fp().get_memory_fd_properties_khr)(
device.handle(),
vk::ExternalMemoryHandleTypeFlags::DMA_BUF_EXT,
dup,
&mut p,
);
p.memory_type_bits
};
let req = device.get_image_memory_requirements(img);
let bits = req.memory_type_bits & fd_props;
let ti = find_mem(
mem_props,
if bits != 0 {
bits
} else {
req.memory_type_bits
},
vk::MemoryPropertyFlags::empty(),
);
let mut ded = vk::MemoryDedicatedAllocateInfo::default().image(img);
let mut import = vk::ImportMemoryFdInfoKHR::default()
.handle_type(vk::ExternalMemoryHandleTypeFlags::DMA_BUF_EXT)
.fd(dup);
let mem = device.allocate_memory(
&vk::MemoryAllocateInfo::default()
.allocation_size(req.size)
.memory_type_index(ti)
.push_next(&mut ded)
.push_next(&mut import),
None,
)?;
device.bind_image_memory(img, mem, 0)?;
let view = device.create_image_view(
&vk::ImageViewCreateInfo::default()
.image(img)
.view_type(vk::ImageViewType::TYPE_2D)
.format(fmt)
.subresource_range(color_range(0)),
None,
)?;
Ok((img, mem, view))
}
pub(crate) unsafe fn make_plain_image(
device: &ash::Device,
mp: &vk::PhysicalDeviceMemoryProperties,
fmt: vk::Format,
w: u32,
h: u32,
usage: vk::ImageUsageFlags,
) -> Result<(vk::Image, vk::DeviceMemory, vk::ImageView)> {
let img = device.create_image(
&vk::ImageCreateInfo::default()
.image_type(vk::ImageType::TYPE_2D)
.format(fmt)
.extent(vk::Extent3D {
width: w,
height: h,
depth: 1,
})
.mip_levels(1)
.array_layers(1)
.samples(vk::SampleCountFlags::TYPE_1)
.tiling(vk::ImageTiling::OPTIMAL)
.usage(usage)
.initial_layout(vk::ImageLayout::UNDEFINED),
None,
)?;
let req = device.get_image_memory_requirements(img);
let mem = device.allocate_memory(
&vk::MemoryAllocateInfo::default()
.allocation_size(req.size)
.memory_type_index(find_mem(
mp,
req.memory_type_bits,
vk::MemoryPropertyFlags::DEVICE_LOCAL,
)),
None,
)?;
device.bind_image_memory(img, mem, 0)?;
let view = make_view(device, img, fmt, 0)?;
Ok((img, mem, view))
}
@@ -10,13 +10,14 @@
//! The AV1 encode structs our pinned `ash 0.38` predates are vendored in `vk_av1_encode.rs`.
#![allow(clippy::too_many_arguments)]
use crate::capture::{CapturedFrame, FramePayload, PixelFormat};
use super::vk_util::{color_range, find_mem, make_plain_image, make_view, pixel_to_vk};
use crate::capture::{CapturedFrame, FramePayload};
use crate::encode::{Codec, EncodedFrame, Encoder, EncoderCaps};
use anyhow::{bail, Context, Result};
use ash::vk;
use std::collections::VecDeque;
use std::ffi::c_void;
use std::os::fd::{AsRawFd, IntoRawFd};
use std::os::fd::AsRawFd;
const NV12: vk::Format = vk::Format::G8_B8R8_2PLANE_420_UNORM;
/// Max resident dmabuf imports (comfortably above any PipeWire pool depth; imports alias existing
@@ -700,81 +701,7 @@ impl VulkanVideoEncoder {
cw: u32,
ch: u32,
) -> Result<(vk::Image, vk::DeviceMemory, vk::ImageView)> {
let fmt = fourcc_to_vk(d.fourcc)
.with_context(|| format!("unsupported dmabuf fourcc {:#x}", d.fourcc))?;
let plane = [vk::SubresourceLayout::default()
.offset(d.offset as u64)
.row_pitch(d.stride as u64)];
let mut drm = vk::ImageDrmFormatModifierExplicitCreateInfoEXT::default()
.drm_format_modifier(d.modifier)
.plane_layouts(&plane);
let mut ext = vk::ExternalMemoryImageCreateInfo::default()
.handle_types(vk::ExternalMemoryHandleTypeFlags::DMA_BUF_EXT);
let img = self.device.create_image(
&vk::ImageCreateInfo::default()
.image_type(vk::ImageType::TYPE_2D)
.format(fmt)
.extent(vk::Extent3D {
width: cw,
height: ch,
depth: 1,
})
.mip_levels(1)
.array_layers(1)
.samples(vk::SampleCountFlags::TYPE_1)
.tiling(vk::ImageTiling::DRM_FORMAT_MODIFIER_EXT)
.usage(vk::ImageUsageFlags::SAMPLED)
.sharing_mode(vk::SharingMode::EXCLUSIVE)
.initial_layout(vk::ImageLayout::UNDEFINED)
.push_next(&mut ext)
.push_next(&mut drm),
None,
)?;
// dup the fd; Vulkan takes ownership of the dup on a successful import.
let dup = d.fd.try_clone().context("dup dmabuf fd")?.into_raw_fd();
let fd_props = {
let mut p = vk::MemoryFdPropertiesKHR::default();
let _ = (self.ext_fd.fp().get_memory_fd_properties_khr)(
self.device.handle(),
vk::ExternalMemoryHandleTypeFlags::DMA_BUF_EXT,
dup,
&mut p,
);
p.memory_type_bits
};
let req = self.device.get_image_memory_requirements(img);
let bits = req.memory_type_bits & fd_props;
let ti = find_mem(
&self.mem_props,
if bits != 0 {
bits
} else {
req.memory_type_bits
},
vk::MemoryPropertyFlags::empty(),
);
let mut ded = vk::MemoryDedicatedAllocateInfo::default().image(img);
let mut import = vk::ImportMemoryFdInfoKHR::default()
.handle_type(vk::ExternalMemoryHandleTypeFlags::DMA_BUF_EXT)
.fd(dup);
let mem = self.device.allocate_memory(
&vk::MemoryAllocateInfo::default()
.allocation_size(req.size)
.memory_type_index(ti)
.push_next(&mut ded)
.push_next(&mut import),
None,
)?;
self.device.bind_image_memory(img, mem, 0)?;
let view = self.device.create_image_view(
&vk::ImageViewCreateInfo::default()
.image(img)
.view_type(vk::ImageViewType::TYPE_2D)
.format(fmt)
.subresource_range(color_range(0)),
None,
)?;
Ok((img, mem, view))
super::vk_util::import_rgb_dmabuf(&self.device, &self.ext_fd, &self.mem_props, d, cw, ch)
}
/// Import a dmabuf, reusing a cached per-buffer import when the same underlying buffer recurs
@@ -1998,112 +1925,10 @@ impl Drop for VulkanVideoEncoder {
// ---------- free helpers ----------
fn color_range(layer: u32) -> vk::ImageSubresourceRange {
vk::ImageSubresourceRange {
aspect_mask: vk::ImageAspectFlags::COLOR,
base_mip_level: 0,
level_count: 1,
base_array_layer: layer,
layer_count: 1,
}
}
fn align_up(v: u64, a: u64) -> u64 {
v.div_ceil(a) * a
}
unsafe fn find_mem(
mp: &vk::PhysicalDeviceMemoryProperties,
bits: u32,
want: vk::MemoryPropertyFlags,
) -> u32 {
for i in 0..mp.memory_type_count {
if (bits & (1 << i)) != 0 && mp.memory_types[i as usize].property_flags.contains(want) {
return i;
}
}
0
}
/// DRM fourcc -> the VkFormat whose *color* components match (Vulkan handles the byte swizzle).
fn fourcc_to_vk(fourcc: u32) -> Option<vk::Format> {
// fourcc_code(a,b,c,d) = a | b<<8 | c<<16 | d<<24
const XR24: u32 = 0x3432_5258; // XRGB8888
const AR24: u32 = 0x3432_5241; // ARGB8888
const XB24: u32 = 0x3432_4258; // XBGR8888
const AB24: u32 = 0x3432_4241; // ABGR8888
match fourcc {
XR24 | AR24 => Some(vk::Format::B8G8R8A8_UNORM),
XB24 | AB24 => Some(vk::Format::R8G8B8A8_UNORM),
_ => None,
}
}
fn pixel_to_vk(fmt: PixelFormat) -> Option<vk::Format> {
match fmt {
PixelFormat::Bgrx | PixelFormat::Bgra => Some(vk::Format::B8G8R8A8_UNORM),
PixelFormat::Rgbx | PixelFormat::Rgba => Some(vk::Format::R8G8B8A8_UNORM),
_ => None,
}
}
unsafe fn make_view(
device: &ash::Device,
image: vk::Image,
fmt: vk::Format,
layer: u32,
) -> Result<vk::ImageView> {
Ok(device.create_image_view(
&vk::ImageViewCreateInfo::default()
.image(image)
.view_type(vk::ImageViewType::TYPE_2D)
.format(fmt)
.subresource_range(color_range(layer)),
None,
)?)
}
unsafe fn make_plain_image(
device: &ash::Device,
mp: &vk::PhysicalDeviceMemoryProperties,
fmt: vk::Format,
w: u32,
h: u32,
usage: vk::ImageUsageFlags,
) -> Result<(vk::Image, vk::DeviceMemory, vk::ImageView)> {
let img = device.create_image(
&vk::ImageCreateInfo::default()
.image_type(vk::ImageType::TYPE_2D)
.format(fmt)
.extent(vk::Extent3D {
width: w,
height: h,
depth: 1,
})
.mip_levels(1)
.array_layers(1)
.samples(vk::SampleCountFlags::TYPE_1)
.tiling(vk::ImageTiling::OPTIMAL)
.usage(usage)
.initial_layout(vk::ImageLayout::UNDEFINED),
None,
)?;
let req = device.get_image_memory_requirements(img);
let mem = device.allocate_memory(
&vk::MemoryAllocateInfo::default()
.allocation_size(req.size)
.memory_type_index(find_mem(
mp,
req.memory_type_bits,
vk::MemoryPropertyFlags::DEVICE_LOCAL,
)),
None,
)?;
device.bind_image_memory(img, mem, 0)?;
let view = make_view(device, img, fmt, 0)?;
Ok((img, mem, view))
}
unsafe fn make_video_image(
device: &ash::Device,
mp: &vk::PhysicalDeviceMemoryProperties,
@@ -913,6 +913,7 @@ fn codec_props(codec: Codec) -> CodecProps {
intra_refresh: None,
ltr: None,
},
Codec::PyroWave => unreachable!("PyroWave never opens the AMF backend"),
}
}
@@ -1525,6 +1526,7 @@ impl AmfEncoder {
)?;
}
}
Codec::PyroWave => unreachable!("PyroWave never opens the AMF backend"),
}
// Colour signalling, mirroring `open_win_encoder`: BT.709 limited (SDR) or BT.2020 PQ
// (HDR) — VUI on AVC/HEVC, sequence-header colour config on AV1. Required when HDR — a
@@ -2171,6 +2173,7 @@ impl Encoder for AmfEncoder {
// The static KEY_FRAME_ALIGNED header-insertion mode already puts a sequence
// header OBU on every key frame; there is no per-surface twin.
Codec::Av1 => {}
Codec::PyroWave => unreachable!("PyroWave never opens the AMF backend"),
}
}
// LTR-RFI per-frame properties (design: the AMD twin of NVENC intra-refresh recovery).
@@ -244,6 +244,7 @@ fn codec_guid(codec: Codec) -> nv::GUID {
Codec::H264 => nv::NV_ENC_CODEC_H264_GUID,
Codec::H265 => nv::NV_ENC_CODEC_HEVC_GUID,
Codec::Av1 => nv::NV_ENC_CODEC_AV1_GUID,
Codec::PyroWave => unreachable!("PyroWave never opens the direct-NVENC backend"),
}
}
@@ -757,6 +758,7 @@ impl NvencD3d11Encoder {
}
// H.264 has no tier; the preset default level is already autoselect.
Codec::H264 => {}
Codec::PyroWave => unreachable!("PyroWave never opens the direct-NVENC backend"),
}
// Chroma + bit depth. Full-chroma 4:4:4 (HEVC Range Extensions) takes precedence and composes
@@ -795,6 +797,7 @@ impl NvencD3d11Encoder {
cfg.encodeCodecConfig.hevcConfig.set_pixelBitDepthMinus8(2);
// 10 - 8
}
Codec::PyroWave => unreachable!("PyroWave never opens the direct-NVENC backend"),
Codec::Av1 => {
cfg.encodeCodecConfig.av1Config.set_pixelBitDepthMinus8(2);
// The input rides at its real depth; NVENC upconverts (mirrors the HEVC path).
@@ -864,6 +867,7 @@ impl NvencD3d11Encoder {
av1.matrixCoefficients = mat;
av1.colorRange = 0; // studio/limited swing
}
Codec::PyroWave => unreachable!("PyroWave never opens the direct-NVENC backend"),
}
}
@@ -885,6 +889,7 @@ impl NvencD3d11Encoder {
Codec::Av1 => {
cfg.encodeCodecConfig.av1Config.maxNumRefFramesInDPB = RFI_DPB;
}
Codec::PyroWave => unreachable!("PyroWave never opens the direct-NVENC backend"),
}
}
Ok(cfg)
@@ -1456,6 +1461,9 @@ impl Encoder for NvencD3d11Encoder {
}
// AV1 mastering/CLL ride METADATA OBUs, not SEI — separate follow-up.
Codec::Av1 => {}
Codec::PyroWave => {
unreachable!("PyroWave never opens the direct-NVENC backend")
}
}
}
(api().encode_picture)(self.encoder, &mut pic)
+2
View File
@@ -873,6 +873,8 @@ fn parse_spike(args: &[String]) -> Result<Options> {
Codec::H264 => "h264",
Codec::H265 => "h265",
Codec::Av1 => "obu",
// Raw concatenated PyroWave packets — a lab dump, not an FFmpeg-playable stream.
Codec::PyroWave => "pyrowave",
};
PathBuf::from(format!("/tmp/punktfunk-spike.{ext}"))
});
+3
View File
@@ -310,6 +310,8 @@ enum ApiCodec {
H264,
H265,
Av1,
/// PyroWave — the opt-in wired-LAN intra-only wavelet codec.
PyroWave,
}
impl From<Codec> for ApiCodec {
@@ -318,6 +320,7 @@ impl From<Codec> for ApiCodec {
Codec::H264 => ApiCodec::H264,
Codec::H265 => ApiCodec::H265,
Codec::Av1 => ApiCodec::Av1,
Codec::PyroWave => ApiCodec::PyroWave,
}
}
}
+30
View File
@@ -0,0 +1,30 @@
cmake_minimum_required(VERSION 3.20)
project(pyrowave-sys LANGUAGES CXX C)
if(NOT DEFINED PYROWAVE_VENDOR_DIR)
set(PYROWAVE_VENDOR_DIR ${CMAKE_CURRENT_SOURCE_DIR}/vendor/pyrowave)
endif()
# Mirror upstream pyrowave's standalone (top-level) Granite configuration
# (CMakeLists.txt lines 88-99 at the vendored pin), but produce static
# archives only the Rust crate links the C API statically.
set(GRANITE_RENDERER OFF CACHE BOOL "" FORCE)
set(GRANITE_VULKAN_SPIRV_CROSS OFF CACHE BOOL "" FORCE)
set(GRANITE_VULKAN_SHADER_MANAGER_RUNTIME_COMPILER OFF CACHE BOOL "" FORCE)
set(GRANITE_VULKAN_SYSTEM_HANDLES OFF CACHE BOOL "" FORCE)
set(GRANITE_SHADER_COMPILER_OPTIMIZE OFF CACHE BOOL "" FORCE)
set(GRANITE_POSITION_INDEPENDENT ON CACHE BOOL "" FORCE)
set(GRANITE_VULKAN_FOSSILIZE OFF CACHE BOOL "" FORCE)
set(GRANITE_SHIPPING ON CACHE BOOL "" FORCE)
set(GRANITE_PLATFORM "null" CACHE STRING "" FORCE)
add_subdirectory(${PYROWAVE_VENDOR_DIR}/Granite ${CMAKE_CURRENT_BINARY_DIR}/Granite EXCLUDE_FROM_ALL)
# Not top-level from here, so upstream's CMake only defines the static
# `pyrowave` codec library (shared lib / tests / install rules are skipped).
add_subdirectory(${PYROWAVE_VENDOR_DIR} ${CMAKE_CURRENT_BINARY_DIR}/pyrowave EXCLUDE_FROM_ALL)
# The C API as a static archive. Upstream only ships it as a SHARED target;
# compiling pyrowave_c.cpp against the same deps is equivalent minus the
# dllexport attributes (PYROWAVE_EXPORT_SYMBOLS stays undefined).
add_library(pyrowave-capi STATIC ${PYROWAVE_VENDOR_DIR}/pyrowave_c.cpp)
target_link_libraries(pyrowave-capi PRIVATE pyrowave granite-vulkan)
+17
View File
@@ -0,0 +1,17 @@
[package]
name = "pyrowave-sys"
description = "Vendored PyroWave (Themaister's intra-only wavelet codec, Vulkan compute) built from source + bindgen over its C API — the LAN low-latency codec backend (design/pyrowave-codec-plan.md). Upstream pin recorded in vendor/pyrowave/PUNKTFUNK-VENDOR.txt; bumping it is a protocol-affecting change (§4.2)."
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
authors.workspace = true
repository.workspace = true
links = "pyrowave"
[build-dependencies]
# Same CMake-from-vendored-source model as opus/audiopus_sys: reproducible
# offline builds (CI, MSVC, flatpak — the flatpak builder has no network).
cmake = "0.1"
# Same bindgen configuration as pf-ffvk (runtime = dlopen libclang).
bindgen = { version = "0.72", features = ["runtime"], default-features = false }
+107
View File
@@ -0,0 +1,107 @@
//! Build the vendored PyroWave codec (C++/Vulkan compute) as static archives via
//! CMake and generate bindings over its C API (`pyrowave.h`).
//!
//! Linux + Windows only — the platforms whose hosts/clients run the Vulkan codec
//! path (design/pyrowave-codec-plan.md §5). Other targets get an empty bindings
//! file so the workspace builds everywhere (the Apple client is a native Metal
//! port, §4.7 — it never links this crate).
//!
//! Everything compiles from the committed vendor tree: no network, no system
//! pyrowave, no pkg-config — CI, MSVC, and the offline flatpak builder all get
//! reproducible builds (§4.1). Vulkan headers come vendored too; only a libclang
//! for bindgen is required on the build machine.
use std::env;
use std::path::PathBuf;
fn main() {
println!("cargo:rerun-if-changed=wrapper.h");
println!("cargo:rerun-if-changed=CMakeLists.txt");
println!("cargo:rerun-if-changed=vendor/pyrowave");
let out = PathBuf::from(env::var("OUT_DIR").unwrap());
let bindings_path = out.join("bindings.rs");
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
if target_os != "linux" && target_os != "windows" {
std::fs::write(
&bindings_path,
"// pyrowave-sys: Linux/Windows-only, empty on this target\n",
)
.unwrap();
return;
}
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
let vendor = manifest_dir.join("vendor/pyrowave");
let vk_include = vendor.join("Granite/third_party/khronos/vulkan-headers/include");
// Always Release: a debug build of the wavelet kernels' host code buys no
// debuggability (the hot path is GPU shaders baked into the source) and the
// MSVC debug CRT would clash with Rust's release CRT.
let dst = cmake::Config::new(&manifest_dir)
.profile("Release")
.build_target("pyrowave-capi")
.build();
let build = dst.join("build");
// Static link closure, dependents before dependencies (GNU ld is order-sensitive).
println!("cargo:rustc-link-search=native={}", build.display());
for sub in [
"pyrowave",
"Granite/vulkan",
"Granite/util",
"Granite/math",
"Granite/third_party",
] {
println!(
"cargo:rustc-link-search=native={}",
build.join(sub).display()
);
// MSVC multi-config generators put archives in a Release/ subdir.
println!(
"cargo:rustc-link-search=native={}",
build.join(sub).join("Release").display()
);
}
println!(
"cargo:rustc-link-search=native={}",
build.join("Release").display()
);
for lib in [
"pyrowave-capi",
"pyrowave",
"granite-vulkan",
"granite-util",
"granite-math",
"granite-volk",
] {
println!("cargo:rustc-link-lib=static={lib}");
}
if target_os == "linux" {
println!("cargo:rustc-link-lib=dylib=stdc++");
// volk loads the Vulkan loader at runtime.
println!("cargo:rustc-link-lib=dylib=dl");
println!("cargo:rustc-link-lib=dylib=pthread");
}
if target_os == "windows" {
// Granite's breadcrumbs tracker raises a MessageBoxA on device hang.
println!("cargo:rustc-link-lib=dylib=user32");
}
let bindings = bindgen::Builder::default()
.header("wrapper.h")
.clang_arg(format!("-I{}", vendor.display()))
.clang_arg(format!("-I{}", vk_include.display()))
.allowlist_function("pyrowave_.*")
.allowlist_type("pyrowave_.*")
.allowlist_var("PYROWAVE_.*")
// The Vk* handles/structs the API mentions come along via the allowlist
// closure; they are ABI-identical to ash's, callers cast at the seam.
.derive_default(true)
.generate()
.expect("bindgen failed for pyrowave.h");
bindings
.write_to_file(&bindings_path)
.expect("failed to write pyrowave bindings");
}
+29
View File
@@ -0,0 +1,29 @@
//! Raw FFI bindings to the vendored PyroWave C API (`pyrowave.h`).
//!
//! Empty on targets other than Linux/Windows — see build.rs. The safe wrapper
//! lives with its consumer (`punktfunk-host`'s encoder backend, and later the
//! Rust clients' decoder backend); this crate is bindings only.
#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
// Bindgen output for a C API: u128 layout warnings and the like are upstream's concern.
#![allow(improper_ctypes)]
#[cfg(any(target_os = "linux", target_os = "windows"))]
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
#[cfg(all(test, any(target_os = "linux", target_os = "windows")))]
mod tests {
use super::*;
/// Link sanity: statically linked archives resolve and the vendored pin is
/// the API version we designed against. No GPU or Vulkan loader required —
/// the version query touches no device state.
#[test]
fn api_version_matches_vendored_pin() {
let (mut major, mut minor, mut patch) = (0u32, 0u32, 0u32);
unsafe { pyrowave_get_api_version(&mut major, &mut minor, &mut patch) };
assert_eq!((major, minor, patch), (0, 4, 0), "vendored pyrowave API version moved — re-check the §4.2 protocol coupling before bumping");
}
}
+173
View File
@@ -0,0 +1,173 @@
cmake_minimum_required(VERSION 3.20)
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_C_STANDARD 99)
project(pyrowave LANGUAGES CXX C)
if (CMAKE_COMPILER_IS_GNUCXX OR (${CMAKE_CXX_COMPILER_ID} MATCHES "Clang"))
set(PYROWAVE_CXX_FLAGS -Wshadow -Wall -Wextra -Wno-comment -Wno-missing-field-initializers -Wno-empty-body -fvisibility=hidden)
if (${CMAKE_CXX_COMPILER_ID} MATCHES "Clang")
set(PYROWAVE_CXX_FLAGS ${PYROWAVE_CXX_FLAGS} -Wno-backslash-newline-escape)
endif()
if (NOT (${CMAKE_BUILD_TYPE} MATCHES "Release"))
message("Enabling frame pointer for profiling/debug.")
set(PYROWAVE_CXX_FLAGS ${PYROWAVE_CXX_FLAGS} -fno-omit-frame-pointer)
endif()
if (CMAKE_SYSTEM_PROCESSOR MATCHES "(x86)|(X86)|(amd64)|(AMD64)")
message("Enabling SSE3 support.")
set(PYROWAVE_CXX_FLAGS ${PYROWAVE_CXX_FLAGS} -msse3)
endif()
elseif (MSVC)
set(PYROWAVE_CXX_FLAGS /D_CRT_SECURE_NO_WARNINGS /wd4267 /wd4244 /wd4309 /wd4005 /MP)
endif()
add_library(pyrowave STATIC
pyrowave_config.hpp shaders/slangmosh.hpp
pyrowave_encoder.hpp pyrowave_encoder.cpp
pyrowave_decoder.hpp pyrowave_decoder.cpp
pyrowave_common.hpp pyrowave_common.cpp)
target_include_directories(pyrowave PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_compile_options(pyrowave PRIVATE ${PYROWAVE_CXX_FLAGS})
set_target_properties(pyrowave PROPERTIES POSITION_INDEPENDENT_CODE ON)
option(PYROWAVE_DEVEL "Build PyroWave as a standalone sandbox with development tooling." OFF)
if (ANDROID)
# Mobile chips really like FP16
target_compile_definitions(pyrowave PRIVATE PYROWAVE_PRECISION=0)
message("Forcing FP16 default on Android.")
else()
option(PYROWAVE_FP32_STORAGE "Configure for full FP32 and extended precision." OFF)
option(PYROWAVE_FP32_MATH "Configure for FP32 arithmetic, but reduced range storage." ON)
if (PYROWAVE_FP32_STORAGE)
target_compile_definitions(pyrowave PRIVATE PYROWAVE_PRECISION=2)
elseif(PYROWAVE_FP32_MATH)
target_compile_definitions(pyrowave PRIVATE PYROWAVE_PRECISION=1)
else()
target_compile_definitions(pyrowave PRIVATE PYROWAVE_PRECISION=0)
endif()
endif()
if (PYROWAVE_DEVEL)
add_library(pyrowave-utils STATIC yuv4mpeg.cpp yuv4mpeg.hpp)
# Standalone build, build the sandbox.
set(GRANITE_RENDERER ON CACHE BOOL "" FORCE)
set(GRANITE_VULKAN_FOSSILIZE OFF CACHE BOOL "" FORCE)
set(GRANITE_SHIPPING OFF CACHE BOOL "" FORCE)
set(GRANITE_PLATFORM "SDL" CACHE STRING "" FORCE)
set(GRANITE_HIDDEN ON CACHE BOOL "" FORCE)
set(GRANITE_POSITION_INDEPENDENT ON CACHE BOOL "" FORCE)
add_subdirectory(Granite EXCLUDE_FROM_ALL)
add_granite_offline_tool(pyrowave-sandbox sandbox.cpp)
target_link_libraries(pyrowave-sandbox PRIVATE pyrowave)
target_compile_definitions(pyrowave-sandbox PRIVATE ASSET_DIRECTORY=\"${CMAKE_CURRENT_SOURCE_DIR}/shaders\")
target_link_libraries(pyrowave-sandbox PRIVATE granite-renderer pyrowave-utils)
add_granite_offline_tool(pyrowave-bench bench.cpp)
target_link_libraries(pyrowave-bench PRIVATE pyrowave)
target_link_libraries(pyrowave-bench PRIVATE granite-vulkan pyrowave-utils)
add_granite_offline_tool(pyrowave-encode encode.cpp)
target_link_libraries(pyrowave-encode PRIVATE pyrowave)
target_link_libraries(pyrowave-encode PRIVATE granite-vulkan pyrowave-utils)
add_granite_offline_tool(pyrowave-decode decode.cpp)
target_link_libraries(pyrowave-decode PRIVATE pyrowave)
target_link_libraries(pyrowave-decode PRIVATE granite-vulkan pyrowave-utils)
add_granite_offline_tool(pyrowave-psnr psnr.cpp)
target_link_libraries(pyrowave-psnr PRIVATE pyrowave-utils)
add_granite_application(pyrowave-viewer viewer.cpp)
target_link_libraries(pyrowave-viewer PRIVATE pyrowave pyrowave-utils)
if (NOT ANDROID)
target_compile_definitions(pyrowave-viewer PRIVATE ASSET_DIRECTORY=\"${CMAKE_CURRENT_SOURCE_DIR}/shaders\")
endif()
elseif (${PROJECT_IS_TOP_LEVEL} AND (NOT TARGET granite-vulkan))
set(GRANITE_RENDERER OFF CACHE BOOL "" FORCE)
set(GRANITE_VULKAN_SPIRV_CROSS OFF CACHE BOOL "" FORCE)
set(GRANITE_VULKAN_SHADER_MANAGER_RUNTIME_COMPILER OFF CACHE BOOL "" FORCE)
set(GRANITE_VULKAN_SYSTEM_HANDLES OFF CACHE BOOL "" FORCE)
set(GRANITE_SHADER_COMPILER_OPTIMIZE OFF CACHE BOOL "" FORCE)
set(GRANITE_POSITION_INDEPENDENT ON CACHE BOOL "" FORCE)
set(GRANITE_VULKAN_FOSSILIZE OFF CACHE BOOL "" FORCE)
set(GRANITE_SHIPPING ON CACHE BOOL "" FORCE)
set(GRANITE_PLATFORM "null" CACHE STRING "" FORCE)
add_subdirectory(Granite EXCLUDE_FROM_ALL)
endif()
target_link_libraries(pyrowave PRIVATE granite-vulkan granite-math)
if (${PROJECT_IS_TOP_LEVEL})
add_library(pyrowave-shared SHARED pyrowave_c.cpp pyrowave.h)
target_link_libraries(pyrowave-shared PRIVATE pyrowave granite-vulkan)
target_compile_definitions(pyrowave-shared PRIVATE PYROWAVE_EXPORT_SYMBOLS)
target_compile_options(pyrowave-shared PRIVATE ${PYROWAVE_CXX_FLAGS})
add_executable(pyrowave-c-test pyrowave_c_test.cpp)
target_link_libraries(pyrowave-c-test PRIVATE pyrowave-shared granite-volk-headers)
target_compile_options(pyrowave-c-test PRIVATE ${PYROWAVE_CXX_FLAGS})
add_executable(pyrowave-c-interop-test pyrowave_c_interop_test.cpp com_ptr.hpp)
target_link_libraries(pyrowave-c-interop-test PRIVATE pyrowave-shared granite-vulkan)
target_compile_options(pyrowave-c-interop-test PRIVATE ${PYROWAVE_CXX_FLAGS})
add_executable(pyrowave-device-validation pyrowave_device_validation.cpp com_ptr.hpp)
target_link_libraries(pyrowave-device-validation PRIVATE pyrowave-shared granite-util granite-volk-headers)
target_compile_options(pyrowave-device-validation PRIVATE ${PYROWAVE_CXX_FLAGS})
set(PYROWAVE_API_VERSION_MAJOR 0)
set(PYROWAVE_API_VERSION_MINOR 4)
set(PYROWAVE_API_VERSION_PATCH 0)
set(PYROWAVE_API_VERSION ${PYROWAVE_API_VERSION_MAJOR}.${PYROWAVE_API_VERSION_MINOR}.${PYROWAVE_API_VERSION_PATCH})
# DLL_NAME_WITH_SOVERSION requires CMake 3.27+. SteamRT doesn't have CMake 3.27 yet though.
if (WIN32 AND (CMAKE_VERSION VERSION_LESS "3.27"))
message(FATAL_ERROR "CMake build on Windows expects version 3.27 at minimum.")
endif()
set_target_properties(pyrowave-shared
PROPERTIES VERSION ${PYROWAVE_API_VERSION}
SOVERSION ${PYROWAVE_API_VERSION_MAJOR}
DLL_NAME_WITH_SOVERSION ON)
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/pyrowave.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/pyrowave)
install(TARGETS pyrowave-shared EXPORT pyrowave-sharedConfig
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/pyrowave)
install(EXPORT pyrowave-sharedConfig DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/pyrowave-shared/cmake)
install(TARGETS pyrowave-device-validation RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
set(PYROWAVE_INSTALL_LIB_DIR ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR})
set(PYROWAVE_INSTALL_INC_DIR ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_INCLUDEDIR}/pyrowave)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/pkg-config/pyrowave-shared.pc.in
${CMAKE_CURRENT_BINARY_DIR}/pyrowave-shared.pc @ONLY)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/pyrowave-shared.pc DESTINATION ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATAROOTDIR}/pkgconfig)
if (WIN32 AND CMAKE_COMPILER_IS_GNUCXX)
target_link_libraries(pyrowave-shared PRIVATE -static gcc stdc++ winpthread)
target_link_libraries(pyrowave-c-test PRIVATE -static gcc stdc++ winpthread)
target_link_libraries(pyrowave-c-interop-test PRIVATE -static gcc stdc++ winpthread)
target_link_libraries(pyrowave-device-validation PRIVATE -static gcc stdc++ winpthread)
endif()
if (NOT WIN32)
target_link_libraries(pyrowave-shared PRIVATE -pthread)
target_link_libraries(pyrowave-c-test PRIVATE -pthread)
target_link_libraries(pyrowave-c-interop-test PRIVATE -pthread)
target_link_libraries(pyrowave-device-validation PRIVATE -pthread)
endif()
if (WIN32)
target_sources(pyrowave-shared PRIVATE pyrowave-shared.def)
set_target_properties(pyrowave-shared PROPERTIES PREFIX "lib")
target_link_libraries(pyrowave-c-interop-test PRIVATE d3d11 d3d12 dxgi)
else()
target_link_options(pyrowave-shared PRIVATE -Wl,--version-script=${CMAKE_CURRENT_SOURCE_DIR}/link.T)
endif()
endif()
+167
View File
@@ -0,0 +1,167 @@
# The style used for all options not specifically set in the configuration.
BasedOnStyle: LLVM
# The extra indent or outdent of access modifiers, e.g. public:.
AccessModifierOffset: -4
# If true, aligns escaped newlines as far left as possible. Otherwise puts them into the right-most column.
AlignEscapedNewlinesLeft: true
# If true, aligns trailing comments.
AlignTrailingComments: false
# Allow putting all parameters of a function declaration onto the next line even if BinPackParameters is false.
AllowAllParametersOfDeclarationOnNextLine: false
# Allows contracting simple braced statements to a single line.
AllowShortBlocksOnASingleLine: false
# If true, short case labels will be contracted to a single line.
AllowShortCaseLabelsOnASingleLine: false
# Dependent on the value, int f() { return 0; } can be put on a single line. Possible values: None, Inline, All.
AllowShortFunctionsOnASingleLine: None
# If true, if (a) return; can be put on a single line.
AllowShortIfStatementsOnASingleLine: false
# If true, while (true) continue; can be put on a single line.
AllowShortLoopsOnASingleLine: false
# If true, always break after function definition return types.
AlwaysBreakAfterDefinitionReturnType: false
# If true, always break before multiline string literals.
AlwaysBreakBeforeMultilineStrings: false
# If true, always break after the template<...> of a template declaration.
AlwaysBreakTemplateDeclarations: true
# If false, a function call's arguments will either be all on the same line or will have one line each.
BinPackArguments: true
# If false, a function declaration's or function definition's parameters will either all be on the same line
# or will have one line each.
BinPackParameters: true
# The way to wrap binary operators. Possible values: None, NonAssignment, All.
BreakBeforeBinaryOperators: None
# The brace breaking style to use. Possible values: Attach, Linux, Stroustrup, Allman, GNU.
BreakBeforeBraces: Allman
# If true, ternary operators will be placed after line breaks.
BreakBeforeTernaryOperators: false
# Always break constructor initializers before commas and align the commas with the colon.
BreakConstructorInitializersBeforeComma: true
# The column limit. A column limit of 0 means that there is no column limit.
ColumnLimit: 120
# A regular expression that describes comments with special meaning, which should not be split into lines or otherwise changed.
CommentPragmas: '^ *'
# If the constructor initializers don't fit on a line, put each initializer on its own line.
ConstructorInitializerAllOnOneLineOrOnePerLine: false
# The number of characters to use for indentation of constructor initializer lists.
ConstructorInitializerIndentWidth: 4
# Indent width for line continuations.
ContinuationIndentWidth: 4
# If true, format braced lists as best suited for C++11 braced lists.
Cpp11BracedListStyle: false
# Disables formatting at all.
DisableFormat: false
# A vector of macros that should be interpreted as foreach loops instead of as function calls.
#ForEachMacros: ''
# Indent case labels one level from the switch statement.
# When false, use the same indentation level as for the switch statement.
# Switch statement body is always indented one level more than case labels.
IndentCaseLabels: false
# The number of columns to use for indentation.
IndentWidth: 4
# Indent if a function definition or declaration is wrapped after the type.
IndentWrappedFunctionNames: false
# If true, empty lines at the start of blocks are kept.
KeepEmptyLinesAtTheStartOfBlocks: true
# Language, this format style is targeted at. Possible values: None, Cpp, Java, JavaScript, Proto.
Language: Cpp
# The maximum number of consecutive empty lines to keep.
MaxEmptyLinesToKeep: 1
# The indentation used for namespaces. Possible values: None, Inner, All.
NamespaceIndentation: None
# The penalty for breaking a function call after "call(".
PenaltyBreakBeforeFirstCallParameter: 19
# The penalty for each line break introduced inside a comment.
PenaltyBreakComment: 300
# The penalty for breaking before the first <<.
PenaltyBreakFirstLessLess: 120
# The penalty for each line break introduced inside a string literal.
PenaltyBreakString: 1000
# The penalty for each character outside of the column limit.
PenaltyExcessCharacter: 1000000
# Penalty for putting the return type of a function onto its own line.
PenaltyReturnTypeOnItsOwnLine: 1000000000
# Pointer and reference alignment style. Possible values: Left, Right, Middle.
PointerAlignment: Right
# If true, a space may be inserted after C style casts.
SpaceAfterCStyleCast: false
# If false, spaces will be removed before assignment operators.
SpaceBeforeAssignmentOperators: true
# Defines in which cases to put a space before opening parentheses. Possible values: Never, ControlStatements, Always.
SpaceBeforeParens: ControlStatements
# If true, spaces may be inserted into '()'.
SpaceInEmptyParentheses: false
# The number of spaces before trailing line comments (// - comments).
SpacesBeforeTrailingComments: 1
# If true, spaces will be inserted after '<' and before '>' in template argument lists.
SpacesInAngles: false
# If true, spaces may be inserted into C style casts.
SpacesInCStyleCastParentheses: false
# If true, spaces are inserted inside container literals (e.g. ObjC and Javascript array and dict literals).
SpacesInContainerLiterals: false
# If true, spaces will be inserted after '(' and before ')'.
SpacesInParentheses: false
# If true, spaces will be inserted after '[' and befor']'.
SpacesInSquareBrackets: false
# Format compatible with this standard, e.g. use A<A<int> > instead of A<A<int>> for LS_Cpp03. Possible values: Cpp03, Cpp11, Auto.
Standard: Cpp11
# The number of columns used for tab stops.
TabWidth: 4
# The way to use tab characters in the resulting file. Possible values: Never, ForIndentation, Always.
UseTab: ForIndentation
# Do not reflow comments
ReflowComments: false
@@ -0,0 +1,54 @@
[submodule "third_party/spirv-cross"]
path = third_party/spirv-cross
url = https://github.com/KhronosGroup/SPIRV-Cross
[submodule "third_party/shaderc"]
path = third_party/shaderc
url = https://github.com/google/shaderc.git
[submodule "third_party/glslang"]
path = third_party/glslang
url = https://github.com/Themaister/glslang.git
[submodule "third_party/spirv-tools"]
path = third_party/spirv-tools
url = https://github.com/Themaister/SPIRV-Tools
[submodule "third_party/spirv-headers"]
path = third_party/spirv-headers
url = https://github.com/KhronosGroup/SPIRV-Headers
[submodule "third_party/rapidjson"]
path = third_party/rapidjson
url = https://github.com/miloyip/rapidjson
[submodule "third_party/stb/stb"]
path = third_party/stb/stb
url = https://github.com/nothings/stb
[submodule "third_party/astc-encoder"]
path = third_party/astc-encoder
url = https://github.com/ARM-software/astc-encoder
[submodule "third_party/fossilize"]
path = third_party/fossilize
url = https://github.com/ValveSoftware/Fossilize
[submodule "third_party/volk"]
path = third_party/volk
url = https://github.com/zeux/volk
[submodule "third_party/meshoptimizer"]
path = third_party/meshoptimizer
url = https://github.com/zeux/meshoptimizer
[submodule "third_party/muFFT"]
path = third_party/muFFT
url = https://github.com/Themaister/muFFT
[submodule "third_party/oboe"]
path = third_party/oboe
url = https://github.com/google/oboe
[submodule "third_party/khronos/vulkan-headers"]
path = third_party/khronos/vulkan-headers
url = https://github.com/KhronosGroup/Vulkan-Headers
[submodule "third_party/fsr2"]
path = third_party/fsr2
url = https://github.com/Themaister/FidelityFX-FSR2
[submodule "third_party/sdl3"]
path = third_party/sdl3
url = https://github.com/libsdl-org/SDL
[submodule "third_party/pyroenc"]
path = third_party/pyroenc
url = https://github.com/HansKristian-Work/pyroenc
[submodule "third_party/pyrowave"]
path = third_party/pyrowave
url = https://github.com/Themaister/pyrowave
@@ -0,0 +1,386 @@
cmake_minimum_required(VERSION 3.6)
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_C_STANDARD 99)
project(Granite LANGUAGES CXX C)
if (CMAKE_COMPILER_IS_GNUCXX OR (${CMAKE_CXX_COMPILER_ID} MATCHES "Clang"))
set(GRANITE_CXX_FLAGS -Wshadow -Wall -Wextra -Wno-comment -Wno-missing-field-initializers -Wno-empty-body)
if (${CMAKE_CXX_COMPILER_ID} MATCHES "Clang")
set(GRANITE_CXX_FLAGS ${GRANITE_CXX_FLAGS} -Wno-backslash-newline-escape)
endif()
if (NOT (${CMAKE_BUILD_TYPE} MATCHES "Release"))
message("Enabling frame pointer for profiling/debug.")
set(GRANITE_CXX_FLAGS ${GRANITE_CXX_FLAGS} -fno-omit-frame-pointer)
endif()
if (CMAKE_SYSTEM_PROCESSOR MATCHES "(x86)|(X86)|(amd64)|(AMD64)")
message("Enabling SSE3 support.")
set(GRANITE_CXX_FLAGS ${GRANITE_CXX_FLAGS} -msse3)
endif()
elseif (MSVC)
set(GRANITE_CXX_FLAGS /D_CRT_SECURE_NO_WARNINGS /wd4267 /wd4244 /wd4309 /wd4005 /MP)
endif()
include(GNUInstallDirs)
option(GRANITE_SHARED "Build Granite as a shared library." OFF)
option(GRANITE_ISPC_TEXTURE_COMPRESSION "Enable ISPC texture compression" OFF)
option(GRANITE_ASTC_ENCODER_COMPRESSION "Enable astc-encoder texture compression" OFF)
option(GRANITE_TOOLS "Build Granite tools." ON)
option(GRANITE_KHR_DISPLAY_ACQUIRE_XLIB "Try to acquire Xlib display when using VK_KHR_display." OFF)
option(GRANITE_ANDROID_APK_FILESYSTEM "Use APK file system for assets and builtin files." ON)
option(GRANITE_ANDROID_SWAPPY "Use swappy on Android." OFF)
option(GRANITE_SHADER_COMPILER_OPTIMIZE "Optimize SPIR-V." ON)
option(GRANITE_VULKAN_SYSTEM_HANDLES "Add system handle support to Vulkan backend." ON)
option(GRANITE_RENDERER "Add higher level rendering functionality." ON)
option(GRANITE_VULKAN_SPIRV_CROSS "Add reflection support to Vulkan backend." ON)
option(GRANITE_VULKAN_SHADER_MANAGER_RUNTIME_COMPILER "Enable Vulkan GLSL runtime compiler." ON)
option(GRANITE_VULKAN_FOSSILIZE "Enable support for Fossilize." ON)
option(GRANITE_VULKAN_PROFILES "Enable profiles support." OFF)
option(GRANITE_VULKAN_DXGI_INTEROP "Enable DXGI interop when running on Windows." ON)
option(GRANITE_VULKAN_POST_MORTEM "Add extra support for port-mortem debug." OFF)
option(GRANITE_AUDIO "Enable Audio support." OFF)
option(GRANITE_PLATFORM "Granite Platform" "SDL")
option(GRANITE_HIDDEN "Declare symbols as hidden by default. Useful if you build Granite as a static library and you link to it in your shared library." OFF)
option(GRANITE_SANITIZE_ADDRESS "Sanitize address" OFF)
option(GRANITE_SANITIZE_THREADS "Sanitize threads" OFF)
option(GRANITE_SANITIZE_MEMORY "Sanitize memory" OFF)
option(GRANITE_TARGET_NATIVE "Target native arch (-march=native)" OFF)
option(GRANITE_BULLET "Enable Bullet support." OFF)
option(GRANITE_RENDERDOC_CAPTURE "Enable support for RenderDoc capture from API." ON)
option(GRANITE_INSTALL_TARGETS "Add install targets." ON)
option(GRANITE_INSTALL_EXE_TARGETS "Add executable install targets." OFF)
option(GRANITE_FFMPEG "Enable FFmpeg." OFF)
option(GRANITE_FFMPEG_VULKAN "Enable experimental Vulkan HW decode support in FFmpeg." OFF)
option(GRANITE_FAST_MATH "Enable fast math." OFF)
option(GRANITE_SHIPPING "Disable code paths not related to development." OFF)
option(GRANITE_SYSTEM_SDL "Use system SDL3 instead of vendored submodule." OFF)
if (GRANITE_FAST_MATH)
message("Enabling fast math.")
if (CMAKE_COMPILER_IS_GNUCXX OR (${CMAKE_CXX_COMPILER_ID} MATCHES "Clang"))
set(GRANITE_CXX_FLAGS ${GRANITE_CXX_FLAGS} -ffast-math)
elseif (MSVC)
set(GRANITE_CXX_FLAGS ${GRANITE_CXX_FLAGS} /fp:fast)
endif()
else()
message("Enabling precise math.")
if (MSVC)
set(GRANITE_CXX_FLAGS ${GRANITE_CXX_FLAGS} /fp:precise)
endif()
endif()
if (NOT GRANITE_PLATFORM)
set(GRANITE_PLATFORM "SDL")
endif()
if (GRANITE_HIDDEN)
if (CMAKE_COMPILER_IS_GNUCXX OR (${CMAKE_CXX_COMPILER_ID} MATCHES "Clang"))
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fvisibility=hidden")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fvisibility=hidden")
endif()
elseif(${GRANITE_PLATFORM} MATCHES "libretro")
if (NOT GRANITE_SHARED)
if (CMAKE_COMPILER_IS_GNUCXX OR (${CMAKE_CXX_COMPILER_ID} MATCHES "Clang"))
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fvisibility=hidden")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fvisibility=hidden")
endif()
endif()
endif()
if (GRANITE_TARGET_NATIVE)
if (CMAKE_COMPILER_IS_GNUCXX OR (${CMAKE_CXX_COMPILER_ID} MATCHES "Clang"))
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -march=native")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -march=native")
endif()
endif()
set(GRANITE_LINK_FLAGS)
if (${CMAKE_VERSION} GREATER "3.13")
# Executable targets are not stripped in release mode on Android, do it explicitly.
add_link_options($<$<AND:$<PLATFORM_ID:Android>,$<CONFIG:RELEASE>>:-s>)
endif()
if (GRANITE_SANITIZE_ADDRESS)
set(GRANITE_CXX_FLAGS ${GRANITE_CXX_FLAGS} -fsanitize=address)
set(GRANITE_LINK_FLAGS "${GRANITE_LINK_FLAGS} -fsanitize=address")
endif()
if (GRANITE_SANITIZE_THREADS)
set(GRANITE_CXX_FLAGS ${GRANITE_CXX_FLAGS} -fsanitize=thread)
set(GRANITE_LINK_FLAGS "${GRANITE_LINK_FLAGS} -fsanitize=thread")
endif()
if (GRANITE_SANITIZE_MEMORY)
set(GRANITE_CXX_FLAGS ${GRANITE_CXX_FLAGS} -fsanitize=memory)
set(GRANITE_LINK_FLAGS "${GRANITE_LINK_FLAGS} -fsanitize=memory")
endif()
if (ANDROID)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
endif()
if (GRANITE_SHARED)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
endif()
if (GRANITE_SHARED)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
else()
if (${GRANITE_PLATFORM} MATCHES "libretro")
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
elseif (GRANITE_POSITION_INDEPENDENT)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
elseif (ANDROID)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
else()
set(CMAKE_POSITION_INDEPENDENT_CODE OFF)
endif()
endif()
set(GRANITE_VERSION_MAJOR 0)
set(GRANITE_VERSION_MINOR 0)
set(GRANITE_VERSION_PATCH 0)
# Make sure .dlls are placed next to .exe, since there is no RPATH.
if (WIN32)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})
endif()
function(granite_install_executable NAME)
install(TARGETS ${NAME} LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
if (NOT WIN32)
set_target_properties(${NAME} PROPERTIES INSTALL_RPATH "\$ORIGIN/../lib")
endif()
endfunction()
macro(granite_setup_default_link_libraries NAME)
if (NOT WIN32)
target_link_libraries(${NAME} PRIVATE -pthread dl m)
find_library(LIBRT_LIBRARY rt)
if (EXISTS ${LIBRT_LIBRARY})
target_link_libraries(${NAME} PUBLIC rt)
endif()
endif()
if (WIN32 AND CMAKE_COMPILER_IS_GNUCXX)
target_link_libraries(${NAME} PRIVATE -static gcc stdc++ winpthread)
endif()
endmacro()
function(add_granite_third_party_lib NAME)
if (GRANITE_SHARED)
add_library(${NAME} SHARED ${ARGN})
if (GRANITE_INSTALL_TARGETS)
if (WIN32)
install(TARGETS ${NAME} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR})
else()
install(TARGETS ${NAME} DESTINATION ${CMAKE_INSTALL_LIBDIR})
set_target_properties(${NAME} PROPERTIES INSTALL_RPATH "\$ORIGIN/")
set_target_properties(${NAME} PROPERTIES
VERSION ${GRANITE_VERSION_MAJOR}.${GRANITE_VERSION_MINOR}.${GRANITE_VERSION_PATCH}
SOVERSION ${GRANITE_VERSION_MAJOR})
endif()
endif()
granite_setup_default_link_libraries(${NAME})
if (CMAKE_COMPILER_IS_GNUCXX OR (${CMAKE_CXX_COMPILER_ID} MATCHES "Clang"))
set_target_properties(${NAME} PROPERTIES LINK_FLAGS "-Wl,--no-undefined")
endif()
if (MSVC)
set_target_properties(${NAME} PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS ON)
endif()
else()
add_library(${NAME} STATIC ${ARGN})
if (MSVC)
set_target_properties(${NAME} PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS OFF)
endif()
endif()
endfunction()
function(add_granite_internal_lib NAME)
add_granite_third_party_lib(${NAME} ${ARGN})
target_compile_options(${NAME} PRIVATE ${GRANITE_CXX_FLAGS})
if (GRANITE_SHARED AND MSVC)
set_target_properties(${NAME} PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS ON)
endif()
endfunction()
function(add_granite_internal_static_lib NAME)
add_library(${NAME} STATIC ${ARGN})
target_compile_options(${NAME} PRIVATE ${GRANITE_CXX_FLAGS})
endfunction()
add_subdirectory(third_party)
add_subdirectory(util)
add_subdirectory(path)
add_subdirectory(math)
add_subdirectory(threading)
add_subdirectory(compiler)
add_subdirectory(filesystem)
add_subdirectory(vulkan)
add_subdirectory(ecs)
add_subdirectory(event)
if (GRANITE_VULKAN_SYSTEM_HANDLES AND GRANITE_RENDERER)
add_subdirectory(renderer)
add_subdirectory(ui)
add_subdirectory(scene-export)
endif()
if (GRANITE_FFMPEG)
add_subdirectory(video)
endif()
add_subdirectory(application)
if (GRANITE_AUDIO)
add_subdirectory(audio)
endif()
if (GRANITE_BULLET)
add_subdirectory(physics)
endif()
add_library(granite-base INTERFACE)
target_link_libraries(granite-base INTERFACE granite-util granite-path granite-application granite-vulkan)
if (TARGET granite-renderer)
target_link_libraries(granite-base INTERFACE granite-renderer granite-ui)
endif()
function(add_granite_executable NAME)
if (ANDROID)
add_library(${NAME} SHARED ${ARGN})
target_link_libraries(${NAME} PRIVATE log android)
elseif(${GRANITE_PLATFORM} MATCHES "libretro")
add_library(${NAME} SHARED ${ARGN})
if (CMAKE_COMPILER_IS_GNUCXX OR (${CMAKE_CXX_COMPILER_ID} MATCHES "Clang"))
set_target_properties(${NAME} PROPERTIES LINK_FLAGS "-Wl,--no-undefined")
endif()
else()
add_executable(${NAME} ${ARGN})
endif()
# Need this for some reason on OSX.
if (APPLE)
set_target_properties(${NAME} PROPERTIES LINK_FLAGS "-Wl,-all_load")
endif()
target_link_libraries(${NAME} PRIVATE granite-base granite-application-global-init)
target_compile_options(${NAME} PRIVATE ${GRANITE_CXX_FLAGS})
granite_setup_default_link_libraries(${NAME})
if (GRANITE_SANITIZE_ADDRESS OR GRANITE_SANITIZE_THREADS OR GRANITE_SANITIZE_MEMORY)
set_target_properties(${NAME} PROPERTIES LINK_FLAGS "${GRANITE_LINK_FLAGS}")
endif()
endfunction()
function(add_granite_application TARGET_NAME)
if (WIN32 AND (NOT ${GRANITE_PLATFORM} MATCHES "libretro"))
add_granite_executable(${TARGET_NAME} WIN32 ${ARGN})
else()
add_granite_executable(${TARGET_NAME} ${ARGN})
endif()
if (ANDROID)
target_link_libraries(${TARGET_NAME} PRIVATE granite-base granite-platform)
elseif (${GRANITE_PLATFORM} MATCHES "libretro")
target_link_libraries(${TARGET_NAME} PRIVATE granite-base granite-platform)
else()
target_link_libraries(${TARGET_NAME} PRIVATE granite-application-entry)
endif()
granite_setup_default_link_libraries(${TARGET_NAME})
if (GRANITE_INSTALL_EXE_TARGETS)
granite_install_executable(${TARGET_NAME})
endif()
endfunction()
function(add_granite_headless_application TARGET_NAME)
add_executable(${TARGET_NAME} ${ARGN})
target_compile_options(${TARGET_NAME} PRIVATE ${GRANITE_CXX_FLAGS})
if (GRANITE_SANITIZE_ADDRESS OR GRANITE_SANITIZE_THREADS OR GRANITE_SANITIZE_MEMORY)
set_target_properties(${TARGET_NAME} PROPERTIES LINK_FLAGS "${GRANITE_LINK_FLAGS}")
endif()
if (ANDROID)
target_link_libraries(${TARGET_NAME} PRIVATE log android)
endif()
target_link_libraries(${TARGET_NAME} PRIVATE granite-base granite-application-entry-headless)
granite_setup_default_link_libraries(${TARGET_NAME})
if (GRANITE_INSTALL_EXE_TARGETS)
granite_install_executable(${TARGET_NAME})
endif()
endfunction()
function(add_granite_offline_tool NAME)
add_executable(${NAME} ${ARGN})
target_compile_options(${NAME} PRIVATE ${GRANITE_CXX_FLAGS})
# Need this for some reason on OSX.
if (APPLE)
set_target_properties(${NAME} PROPERTIES LINK_FLAGS "-Wl,-all_load")
endif()
target_link_libraries(${NAME} PRIVATE granite-base granite-application-global-init)
target_compile_options(${NAME} PRIVATE ${GRANITE_CXX_FLAGS})
if (GRANITE_SANITIZE_ADDRESS OR GRANITE_SANITIZE_THREADS OR GRANITE_SANITIZE_MEMORY)
set_target_properties(${NAME} PROPERTIES LINK_FLAGS "${GRANITE_LINK_FLAGS}")
endif()
granite_setup_default_link_libraries(${NAME})
if (GRANITE_INSTALL_EXE_TARGETS)
granite_install_executable(${NAME})
endif()
endfunction()
if ("${CMAKE_CURRENT_SOURCE_DIR}" STREQUAL "${CMAKE_SOURCE_DIR}")
set(GRANITE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
else()
set(GRANITE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}" PARENT_SCOPE)
set(GRANITE_CXX_FLAGS ${GRANITE_CXX_FLAGS} PARENT_SCOPE)
set(GRANITE_LINK_FLAGS ${GRANITE_LINK_FLAGS} PARENT_SCOPE)
endif()
function(add_blob_archive_target TARGET_NAME BLOB_NAME)
find_package(Python3 REQUIRED)
set_source_files_properties(
${CMAKE_BINARY_DIR}/${BLOB_NAME}.h
${CMAKE_BINARY_DIR}/${BLOB_NAME}.c
PROPERTIES GENERATED ON)
add_library(${TARGET_NAME} STATIC
${CMAKE_BINARY_DIR}/${BLOB_NAME}.h
${CMAKE_BINARY_DIR}/${BLOB_NAME}.c)
target_include_directories(${TARGET_NAME} PUBLIC ${CMAKE_BINARY_DIR})
# ... CMake, y u gotta be like this.
set(_arg_list)
set(_arg_counter 0)
foreach(_arg IN LISTS ARGN)
math(EXPR _arg_counter_mod "${_arg_counter} % 2")
if (NOT ${_arg_counter_mod})
list(APPEND _arg_list --input)
endif()
list(APPEND _arg_list ${_arg})
math(EXPR _arg_counter "${_arg_counter} + 1")
endforeach()
add_custom_command(OUTPUT
${CMAKE_BINARY_DIR}/${BLOB_NAME}.h
${CMAKE_BINARY_DIR}/${BLOB_NAME}.c
COMMAND ${Python3_EXECUTABLE}
${GRANITE_SOURCE_DIR}/tools/blobify.py
--output ${CMAKE_BINARY_DIR}/${BLOB_NAME}.blob
${_arg_list}
COMMAND ${Python3_EXECUTABLE}
${GRANITE_SOURCE_DIR}/tools/bin_to_text.py
--output ${CMAKE_BINARY_DIR}/${BLOB_NAME}
--blob-name ${BLOB_NAME}
${CMAKE_BINARY_DIR}/${BLOB_NAME}.blob
BYPRODUCTS
${CMAKE_BINARY_DIR}/${BLOB_NAME}.blob)
endfunction()
if (TARGET granite-renderer)
add_subdirectory(tests)
add_subdirectory(viewer)
if (GRANITE_TOOLS)
add_subdirectory(tools)
add_subdirectory(renderer/fft/test)
endif()
endif()
if (GRANITE_TOOLS AND GRANITE_VULKAN_SPIRV_CROSS AND GRANITE_VULKAN_SHADER_MANAGER_RUNTIME_COMPILER)
add_subdirectory(slangmosh)
endif()
+20
View File
@@ -0,0 +1,20 @@
Copyright (c) 2017-2026 Hans-Kristian Arntzen
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+632
View File
@@ -0,0 +1,632 @@
# Granite code base overview
The top level structure contains several folders which contain high level concepts.
## Build system
The build system is pure CMake and should be very straight forward to use.
To build Granite as a standalone project for example to run the glTF viewer, it's standard CMake:
```
mkdir build
cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
make -j16 # Or whatever build system cmake spits out.
./viewer/gltf-viewer /path/to/my.gltf
```
A Python installation is probably necessary for SPIRV-Tools to build.
### Submodules
Granite uses submodules to pull in third party modules. Make sure that
`git submodule update --init` is called when checking out new versions of Granite.
### Building Android apps with Granite
There is no builtin way of building Android apps, but it's fairly straight forward to set up.
You can use the `gltf-viewer` as an example.
#### Setting up the Gradle app folder
You'll need an app folder. This is found under `viewer/app`, but you will need to make your own.
Here you place the Android manifest. The manifest needs to point to the GraniteActivity.
This activity is basically just NativeActivity with a few extra utility functions.
The lib_name needs to point to a particular native library which is the
CMake target you have chosen to use for your CMake `add_granite_application()`.
See `viewer/CMakeLists.txt` for how to use `add_granite_application()`.
See `viewer/app/AndroidManifest.xml` for an example manifest file.
`viewer/app/build.gradle` connects CMake, the manifest, where to pull assets from, etc.
Make sure that the builtin resources in assets/ are pulled in as well as your own assets.
The viewer application has its own asset folder in `viewer/assets`.
For example, to try the Android version of the `gltf-viewer`, place a scene called `scene.glb`
into `viewer/assets` and build. By default, the Android app will load from `assets://scene.glb`, which will
point to the APKs asset manager. You can place `android.json`, `config.json` and `quirks.json` into the assets folder as well.
See the code for more detail.
In `viewer/app/res`, various icons and string resources should go as normal.
The default `AndroidManifest.xml` points to a built-in Android icon, so you'll probably have to add that.
In `viewer/app/build.gradle` replace what is needed. Likely, only the `targets` line.
Otherwise, it can mostly be copy-pasted.
`viewer/build.gradle` is weird magic that has to be there.
The gradle plugin version might depend a bit on your Android Studio installation.
`viewer/settings.gradle` pulls in your app as well as the simple, common GraniteActivity Java cruft.
Once you've set it up, a normal gradle build should suffice.
### Using Granite as a dependency
Granite is designed to be used as a statically linked library, but can be built with position independent code if you need
to wrap your application in a dynamic library (e.g. libretro target).
The normal idea is to have Granite somewhere as a folder in your tree, either a submodule, symlink or whatever.
Create a CMake project and add Granite as a subdirectory.
Use `add_granite_application()` to set up the build.
It's mostly just a convenience script to link in relevant targets.
```
cmake_minimum_required(VERSION 3.5)
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_C_STANDARD 99)
project(AppBringup LANGUAGES CXX C)
add_subdirectory(Granite EXCLUDE_FROM_ALL)
add_granite_application(app-bringup app_bringup.cpp)
target_link_libraries(app-bringup renderer)
if (NOT ANDROID)
target_compile_definitions(app-bringup PRIVATE ASSET_DIRECTORY=\"${CMAKE_CURRENT_SOURCE_DIR}/assets\")
endif()
```
### Using Granite as a pure Vulkan backend
Some projects would only be interested in the raw Vulkan backend.
The Vulkan backend has some dependencies on other Granite modules which might end up unwieldy.
Most of these can be stripped out, e.g.:
```
# If using Granite as part of your shared library.
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
set(GRANITE_POSITION_INDEPENDENT ON CACHE STRING "Granite position independent" FORCE)
set(GRANITE_HIDDEN ON CACHE STRING "Granite hidden" FORCE)
# Disable dependencies on other modules.
set(GRANITE_VULKAN_FOSSILIZE OFF CACHE STRING "Vulkan Fossilize" FORCE)
set(GRANITE_VULKAN_SYSTEM_HANDLES OFF CACHE STRING "Vulkan filesystem" FORCE)
add_subdirectory(Granite EXCLUDE_FROM_ALL)
```
At this point, it should only depend on SPIRV-Cross and volk.
## `application/`
This module implements anything related to an applications lifecycle.
Platform specific application things go in here as well, like various backends for `VkSurfaceKHR`,
and input handling.
- Android
- GLFW
- Headless (no WSI, used for benchmarking)
- VK_KHR_display
- libretro
- Custom plugin surface (used if you have some fancy, special purpose surface code).
This module defines the "Application" interface, used by the platform code in the main loop.
The platform code implements main() or whatever equivalent the platform requires.
The gltf-viewer application is also implemented here by `SceneViewerApplication`.
This application can load glTF scenes, and you can move around, add lights, I use it as a sandbox for testing stuff.
See `application.cpp` for how to implement a `Granite::Application` interface.
You basically implement a `render_frame()` callback.
Applications are expected to implement `Granite::application_create()`, which should create
an instance of the application, and the platform code will pump this through.
Applications do not own their main loop as some platforms do not support that.
It is perfectly possible to avoid this and just use the Vulkan backend API directly.
This is useful for standalone tooling which needs to use the GPU.
See `tools/convert_cube_to_environment.cpp` as an example here.
## `assets/`
Here the builtin assets for Granite are found. Various shaders for the most part and a couple of look-up textures.
These are not relevant if you only use Granite for its Vulkan backend.
## `compiler/`
Here `third_party/shaderc` is used to implement GLSL -> SPIR-V compilation in run-time,
with or without optimizations with SPIRV-Tools-opt.
I do this for convenience sake.
A production application will probably want to pre-compile shaders and ship the SPIR-V only.
## `event/`
Here you find the event manager. The event manager is global, and is used to dispatch events throughout the application in a decoupled way.
There are two kinds of events, latched and immediate. Latched events are events which continue to persist until they are destroyed.
This is used for things like "device created" and "swapchain created". The device remains alive until it dies, similar with the swapchain.
For new event handler registrations, the event manager will make sure to fire events for device created events even if the device created event has already been fired.
This allows subsystems to always know when the Vulkan device is alive, or dead without having to plumb a ton of state through all the subsystems.
Immediate events are things like "key pressed", "mouse moved" etc. These events fire once and disappear.
Event handlers must be member function which inherit from `Granite::EventHandler`.
You can easily create your own events by tagging a type.
E.g.:
```
class SwapchainParameterEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(SwapchainParameterEvent)
};
class TestApp : public Granite::EventHandler
{
TestApp()
{
EVENT_MANAGER_REGISTER_LATCH(TestApp, on_swapchain_created, on_swapchain_destroyed, SwapchainParameterEvent);
EVENT_MANAGER_REGISTER(TestApp, on_key_pressed, KeyboardEvent);
}
// return true to keep responding to these events, false to detach yourself.
bool on_key_pressed(const KeyboardEvent &e);
void on_swapchain_created(const SwapchainParameterEvent &e);
void on_swapchain_destroyed(const SwapchainParameterEvent &);
};
```
This module also implements a simple Entity Component System (ECS). (Not sure why I moved it to event/, but anyways).
This is used by the scene graph for various purposes. Some pretty funky variadic template magic happens here.
You create entities, and components are added to them. You can query for component groups, e.g. "give me all renderables which should be rendered opaque", things like that.
It's a very neat system for different kind of queries.
It's likely very unoptimized compared to what it could be,
but I haven't had any issues except that removing components is quite costly.
## `filesystem/`
This implements a filesystem.
The design here is a bit radical. The only file operations supported are `mmap()` or `MapViewOfFile()`. No stream API exists.
On Android, the APK buffer mapping feature is used (which should basically map to mmap anyways for uncompressed files).
The path system is based around protocols where a path is `protocol://path/inside/protocol`. The builtin protocols are:
- `builtin://`. Should always refer to `assets/` in some shape or form, where you can find the Granite builtin shaders and so on.
- `assets://`. Application specific assets. Usually set up by the application manually.
- `cache://`. Used for various caching purposes internally. Usually just points to `build_directory/cache`.
Paths without a protocol are thought to be of the `file://` protocol, which is basically the same as just using the normal filesystem
path system directly.
The filesystem supports notions of notifications when anything changes. There is a backend for inotify on Linux.
This is used to automatically reload textures and shaders when they are modified on disk. Very useful for hacking.
The protocols are set up automatically on startup, but can be overridden by the application.
## `math/`
A simple math library cloning the parts of GLM I need. It used to be GLM, but it got bogged down in very long compile times,
so I rewrote only what I needed and sped up compilation by ~40/50% o_O.
Matrix math conventions are like GLM:
- Matrices are column major. Indexing into a matrix picks out columns.
- Quaternions are declared in order W, X, Y, Z.
- The coordinate system is GL-style view space, a right handed coordinate system. In view space +X is right, +Y is up, camera points towards -Z.
- Clip space is Vulkan-like (duh). X is right, Y is down (top-left, not bottom-left like GL!), Z = 1 is far plane, Z = 0 is near plane.
Basically the projection matrices are like GL, except Y is flipped. I could have gone for bottom-left clip space,
but I would require VK_KHR_maintenance1 which isn't supported on some old Android phones (yay ...).
## `network/`
Some Linux-only networking code lying around.
It is used by the networked filesystem code, but it hasn't been worked on for a long time.
## `renderer/`
The biggest module. This module is responsible for implementing the high level rendering
code which deals with the problem of meshes, materials, scene graphs, lights, etc, etc.
### `renderer/fft/`
A Vulkan port of my GLFFT library. Used by Ocean rendering.
### `renderer/lights/`
Light rendering, including a clustered shading system.
### `renderer/post/`
Post-FX. HDR bloom and tonemapping, FXAA, SMAA and TAA.
### `renderer/utils/`
Some simple utility functions, like converting cubes and equirect into IBL and so on.
It can also read back and save textures (GTX format) to disk, even for arrays, cubes, etc.
### `abstract_renderable.hpp`
Renderable objects inherit from this. The `Renderer` will ask the `AbstractRenderable` for rendering info, which is pushed into a `RenderQueue`.
See `mesh.cpp` for the `StaticMesh` implementation.
### `render_components.hpp`
Some component types used in the renderer, like `OpaqueComponent`, `RenderableComponent`, etc.
Applications will pull out the components which are relevant and act on all of entities which have
certain combinations of components.
### The flow of rendering objects
You should study `SceneViewerApplication` for details.
The architecture for rendering glTF scenes is roughly as follows:
#### Setup
- Load scene with `SceneLoader`. This gives you a `Scene` which is basically an ECS paired with a node hierarchy.
- Have a `RenderContext`. This is responsible for containing global camera information, view and projection matrices,
frustum planes (for culling) and lighting setup.
- Set up a `LightingParameters` struct. Fill in relevant data, and pass it to `RenderContext::set_lighting_parameters()`.
- Make some Camera, either an `FPSCamera` or pick a camera from the `Scene`.
- Set up a `Renderer` with a constructor argument depending if it's a forward, deferred or lighting renderer.
If using forward, you need to set options using `Renderer::set_mesh_renderer_options_from_lighting()`.
This makes sure that the shaders will support all the features required from the Lighting setup.
#### Per-frame
You can modify `Scene::Node` transforms every frame for say, animation.
Every frame you need to call `Scene::update_cached_transforms()`. This will walk through the node hierarchy and update
world space `AABB`, world model matrix as well as normal matrices, or the transforms for all bones for skinned meshes.
Also, we need to update the `RenderContext` based on the Camera. `RenderContext::set_camera()` will do this.
Keep a `VisibilityList` around.
Query the `Scene` for renderables. For forward rendering, you could do something like:
- `Scene::gather_visible_opaque_renderables()`
- `Scene::gather_visible_transparent_renderables()`
- `Scene::gather_unbounded_renderables()`
You pass in the render context and a visibility list, and out comes all the objects you need to render.
To actually render, you would do:
- `Renderer::begin()`: Resets render queues.
- `Renderer::push_renderables()`: Calls `AbstractRenderable::get_render_info()` to push data into the render queue.
For depth-only rendering, use `push_depth_renderables` to get simpler rendering.
- `Renderer::flush()`: Sorts the queue as appropriate, batches, and submits commands to `CommandBuffer`.
You will need to be in a render pass (see Vulkan section) to call `flush()`.
It is possible to pass in various flags to `flush()` which controls some common render state.
### Render graph
A powerful system for declaring the rendering you're doing up front, and have the render graph sort out dependencies and synchronization.
Used by the `SceneViewerApplication`.
### Scene and scene loader
Loads glTF scene, and constructs a `Scene` from it. The scene contains a node hierarchy as well as an Entity Component System to let application query relevant object types.
### Shader suite
The renderer uses a bank of shaders which renderable objects will pull shaders from. To get alternative shaders for glTF meshes and other renderables, this could be modified.
## `scene_formats/`
Deals with glTF file format import and export as well as dealing with compressed textures.
- BC1/3/6/7 is supported through ISPC
- BC 4/5 is supported by custom code
- ASTC is supported by ISPC or astcenc
A special purpose texture file format is defined here as well (GTX, Granite Texture Format, totally not confusing to anyone :P).
It's designed to be loaded directly as a memory mapped file and integrates nicely with the filesystem code.
This used to use GLI to load KTX files, but there was no clean way to pipe it through with `mmap()`
and compile times soared.
## `tests/`
A couple ad-hoc tests and sandboxy code to test things quick and dirty.
You can find some API usage examples here.
## `third_party/`
Submodule heaven. All third party submodules are checked out here. See README.md for which submodules are used.
## `threading/`
A fairly straight forward thread pool with task scheduling and dependency tracking. Not used that extensively yet.
Used for texture loading in the texture manager and for the CPU clustering implementation.
The idea is to make the render graph automatically thread everything through this, but that's a pretty large TODO.
## `tools/`
Some useful tools for special purposes. Read source for more details.
### `aa-bench`
Benchmarks AA implementations.
### `sweep_*.py`
Benchmarks and analyzes a glTF scene rendered in many different ways.
### `gltf-repacker`
Optimizes a glTF scene.
Removes duplicate mesh data, quantizes attributes, optimizes meshes, compresses textures (into GTX format), etc.
### `obj-to-gltf`
A crude converter from OBJ to glTF. Needed a special purpose converter once for Sponza.
## `ui/`
A bare-bones retained mode UI experiment.
## `util/`
Various standalone utility classes.
The most widely used one is the `IntrusivePtr` and `IntrusiveList`, used by the Vulkan backend
to deal with ref-counted handles.
The object pool is also used extensively. Pairing object pool with intrusive pointer was found
to be very nifty indeed.
## `viewer/`
A standalone glTF viewer. It uses `SceneViewerApplication` directly.
## `vulkan/`
This is the Vulkan backend and is the part of the code base which needs most
explanation and rationale behind its design.
### `context.cpp` and `context.hpp`
Here we have the "context". This is the module which:
- Initializes the Vulkan loader
- Creates a VkInstance
- Creates a VkDevice
- Initializes various VkQueues.
The goal is to find 3 queues.
One for graphics or "general" workloads,
one compute queue (ideally async compute in a different queue family),
and one transfer/DMA queue (ideally a separate DMA engine).
The queues will alias each other if no "ideal" queue can be found.
The context owns the lifetime for a VkInstance or VkDevice.
Validation layers are also hooked up and reported here.
### `wsi.cpp` and `wsi.hpp`
In the layer below we have the WSI or windowing system integration.
This module is responsible for managing `VkSurfaceKHR` and `VkSwapchainKHR`.
Surfaces are created and interacted with through the `WSIPlatform`
interface which the platform code (or application code) is responsible for implementing.
The module also has support for external swapchains, i.e. a swapchain whose images do not belong to a `VkSwapchainKHR`.
This is is useful for applications which want to use offscreen rendering only.
In this case, the implementation can simply pretend it's working with swapchain images
and provide release semaphores and take acquire semaphores directly from the application.
The WSI owns a `Device` instance as well, so `WSI` is essentially a superset of `Device`.
Normally, an `Application` instance owns a `WSI` instance as well,
so applications normally don't need to know about this.
The main entry points into WSI which are called on a per-frame basis is:
- `WSI::begin_frame()`: Acquires the swapchain, and calls `Device::begin_frame()`
- `WSI::end_frame()`: Flushes the frame with `Device::end_frame()`. If the swapchain was rendered into,
`vkQueuePresentKHR` will be called and the backbuffer is flipped on-screen. If the swapchain was not rendered into,
the implementation will stall with `vkDeviceWaitIdle` as it assumes the application is in some
kind of loading scenario and old resources should be flushed out.
This is typically called by the platform code via `Application::run_frame()`.
### `device.cpp` and `device.hpp`
This is the main interface to Vulkan, `Vulkan::Device`. Here you:
- Create and allocate resources
- Request command buffers
- Submit command buffers and signal fences and/or semaphores
- Wait for semaphores, etc
- Various physical device queries (like format support, etc)
- Interact with texture and shader managers
#### Per-frame resources
To manage synchronization between CPU and GPU at a higher level, the implementation
has a concept of frames. Each frame corresponds to a swapchain frame.
Each frame owns a data structure which serves as a pool of various resources to be deleted or recycled.
If you allocate command buffers, it comes from the pool of the current frame.
If you delete a resource like a buffer or image, it will be recycled back into
the per-frame pool, and deleted later.
Each frame has a list of fences to wait for and/or recycle when that frame index begins.
When a particular frame has begun (usually pumped through by WSI acquire), fences are waited on,
and deferred actions happen. This will be resetting command pools, descriptor set allocator state updated,
buffers and images deleted, fences and semaphores recycled, and so on.
This scheme means we avoid having to think too hard about waiting on GPU to complete stuff
before we touch them on CPU.
#### Command buffer requests and submissions
To submit work to the GPU, you can request a command buffer with a specific type:
- Generic
- Async compute
- Async graphics (tries to run graphics on the compute queue if it can support it, very special purpose)
- Transfer
This maps directly to a `VkCommandBuffer`, but has a lot of plumbing around it to make it easy.
There is a separate section for the details.
When you request a command buffer you lock the current frame, and it cannot end until you submit
the command buffer. The reasoning for this is multi-threading complicated things
and the fact that command buffers are owned by a single `VkCommandPool`,
and thus allocators which are tied to a particular frame.
I chose not to decouple command buffer allocation from the frame,
or we would end up with a huge number of separate pools,
which on some implementations would be pretty terrible.
Locking is only a potential problem with multi-threading if we try to record
some command buffer completely asynchronously with the swapchain and that operation takes a long time,
but recording the actual command buffer should be a quick and easy operation.
If locking ever becomes a problem, this needs to be redesigned a bit.
The main case where this "async command buffer" recording happens is threaded image uploads which are done by the texture manager.
However, just recording a simple `copy_buffer_to_image()` isn't the most expensive thing in the world.
#### Resources
Resources which are held by the application are managed through a "smart pointer",
but it is effectively typedef-ed away. All resource types like Buffer, Image,
etc implement `IntrusivePtrEnabled`, which embeds a shared_ptr-like control block internally.
The intent here is that we can easily make it single-threaded ref-count or multi-threaded (atomics),
by flipping a define or by changing the IntrusivePtrEnabled inheritance.
Resource handle memory is managed through an `ObjectPool`,
which means freeing and allocating objects should minimize the required heap allocations.
When the `IntrusivePtr` is destroyed, it recycles itself properly into the respective pools.
#### Allocating GPU memory
Allocating GPU memory is done by a custom heap memory allocator.
This predates the de-factor memory allocator from AMD by a long while, but it's basically the same concept.
To the host side, you have some choices when you allocate buffers and images.
Host-visible memory is always persistently mapped with `vkMapMemory()`.
Mapping and unmapping in the API is basically free,
except maybe `vkFlushMappedRanges` and similar if you're using incoherent memory.
For buffers you can decide between:
- Device: The buffer will be kept in `DEVICE_LOCAL` memory.
It may or may not be `HOST_VISIBLE`.
- Host: The buffer will be kept in `HOST_VISIBLE | HOST_COHERENT`.
Designed for upload to the GPU, because it's likely not `CACHED`.
- CachedHost: `HOST_VISIBLE | HOST_CACHED`. May not be `COHERENT`,
but the details here are abstracted through `Device::map_host_buffer()`.
Use it for readbacks from GPU to CPU.
For images you can pick:
- Physical: Backed by physical memory.
- Transient: Only backed by on-chip tile memory. Use for g-buffers, etc, although there is a simpler interface
for requesting transient surfaces, see `Device::get_transient_attachment()`.
E.g.:
```
CommandBufferHandle cmd = device->request_command_buffer();
cmd->set_texture(set = 0, binding = 1, view = my_texture->get_view(), sampler = StockSampler::LinearClamp);
```
#### Shaders
Shader objects can be requested from the Device by providing a SPIR-V blob.
Granite will manage these internally and build reflection information using SPIRV-Cross.
Programs are linked together using multiple Shader objects, this creates the final pipeline layout,
and we set up descriptor set allocators based on the associated descriptor set layouts.
#### Texture and shader managers
If filesystem support is built in (default), the Granite device also supports
a texture and shader manager. These allow you to pass in paths, and get handles back.
Through the magic of inotify, the backing shaders will be recompiled and textures will automatically update themselves.
The texture manager loads images in the background in the thread pool.
#### Submitting command buffers, signalling sync objects and waiting
You can submit command buffers using `Device::submit()`.
This will queue up submissions internally until flushed, and submit it as a batch.
You can pass in either a pointer to `Fence`, and/or `Semaphore`.
These map directly to `VkFence` and `VkSemaphore` respectively.
If you signal a fence or semaphore,
there is an implicit flush to ensure that we don't end up in a wait-before-signal scenario.
Fences can wait on CPU, while semaphores can be waited on using `Device::add_wait_semaphore()` in a particular queue.
Note that semaphores can only be waited on once (Vulkan restriction), unlike fences.
You can manually flush using `Device::flush_frame()`, wait idle and reclaim all pending resources with `Device::wait_idle()`,
or only signal fence or semaphores using `Device::submit_empty()`.
### `command_buffer.hpp` and `command_buffer.cpp`
The aim of Granite is a "mid-level" abstraction. Some convenience is allowed at the cost of CPU cycles,
but not so much that we're back to GL levels of silliness.
#### Barriers and image layouts
Granite does not attempt to perform any synchronization on behalf of the application,
except for a few isolated cases like `create_buffer()` and `create_image()`,
where we can just wait on `VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT`, and block all possible consumers of the resource.
Another exception is rendering to the swap chain.
The backend will wait for the acquire semaphore and deal with layout transitions automatically.
The other exception is transient images, these are synchronized with `VK_SUBPASS_EXTERNAL` dependencies,
and implicit layout transitions in a render pass. Applications do not need to synchronize this.
It is your responsibility to synchronize with pipeline barriers, events, or semaphores.
You will also need to change image layouts manually.
The render graph is designed to remove most of the need to do manual synchronization like this.
Image handles can have one of two layouts "Optimal" and "General". If the image is in Optimal, the command buffer recorder
will always assume that the image is being used in its related, optimal layout. E.g., when sampling, it must be
`SHADER_READ_ONLY_OPTIMAL`. In General, the image layout is assumed to always be the catch-all `VK_IMAGE_LAYOUT_GENERAL`.
#### Descriptor set management
Descriptor sets are managed internally.
Granite uses a more traditional binding model where you bind resources to slots like in GL/D3D11, except,
to be more Vulkan-like, descriptor sets and bindings are separated. And there is no remapping of bindings.
On draw call time, Granite will build new descriptor sets for you, or reuse them if it has seen it before.
The application is freed from the burden of building descriptor sets by hand.
Descriptor set memory is managed internally as well, and recycled as appropriate.
#### Pipeline management
Granite also uses a more classic way of setting rendering state.
State can be saved and restored in a more stack-like way which removes most need to set specific rendering state
all the time in scene rendering. Pipelines are compiled on-demand.
Since on-demand pipeline creation can cause issues, Granite supports pipeline caches, as well as Fossilize, which
allows us to prewarm the internal hashmaps with VkPipelines ready to go if we so choose.
#### Allocating scratch data (VBO, IBO, UBO)
Sometimes you just need to stream out data and forget about it, like vertex buffers, index buffers, and in particular,
uniform buffer data. To avoid having to manage this memory explicitly, the command buffers has convenience functions
to allocate and bind. This allocation is basically free because it's backed by a pool of linear allocators.
Always use this for streamed data which can be discarded after you're done rendering.
#### Updating textures asynchronously
Similar to creating staging data for VBO, IBO and UBOs, you can do similar kind of updates to textures.
It will allocate staging data for you, issue `vkCmdCopyBufferToImage` commands and give you a pointer you can write.
#### Drawing
Granite supports the basic draw commands you'd expect.
On draw, any dirty state or dirty descriptor sets are resolved,
just like you would expect in an older engine.
#### Render passes
Granite has very explicit render passes, and maps almost 1:1 to Vulkan.
You fill in a `RenderPassInfo` and call `CommandBuffer::begin_render_pass()` and `end_render_pass()`.
You can declare a full multipass setup, but you are freed from the burden of figuring out subpass dependencies,
layout transitions in the render passes, etc.
For the simple non-multipass case, you need to set up:
- Color attachments w/ count
- Depth stencil attachment
- Which attachments should be cleared, and to what color.
- Which attachments should be loaded to tile.
- Which attachments should be stored and not discarded.
- Whether depth-stencil is read-only.
##### Rendering to swap chain
There is a special purpose function to render to the swap chain.
Use `Device::get_swapchain_render_pass()`.
You can pick if you want color-only, color/depth or color/depth/stencil attachments.
+205
View File
@@ -0,0 +1,205 @@
# Granite
Granite is my personal Vulkan renderer project.
## Why release this?
The most interesting part of this project compared to the other open-source Vulkan renderers so far
is probably the render graph implementation.
The project is on GitHub in the hope it might be useful as-is
for learning purposes or generating implementation ideas.
### Disclaimer
**Do not expect any support or help.
Pull requests will likely be ignored or dismissed.**
## License
The code is licensed under MIT. Feel free to use it for whatever purpose.
## High-level documentation
See `OVERVIEW.md`.
## Low-level rendering backend
The rendering backend focuses entirely on Vulkan,
so it reuses Vulkan enums and data structures where appropriate.
However, the API greatly simplifies the more painful points of writing straight Vulkan.
It's not designed to be the fastest renderer ever made, it's likely a happy middle ground between
"perfect" Vulkan and OpenGL/D3D11 w.r.t. CPU overhead.
- Memory manager
- Deferred destruction and release of API objects and memory
- Automatic descriptor set management
- Linear allocators for vertex/index/uniform/staging data
- Automatic pipeline creation
- Command buffer tracks state similar to older APIs
- Uses TRANSFER-queue on desktop to upload linear-allocated vertex/index/uniform data in bulk
- Vulkan GLSL for shaders, shaders are compiled in runtime with shaderc
- Pipeline cache save-to-disk and reload
- Warm up internal hashmaps with Fossilize
- Easier-to-use fences and semaphores
Missing bits:
- Multithreaded rendering
- Precompile all shaders to optimized SPIR-V
Implementation is found in `vulkan/`.
## High-level rendering backend
A basic scene graph, component system and other higher-level scaffolding lives in `renderer/`.
This is probably the most unoptimized and naive part.
## PBR renderer
Pretty barebones, half-assed PBR renderer. Very simplified IBL support.
Fancy rendering is not the real motivation behind this project.
## Post-AA
Fairly straight forward FXAA, SMAA and TAA (no true velocity buffer though).
## Automatic shader recompile and texture reload (Linux/Android only)
Immediately when shaders are modified or textures are changed, the resources are automatically reloaded.
The implementation uses inotify to do this,
so it's exclusive to Linux unless a backend is implemented on Windows (no).
## Network VFS
For Linux host and Android device,
assets and shaders can be pulled over TCP (via ADB port-forwarding) with `network/netfs_server.cpp`.
Quite convenient.
## Validation
In debug build, LunarG validation layers are enabled.
Granite is squeaky clean.
## Render graph
`renderer/render_graph.hpp` and `renderer/render_graph.cpp` contains a fairly complete
render graph. It supports:
- Automatic layout transitions
- Automatic loadOp/storeOp usage
- Automatic scaled loadOp for simple lower-res game -> high-res UI rendering scenarios
- Uses async compute queues automatically
- Optimal barrier placement, signals as early as possible, waits as late as possible
VkEvent is used for in-queue resources, VkSemaphore for cross-queue resources
- Basic render target aliasing
- Can merge two or more passes into multiple subpasses for efficient rendering on tile-based architectures
- Automatic mip-mapping if requested
- Uses transient attachments automatically to save memory on tile-based architectures
- Render target history, read previous frame's results in next frame for feedback
- Conditional render passes, can preserve render passes if necessary
- Render passes are reordered for optimal (?) overlap in execution
- Automatic, optimal multisampled resolve with pResolveAttachments
I have written up a longer blog post about its implementation
[here](http://themaister.net/blog/2017/08/15/render-graphs-and-vulkan-a-deep-dive/).
The default application scene renderer in `application/application.cpp` sets up
a render graph which does:
- Conditionally renders a shadow map covering entire scene
- Renders a close shadow map
- Automatically pulls in reflection/refraction render passes if present in the scene graph
- Renders scene G-Buffer with deferred
- Lighting pass (merged with G-Buffer pass into a single render pass)
- Bloom threshold pass
- Bloom pyramid downsampling
- Async compute is kicked off to get average luminance of scene, adjusts exposure
- Two upsampling steps to complete blurring in parallel with async
- Tonemap (HDR + Bloom) rendered to backbuffer (sRGB)
- (Potentially UI can be rendered on top with merged subpasses)
## Scene format
glTF 2.0 with PBR materials is mostly supported.
A custom JSON format is also added in order to plug multiple
glTF files together for rapid prototyping of test scenes.
## Texture formats
- PNG, JPG, TGA, HDR (via stb)
- GTX (Granite Texture Format, custom texture format for compressed formats)
ASTC, ETC2 and BCn/DXTn compressed formats are supported.
## `gltf-repacker`
There's a tool to repack glTF models.
Textures can be compressed to ASTC or BC using ISPC Texture Compressor.
zeux's meshoptimizer library can also optimize meshes.
The glTF emitted uses some Granite specific extras to be more optimal, so it's mostly for internal use.
## Compilers
Tested on GCC, Clang, and MSVC 2017.
## Platforms
- SDL3 (Linux / Windows)
- `VK_KHR_display` (headless Linux w/ basic keyboard, mouse, gamepad support)
- libretro Vulkan HW interface
- Headless (benchmarking)
- Custom surface plugin
- Android
## Vulkan implementations tested
- AMD Linux (Mesa, AMDVLK)
- Intel Linux (Mesa)
- AMD Windows
- nVidia Linux
- Arm Mali (Galaxy S7/S8/S9)
- Pixel C tablet (Tegra X1)
## Build
Plain CMake. Remember to check out submodules with `git submodule update --init`.
```
mkdir build
cd build
cmake .. -DCMAKE_BUILD_TYPE=Release -G Ninja
ninja -j16 # YMMV :3
```
For MSVC, it should work to use the appropriate `-G` flag.
There aren't any real samples yet, so not much to do unless you use Granite as a submodule.
`viewer/gltf-viewer` is a basic glTF viewer used as my sandbox for more complex testing.
Try some models from glTF-Sample-Models.
### Android
Something ala:
```
cd viewer
gradle build
```
Assets used in the default `gltf-viewer` target are pulled from `viewer/assets`.
### Third party software
These are pulled in as submodules.
- [SDL3](https://github.com/libsdl-org/SDL)
- [glslang](https://github.com/google/glslang.git)
- [rapidjson](https://github.com/miloyip/rapidjson)
- [shaderc](https://github.com/google/shaderc.git)
- [SPIRV-Cross](https://github.com/KhronosGroup/SPIRV-Cross)
- [SPIRV-Headers](https://github.com/KhronosGroup/SPIRV-Headers)
- [SPIRV-Tools](https://github.com/KhronosGroup/SPIRV-Tools)
- [stb](https://github.com/nothings/stb)
- [volk](https://github.com/zeux/volk)
- [meshoptimizer](https://github.com/zeux/meshoptimizer)
- [Fossilize](https://github.com/ValveSoftware/Fossilize)
- [muFFT](https://github.com/Themaister/muFFT)
- MikkTSpace (inlined into `third_party/mikktspace`)
@@ -0,0 +1,64 @@
add_granite_internal_lib(granite-application
application.hpp
application_glue.hpp
application.cpp)
if (GRANITE_FFMPEG)
target_link_libraries(granite-application PRIVATE granite-video)
endif()
target_link_libraries(granite-application PUBLIC
granite-vulkan
granite-event
granite-input
granite-application-global-init
granite-application-events
granite-threading
granite-filesystem)
if (NOT (${GRANITE_PLATFORM} MATCHES "null"))
target_sources(granite-application PRIVATE platforms/application_headless.cpp)
target_link_libraries(granite-application PRIVATE granite-stb granite-rapidjson)
endif()
target_include_directories(granite-application PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
if (TARGET granite-renderer)
target_link_libraries(granite-application PUBLIC granite-renderer PRIVATE granite-ui)
target_compile_definitions(granite-application PRIVATE HAVE_GRANITE_RENDERER)
add_granite_internal_lib(granite-scene-viewer scene_viewer_application.cpp scene_viewer_application.hpp)
target_include_directories(granite-scene-viewer PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(granite-scene-viewer
PUBLIC granite-scene-export granite-ui granite-application
PRIVATE granite-rapidjson)
endif()
# Can be defined by application to get a custom entry point.
# Otherwise, get a default one.
if (NOT TARGET granite-application-interface-query)
add_granite_internal_lib(granite-application-interface-query STATIC application_interface_query.cpp)
target_compile_options(granite-application-interface-query PRIVATE ${GRANITE_CXX_FLAGS})
endif()
if (NOT ANDROID)
add_granite_internal_lib(granite-application-entry STATIC application_entry.cpp)
target_compile_options(granite-application-entry PRIVATE ${GRANITE_CXX_FLAGS})
target_link_libraries(granite-application-entry
PRIVATE granite-application-interface-query granite-application granite-platform granite-filesystem)
endif()
add_library(granite-application-entry-headless STATIC application_entry.cpp)
target_compile_definitions(granite-application-entry-headless PRIVATE APPLICATION_ENTRY_HEADLESS=1)
target_link_libraries(granite-application-entry-headless
PRIVATE granite-application-interface-query granite-application granite-platform granite-filesystem)
target_compile_options(granite-application-entry-headless PRIVATE ${GRANITE_CXX_FLAGS})
add_subdirectory(events)
add_subdirectory(input)
add_subdirectory(platforms)
add_subdirectory(global)
if (GRANITE_AUDIO)
target_link_libraries(granite-application PRIVATE granite-audio)
endif()
@@ -0,0 +1,344 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#define NOMINMAX
#include "application.hpp"
#include "asset_manager.hpp"
#include "thread_group.hpp"
#ifdef HAVE_GRANITE_RENDERER
#include "material_manager.hpp"
#include "common_renderer_data.hpp"
#endif
#ifdef HAVE_GRANITE_AUDIO
#include "audio_mixer.hpp"
#endif
using namespace Vulkan;
namespace Granite
{
Application::~Application()
{
auto *group = GRANITE_THREAD_GROUP();
if (group)
group->wait_idle();
teardown_wsi();
}
bool Application::init_platform(std::unique_ptr<WSIPlatform> new_platform)
{
#ifdef HAVE_GRANITE_RENDERER
if (auto *common = GRANITE_COMMON_RENDERER_DATA())
common->initialize_static_assets(GRANITE_ASSET_MANAGER(), GRANITE_FILESYSTEM());
#endif
platform = std::move(new_platform);
application_wsi.set_platform(platform.get());
if (auto *event = GRANITE_EVENT_MANAGER())
event->enqueue_latched<ApplicationWSIPlatformEvent>(*platform);
return true;
}
void Application::teardown_wsi()
{
if (auto *event = GRANITE_EVENT_MANAGER())
{
event->dequeue_all_latched(DevicePipelineReadyEvent::get_type_id());
event->dequeue_all_latched(DeviceShaderModuleReadyEvent::get_type_id());
event->dequeue_all_latched(ApplicationWSIPlatformEvent::get_type_id());
}
application_wsi.teardown();
ready_modules = false;
ready_pipelines = false;
}
bool Application::init_wsi(Vulkan::ContextHandle context)
{
if (context)
{
if (!application_wsi.init_from_existing_context(std::move(context)))
return false;
}
else
{
Context::SystemHandles system_handles;
system_handles.filesystem = GRANITE_FILESYSTEM();
system_handles.thread_group = GRANITE_THREAD_GROUP();
system_handles.asset_manager = GRANITE_ASSET_MANAGER();
system_handles.timeline_trace_file = system_handles.thread_group->get_timeline_trace_file();
if (!application_wsi.init_context_from_platform(
system_handles.thread_group->get_num_threads() + 1,
system_handles))
{
return false;
}
}
if (!application_wsi.init_device())
return false;
application_wsi.get_device().begin_shader_caches();
{
GRANITE_SCOPED_TIMELINE_EVENT("wsi-init-swapchain");
if (!platform->has_external_swapchain() && !application_wsi.init_surface_swapchain())
return false;
}
return true;
}
void Application::poll_input_tracker_async(Granite::InputTrackerHandler *override_handler)
{
get_platform().poll_input_async(override_handler);
}
bool Application::poll()
{
auto &wsi = get_wsi();
if (!get_platform().alive(wsi))
return false;
if (requested_shutdown)
return false;
auto *fs = GRANITE_FILESYSTEM();
auto *em = GRANITE_EVENT_MANAGER();
if (fs)
fs->poll_notifications();
if (em)
em->dispatch();
#ifdef HAVE_GRANITE_AUDIO
auto *backend = GRANITE_AUDIO_BACKEND();
if (backend)
backend->heartbeat();
auto *am = GRANITE_AUDIO_MIXER();
if (am)
{
// Pump through events from audio thread.
auto &queue = am->get_message_queue();
Util::MessageQueuePayload payload;
while ((payload = queue.read_message()))
{
auto &event = payload.as<Event>();
if (em)
em->dispatch_inline(event);
queue.recycle_payload(std::move(payload));
}
// Recycle dead streams.
am->dispose_dead_streams();
}
#endif
return true;
}
void Application::check_initialization_progress()
{
auto &device = get_wsi().get_device();
if (!ready_modules)
{
if (device.query_initialization_progress(Device::InitializationStage::CacheMaintenance) >= 100 &&
device.query_initialization_progress(Device::InitializationStage::ShaderModules) >= 100)
{
if (auto *manager = GRANITE_ASSET_MANAGER())
{
// Now is a good time to kick shader manager since it might require compute shaders for decode.
manager->iterate(GRANITE_THREAD_GROUP());
}
if (auto *event = GRANITE_EVENT_MANAGER())
{
GRANITE_SCOPED_TIMELINE_EVENT("dispatch-ready-modules");
#ifdef HAVE_GRANITE_RENDERER
auto *manager = &device.get_shader_manager();
#else
constexpr Vulkan::ShaderManager *manager = nullptr;
#endif
event->enqueue_latched<DeviceShaderModuleReadyEvent>(&device, manager);
}
ready_modules = true;
}
}
if (!ready_pipelines)
{
if (device.query_initialization_progress(Device::InitializationStage::Pipelines) >= 100)
{
if (auto *event = GRANITE_EVENT_MANAGER())
{
GRANITE_SCOPED_TIMELINE_EVENT("dispatch-ready-pipelines");
#ifdef HAVE_GRANITE_RENDERER
auto *manager = &device.get_shader_manager();
#else
constexpr Vulkan::ShaderManager *manager = nullptr;
#endif
event->enqueue_latched<DevicePipelineReadyEvent>(&device, manager);
}
ready_pipelines = true;
}
}
}
void Application::show_message_box(const std::string &str, Vulkan::WSIPlatform::MessageType type)
{
if (platform)
platform->show_message_box(str, type);
switch (type)
{
case Vulkan::WSIPlatform::MessageType::Error:
LOGE("%s\n", str.c_str());
break;
case Vulkan::WSIPlatform::MessageType::Warning:
LOGW("%s\n", str.c_str());
break;
case Vulkan::WSIPlatform::MessageType::Info:
LOGI("%s\n", str.c_str());
break;
}
}
void Application::run_frame()
{
check_initialization_progress();
{
GRANITE_SCOPED_TIMELINE_EVENT("wsi-begin-frame");
if (!application_wsi.begin_frame())
{
LOGE("Failed to begin frame. Fatal error. Shutting down.\n");
request_shutdown();
return;
}
}
double smooth_frame_time = application_wsi.get_smooth_frame_time();
double smooth_elapsed = application_wsi.get_smooth_elapsed_time();
if (!ready_modules)
{
GRANITE_SCOPED_TIMELINE_EVENT("render-early-loading");
render_early_loading(smooth_frame_time, smooth_elapsed);
}
else if (!ready_pipelines)
{
GRANITE_SCOPED_TIMELINE_EVENT("render-loading");
render_loading(smooth_frame_time, smooth_elapsed);
}
else
{
GRANITE_SCOPED_TIMELINE_EVENT("render-frame");
render_frame(smooth_frame_time, smooth_elapsed);
}
{
GRANITE_SCOPED_TIMELINE_EVENT("wsi-end-frame");
application_wsi.end_frame();
}
{
GRANITE_SCOPED_TIMELINE_EVENT("post-frame");
post_frame();
}
}
void Application::render_early_loading(double, double)
{
auto &device = application_wsi.get_device();
auto cmd = device.request_command_buffer();
auto rp = device.get_swapchain_render_pass(SwapchainRenderPass::ColorOnly);
rp.clear_color[0].float32[0] = 0.01f;
rp.clear_color[0].float32[2] = 0.02f;
rp.clear_color[0].float32[3] = 0.03f;
cmd->begin_render_pass(rp);
auto vp = cmd->get_viewport();
VkClearRect rect = {};
rect.layerCount = 1;
rect.rect.extent = {
uint32_t(vp.width * 0.01f * float(device.query_initialization_progress(Device::InitializationStage::ShaderModules))),
uint32_t(vp.height),
};
VkClearValue value = {};
value.color.float32[0] = 0.08f;
value.color.float32[1] = 0.01f;
value.color.float32[2] = 0.01f;
if (rect.rect.extent.width > 0)
cmd->clear_quad(0, rect, value);
cmd->end_render_pass();
device.submit(cmd);
}
void Application::render_loading(double, double)
{
auto &device = application_wsi.get_device();
auto cmd = device.request_command_buffer();
auto rp = device.get_swapchain_render_pass(SwapchainRenderPass::ColorOnly);
rp.clear_color[0].float32[0] = 0.01f;
rp.clear_color[0].float32[2] = 0.02f;
rp.clear_color[0].float32[3] = 0.03f;
cmd->begin_render_pass(rp);
auto vp = cmd->get_viewport();
VkClearRect rect = {};
rect.layerCount = 1;
rect.rect.extent = {
uint32_t(vp.width * 0.01f * float(device.query_initialization_progress(Device::InitializationStage::Pipelines))),
uint32_t(vp.height),
};
VkClearValue value = {};
value.color.float32[0] = 0.01f;
value.color.float32[1] = 0.08f;
value.color.float32[2] = 0.01f;
if (rect.rect.extent.width > 0)
cmd->clear_quad(0, rect, value);
cmd->end_render_pass();
device.submit(cmd);
}
void Application::post_frame()
{
// Texture manager might require shaders to be ready before we can submit work.
if (ready_modules)
{
if (auto *manager = GRANITE_ASSET_MANAGER())
manager->iterate(GRANITE_THREAD_GROUP());
}
if (auto *manager = Global::material_manager())
manager->iterate(Global::asset_manager());
}
}
@@ -0,0 +1,106 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include "wsi.hpp"
#include "application_wsi_events.hpp"
#include "input.hpp"
#include "application_glue.hpp"
namespace Granite
{
class Application
{
public:
virtual ~Application();
virtual void render_frame(double frame_time, double elapsed_time) = 0;
bool init_platform(std::unique_ptr<Vulkan::WSIPlatform> platform);
bool init_wsi(Vulkan::ContextHandle context = {});
void teardown_wsi();
// Called after the frame is submitted for presentation.
// Can do "garbage collection" or similar batched cleanup
// that does not depend on submitting more graphics work.
virtual void post_frame();
// In early loading, we have not loaded SPIR-V yet.
// Rendering a background color or extremely basic shaders could work here.
// If compiling without SPIRV-Cross or compiler support in a shipping configuration,
// any SPIR-V must be provided inline through slangmosh or similar.
virtual void render_early_loading(double frame_time, double elapsed_time);
// In loading, we have access to SPIR-V, but compiling pipelines is not done yet.
// This stage is more suited for rendering splash screens or similar.
virtual void render_loading(double frame_time, double elapsed_time);
Vulkan::WSI &get_wsi()
{
return application_wsi;
}
Vulkan::WSIPlatform &get_platform()
{
return *platform;
}
virtual std::string get_name()
{
return "granite";
}
virtual unsigned get_version()
{
return 0;
}
virtual unsigned get_default_width()
{
return 1280;
}
virtual unsigned get_default_height()
{
return 720;
}
bool poll();
void run_frame();
void show_message_box(const std::string &str, Vulkan::WSIPlatform::MessageType type);
protected:
void request_shutdown()
{
requested_shutdown = true;
}
void poll_input_tracker_async(InputTrackerHandler *override_handler);
private:
std::unique_ptr<Vulkan::WSIPlatform> platform;
Vulkan::WSI application_wsi;
bool requested_shutdown = false;
// Ready state for deferred device initialization.
bool ready_modules = false;
bool ready_pipelines = false;
void check_initialization_progress();
};
}
@@ -0,0 +1,100 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "application.hpp"
#include "filesystem.hpp"
#include "path_utils.hpp"
//#define USE_FP_EXCEPTIONS
#ifdef USE_FP_EXCEPTIONS
#include <fenv.h>
#endif
#if defined(_WIN32) && !defined(APPLICATION_ENTRY_HEADLESS)
#define USE_WINMAIN
#endif
#ifdef USE_WINMAIN
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <shellapi.h>
#include <vector>
#endif
namespace Granite
{
// Make sure this is linked in.
void application_dummy()
{
}
// Alternatively, make sure this is linked in.
// Implementation is here to trick a linker to always let main() in static library work.
void application_setup_default_filesystem(const char *default_asset_directory)
{
auto *filesystem = GRANITE_FILESYSTEM();
if (filesystem)
Filesystem::setup_default_filesystem(filesystem, default_asset_directory);
}
}
#ifdef USE_WINMAIN
int CALLBACK WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
#else
int main(int argc, char *argv[])
#endif
{
#ifdef USE_FP_EXCEPTIONS
feenableexcept(FE_DIVBYZERO | FE_INVALID | FE_OVERFLOW);
#endif
#ifdef USE_WINMAIN
int argc;
wchar_t **wide_argv = CommandLineToArgvW(GetCommandLineW(), &argc);
std::vector<char *> argv_buffer(argc + 1);
char **argv = nullptr;
std::vector<std::string> argv_strings(argc);
if (wide_argv)
{
argv = argv_buffer.data();
for (int i = 0; i < argc; i++)
{
argv_strings[i] = Granite::Path::to_utf8(wide_argv[i]);
argv_buffer[i] = const_cast<char *>(argv_strings[i].c_str());
}
}
#endif
#ifdef APPLICATION_ENTRY_HEADLESS
int ret = Granite::application_main_headless(Granite::query_application_interface,
Granite::application_create,
argc, argv);
#else
int ret = Granite::application_main(Granite::query_application_interface,
Granite::application_create,
argc, argv);
#endif
return ret;
}
@@ -0,0 +1,68 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include <stddef.h>
#include <stdint.h>
namespace Granite
{
enum class ApplicationQuery
{
DefaultManagerFlags
};
class Application;
int application_main(
bool (*query_application_interface)(ApplicationQuery, void *data, size_t size),
Application *(*create_application)(int, char **),
int argc, char **argv);
int application_main_headless(
bool (*query_application_interface)(ApplicationQuery, void *data, size_t size),
Application *(*create_application)(int, char **),
int argc, char **argv);
extern Application *application_create(int argc, char *argv[]);
struct ApplicationQueryDefaultManagerFlags
{
uint32_t manager_feature_flags;
};
extern bool query_application_interface(ApplicationQuery query, void *data, size_t size);
// Call this or setup_default_filesystem to ensure application-main is linked in correctly without having to mess around
// with -Wl,--whole-archive.
void application_dummy();
void application_setup_default_filesystem(const char *default_asset_directory);
}
#ifdef ASSET_DIRECTORY
#define GRANITE_APPLICATION_SETUP_FILESYSTEM() ::Granite::application_setup_default_filesystem(ASSET_DIRECTORY)
#else
#define GRANITE_APPLICATION_SETUP_FILESYSTEM() ::Granite::application_setup_default_filesystem(nullptr)
#endif
#define GRANITE_APPLICATION_DECL_DEFAULT_QUERY() namespace Granite { bool query_application_interface(Granite::ApplicationQuery, void *, size_t) { return false; } }
@@ -0,0 +1,2 @@
#include "application_glue.hpp"
GRANITE_APPLICATION_DECL_DEFAULT_QUERY()
@@ -0,0 +1,7 @@
add_granite_internal_lib(granite-application-events
application_wsi.hpp
application_wsi.cpp application_wsi_events.hpp
application_events.hpp)
target_include_directories(granite-application-events PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(granite-application-events
PRIVATE granite-vulkan granite-event granite-input granite-application-global)
@@ -0,0 +1,55 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include "event.hpp"
namespace Granite
{
enum class ApplicationLifecycle
{
Running,
Paused,
Stopped,
Dead
};
class ApplicationLifecycleEvent : public Event
{
public:
GRANITE_EVENT_TYPE_DECL(ApplicationLifecycleEvent)
explicit ApplicationLifecycleEvent(ApplicationLifecycle lifecycle_)
: lifecycle(lifecycle_)
{
}
ApplicationLifecycle get_lifecycle() const
{
return lifecycle;
}
private:
ApplicationLifecycle lifecycle;
};
}
@@ -0,0 +1,188 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "application_wsi.hpp"
#include "application_wsi_events.hpp"
#include "application_events.hpp"
#include "global_managers.hpp"
#include "event.hpp"
namespace Granite
{
GraniteWSIPlatform::GraniteWSIPlatform()
{
input_tracker.set_input_handler(this);
}
void GraniteWSIPlatform::event_swapchain_created(Vulkan::Device *device, VkSwapchainKHR, unsigned width, unsigned height,
float aspect_ratio, size_t image_count, VkFormat format,
VkColorSpaceKHR color_space,
VkSurfaceTransformFlagBitsKHR transform)
{
auto *em = GRANITE_EVENT_MANAGER();
if (em)
{
em->enqueue_latched<Vulkan::SwapchainParameterEvent>(device, width, height, aspect_ratio, image_count, format,
color_space, transform);
}
}
void GraniteWSIPlatform::event_swapchain_destroyed()
{
auto *em = GRANITE_EVENT_MANAGER();
if (em)
em->dequeue_all_latched(Vulkan::SwapchainParameterEvent::get_type_id());
}
void GraniteWSIPlatform::event_device_created(Vulkan::Device *device)
{
auto *em = GRANITE_EVENT_MANAGER();
if (em)
em->enqueue_latched<Vulkan::DeviceCreatedEvent>(device);
}
void GraniteWSIPlatform::event_device_destroyed()
{
auto *em = GRANITE_EVENT_MANAGER();
if (em)
em->dequeue_all_latched(Vulkan::DeviceCreatedEvent::get_type_id());
}
void GraniteWSIPlatform::event_swapchain_index(Vulkan::Device *device, unsigned index)
{
auto *em = GRANITE_EVENT_MANAGER();
if (em)
{
em->dequeue_all_latched(Vulkan::SwapchainIndexEvent::get_type_id());
em->enqueue_latched<Vulkan::SwapchainIndexEvent>(device, index);
}
}
void GraniteWSIPlatform::event_frame_tick(double, double)
{
}
template <typename T>
void GraniteWSIPlatform::dispatch_template(const T &t)
{
if (auto *em = GRANITE_EVENT_MANAGER())
em->dispatch_inline(t);
}
template <typename T>
void GraniteWSIPlatform::dispatch_template_filter(const T &t)
{
auto *ui = Global::ui_manager();
if (ui && !ui->filter_input_event(t))
return;
dispatch_template(t);
}
template <typename Func>
void GraniteWSIPlatform::dispatch_or_defer(Func &&func)
{
if (in_async_input)
captured.emplace_back(std::forward<Func>(func));
else
func();
}
#define WORK(work) dispatch_or_defer([this, e]() { work; })
void GraniteWSIPlatform::dispatch(const TouchDownEvent &e)
{
WORK(dispatch_template_filter(e));
}
void GraniteWSIPlatform::dispatch(const TouchUpEvent &e)
{
WORK(dispatch_template_filter(e));
}
void GraniteWSIPlatform::dispatch(const TouchGestureEvent &e)
{
WORK(dispatch_template_filter(e));
}
void GraniteWSIPlatform::dispatch(const JoypadButtonEvent &e)
{
WORK(dispatch_template_filter(e));
}
void GraniteWSIPlatform::dispatch(const JoypadAxisEvent &e)
{
WORK(dispatch_template_filter(e));
}
void GraniteWSIPlatform::dispatch(const KeyboardEvent &e)
{
WORK(dispatch_template_filter(e));
}
void GraniteWSIPlatform::dispatch(const OrientationEvent &e)
{
WORK(dispatch_template_filter(e));
}
void GraniteWSIPlatform::dispatch(const MouseButtonEvent &e)
{
WORK(dispatch_template_filter(e));
}
void GraniteWSIPlatform::dispatch(const MouseMoveEvent &e)
{
WORK(dispatch_template_filter(e));
}
void GraniteWSIPlatform::dispatch(const JoypadStateEvent &e)
{
WORK(dispatch_template(e));
}
void GraniteWSIPlatform::dispatch(const InputStateEvent &e)
{
WORK(dispatch_template(e));
}
void GraniteWSIPlatform::dispatch(const JoypadConnectionEvent &e)
{
WORK(dispatch_template(e));
}
void GraniteWSIPlatform::begin_async_input_handling()
{
in_async_input = true;
}
void GraniteWSIPlatform::end_async_input_handling()
{
in_async_input = false;
}
void GraniteWSIPlatform::flush_deferred_input_events()
{
VK_ASSERT(!in_async_input);
for (auto &func : captured)
func();
captured.clear();
}
}
@@ -0,0 +1,81 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include "wsi.hpp"
#include "input.hpp"
#include <functional>
namespace Granite
{
class GraniteWSIPlatform : public Vulkan::WSIPlatform, private InputTrackerHandler
{
public:
GraniteWSIPlatform();
InputTracker &get_input_tracker()
{
return input_tracker;
}
protected:
void event_device_created(Vulkan::Device *device) override;
void event_device_destroyed() override;
void event_swapchain_created(Vulkan::Device *device, VkSwapchainKHR swapchain,
unsigned width, unsigned height,
float aspect_ratio, size_t image_count,
VkFormat format, VkColorSpaceKHR color_space,
VkSurfaceTransformFlagBitsKHR pre_rotate) override;
void event_swapchain_destroyed() override;
void event_swapchain_index(Vulkan::Device *device, unsigned index) override;
void event_frame_tick(double frame, double elapsed) override;
void begin_async_input_handling();
void end_async_input_handling();
void flush_deferred_input_events();
private:
InputTracker input_tracker;
void dispatch(const TouchDownEvent &e) override;
void dispatch(const TouchUpEvent &e) override;
void dispatch(const TouchGestureEvent &e) override;
void dispatch(const JoypadButtonEvent &e) override;
void dispatch(const JoypadAxisEvent &e) override;
void dispatch(const KeyboardEvent &e) override;
void dispatch(const OrientationEvent &e) override;
void dispatch(const MouseButtonEvent &e) override;
void dispatch(const MouseMoveEvent &e) override;
void dispatch(const JoypadStateEvent &e) override;
void dispatch(const InputStateEvent &e) override;
void dispatch(const JoypadConnectionEvent &e) override;
template <typename T>
void dispatch_template_filter(const T &t);
template <typename T>
void dispatch_template(const T &t);
template <typename Func>
void dispatch_or_defer(Func &&func);
bool in_async_input = false;
std::vector<std::function<void ()>> captured;
};
}
@@ -0,0 +1,258 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include "event.hpp"
#include "vulkan_headers.hpp"
namespace Vulkan
{
class Device;
class ShaderManager;
class WSIPlatform;
class DeviceCreatedEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(DeviceCreatedEvent)
explicit DeviceCreatedEvent(Device *device_)
: device(*device_)
{}
Device &get_device() const
{
return device;
}
private:
Device &device;
};
class DeviceShaderModuleReadyEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(DeviceShaderModuleReadyEvent)
explicit DeviceShaderModuleReadyEvent(Device *device_, ShaderManager *manager_)
: device(*device_), manager(*manager_)
{}
Device &get_device() const
{
return device;
}
ShaderManager &get_shader_manager() const
{
return manager;
}
private:
Device &device;
ShaderManager &manager;
};
class DevicePipelineReadyEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(DevicePipelineReadyEvent)
explicit DevicePipelineReadyEvent(Device *device_, ShaderManager *manager_)
: device(*device_), manager(*manager_)
{}
Device &get_device() const
{
return device;
}
ShaderManager &get_shader_manager() const
{
return manager;
}
private:
Device &device;
ShaderManager &manager;
};
class SwapchainParameterEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(SwapchainParameterEvent)
SwapchainParameterEvent(Device *device_,
unsigned width_, unsigned height_,
float aspect_ratio_, unsigned count_,
VkFormat format_, VkColorSpaceKHR color_space_,
VkSurfaceTransformFlagBitsKHR transform_)
: device(*device_), width(width_), height(height_),
aspect_ratio(aspect_ratio_), image_count(count_), format(format_), color_space(color_space_), transform(transform_)
{}
Device &get_device() const
{
return device;
}
unsigned get_width() const
{
return width;
}
unsigned get_height() const
{
return height;
}
float get_aspect_ratio() const
{
return aspect_ratio;
}
unsigned get_image_count() const
{
return image_count;
}
VkFormat get_format() const
{
return format;
}
VkColorSpaceKHR get_color_space() const
{
return color_space;
}
VkSurfaceTransformFlagBitsKHR get_prerotate() const
{
return transform;
}
private:
Device &device;
unsigned width;
unsigned height;
float aspect_ratio;
unsigned image_count;
VkFormat format;
VkColorSpaceKHR color_space;
VkSurfaceTransformFlagBitsKHR transform;
};
class SwapchainIndexEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(SwapchainIndexEvent)
SwapchainIndexEvent(Device *device_, unsigned index_)
: device(*device_), index(index_)
{}
Device &get_device() const
{
return device;
}
unsigned get_index() const
{
return index;
}
private:
Device &device;
unsigned index;
};
class ApplicationWSIPlatformEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(ApplicationWSIPlatformEvent)
explicit ApplicationWSIPlatformEvent(WSIPlatform &platform_)
: platform(platform_)
{}
WSIPlatform &get_platform() const
{
return platform;
}
private:
WSIPlatform &platform;
};
class ApplicationWindowFileDropEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(ApplicationWindowFileDropEvent)
explicit ApplicationWindowFileDropEvent(std::string path_)
: path(std::move(path_))
{}
const std::string &get_path() const
{
return path;
}
private:
std::string path;
};
class ApplicationWindowTextDropEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(ApplicationWindowTextDropEvent)
explicit ApplicationWindowTextDropEvent(std::string str_)
: str(std::move(str_))
{}
const std::string &get_text() const
{
return str;
}
private:
std::string str;
};
class ApplicationSoftKeyboardUpdateEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(ApplicationSoftKeyboardUpdateEvent)
explicit ApplicationSoftKeyboardUpdateEvent(std::string str_)
: str(std::move(str_))
{
}
const std::string &get_text() const
{
return str;
}
private:
std::string str;
};
}
@@ -0,0 +1,27 @@
add_granite_internal_lib(granite-application-global
global_managers.hpp global_managers.cpp)
target_include_directories(granite-application-global PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(granite-application-global PUBLIC granite-util)
add_granite_internal_lib(granite-application-global-init
global_managers_init.hpp global_managers_init.cpp)
target_include_directories(granite-application-global-init PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(granite-application-global-init
PUBLIC granite-application-global
PRIVATE granite-threading granite-event granite-filesystem)
if (TARGET granite-renderer)
target_link_libraries(granite-application-global-init PRIVATE granite-renderer granite-ui)
target_compile_definitions(granite-application-global-init PRIVATE HAVE_GRANITE_RENDERER)
endif()
if (GRANITE_AUDIO)
target_link_libraries(granite-application-global-init PRIVATE granite-audio)
endif()
if (GRANITE_BULLET)
target_link_libraries(granite-application-global-init PRIVATE granite-physics)
endif()
add_library(granite-application-global-interface INTERFACE)
target_include_directories(granite-application-global-interface INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})
@@ -0,0 +1,308 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "global_managers.hpp"
#include "environment.hpp"
#include "logging.hpp"
#include <thread>
#include <assert.h>
#include <stdlib.h>
namespace Granite
{
namespace Global
{
// Could use unique_ptr here, but would be nice to avoid global ctor/dtor.
struct GlobalManagers
{
Factory *factory;
FilesystemInterface *filesystem;
AssetManagerInterface *asset_manager;
MaterialManagerInterface *material_manager;
EventManagerInterface *event_manager;
ThreadGroupInterface *thread_group;
UI::UIManagerInterface *ui_manager;
CommonRendererDataInterface *common_renderer_data;
Util::MessageQueueInterface *logging;
Audio::BackendInterface *audio_backend;
Audio::MixerInterface *audio_mixer;
PhysicsSystemInterface *physics;
};
static thread_local GlobalManagers global_managers;
GlobalManagersHandle create_thread_context()
{
return GlobalManagersHandle(new GlobalManagers(global_managers));
}
void delete_thread_context(GlobalManagers *managers)
{
delete managers;
}
void GlobalManagerDeleter::operator()(GlobalManagers *managers)
{
delete_thread_context(managers);
}
void set_thread_context(const GlobalManagers &managers)
{
global_managers = managers;
if (managers.thread_group)
managers.thread_group->set_thread_context();
if (managers.logging)
Util::set_thread_logging_interface(managers.logging);
}
void clear_thread_context()
{
global_managers = {};
}
Util::MessageQueueInterface *message_queue()
{
return global_managers.logging;
}
FilesystemInterface *filesystem()
{
return global_managers.filesystem;
}
AssetManagerInterface *asset_manager()
{
return global_managers.asset_manager;
}
MaterialManagerInterface *material_manager()
{
return global_managers.material_manager;
}
EventManagerInterface *event_manager()
{
return global_managers.event_manager;
}
ThreadGroupInterface *thread_group()
{
return global_managers.thread_group;
}
UI::UIManagerInterface *ui_manager()
{
return global_managers.ui_manager;
}
CommonRendererDataInterface *common_renderer_data()
{
return global_managers.common_renderer_data;
}
Audio::BackendInterface *audio_backend() { return global_managers.audio_backend; }
Audio::MixerInterface *audio_mixer() { return global_managers.audio_mixer; }
void install_audio_system(Audio::BackendInterface *backend, Audio::MixerInterface *mixer)
{
delete global_managers.audio_mixer;
global_managers.audio_mixer = mixer;
delete global_managers.audio_backend;
global_managers.audio_backend = backend;
}
PhysicsSystemInterface *physics()
{
return global_managers.physics;
}
void init(Factory &factory, ManagerFeatureFlags flags, unsigned max_threads, float audio_sample_rate)
{
assert(!global_managers.factory || global_managers.factory == &factory);
global_managers.factory = &factory;
if (flags & MANAGER_FEATURE_EVENT_BIT)
{
if (!global_managers.event_manager)
global_managers.event_manager = factory.create_event_manager();
}
if (flags & MANAGER_FEATURE_FILESYSTEM_BIT)
{
if (!global_managers.filesystem)
global_managers.filesystem = factory.create_filesystem();
}
if (flags & MANAGER_FEATURE_ASSET_MANAGER_BIT)
{
if (!global_managers.asset_manager)
global_managers.asset_manager = factory.create_asset_manager();
}
if (flags & MANAGER_FEATURE_MATERIAL_MANAGER_BIT)
{
if (!global_managers.material_manager)
global_managers.material_manager = factory.create_material_manager();
}
bool kick_threads = false;
if (flags & MANAGER_FEATURE_THREAD_GROUP_BIT)
{
if (!global_managers.thread_group)
{
global_managers.thread_group = factory.create_thread_group();
kick_threads = true;
}
}
if (flags & MANAGER_FEATURE_UI_MANAGER_BIT)
{
if (!global_managers.ui_manager)
global_managers.ui_manager = factory.create_ui_manager();
}
if (flags & MANAGER_FEATURE_COMMON_RENDERER_DATA_BIT)
{
if (!global_managers.common_renderer_data)
global_managers.common_renderer_data = factory.create_common_renderer_data();
}
if (flags & MANAGER_FEATURE_LOGGING_BIT)
{
if (!global_managers.logging)
global_managers.logging = factory.create_message_queue();
Util::set_thread_logging_interface(global_managers.logging);
}
if (flags & MANAGER_FEATURE_PHYSICS_BIT)
{
if (!global_managers.physics)
global_managers.physics = factory.create_physics_system();
}
if (flags & MANAGER_FEATURE_AUDIO_MIXER_BIT)
{
if (!global_managers.audio_mixer)
global_managers.audio_mixer = factory.create_audio_mixer();
}
if (flags & MANAGER_FEATURE_AUDIO_BACKEND_BIT)
{
if (!global_managers.audio_backend)
global_managers.audio_backend = factory.create_audio_backend(global_managers.audio_mixer, audio_sample_rate, 2);
}
// Kick threads after all global managers are set up.
if (kick_threads)
{
unsigned cpu_threads = std::thread::hardware_concurrency();
cpu_threads = cpu_threads > 1 ? (cpu_threads - 1u) : 1u;
if (cpu_threads > max_threads)
cpu_threads = max_threads;
cpu_threads = Util::get_environment_uint("GRANITE_NUM_WORKER_THREADS", cpu_threads);
unsigned background_cpu_threads = (cpu_threads + 1) / 2;
global_managers.thread_group->start(cpu_threads, background_cpu_threads,
[ctx = std::shared_ptr<GlobalManagers>(create_thread_context())] {
set_thread_context(*ctx);
});
}
}
void deinit()
{
if (!global_managers.factory)
return;
if (global_managers.audio_backend)
global_managers.audio_backend->stop();
delete global_managers.audio_backend;
delete global_managers.audio_mixer;
delete global_managers.physics;
delete global_managers.common_renderer_data;
delete global_managers.ui_manager;
delete global_managers.thread_group;
delete global_managers.material_manager;
delete global_managers.asset_manager;
delete global_managers.filesystem;
delete global_managers.event_manager;
delete global_managers.logging;
global_managers.audio_backend = nullptr;
global_managers.audio_mixer = nullptr;
global_managers.physics = nullptr;
global_managers.common_renderer_data = nullptr;
global_managers.filesystem = nullptr;
global_managers.material_manager = nullptr;
global_managers.asset_manager = nullptr;
global_managers.event_manager = nullptr;
global_managers.thread_group = nullptr;
global_managers.ui_manager = nullptr;
global_managers.logging = nullptr;
global_managers.factory = nullptr;
}
void start_audio_system()
{
if (!global_managers.audio_backend)
return;
if (!global_managers.audio_backend->start())
{
LOGE("Failed to start audio subsystem!\n");
return;
}
if (global_managers.event_manager && global_managers.audio_mixer)
global_managers.audio_mixer->event_start(*global_managers.event_manager);
}
void stop_audio_system()
{
if (!global_managers.audio_backend)
return;
if (!global_managers.audio_backend->stop())
LOGE("Failed to stop audio subsystem!\n");
if (global_managers.event_manager && global_managers.audio_mixer)
global_managers.audio_mixer->event_stop(*global_managers.event_manager);
}
FilesystemInterface *Factory::create_filesystem() { return nullptr; }
AssetManagerInterface *Factory::create_asset_manager() { return nullptr; }
MaterialManagerInterface *Factory::create_material_manager() { return nullptr; }
EventManagerInterface *Factory::create_event_manager() { return nullptr; }
ThreadGroupInterface *Factory::create_thread_group() { return nullptr; }
CommonRendererDataInterface *Factory::create_common_renderer_data() { return nullptr; }
PhysicsSystemInterface *Factory::create_physics_system() { return nullptr; }
Audio::BackendInterface *Factory::create_audio_backend(Audio::MixerInterface *, float, unsigned) { return nullptr; }
Audio::MixerInterface *Factory::create_audio_mixer() { return nullptr; }
UI::UIManagerInterface *Factory::create_ui_manager() { return nullptr; }
Util::MessageQueueInterface *Factory::create_message_queue() { return nullptr; }
}
}
@@ -0,0 +1,127 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include <stdint.h>
#include <functional>
#include <memory>
#include <limits.h>
#include "global_managers_interface.hpp"
namespace Granite
{
namespace Global
{
enum ManagerFeatureFlagBits
{
MANAGER_FEATURE_FILESYSTEM_BIT = 1 << 0,
MANAGER_FEATURE_EVENT_BIT = 1 << 1,
MANAGER_FEATURE_THREAD_GROUP_BIT = 1 << 2,
MANAGER_FEATURE_UI_MANAGER_BIT = 1 << 3,
MANAGER_FEATURE_AUDIO_MIXER_BIT = 1 << 4,
MANAGER_FEATURE_AUDIO_BACKEND_BIT = 1 << 5,
MANAGER_FEATURE_COMMON_RENDERER_DATA_BIT = 1 << 6,
MANAGER_FEATURE_PHYSICS_BIT = 1 << 7,
MANAGER_FEATURE_LOGGING_BIT = 1 << 8,
MANAGER_FEATURE_ASSET_MANAGER_BIT = 1 << 9,
MANAGER_FEATURE_MATERIAL_MANAGER_BIT = 1 << 10,
MANAGER_FEATURE_DEFAULT_BITS = (MANAGER_FEATURE_FILESYSTEM_BIT |
MANAGER_FEATURE_ASSET_MANAGER_BIT |
MANAGER_FEATURE_MATERIAL_MANAGER_BIT |
MANAGER_FEATURE_EVENT_BIT |
MANAGER_FEATURE_THREAD_GROUP_BIT |
MANAGER_FEATURE_COMMON_RENDERER_DATA_BIT |
MANAGER_FEATURE_UI_MANAGER_BIT |
MANAGER_FEATURE_AUDIO_MIXER_BIT |
MANAGER_FEATURE_AUDIO_BACKEND_BIT)
};
using ManagerFeatureFlags = uint32_t;
// Decouple creation from global TLS storage.
// This avoids some nasty cyclical dependencies.
class Factory
{
public:
virtual ~Factory() = default;
virtual FilesystemInterface *create_filesystem();
virtual AssetManagerInterface *create_asset_manager();
virtual MaterialManagerInterface *create_material_manager();
virtual EventManagerInterface *create_event_manager();
virtual ThreadGroupInterface *create_thread_group();
virtual CommonRendererDataInterface *create_common_renderer_data();
virtual PhysicsSystemInterface *create_physics_system();
virtual Audio::BackendInterface *create_audio_backend(Audio::MixerInterface *mixer,
float sample_rate,
unsigned channels);
virtual Audio::MixerInterface *create_audio_mixer();
virtual UI::UIManagerInterface *create_ui_manager();
virtual Util::MessageQueueInterface *create_message_queue();
};
void init(Factory &factory, ManagerFeatureFlags flags = MANAGER_FEATURE_DEFAULT_BITS,
unsigned max_threads = UINT_MAX, float audio_sample_rate = -1.0f);
void deinit();
// Used if the application wants to use multiple instances of Granite in the same process.
// This allows each thread to be associated to a global context.
struct GlobalManagers;
struct GlobalManagerDeleter
{
void operator()(GlobalManagers *managers);
};
using GlobalManagersHandle = std::unique_ptr<GlobalManagers, GlobalManagerDeleter>;
GlobalManagersHandle create_thread_context();
void delete_thread_context(GlobalManagers *managers);
void set_thread_context(const GlobalManagers &managers);
void clear_thread_context();
void start_audio_system();
void stop_audio_system();
void install_audio_system(Audio::BackendInterface *backend, Audio::MixerInterface *mixer);
Util::MessageQueueInterface *message_queue();
FilesystemInterface *filesystem();
AssetManagerInterface *asset_manager();
MaterialManagerInterface *material_manager();
EventManagerInterface *event_manager();
ThreadGroupInterface *thread_group();
UI::UIManagerInterface *ui_manager();
CommonRendererDataInterface *common_renderer_data();
Audio::BackendInterface *audio_backend();
Audio::MixerInterface *audio_mixer();
PhysicsSystemInterface *physics();
}
}
#define GRANITE_MESSAGE_QUEUE() static_cast<::Util::MessageQueue *>(::Granite::Global::message_queue())
#define GRANITE_FILESYSTEM() static_cast<::Granite::Filesystem *>(::Granite::Global::filesystem())
#define GRANITE_ASSET_MANAGER() static_cast<::Granite::AssetManager *>(::Granite::Global::asset_manager())
#define GRANITE_MATERIAL_MANAGER() static_cast<::Granite::MaterialManager *>(::Granite::Global::material_manager())
#define GRANITE_EVENT_MANAGER() static_cast<::Granite::EventManager *>(::Granite::Global::event_manager())
#define GRANITE_THREAD_GROUP() static_cast<::Granite::ThreadGroup *>(::Granite::Global::thread_group())
#define GRANITE_UI_MANAGER() static_cast<::Granite::UI::UIManager *>(::Granite::Global::ui_manager())
#define GRANITE_COMMON_RENDERER_DATA() static_cast<::Granite::CommonRendererData *>(::Granite::Global::common_renderer_data())
#define GRANITE_AUDIO_BACKEND() static_cast<::Granite::Audio::Backend *>(::Granite::Global::audio_backend())
#define GRANITE_AUDIO_MIXER() static_cast<::Granite::Audio::Mixer *>(::Granite::Global::audio_mixer())
#define GRANITE_PHYSICS() static_cast<::Granite::PhysicsSystem *>(::Granite::Global::physics())
@@ -0,0 +1,137 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "global_managers_init.hpp"
#include "global_managers.hpp"
#include "event.hpp"
#include "thread_group.hpp"
#include "filesystem.hpp"
#include "asset_manager.hpp"
#ifdef HAVE_GRANITE_RENDERER
#include "material_manager.hpp"
#include "common_renderer_data.hpp"
#include "ui_manager.hpp"
#endif
#ifdef HAVE_GRANITE_AUDIO
#include "audio_mixer.hpp"
#include "audio_interface.hpp"
#endif
#ifdef HAVE_GRANITE_PHYSICS
#include "physics_system.hpp"
#endif
namespace Granite
{
namespace Global
{
struct FactoryImplementation : Factory
{
FilesystemInterface *create_filesystem() override
{
return new Filesystem;
}
AssetManagerInterface *create_asset_manager() override
{
return new AssetManager;
}
EventManagerInterface *create_event_manager() override
{
return new EventManager;
}
ThreadGroupInterface *create_thread_group() override
{
return new ThreadGroup;
}
CommonRendererDataInterface *create_common_renderer_data() override
{
#ifdef HAVE_GRANITE_RENDERER
return new CommonRendererData;
#else
return nullptr;
#endif
}
UI::UIManagerInterface *create_ui_manager() override
{
#ifdef HAVE_GRANITE_RENDERER
return new UI::UIManager;
#else
return nullptr;
#endif
}
MaterialManagerInterface *create_material_manager() override
{
#ifdef HAVE_GRANITE_RENDERER
return new MaterialManager;
#else
return nullptr;
#endif
}
Audio::MixerInterface *create_audio_mixer() override
{
#ifdef HAVE_GRANITE_AUDIO
return new Audio::Mixer;
#else
return nullptr;
#endif
}
Audio::BackendInterface *create_audio_backend(Audio::MixerInterface *iface, float sample_rate, unsigned channels) override
{
#ifdef HAVE_GRANITE_AUDIO
if (iface)
return Audio::create_default_audio_backend(static_cast<Audio::Mixer *>(iface), sample_rate, channels);
else
return nullptr;
#else
(void)iface;
(void)sample_rate;
(void)channels;
return nullptr;
#endif
}
PhysicsSystemInterface *create_physics_system() override
{
#ifdef HAVE_GRANITE_PHYSICS
return new PhysicsSystem;
#else
return nullptr;
#endif
}
};
static FactoryImplementation factory;
void init(ManagerFeatureFlags flags, unsigned max_threads, float audio_sample_rate)
{
init(factory, flags, max_threads, audio_sample_rate);
}
}
}
@@ -0,0 +1,34 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include "global_managers.hpp"
namespace Granite
{
namespace Global
{
void init(ManagerFeatureFlags flags = MANAGER_FEATURE_DEFAULT_BITS,
unsigned max_threads = UINT_MAX, float audio_sample_rate = -1.0f);
}
}
@@ -0,0 +1,133 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include <functional>
#include <string>
#include "logging.hpp"
namespace Util
{
class MessageQueueInterface : public LoggingInterface
{
public:
virtual ~MessageQueueInterface() = default;
};
}
namespace Granite
{
class FilesystemInterface
{
public:
virtual ~FilesystemInterface() = default;
virtual bool load_text_file(const std::string &path, std::string &str) = 0;
};
class AssetManagerInterface
{
public:
virtual ~AssetManagerInterface() = default;
};
class MaterialManagerInterface
{
public:
virtual ~MaterialManagerInterface() = default;
virtual void iterate(AssetManagerInterface *iface) = 0;
};
class ThreadGroupInterface
{
public:
virtual ~ThreadGroupInterface() = default;
virtual void start(unsigned foreground_count, unsigned background_count,
const std::function<void()> &cb) = 0;
virtual void set_thread_context() = 0;
};
class EventManagerInterface
{
public:
virtual ~EventManagerInterface() = default;
};
class CommonRendererDataInterface
{
public:
virtual ~CommonRendererDataInterface() = default;
};
class PhysicsSystemInterface
{
public:
virtual ~PhysicsSystemInterface() = default;
};
class TouchDownEvent;
class TouchUpEvent;
class MouseMoveEvent;
class KeyboardEvent;
class OrientationEvent;
class TouchGestureEvent;
class MouseButtonEvent;
class JoypadButtonEvent;
class JoypadAxisEvent;
namespace UI
{
class UIManagerInterface
{
public:
virtual ~UIManagerInterface() = default;
virtual bool filter_input_event(const TouchDownEvent &e) = 0;
virtual bool filter_input_event(const TouchUpEvent &e) = 0;
virtual bool filter_input_event(const MouseMoveEvent &e) = 0;
virtual bool filter_input_event(const KeyboardEvent &e) = 0;
virtual bool filter_input_event(const OrientationEvent &e) = 0;
virtual bool filter_input_event(const TouchGestureEvent &e) = 0;
virtual bool filter_input_event(const MouseButtonEvent &e) = 0;
virtual bool filter_input_event(const JoypadButtonEvent &e) = 0;
virtual bool filter_input_event(const JoypadAxisEvent &e) = 0;
};
}
namespace Audio
{
class BackendInterface
{
public:
virtual ~BackendInterface() = default;
virtual bool start() = 0;
virtual bool stop() = 0;
};
class MixerInterface
{
public:
virtual ~MixerInterface() = default;
virtual void event_start(EventManagerInterface &event_manager) = 0;
virtual void event_stop(EventManagerInterface &event_manager) = 0;
};
}
}
@@ -0,0 +1,14 @@
add_granite_internal_lib(granite-input input.hpp input.cpp)
target_include_directories(granite-input PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(granite-input PUBLIC granite-util granite-event granite-math)
if (${GRANITE_PLATFORM} MATCHES "SDL")
add_granite_internal_static_lib(granite-input-sdl input_sdl.cpp input_sdl.hpp)
target_link_libraries(granite-input-sdl PUBLIC granite-input)
if (GRANITE_SYSTEM_SDL)
find_package(SDL3 REQUIRED)
target_link_libraries(granite-input-sdl PUBLIC SDL3::SDL3-shared)
else()
target_link_libraries(granite-input-sdl PUBLIC SDL3-static)
endif()
endif()
@@ -0,0 +1,376 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "input.hpp"
#include "event.hpp"
#include "muglm/muglm_impl.hpp"
#include "logging.hpp"
#include <algorithm>
#include <string.h>
using namespace Util;
namespace Granite
{
const char *joypad_key_to_tag(JoypadKey key)
{
#define D(k) case JoypadKey::k: return #k
switch (key)
{
D(Left);
D(Right);
D(Up);
D(Down);
D(LeftShoulder);
D(RightShoulder);
D(West);
D(East);
D(North);
D(South);
D(LeftThumb);
D(RightThumb);
D(Mode);
D(Start);
D(Select);
default:
return "Unknown";
}
#undef D
}
const char *joypad_axis_to_tag(JoypadAxis axis)
{
#define D(k) case JoypadAxis::k: return #k
switch (axis)
{
D(LeftX);
D(LeftY);
D(RightX);
D(RightY);
D(LeftTrigger);
D(RightTrigger);
default:
return "Unknown";
}
#undef D
}
void InputTracker::orientation_event(quat rot)
{
OrientationEvent event(rot);
if (handler)
handler->dispatch(event);
}
void InputTracker::on_touch_down(unsigned id, float x, float y)
{
if (touch.active_pointers >= TouchCount)
{
LOGE("Touch pointer overflow!\n");
return;
}
unsigned index = touch.active_pointers++;
auto &pointer = touch.pointers[index];
pointer.id = id;
pointer.start_x = x;
pointer.start_y = y;
pointer.last_x = x;
pointer.last_y = y;
pointer.x = x;
pointer.y = y;
TouchDownEvent event(index, id, x, y, touch.width, touch.height);
if (handler)
handler->dispatch(event);
}
void InputTracker::dispatch_touch_gesture()
{
TouchGestureEvent event(touch);
if (handler)
handler->dispatch(event);
}
void InputTracker::on_touch_move(unsigned id, float x, float y)
{
auto &pointers = touch.pointers;
auto itr = std::find_if(std::begin(pointers), std::begin(pointers) + touch.active_pointers, [id](const TouchState::Pointer &pointer) {
return pointer.id == id;
});
if (itr == std::end(pointers))
{
LOGE("Could not find pointer!\n");
return;
}
itr->x = x;
itr->y = y;
}
void InputTracker::on_touch_up(unsigned id, float x, float y)
{
auto &pointers = touch.pointers;
auto itr = std::find_if(std::begin(pointers), std::begin(pointers) + touch.active_pointers, [id](const TouchState::Pointer &pointer) {
return pointer.id == id;
});
if (itr == std::end(pointers))
{
LOGE("Could not find pointer!\n");
return;
}
auto index = itr - std::begin(pointers);
TouchUpEvent event(itr->id, x, y, itr->start_x, itr->start_y, touch.width, touch.height);
if (handler)
handler->dispatch(event);
memmove(&pointers[index], &pointers[index + 1], (TouchCount - (index + 1)) * sizeof(TouchState::Pointer));
touch.active_pointers--;
}
void InputTracker::joypad_key_state(unsigned index, JoypadKey key, JoypadKeyState state)
{
if (index >= Joypads)
return;
assert(active_joypads & (1u << index));
auto &joy = joypads[index];
unsigned key_index = Util::ecast(key);
unsigned key_mask = 1u << key_index;
if (state == JoypadKeyState::Pressed)
{
if ((joy.button_mask & key_mask) == 0)
{
JoypadButtonEvent event(index, key, state);
if (handler)
handler->dispatch(event);
}
joy.button_mask |= key_mask;
}
else if (state == JoypadKeyState::Released)
{
if ((joy.button_mask & key_mask) != 0)
{
JoypadButtonEvent event(index, key, state);
if (handler)
handler->dispatch(event);
}
joy.button_mask &= ~key_mask;
}
}
void JoypadState::snap_deadzone(float deadzone)
{
memcpy(snapped_axis, raw_axis, sizeof(raw_axis));
static const JoypadAxis fused_axes[2][2] = {
{ JoypadAxis::LeftX, JoypadAxis::LeftY },
{ JoypadAxis::RightX, JoypadAxis::RightY },
};
for (auto &fused : fused_axes)
{
if (std::abs(raw_axis[int(fused[0])]) < deadzone && std::abs(raw_axis[int(fused[1])]) < deadzone)
for (auto &axis : fused)
snapped_axis[int(axis)] = 0.0f;
}
}
void InputTracker::joyaxis_state(unsigned index, JoypadAxis axis, float value)
{
if (index >= Joypads)
return;
assert(active_joypads & (1u << index));
auto &joy = joypads[index];
unsigned axis_index = Util::ecast(axis);
auto &a = joy.raw_axis[axis_index];
if (a != value)
{
JoypadAxisEvent event(index, axis, value);
if (handler)
handler->dispatch(event);
}
a = value;
}
void InputTracker::key_event(Key key, KeyState state)
{
if (state == KeyState::Released)
key_state &= ~(1ull << ecast(key));
else if (state == KeyState::Pressed)
key_state |= 1ull << ecast(key);
KeyboardEvent event(key, state);
if (handler)
handler->dispatch(event);
}
void InputTracker::mouse_button_event(Granite::MouseButton button, bool pressed)
{
mouse_button_event(button, last_mouse_x, last_mouse_y, pressed);
}
void InputTracker::mouse_button_event(MouseButton button, double x, double y, bool pressed)
{
if (pressed)
mouse_button_state |= 1ull << ecast(button);
else
mouse_button_state &= ~(1ull << ecast(button));
if (mouse_active)
{
last_mouse_x = x;
last_mouse_y = y;
}
MouseButtonEvent event(button, x, y, pressed);
if (handler)
handler->dispatch(event);
}
void InputTracker::mouse_move_event_relative(double x, double y)
{
x *= mouse_speed_x;
y *= mouse_speed_y;
if (mouse_active)
{
last_mouse_x += x;
last_mouse_y += y;
last_mouse_x = clamp(last_mouse_x, mouse_relative_range_x,
mouse_relative_range_x + mouse_relative_range_width);
last_mouse_y = clamp(last_mouse_y, mouse_relative_range_y,
mouse_relative_range_y + mouse_relative_range_height);
MouseMoveEvent event(x, y, last_mouse_x, last_mouse_y, key_state, mouse_button_state);
if (handler)
handler->dispatch(event);
}
}
void InputTracker::mouse_move_event_absolute(double x, double y)
{
if (mouse_active)
{
double delta_x = x - last_mouse_x;
double delta_y = y - last_mouse_y;
last_mouse_x = x;
last_mouse_y = y;
MouseMoveEvent event(delta_x, delta_y, x, y, key_state, mouse_button_state);
if (handler)
handler->dispatch(event);
}
}
void InputTracker::mouse_move_event_absolute_normalized(double x, double y)
{
mouse_move_event_absolute(x * double(touch.width), y * double(touch.height));
}
void InputTracker::mouse_button_event_normalized(MouseButton button, double x, double y, bool pressed)
{
mouse_button_event(button, x * double(touch.width), y * double(touch.height), pressed);
}
void InputTracker::mouse_enter(double x, double y)
{
mouse_active = true;
last_mouse_x = x;
last_mouse_y = y;
}
void InputTracker::mouse_leave()
{
mouse_active = false;
}
void InputTracker::dispatch_current_state(double delta_time, InputTrackerHandler *override_handler)
{
if (!override_handler)
override_handler = handler;
if (override_handler)
{
for (auto &pad : joypads)
pad.snap_deadzone(axis_deadzone);
override_handler->dispatch(JoypadStateEvent{active_joypads, joypads, Joypads, delta_time});
override_handler->dispatch(InputStateEvent{last_mouse_x, last_mouse_y,
delta_time, key_state, mouse_button_state, mouse_active});
}
}
int InputTracker::find_vacant_joypad_index() const
{
for (int i = 0; i < Joypads; i++)
{
if ((active_joypads & (1 << i)) == 0)
return i;
}
return -1;
}
void InputTracker::enable_joypad(unsigned index, uint32_t vid, uint32_t pid)
{
if (index >= Joypads)
return;
if (active_joypads & (1u << index))
return;
active_joypads |= 1u << index;
joypads[index] = {};
joypads[index].vid = vid;
joypads[index].pid = pid;
JoypadConnectionEvent event(index, true, vid, pid);
if (handler)
handler->dispatch(event);
}
void InputTracker::disable_joypad(unsigned index, uint32_t vid, uint32_t pid)
{
if (index >= Joypads)
return;
if ((active_joypads & (1u << index)) == 0)
return;
active_joypads &= ~(1u << index);
joypads[index] = {};
JoypadConnectionEvent event(index, false, vid, pid);
if (handler)
handler->dispatch(event);
}
std::mutex &InputTracker::get_lock()
{
return dispatch_lock;
}
}
@@ -0,0 +1,719 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include "enum_cast.hpp"
#include "event.hpp"
#include <stdint.h>
#include "math.hpp"
#include <limits.h>
#include <float.h>
#include <mutex>
namespace Granite
{
enum class JoypadKey
{
Left,
Right,
Up,
Down,
East,
South,
West,
North,
LeftShoulder,
RightShoulder,
LeftThumb,
RightThumb,
Start,
Select,
Mode,
Count,
Unknown
};
const char *joypad_key_to_tag(JoypadKey key);
enum class JoypadAxis
{
LeftX,
LeftY,
RightX,
RightY,
LeftTrigger,
RightTrigger,
Count,
Unknown
};
const char *joypad_axis_to_tag(JoypadAxis axis);
enum class JoypadKeyState
{
Pressed,
Released,
Count
};
enum class Key
{
Unknown,
A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z,
Return,
LeftCtrl,
LeftAlt,
LeftShift,
Space,
Escape,
Left, Right, Up, Down,
_1, _2, _3, _4, _5, _6, _7, _8, _9, _0,
Count
};
enum class MouseButton
{
Left,
Middle,
Right,
Count
};
enum class KeyState
{
Pressed,
Released,
Repeat,
Count
};
static_assert(Util::ecast(Key::Count) <= 64, "Cannot have more than 64 keys for bit-packing.");
struct TouchState
{
enum { PointerCount = 16 };
struct Pointer
{
unsigned id;
float start_x;
float start_y;
float last_x;
float last_y;
float x;
float y;
};
Pointer pointers[PointerCount] = {};
unsigned active_pointers = 0;
unsigned width;
unsigned height;
};
struct JoypadState
{
bool is_button_pressed(JoypadKey key) const
{
return (button_mask & (1u << Util::ecast(key))) != 0;
}
float get_axis(JoypadAxis a) const
{
return snapped_axis[Util::ecast(a)];
}
void snap_deadzone(float deadzone);
float raw_axis[Util::ecast(JoypadAxis::Count)] = {};
float snapped_axis[Util::ecast(JoypadAxis::Count)] = {};
uint32_t button_mask = 0;
uint32_t vid = 0;
uint32_t pid = 0;
};
static_assert(Util::ecast(JoypadKey::Count) <= 32, "Cannot have more than 32 joypad buttons.");
class InputTrackerHandler;
class InputTracker
{
public:
void key_event(Key key, KeyState state);
void mouse_button_event(MouseButton button, double x, double y, bool pressed);
void mouse_button_event_normalized(MouseButton button, double x, double y, bool pressed);
void mouse_button_event(MouseButton button, bool pressed);
void mouse_move_event_absolute(double x, double y);
void mouse_move_event_absolute_normalized(double x, double y);
void mouse_move_event_relative(double x, double y);
void dispatch_current_state(double delta_time, InputTrackerHandler *override_handler = nullptr);
void orientation_event(quat rot);
void joypad_key_state(unsigned index, JoypadKey key, JoypadKeyState state);
void joyaxis_state(unsigned index, JoypadAxis axis, float value);
void on_touch_down(unsigned id, float x, float y);
void on_touch_move(unsigned id, float x, float y);
void on_touch_up(unsigned id, float x, float y);
void mouse_enter(double x, double y);
void mouse_leave();
bool key_pressed(Key key) const
{
return (key_state & (1ull << Util::ecast(key))) != 0;
}
bool joykey_pressed(unsigned index, JoypadKey key) const
{
if (index >= Joypads)
return false;
return (joypads[index].button_mask & (1u << Util::ecast(key))) != 0;
}
bool mouse_button_pressed(MouseButton button) const
{
return (mouse_button_state & (1ull << Util::ecast(button))) != 0;
}
void dispatch_touch_gesture();
void set_axis_deadzone(float deadzone)
{
axis_deadzone = deadzone;
}
void set_relative_mouse_rect(double x, double y, double width, double height)
{
mouse_relative_range_x = x;
mouse_relative_range_y = y;
mouse_relative_range_width = width;
mouse_relative_range_height = height;
}
void set_relative_mouse_speed(double speed_x, double speed_y)
{
mouse_speed_x = speed_x;
mouse_speed_y = speed_y;
}
void enable_joypad(unsigned index, uint32_t vid, uint32_t pid);
void disable_joypad(unsigned index, uint32_t vid, uint32_t pid);
int find_vacant_joypad_index() const;
void set_touch_resolution(unsigned width, unsigned height)
{
touch.width = width;
touch.height = height;
}
void set_input_handler(InputTrackerHandler *handler_)
{
handler = handler_;
}
// To support dispatching input manager (i.e. polling) state from async threads.
std::mutex &get_lock();
enum { TouchCount = 16 };
enum { Joypads = 8 };
private:
InputTrackerHandler *handler = nullptr;
std::mutex dispatch_lock;
uint64_t key_state = 0;
uint8_t mouse_button_state = 0;
bool mouse_active = false;
double last_mouse_x = 0.0;
double last_mouse_y = 0.0;
double mouse_relative_range_x = 0.0;
double mouse_relative_range_y = 0.0;
double mouse_relative_range_width = DBL_MAX;
double mouse_relative_range_height = DBL_MAX;
double mouse_speed_x = 1.0;
double mouse_speed_y = 1.0;
uint8_t active_joypads = 0;
JoypadState joypads[Joypads] = {};
TouchState touch;
float axis_deadzone = 0.3f;
};
class JoypadConnectionEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(JoypadConnectionEvent)
JoypadConnectionEvent(unsigned index_, bool connected_, uint32_t vid_, uint32_t pid_)
: index(index_), connected(connected_), vid(vid_), pid(pid_)
{
}
unsigned get_index() const
{
return index;
}
bool is_connected() const
{
return connected;
}
uint32_t get_vid() const
{
return vid;
}
uint32_t get_pid() const
{
return pid;
}
private:
unsigned index;
bool connected;
uint32_t vid, pid;
};
class TouchGestureEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(TouchGestureEvent)
explicit TouchGestureEvent(const TouchState &state_)
: state(state_)
{
}
const TouchState &get_state() const
{
return state;
}
private:
const TouchState &state;
};
class TouchDownEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(TouchDownEvent)
TouchDownEvent(unsigned index_, unsigned id_,
float x_, float y_,
unsigned screen_width_, unsigned screen_height_)
: index(index_), id(id_),
x(x_), y(y_),
width(screen_width_), height(screen_height_)
{
}
float get_x() const
{
return x;
}
float get_y() const
{
return y;
}
unsigned get_index() const
{
return index;
}
unsigned get_id() const
{
return id;
}
unsigned get_screen_width() const
{
return width;
}
unsigned get_screen_height() const
{
return height;
}
private:
unsigned index, id;
float x, y;
unsigned width, height;
};
class TouchUpEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(TouchUpEvent)
TouchUpEvent(unsigned id_, float x_, float y_,
float start_x_, float start_y_,
unsigned screen_width_, unsigned screen_height_)
: id(id_), x(x_), y(y_),
start_x(start_x_), start_y(start_y_),
width(screen_width_), height(screen_height_)
{
}
float get_x() const
{
return x;
}
float get_y() const
{
return y;
}
float get_start_x() const
{
return start_x;
}
float get_start_y() const
{
return start_y;
}
unsigned get_id() const
{
return id;
}
unsigned get_screen_width() const
{
return width;
}
unsigned get_screen_height() const
{
return height;
}
private:
unsigned id;
float x, y;
float start_x, start_y;
unsigned width, height;
};
class JoypadButtonEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(JoypadButtonEvent)
JoypadButtonEvent(unsigned index_, JoypadKey key_, JoypadKeyState state_)
: index(index_), key(key_), state(state_)
{
}
unsigned get_index() const
{
return index;
}
JoypadKey get_key() const
{
return key;
}
JoypadKeyState get_state() const
{
return state;
}
private:
unsigned index;
JoypadKey key;
JoypadKeyState state;
};
class JoypadAxisEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(JoypadAxisEvent)
JoypadAxisEvent(unsigned index_, JoypadAxis axis_, float value_)
: index(index_), axis(axis_), value(value_)
{
}
unsigned get_index() const
{
return index;
}
JoypadAxis get_axis() const
{
return axis;
}
float get_value() const
{
return value;
}
private:
unsigned index;
JoypadAxis axis;
float value;
};
class KeyboardEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(KeyboardEvent)
KeyboardEvent(Key key_, KeyState state_)
: key(key_), state(state_)
{
}
Key get_key() const
{
return key;
}
KeyState get_key_state() const
{
return state;
}
private:
Key key;
KeyState state;
};
class OrientationEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(OrientationEvent)
explicit OrientationEvent(const quat &rot_)
: rot(rot_)
{
}
const quat &get_rotation() const
{
return rot;
}
private:
quat rot;
};
class MouseButtonEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(MouseButtonEvent)
MouseButtonEvent(MouseButton button_, double abs_x_, double abs_y_, bool pressed_)
: button(button_), abs_x(abs_x_), abs_y(abs_y_), pressed(pressed_)
{
}
MouseButton get_button() const
{
return button;
}
double get_abs_x() const
{
return abs_x;
}
double get_abs_y() const
{
return abs_y;
}
bool get_pressed() const
{
return pressed;
}
private:
MouseButton button;
double abs_x;
double abs_y;
bool pressed;
};
class MouseMoveEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(MouseMoveEvent);
MouseMoveEvent(double delta_x_, double delta_y_, double abs_x_, double abs_y_,
uint64_t key_mask_, uint8_t btn_mask_)
: delta_x(delta_x_), delta_y(delta_y_),
abs_x(abs_x_), abs_y(abs_y_),
key_mask(key_mask_), btn_mask(btn_mask_)
{
}
bool get_mouse_button_pressed(MouseButton button) const
{
return (btn_mask & (1 << Util::ecast(button))) != 0;
}
bool get_key_pressed(Key key) const
{
return (key_mask & (1ull << Util::ecast(key))) != 0;
}
double get_delta_x() const
{
return delta_x;
}
double get_delta_y() const
{
return delta_y;
}
double get_abs_x() const
{
return abs_x;
}
double get_abs_y() const
{
return abs_y;
}
private:
double delta_x, delta_y, abs_x, abs_y;
uint64_t key_mask;
uint8_t btn_mask;
};
class JoypadStateEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(JoypadStateEvent)
JoypadStateEvent(uint8_t active_mask_, const JoypadState *states_,
unsigned count_, double delta_time_)
: states(states_), count(count_), delta_time(delta_time_), active_mask(active_mask_)
{
}
bool is_connected(unsigned index) const
{
if (index >= count)
return false;
return (active_mask & (1u << index)) != 0;
}
unsigned get_num_indices() const
{
return count;
}
const JoypadState &get_state(unsigned index) const
{
return states[index];
}
double get_delta_time() const
{
return delta_time;
}
private:
const JoypadState *states;
unsigned count;
double delta_time;
uint8_t active_mask;
};
class InputStateEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(InputStateEvent)
InputStateEvent(double abs_x_, double abs_y_,
double delta_time_, uint64_t key_mask_, uint8_t btn_mask_, bool mouse_active_)
: abs_x(abs_x_), abs_y(abs_y_),
delta_time(delta_time_), key_mask(key_mask_),
btn_mask(btn_mask_), mouse_active(mouse_active_)
{
}
double get_delta_time() const
{
return delta_time;
}
bool get_mouse_active() const
{
return mouse_active;
}
bool get_mouse_button_pressed(MouseButton button) const
{
return (btn_mask & (1 << Util::ecast(button))) != 0;
}
bool get_key_pressed(Key key) const
{
return (key_mask & (1ull << Util::ecast(key))) != 0;
}
double get_mouse_x() const
{
return abs_x;
}
double get_mouse_y() const
{
return abs_y;
}
private:
double abs_x, abs_y;
double delta_time;
uint64_t key_mask;
uint8_t btn_mask;
bool mouse_active;
};
class InputTrackerHandler
{
public:
virtual ~InputTrackerHandler() = default;
virtual void dispatch(const TouchDownEvent &e) = 0;
virtual void dispatch(const TouchUpEvent &e) = 0;
virtual void dispatch(const TouchGestureEvent &e) = 0;
virtual void dispatch(const JoypadButtonEvent &e) = 0;
virtual void dispatch(const JoypadAxisEvent &e) = 0;
virtual void dispatch(const KeyboardEvent &e) = 0;
virtual void dispatch(const OrientationEvent &e) = 0;
virtual void dispatch(const MouseButtonEvent &e) = 0;
virtual void dispatch(const MouseMoveEvent &e) = 0;
virtual void dispatch(const JoypadStateEvent &e) = 0;
virtual void dispatch(const InputStateEvent &e) = 0;
virtual void dispatch(const JoypadConnectionEvent &e) = 0;
};
}
@@ -0,0 +1,184 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "input_sdl.hpp"
namespace Granite
{
bool InputTrackerSDL::init(InputTracker &tracker, const Dispatcher &dispatcher)
{
// Open existing gamepads.
int num_pads = 0;
SDL_JoystickID *gamepad_ids = SDL_GetGamepads(&num_pads);
for (int i = 0; i < num_pads; i++)
add_gamepad(gamepad_ids[i], tracker, dispatcher);
if (gamepad_ids)
SDL_free(gamepad_ids);
// Poll these separately, inline in poll_input().
SDL_SetGamepadEventsEnabled(false);
SDL_SetJoystickEventsEnabled(false);
SDL_SetEventEnabled(SDL_EVENT_GAMEPAD_ADDED, true);
SDL_SetEventEnabled(SDL_EVENT_GAMEPAD_REMOVED, true);
SDL_SetEventEnabled(SDL_EVENT_JOYSTICK_UPDATE_COMPLETE, false);
SDL_SetEventEnabled(SDL_EVENT_GAMEPAD_UPDATE_COMPLETE, false);
return true;
}
void InputTrackerSDL::update(InputTracker &tracker)
{
SDL_UpdateGamepads();
for (int i = 0; i < int(InputTracker::Joypads); i++)
{
auto *pad = pads[i];
if (!pad)
continue;
static const struct
{
JoypadKey gkey;
SDL_GamepadButton sdl;
} buttons[] = {
{ JoypadKey::Left, SDL_GAMEPAD_BUTTON_DPAD_LEFT },
{ JoypadKey::Right, SDL_GAMEPAD_BUTTON_DPAD_RIGHT },
{ JoypadKey::Up, SDL_GAMEPAD_BUTTON_DPAD_UP },
{ JoypadKey::Down, SDL_GAMEPAD_BUTTON_DPAD_DOWN },
{ JoypadKey::Start, SDL_GAMEPAD_BUTTON_START },
{ JoypadKey::Select, SDL_GAMEPAD_BUTTON_BACK },
{ JoypadKey::East, SDL_GAMEPAD_BUTTON_EAST },
{ JoypadKey::West, SDL_GAMEPAD_BUTTON_WEST },
{ JoypadKey::North, SDL_GAMEPAD_BUTTON_NORTH },
{ JoypadKey::South, SDL_GAMEPAD_BUTTON_SOUTH },
{ JoypadKey::LeftShoulder, SDL_GAMEPAD_BUTTON_LEFT_SHOULDER },
{ JoypadKey::RightShoulder, SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER },
{ JoypadKey::LeftThumb, SDL_GAMEPAD_BUTTON_LEFT_STICK },
{ JoypadKey::RightThumb, SDL_GAMEPAD_BUTTON_RIGHT_STICK },
{ JoypadKey::Mode, SDL_GAMEPAD_BUTTON_GUIDE },
};
for (auto &b : buttons)
{
tracker.joypad_key_state(i, b.gkey,
SDL_GetGamepadButton(pad, b.sdl) ?
JoypadKeyState::Pressed : JoypadKeyState::Released);
}
static const struct
{
JoypadAxis gaxis;
SDL_GamepadAxis sdl;
} axes[] = {
{ JoypadAxis::LeftX, SDL_GAMEPAD_AXIS_LEFTX },
{ JoypadAxis::LeftY, SDL_GAMEPAD_AXIS_LEFTY },
{ JoypadAxis::RightX, SDL_GAMEPAD_AXIS_RIGHTX },
{ JoypadAxis::RightY, SDL_GAMEPAD_AXIS_RIGHTY },
};
for (auto &a : axes)
{
float value = float(SDL_GetGamepadAxis(pad, a.sdl) - SDL_JOYSTICK_AXIS_MIN) /
float(SDL_JOYSTICK_AXIS_MAX - SDL_JOYSTICK_AXIS_MIN);
value = 2.0f * value - 1.0f;
tracker.joyaxis_state(i, a.gaxis, value);
}
tracker.joyaxis_state(i, JoypadAxis::LeftTrigger,
float(SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_LEFT_TRIGGER)) /
float(SDL_JOYSTICK_AXIS_MAX));
tracker.joyaxis_state(i, JoypadAxis::RightTrigger,
float(SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER)) /
float(SDL_JOYSTICK_AXIS_MAX));
}
}
void InputTrackerSDL::close()
{
for (auto *pad : pads)
if (pad)
SDL_CloseGamepad(pad);
}
bool InputTrackerSDL::process_sdl_event(const SDL_Event &e, InputTracker &tracker,
const InputTrackerSDL::Dispatcher &dispatcher)
{
switch (e.type)
{
case SDL_EVENT_GAMEPAD_ADDED:
{
add_gamepad(e.gdevice.which, tracker, dispatcher);
return true;
}
case SDL_EVENT_GAMEPAD_REMOVED:
{
remove_gamepad(e.gdevice.which, tracker, dispatcher);
return true;
}
default:
break;
}
return false;
}
void InputTrackerSDL::add_gamepad(SDL_JoystickID id, InputTracker &tracker, const Dispatcher &dispatcher)
{
int player_index = SDL_GetJoystickPlayerIndexForID(id);
if (player_index >= 0 && player_index < int(InputTracker::Joypads) && !pads[player_index])
{
uint32_t vid = SDL_GetGamepadVendorForID(id);
uint32_t pid = SDL_GetGamepadProductForID(id);
const char *name = SDL_GetGamepadNameForID(id);
LOGI("Plugging in controller: \"%s\" (%u/%u).\n", name, vid, pid);
const char *mapping = SDL_GetGamepadMappingForID(id);
LOGI(" Using mapping: \"%s\"\n", mapping);
pads[player_index] = SDL_OpenGamepad(id);
ids[player_index] = id;
dispatcher([player_index, vid, pid, &tracker]() {
tracker.enable_joypad(player_index, vid, pid);
});
}
}
void InputTrackerSDL::remove_gamepad(SDL_JoystickID id, InputTracker &tracker, const Dispatcher &dispatcher)
{
for (int i = 0; i < int(InputTracker::Joypads); i++)
{
if (pads[i] && ids[i] == id)
{
uint32_t vid = SDL_GetGamepadVendor(pads[i]);
uint32_t pid = SDL_GetGamepadProduct(pads[i]);
SDL_CloseGamepad(pads[i]);
pads[i] = nullptr;
ids[i] = 0;
dispatcher([i, vid, pid, &tracker]() {
tracker.disable_joypad(i, vid, pid);
});
break;
}
}
}
}
@@ -0,0 +1,45 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include <SDL3/SDL.h>
#include "input.hpp"
#include <functional>
namespace Granite
{
class InputTrackerSDL
{
public:
using Dispatcher = std::function<void (std::function<void ()>)>;
bool init(InputTracker &tracker, const Dispatcher &dispatcher);
void close();
bool process_sdl_event(const SDL_Event &event, InputTracker &tracker, const Dispatcher &dispatcher);
void update(InputTracker &tracker);
private:
SDL_Gamepad *pads[InputTracker::Joypads] = {};
SDL_JoystickID ids[InputTracker::Joypads] = {};
void add_gamepad(SDL_JoystickID id, InputTracker &tracker, const Dispatcher &dispatcher);
void remove_gamepad(SDL_JoystickID id, InputTracker &tracker, const Dispatcher &dispatcher);
};
}
@@ -0,0 +1,53 @@
# Must be static due to Granite::application_create() feedback.
if (ANDROID)
add_granite_internal_static_lib(granite-platform application_android.cpp)
find_package(game-activity REQUIRED CONFIG)
find_package(games-controller REQUIRED CONFIG)
find_package(games-frame-pacing REQUIRED CONFIG)
target_link_libraries(granite-platform PRIVATE granite-rapidjson
game-activity::game-activity_static
games-controller::paddleboat games-frame-pacing::swappy)
target_compile_definitions(granite-platform PRIVATE VK_USE_PLATFORM_ANDROID_KHR)
if (GRANITE_ANDROID_SWAPPY)
target_compile_definitions(granite-platform PRIVATE HAVE_SWAPPY)
endif()
if (GRANITE_ANDROID_APK_FILESYSTEM)
target_compile_definitions(granite-platform PRIVATE ANDROID_APK_FILESYSTEM)
endif()
elseif (${GRANITE_PLATFORM} MATCHES "libretro")
add_granite_internal_static_lib(granite-platform
application_libretro.cpp
application_libretro_utils.cpp
application_libretro_utils.hpp)
target_compile_definitions(granite-platform PUBLIC HAVE_LIBRETRO)
target_include_directories(granite-platform PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/libretro)
elseif (${GRANITE_PLATFORM} MATCHES "SDL")
add_granite_internal_static_lib(granite-platform application_sdl3.cpp)
if (GRANITE_SYSTEM_SDL)
find_package(SDL3 REQUIRED)
target_link_libraries(granite-platform PRIVATE SDL3::SDL3-shared)
else()
target_link_libraries(granite-platform PRIVATE SDL3-static)
if (NOT WIN32)
target_link_libraries(granite-platform PRIVATE dl)
endif()
endif()
target_link_libraries(granite-platform PRIVATE granite-input-sdl)
elseif (${GRANITE_PLATFORM} MATCHES "headless")
add_granite_internal_static_lib(granite-platform application_headless_wrapper.cpp)
elseif (${GRANITE_PLATFORM} MATCHES "null")
add_granite_internal_static_lib(granite-platform application_null.cpp)
else()
message(FATAL "GRANITE_PLATFORM is not set.")
endif()
if (NOT (${GRANITE_PLATFORM} MATCHES "null"))
target_link_libraries(granite-platform PRIVATE granite-application-interface-query granite-application granite-input granite-application-global-init)
endif()
if (GRANITE_AUDIO)
target_link_libraries(granite-platform PRIVATE granite-audio)
endif()
@@ -0,0 +1,20 @@
apply plugin: 'com.android.library'
android {
namespace = 'net.themaister.granite'
compileSdkVersion = 34
buildFeatures.prefab = true
defaultConfig {
minSdkVersion = 26
targetSdkVersion = 34
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.5.1'
implementation 'com.google.android.material:material:1.7.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.core:core:1.9.0'
implementation 'androidx.games:games-activity:4.0.0'
}
@@ -0,0 +1 @@
Place {arm64-v8a,armeabi-v7a}/libVkLayer_*.so here to have it bundled in the APK.
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
android:versionCode="$$VERSION_CODE$$"
android:versionName="$$VERSION_NAME$$">
<!-- Require Vulkan 1.1 -->
<uses-feature android:name="android.hardware.vulkan.version" android:version="0x401000" android:required="true"/>
<uses-feature android:name="android.hardware.vulkan.level" android:version="0" android:required="true"/>
<application android:label="@string/app_name" android:icon="@drawable/$$ICON$$">
<activity android:name="$$ACTIVITY_NAME$$"
android:theme="@style/Application.Fullscreen"
android:launchMode="singleTask"
android:screenOrientation="landscape"
android:exported="true"
android:label="@string/app_name">
<meta-data android:name="android.app.lib_name" android:value="$$NATIVE_TARGET$$" />
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
</manifest>
@@ -0,0 +1,100 @@
apply plugin: 'com.android.application'
android {
namespace = '$$NAMESPACE$$'
compileSdkVersion = 34
defaultConfig {
minSdkVersion = 26
targetSdkVersion = 34
ndk {
abiFilters = [$$ABIS$$]
}
}
ndkVersion = '29.0.14206865'
buildTypes {
debug {
externalNativeBuild {
cmake {
arguments = ["-DANDROID_TOOLCHAIN=clang",
"-DANDROID_STL=c++_shared",
"-DANDROID_ARM_MODE=arm",
"-DANDROID_CPP_FEATURES=exceptions",
"-DGRANITE_SHADER_COMPILER_OPTIMIZE=$$SHADER_OPTIMIZE$$",
"-DGRANITE_VULKAN_FOSSILIZE=$$FOSSILIZE$$",
"-DGRANITE_ANDROID_SWAPPY=$$SWAPPY$$",
"-DCMAKE_BUILD_TYPE=Debug",
"-DANDROID_PLATFORM=android-26",
"-DGRANITE_SHIPPING=ON",
"-DGRANITE_AUDIO=$$AUDIO$$",
"-DGRANITE_BULLET=$$PHYSICS$$",
"-DANDROID_ARM_NEON=ON",
"-DANDROID_USE_LEGACY_TOOLCHAIN_FILE=OFF",
"-DCMAKE_POLICY_VERSION_MINIMUM=3.5"]
targets = ["$$TARGET$$"]
}
}
jniDebuggable = true
}
release {
externalNativeBuild {
cmake {
arguments = ["-DANDROID_TOOLCHAIN=clang",
"-DANDROID_STL=c++_shared",
"-DANDROID_ARM_MODE=arm",
"-DANDROID_CPP_FEATURES=exceptions",
"-DGRANITE_SHADER_COMPILER_OPTIMIZE=$$SHADER_OPTIMIZE$$",
"-DGRANITE_VULKAN_FOSSILIZE=$$FOSSILIZE$$",
"-DGRANITE_ANDROID_SWAPPY=$$SWAPPY$$",
"-DCMAKE_BUILD_TYPE=Release",
"-DANDROID_PLATFORM=android-26",
"-DGRANITE_SHIPPING=ON",
"-DGRANITE_AUDIO=$$AUDIO$$",
"-DGRANITE_BULLET=$$PHYSICS$$",
"-DANDROID_ARM_NEON=ON",
"-DANDROID_USE_LEGACY_TOOLCHAIN_FILE=OFF",
"-DCMAKE_POLICY_VERSION_MINIMUM=3.5"]
targets = ["$$TARGET$$"]
}
}
debuggable = true
signingConfig = signingConfigs.debug
jniDebuggable = true
}
}
sourceSets {
main {
manifest.srcFile 'AndroidManifest.xml'
resources.srcDirs = ['res']
res.srcDirs = ['res']
assets.srcDirs = ['$$ASSETS$$', '$$GRANITE_ASSETS$$']
jniLibs.srcDirs = ['$$EXTERNAL_JNI$$']
$$GRANITE_AUX_ASSETS$$
}
}
externalNativeBuild {
cmake {
path = "$$CMAKELISTS$$"
version = "3.22.0+"
}
}
buildFeatures {
prefab = true
}
}
dependencies {
api project(':granite')
implementation 'androidx.games:games-activity:4.0.0'
implementation 'androidx.games:games-controller:2.0.2'
implementation 'androidx.games:games-frame-pacing:2.1.3'
$$EXTRA_DEPENDENCIES$$
}
@@ -0,0 +1 @@
android.useAndroidX=true
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

@@ -0,0 +1,3 @@
include 'granite'
project(':granite').projectDir = file('$$GRANITE_ANDROID_ACTIVITY_PATH$$')
include '$$APP$$'
@@ -0,0 +1,5 @@
include 'granite'
project(':granite').projectDir = file('$$GRANITE_ANDROID_ACTIVITY_PATH$$')
include 'custom'
project(':custom').projectDir = file('$$ANDROID_ACTIVITY_PATH$$')
include '$$APP$$'
@@ -0,0 +1,26 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:9.0.0-rc01'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
@@ -0,0 +1,146 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package net.themaister.granite;
import com.google.androidgamesdk.GameActivity;
import android.content.Context;
import android.content.Intent;
import android.media.AudioManager;
import android.os.Build;
import android.os.Bundle;
import android.view.Display;
import android.view.View;
import android.view.WindowManager;
import androidx.core.view.WindowCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.core.view.WindowInsetsControllerCompat;
public class GraniteActivity extends GameActivity
{
private final static String TAG = "Granite";
private void hideSystemUI()
{
// This will put the game behind any cutouts and waterfalls on devices which have
// them, so the corresponding insets will be non-zero.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
{
getWindow().getAttributes().layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
// From API 30 onwards, this is the recommended way to hide the system UI, rather than
// using View.setSystemUiVisibility.
View decorView = getWindow().getDecorView();
WindowInsetsControllerCompat controller = new WindowInsetsControllerCompat(getWindow(),
decorView);
controller.hide(WindowInsetsCompat.Type.systemBars());
controller.hide(WindowInsetsCompat.Type.displayCutout());
controller.setSystemBarsBehavior(
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
}
else
{
getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_FULLSCREEN |
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
}
}
@Override
protected void onCreate(Bundle savedState)
{
// When true, the app will fit inside any system UI windows.
// When false, we render behind any system UI windows.
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
hideSystemUI();
setVolumeControlStream(AudioManager.STREAM_MUSIC);
super.onCreate(savedState);
}
@Override
public void onWindowFocusChanged(boolean hasFocus)
{
super.onWindowFocusChanged(hasFocus);
if (hasFocus && Build.VERSION.SDK_INT < Build.VERSION_CODES.R)
hideSystemUI();
}
@Override
public void onBackPressed()
{
// Do nothing. We catch this inside native code instead.
}
public int getDisplayRotation()
{
Display display = getWindowManager().getDefaultDisplay();
if (display == null)
return 0;
return display.getRotation();
}
public int getAudioNativeSampleRate()
{
AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
if (am == null)
return 0;
String sampleRate = am.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE);
if (sampleRate == null)
return 0;
int rate = Integer.parseInt(sampleRate);
return rate;
}
public int getAudioNativeBlockFrames()
{
AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
if (am == null)
return 0;
String frames = am.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER);
if (frames == null)
return 0;
int count = Integer.parseInt(frames);
return count;
}
public String getCommandLineArgument(String key)
{
Intent intent = getIntent();
if (intent == null)
return "";
String extra = intent.getStringExtra(key);
if (extra == null)
return "";
return extra;
}
}
@@ -0,0 +1,2 @@
<resources>
</resources>
@@ -0,0 +1,7 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Application.Fullscreen" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:windowFullscreen">true</item>
<item name="android:windowContentOverlay">@null</item>
</style>
</resources>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,686 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#define NOMINMAX
#include "application.hpp"
#include "application_events.hpp"
#include "application_wsi.hpp"
#include "vulkan_headers.hpp"
#include <thread>
#include <mutex>
#include <condition_variable>
#include "stb_image_write.h"
#include "cli_parser.hpp"
#include "os_filesystem.hpp"
#include "rapidjson_wrapper.hpp"
#include <limits.h>
#include <cmath>
#include "thread_group.hpp"
#include "global_managers_init.hpp"
#include "path_utils.hpp"
#include "thread_group.hpp"
#include "asset_manager.hpp"
#ifdef HAVE_GRANITE_FFMPEG
#include "ffmpeg_encode.hpp"
#endif
#ifdef HAVE_GRANITE_AUDIO
#include "audio_interface.hpp"
#include "audio_mixer.hpp"
#endif
using namespace rapidjson;
using namespace Vulkan;
using namespace Util;
namespace Granite
{
struct WSIPlatformHeadless : Granite::GraniteWSIPlatform
{
public:
~WSIPlatformHeadless() override
{
release_resources();
}
void release_resources() override
{
for (auto &t : swapchain_tasks)
t.reset();
if (last_task_dependency)
last_task_dependency->wait();
last_task_dependency.reset();
auto *em = GRANITE_EVENT_MANAGER();
if (em)
{
em->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
em->enqueue_latched<ApplicationLifecycleEvent>(ApplicationLifecycle::Paused);
em->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
em->enqueue_latched<ApplicationLifecycleEvent>(ApplicationLifecycle::Stopped);
}
swapchain_images.clear();
readback_buffers.clear();
acquire_semaphore.clear();
#ifdef HAVE_GRANITE_FFMPEG
ycbcr_pipelines.clear();
#endif
}
bool alive(Vulkan::WSI &) override
{
return frames < max_frames;
}
void poll_input() override
{
std::lock_guard<std::mutex> holder{get_input_tracker().get_lock()};
get_input_tracker().dispatch_current_state(get_frame_timer().get_frame_time());
}
void poll_input_async(Granite::InputTrackerHandler *override_handler) override
{
std::lock_guard<std::mutex> holder{get_input_tracker().get_lock()};
get_input_tracker().dispatch_current_state(0.0, override_handler);
}
void enable_png_readback(std::string base_path)
{
png_readback = std::move(base_path);
}
std::vector<const char *> get_instance_extensions() override
{
return {};
}
VkSurfaceKHR create_surface(VkInstance, VkPhysicalDevice) override
{
return VK_NULL_HANDLE;
}
uint32_t get_surface_width() override
{
return width;
}
uint32_t get_surface_height() override
{
return height;
}
void notify_resize(unsigned width_, unsigned height_)
{
resize = true;
width = width_;
height = height_;
}
void set_max_frames(unsigned max_frames_)
{
max_frames = max_frames_;
}
bool has_external_swapchain() override
{
return true;
}
bool init(unsigned width_, unsigned height_)
{
width = width_;
height = height_;
if (!Context::init_loader(nullptr))
{
LOGE("Failed to initialize Vulkan loader.\n");
return false;
}
auto *em = GRANITE_EVENT_MANAGER();
if (em)
{
em->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
em->enqueue_latched<ApplicationLifecycleEvent>(ApplicationLifecycle::Stopped);
em->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
em->enqueue_latched<ApplicationLifecycleEvent>(ApplicationLifecycle::Paused);
em->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
em->enqueue_latched<ApplicationLifecycleEvent>(ApplicationLifecycle::Running);
}
return true;
}
bool init_headless(Application *app_)
{
app = app_;
auto context = Util::make_handle<Context>();
Context::SystemHandles system_handles;
system_handles.filesystem = GRANITE_FILESYSTEM();
system_handles.thread_group = GRANITE_THREAD_GROUP();
system_handles.timeline_trace_file = system_handles.thread_group->get_timeline_trace_file();
system_handles.asset_manager = GRANITE_ASSET_MANAGER();
context->set_system_handles(system_handles);
context->set_num_thread_indices(GRANITE_THREAD_GROUP()->get_num_threads() + 1);
const char *khr_surface = VK_KHR_SURFACE_EXTENSION_NAME;
const char *khr_swapchain = VK_KHR_SWAPCHAIN_EXTENSION_NAME;
auto name = app->get_name();
if (name.empty())
name = Path::basename(Path::get_executable_path());
VkApplicationInfo app_info = { VK_STRUCTURE_TYPE_APPLICATION_INFO };
app_info.pEngineName = "Granite";
app_info.pApplicationName = name.empty() ? "Granite" : name.c_str();
app_info.apiVersion = VK_API_VERSION_1_1;
context->set_application_info(&app_info);
if (!context->init_instance_and_device(&khr_surface, 1, &khr_swapchain, 1))
return false;
if (!app->init_wsi(std::move(context)))
return false;
auto &device = app->get_wsi().get_device();
auto info = ImageCreateInfo::render_target(width, height, VK_FORMAT_R8G8B8A8_SRGB);
info.usage |= VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
info.misc |= Vulkan::IMAGE_MISC_MUTABLE_SRGB_BIT;
info.initial_layout = VK_IMAGE_LAYOUT_UNDEFINED;
BufferCreateInfo readback = {};
readback.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT;
readback.domain = BufferDomain::CachedHost;
readback.size = width * height * sizeof(uint32_t);
for (unsigned i = 0; i < SwapchainImages; i++)
{
swapchain_images.push_back(device.create_image(info, nullptr));
readback_buffers.push_back(device.create_buffer(readback, nullptr));
acquire_semaphore.emplace_back(nullptr);
}
// Target present layouts to be more accurate for timing in case PRESENT_SRC forces decompress,
// and also makes sure pipeline caches are valid w.r.t render passes.
for (auto &swap : swapchain_images)
swap->set_swapchain_layout(VK_IMAGE_LAYOUT_PRESENT_SRC_KHR);
app->get_wsi().init_external_swapchain(swapchain_images);
return true;
}
#ifdef HAVE_GRANITE_FFMPEG
void init_headless_recording(std::string path)
{
#ifndef HAVE_GRANITE_RENDERER
LOGE("Need to include system handles in build to encode.\n");
return;
#endif
video_encode_path = std::move(path);
VideoEncoder::Options enc_opts = {};
enc_opts.width = width;
enc_opts.height = height;
double frame_rate = std::round(1.0 / time_step);
enc_opts.frame_timebase.num = 1;
enc_opts.frame_timebase.den = int(frame_rate);
#ifdef HAVE_GRANITE_AUDIO
enc_opts.walltime_to_pts = true;
record_stream.reset(Audio::create_default_audio_record_backend("headless", 44100.0f, 2));
if (record_stream)
encoder.set_audio_record_stream(record_stream.get());
#endif
if (!encoder.init(&app->get_wsi().get_device(), video_encode_path.c_str(), enc_opts))
{
LOGE("Failed to initialize encoder.\n");
video_encode_path.clear();
}
#ifdef HAVE_GRANITE_RENDERER
for (unsigned i = 0; i < SwapchainImages; i++)
{
auto &device = app->get_wsi().get_device();
FFmpegEncode::Shaders<> shaders;
shaders.scaler = device.get_shader_manager().register_compute(
"builtin://shaders/util/scaler.comp")->register_variant({})->get_program();
ycbcr_pipelines.push_back(encoder.create_ycbcr_pipeline(shaders));
}
#endif
#ifdef HAVE_GRANITE_AUDIO
record_stream->start();
#endif
}
#endif
void set_time_step(double t)
{
time_step = t;
}
void begin_frame()
{
auto &wsi = app->get_wsi();
wsi.set_external_frame(frame_index, std::move(acquire_semaphore[frame_index]), time_step);
acquire_semaphore[frame_index] = {};
}
void end_frame()
{
auto &wsi = app->get_wsi();
auto &device = wsi.get_device();
auto release_semaphore = wsi.consume_external_release_semaphore();
if (release_semaphore && release_semaphore->get_semaphore() != VK_NULL_HANDLE)
{
if (swapchain_tasks[frame_index])
{
swapchain_tasks[frame_index]->wait();
swapchain_tasks[frame_index].reset();
}
acquire_semaphore[frame_index] = {};
if (!next_readback_path.empty() || !png_readback.empty())
{
OwnershipTransferInfo transfer_info = {};
transfer_info.old_queue = wsi.get_current_present_queue_type();
transfer_info.new_queue = CommandBuffer::Type::AsyncTransfer;
transfer_info.old_image_layout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
transfer_info.new_image_layout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
transfer_info.dst_pipeline_stage = VK_PIPELINE_STAGE_2_COPY_BIT;
transfer_info.dst_access = VK_ACCESS_TRANSFER_READ_BIT;
auto cmd = request_command_buffer_with_ownership_transfer(device, *swapchain_images[frame_index],
transfer_info, release_semaphore);
cmd->copy_image_to_buffer(*readback_buffers[frame_index], *swapchain_images[frame_index],
0, {}, {width, height, 1},
0, 0, {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1});
cmd->barrier(VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_TRANSFER_WRITE_BIT,
VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_HOST_READ_BIT);
Fence readback_fence;
device.submit(cmd, &readback_fence, 1, &acquire_semaphore[frame_index]);
if (!next_readback_path.empty())
{
swapchain_tasks[frame_index] = GRANITE_THREAD_GROUP()->create_task(
[this, readback_fence, index = frame_index, frame = this->frames, p = std::make_unique<std::string>(next_readback_path)]() mutable {
readback_fence->wait();
dump_frame_single(*p, frame, index);
});
next_readback_path.clear();
}
else
{
swapchain_tasks[frame_index] = GRANITE_THREAD_GROUP()->create_task(
[this, readback_fence, index = frame_index, frame = this->frames]() mutable {
readback_fence->wait();
dump_frame(frame, index);
});
}
}
#ifdef HAVE_GRANITE_FFMPEG
else if (!video_encode_path.empty())
{
auto pts = encoder.sample_realtime_pts();
OwnershipTransferInfo transfer_info = {};
transfer_info.old_queue = wsi.get_current_present_queue_type();
transfer_info.new_queue = CommandBuffer::Type::AsyncCompute;
transfer_info.old_image_layout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
transfer_info.new_image_layout = VK_IMAGE_LAYOUT_READ_ONLY_OPTIMAL;
transfer_info.dst_pipeline_stage = VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
transfer_info.dst_access = VK_ACCESS_2_SHADER_SAMPLED_READ_BIT;
auto cmd = request_command_buffer_with_ownership_transfer(device, *swapchain_images[frame_index],
transfer_info, release_semaphore);
encoder.process_rgb(*cmd, ycbcr_pipelines[frame_index], swapchain_images[frame_index]->get_view());
encoder.submit_process_rgb(cmd, ycbcr_pipelines[frame_index]);
acquire_semaphore[frame_index] = device.request_semaphore(VK_SEMAPHORE_TYPE_BINARY);
device.submit_empty(CommandBuffer::Type::AsyncCompute, nullptr, acquire_semaphore[frame_index].get());
swapchain_tasks[frame_index] = GRANITE_THREAD_GROUP()->create_task(
[this, index = frame_index, pts]() mutable {
if (!encoder.encode_frame(ycbcr_pipelines[index], pts))
LOGE("Failed to push frame to encoder.\n");
});
}
#endif
else
{
// Do nothing.
acquire_semaphore[frame_index] = std::move(release_semaphore);
}
if (swapchain_tasks[frame_index])
{
swapchain_tasks[frame_index]->set_desc("application-headless-readback");
swapchain_tasks[frame_index]->set_task_class(TaskClass::Background);
if (last_task_dependency)
GRANITE_THREAD_GROUP()->add_dependency(*swapchain_tasks[frame_index], *last_task_dependency);
// Add a dummy task that only serves to chain dependencies.
last_task_dependency = GRANITE_THREAD_GROUP()->create_task();
last_task_dependency->set_task_class(TaskClass::Background);
GRANITE_THREAD_GROUP()->add_dependency(*last_task_dependency, *swapchain_tasks[frame_index]);
swapchain_tasks[frame_index]->flush();
}
}
release_semaphore = {};
frame_index = (frame_index + 1) % SwapchainImages;
frames++;
}
void set_next_readback(std::string path)
{
next_readback_path = std::move(path);
}
void wait_threads()
{
GRANITE_THREAD_GROUP()->wait_idle();
}
private:
unsigned width = 0;
unsigned height = 0;
unsigned frames = 0;
unsigned max_frames = UINT_MAX;
unsigned frame_index = 0;
double time_step = 0.01;
std::string png_readback;
std::string video_encode_path;
enum { SwapchainImages = 4 };
#ifdef HAVE_GRANITE_AUDIO
std::unique_ptr<Audio::RecordStream> record_stream;
#endif
std::vector<ImageHandle> swapchain_images;
std::vector<BufferHandle> readback_buffers;
std::vector<Semaphore> acquire_semaphore;
std::string next_readback_path;
TaskGroupHandle swapchain_tasks[SwapchainImages];
TaskGroupHandle last_task_dependency;
#ifdef HAVE_GRANITE_FFMPEG
VideoEncoder encoder;
std::vector<VideoEncoder::YCbCrPipeline> ycbcr_pipelines;
#endif
void dump_frame_single(const std::string &path, unsigned frame, unsigned index)
{
auto &wsi = app->get_wsi();
auto &device = wsi.get_device();
LOGI("Dumping frame: %u (index: %u)\n", frame, index);
auto *ptr = static_cast<uint32_t *>(device.map_host_buffer(*readback_buffers[index], MEMORY_ACCESS_READ_WRITE_BIT));
for (unsigned i = 0; i < width * height; i++)
ptr[i] |= 0xff000000u;
if (!stbi_write_png(path.c_str(), width, height, 4, ptr, width * 4))
LOGE("Failed to write PNG to disk.\n");
device.unmap_host_buffer(*readback_buffers[index], MEMORY_ACCESS_READ_WRITE_BIT);
}
void dump_frame(unsigned frame, unsigned index)
{
char buffer[64];
sprintf(buffer, "_%05u.png", frame);
auto path = png_readback + buffer;
dump_frame_single(path, frame, index);
}
Application *app = nullptr;
};
}
static void print_help()
{
LOGI("[--png-path <path>] [--stat <output.json>]\n"
"[--fs-assets <path>] [--fs-cache <path>] [--fs-builtin <path>]\n"
"[--video-encode-path <path>]\n"
"[--png-reference-path <path>] [--frames <frames>] [--width <width>] [--height <height>] [--time-step <step>].\n");
}
namespace Granite
{
int application_main_headless(
bool (*query_application_interface)(ApplicationQuery, void *, size_t),
Application *(*create_application)(int, char **),
int argc, char *argv[])
{
if (argc < 1)
return 1;
struct Args
{
std::string png_path;
std::string video_encode_path;
std::string png_reference_path;
std::string stat;
std::string assets;
std::string cache;
std::string builtin;
unsigned max_frames = UINT_MAX;
unsigned width = 1280;
unsigned height = 720;
double time_step = 0.01;
} args;
CLICallbacks cbs;
cbs.add("--frames", [&](CLIParser &parser) { args.max_frames = parser.next_uint(); });
cbs.add("--width", [&](CLIParser &parser) { args.width = parser.next_uint(); });
cbs.add("--height", [&](CLIParser &parser) { args.height = parser.next_uint(); });
cbs.add("--time-step", [&](CLIParser &parser) { args.time_step = parser.next_double(); });
cbs.add("--png-path", [&](CLIParser &parser) { args.png_path = parser.next_string(); });
cbs.add("--png-reference-path", [&](CLIParser &parser) { args.png_reference_path = parser.next_string(); });
cbs.add("--video-encode-path", [&](CLIParser &parser) { args.video_encode_path = parser.next_string(); });
cbs.add("--fs-assets", [&](CLIParser &parser) { args.assets = parser.next_string(); });
cbs.add("--fs-builtin", [&](CLIParser &parser) { args.builtin = parser.next_string(); });
cbs.add("--fs-cache", [&](CLIParser &parser) { args.cache = parser.next_string(); });
cbs.add("--stat", [&](CLIParser &parser) { args.stat = parser.next_string(); });
cbs.add("--help", [](CLIParser &parser)
{
print_help();
parser.end();
});
cbs.error_handler = [&]() { print_help(); };
int exit_code;
if (!Util::parse_cli_filtered(std::move(cbs), argc, argv, exit_code))
return exit_code;
ApplicationQueryDefaultManagerFlags flags{Global::MANAGER_FEATURE_DEFAULT_BITS};
query_application_interface(ApplicationQuery::DefaultManagerFlags, &flags, sizeof(flags));
Granite::Global::init(flags.manager_feature_flags);
if (flags.manager_feature_flags & Global::MANAGER_FEATURE_FILESYSTEM_BIT)
{
if (!args.assets.empty())
GRANITE_FILESYSTEM()->register_protocol("assets", std::make_unique<OSFilesystem>(args.assets));
if (!args.builtin.empty())
GRANITE_FILESYSTEM()->register_protocol("builtin", std::make_unique<OSFilesystem>(args.builtin));
if (!args.cache.empty())
GRANITE_FILESYSTEM()->register_protocol("cache", std::make_unique<OSFilesystem>(args.cache));
}
auto app = std::unique_ptr<Application>(create_application(argc, argv));
if (app)
{
auto platform = std::make_unique<WSIPlatformHeadless>();
if (!platform->init(args.width, args.height))
return 1;
auto *p = platform.get();
if (!app->init_platform(std::move(platform)))
return 1;
p->set_max_frames(args.max_frames);
p->set_time_step(args.time_step);
p->init_headless(app.get());
// Ensure all startup work is complete.
while (app->get_wsi().get_device().query_initialization_progress(Vulkan::Device::InitializationStage::Pipelines) < 100 &&
app->poll())
{
p->begin_frame();
app->run_frame();
p->end_frame();
}
if (!args.png_path.empty())
p->enable_png_readback(args.png_path);
if (!args.video_encode_path.empty())
{
#ifdef HAVE_GRANITE_FFMPEG
p->init_headless_recording(args.video_encode_path);
#else
LOGE("FFmpeg is not enabled in build.\n");
#endif
}
#ifdef HAVE_GRANITE_AUDIO
Global::start_audio_system();
#endif
// Run warm-up frame.
if (app->poll())
{
p->begin_frame();
app->run_frame();
p->end_frame();
}
p->wait_threads();
app->get_wsi().get_device().wait_idle();
app->get_wsi().get_device().timestamp_log_reset();
LOGI("=== Begin run ===\n");
auto start_time = get_current_time_nsecs();
unsigned rendered_frames = 0;
while (app->poll())
{
p->begin_frame();
app->run_frame();
p->end_frame();
if (!args.video_encode_path.empty() || !args.png_path.empty())
{
LOGI(" Queued frame %u (Total time = %.3f ms).\n", rendered_frames,
1e-6 * double(get_current_time_nsecs() - start_time));
}
rendered_frames++;
}
p->wait_threads();
app->get_wsi().get_device().wait_idle();
auto end_time = get_current_time_nsecs();
LOGI("=== End run ===\n");
struct Report
{
std::string tag;
TimestampIntervalReport report;
};
std::vector<Report> reports;
app->get_wsi().get_device().timestamp_log([&](const std::string &tag, const TimestampIntervalReport &report) {
reports.push_back({ tag, report });
});
app->get_wsi().get_device().timestamp_log_reset();
if (rendered_frames)
{
double usec = 1e-3 * double(end_time - start_time) / rendered_frames;
LOGI("Average frame time: %.3f usec\n", usec);
if (!args.stat.empty())
{
Document doc;
doc.SetObject();
auto &allocator = doc.GetAllocator();
doc.AddMember("averageFrameTimeUs", usec, allocator);
doc.AddMember("gpu", StringRef(app->get_wsi().get_context().get_gpu_props().deviceName), allocator);
doc.AddMember("driverVersion", app->get_wsi().get_context().get_gpu_props().driverVersion, allocator);
if (!reports.empty())
{
Value report_objs(kObjectType);
for (auto &rep : reports)
{
Value report_obj(kObjectType);
report_obj.AddMember("timePerAccumulationUs", 1e6 * rep.report.time_per_accumulation, allocator);
report_obj.AddMember("timePerFrameContextUs", 1e6 * rep.report.time_per_frame_context, allocator);
report_obj.AddMember("accumulationsPerFrameContext", rep.report.accumulations_per_frame_context, allocator);
report_objs.AddMember(StringRef(rep.tag), report_obj, allocator);
}
doc.AddMember("performance", report_objs, allocator);
}
StringBuffer buffer;
PrettyWriter<StringBuffer> writer(buffer);
doc.Accept(writer);
if (!GRANITE_FILESYSTEM()->write_string_to_file(args.stat, buffer.GetString()))
LOGE("Failed to write stat file to disk.\n");
}
}
if (!args.png_reference_path.empty())
{
p->set_next_readback(args.png_reference_path);
p->begin_frame();
app->run_frame();
p->end_frame();
}
p->wait_threads();
#ifdef HAVE_GRANITE_AUDIO
Global::stop_audio_system();
#endif
app.reset();
Granite::Global::deinit();
return 0;
}
else
return 1;
}
}
@@ -0,0 +1,33 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "application_glue.hpp"
namespace Granite
{
int application_main(
bool (*query_application_interface)(ApplicationQuery, void *, size_t),
Application *(*create_application)(int, char **), int argc, char *argv[])
{
return application_main_headless(query_application_interface, create_application, argc, argv);
}
}
@@ -0,0 +1,441 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "application_libretro_utils.hpp"
#include "global_managers_init.hpp"
#include "application.hpp"
#include "application_wsi.hpp"
#include "muglm/muglm_impl.hpp"
using namespace Granite;
static Application *app;
static retro_environment_t environ_cb;
static retro_video_refresh_t video_cb;
static retro_audio_sample_batch_t audio_cb;
static retro_input_poll_t input_poll_cb;
static retro_input_state_t input_state_cb;
static retro_usec_t last_frame_time;
static std::string application_name;
static std::string application_internal_resolution;
static unsigned current_width;
static unsigned current_height;
struct WSIPlatformLibretro : Granite::GraniteWSIPlatform
{
VkSurfaceKHR create_surface(VkInstance, VkPhysicalDevice) override
{
return VK_NULL_HANDLE;
}
std::vector<const char *> get_instance_extensions() override
{
return {};
}
unsigned get_surface_width() override
{
return current_width;
}
unsigned get_surface_height() override
{
return current_height;
}
bool alive(Vulkan::WSI &) override
{
return true;
}
void poll_input() override
{
std::lock_guard<std::mutex> holder{get_input_tracker().get_lock()};
input_poll_cb();
auto &tracker = get_input_tracker();
const auto poll_key = [&](unsigned index, JoypadKey key, unsigned retro_key) {
tracker.joypad_key_state(index, key,
input_state_cb(index, RETRO_DEVICE_JOYPAD, 0, retro_key)
? JoypadKeyState::Pressed : JoypadKeyState::Released);
};
const auto poll_axis = [&](unsigned index, JoypadAxis axis, unsigned retro_index, unsigned retro_id) {
tracker.joyaxis_state(index, axis,
clamp(input_state_cb(index, RETRO_DEVICE_ANALOG,
retro_index, retro_id) * (1.0f / 0x7fff), -1.0f, 1.0f));
};
const auto poll_axis_button = [&](unsigned index, JoypadAxis axis, unsigned retro_key) {
tracker.joyaxis_state(index, axis,
input_state_cb(index, RETRO_DEVICE_JOYPAD, 0, retro_key) ? 1.0f : 0.0f);
};
tracker.enable_joypad(0, 0, 0);
tracker.enable_joypad(1, 0, 0);
for (unsigned i = 0; i < 2; i++)
{
poll_key(i, JoypadKey::Left, RETRO_DEVICE_ID_JOYPAD_LEFT);
poll_key(i, JoypadKey::Right, RETRO_DEVICE_ID_JOYPAD_RIGHT);
poll_key(i, JoypadKey::Up, RETRO_DEVICE_ID_JOYPAD_UP);
poll_key(i, JoypadKey::Down, RETRO_DEVICE_ID_JOYPAD_DOWN);
poll_key(i, JoypadKey::Select, RETRO_DEVICE_ID_JOYPAD_SELECT);
poll_key(i, JoypadKey::Start, RETRO_DEVICE_ID_JOYPAD_START);
poll_key(i, JoypadKey::LeftShoulder, RETRO_DEVICE_ID_JOYPAD_L);
poll_key(i, JoypadKey::LeftThumb, RETRO_DEVICE_ID_JOYPAD_L3);
poll_key(i, JoypadKey::RightShoulder, RETRO_DEVICE_ID_JOYPAD_R);
poll_key(i, JoypadKey::RightThumb, RETRO_DEVICE_ID_JOYPAD_R3);
poll_key(i, JoypadKey::South, RETRO_DEVICE_ID_JOYPAD_B);
poll_key(i, JoypadKey::East, RETRO_DEVICE_ID_JOYPAD_A);
poll_key(i, JoypadKey::North, RETRO_DEVICE_ID_JOYPAD_X);
poll_key(i, JoypadKey::West, RETRO_DEVICE_ID_JOYPAD_Y);
poll_axis(i, JoypadAxis::LeftX, RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_ID_ANALOG_X);
poll_axis(i, JoypadAxis::LeftY, RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_ID_ANALOG_Y);
poll_axis(i, JoypadAxis::RightX, RETRO_DEVICE_INDEX_ANALOG_RIGHT, RETRO_DEVICE_ID_ANALOG_X);
poll_axis(i, JoypadAxis::RightY, RETRO_DEVICE_INDEX_ANALOG_RIGHT, RETRO_DEVICE_ID_ANALOG_Y);
poll_axis_button(i, JoypadAxis::LeftTrigger, RETRO_DEVICE_ID_JOYPAD_L2);
poll_axis_button(i, JoypadAxis::RightTrigger, RETRO_DEVICE_ID_JOYPAD_R2);
}
tracker.dispatch_current_state(app->get_platform().get_frame_timer().get_frame_time());
}
void poll_input_async(Granite::InputTrackerHandler *override_handler) override
{
std::lock_guard<std::mutex> holder{get_input_tracker().get_lock()};
get_input_tracker().dispatch_current_state(0.0, override_handler);
}
bool has_external_swapchain() override
{
return true;
}
};
static retro_hw_render_callback hw_render;
RETRO_API void retro_init(void)
{
ApplicationQueryDefaultManagerFlags flags{Global::MANAGER_FEATURE_DEFAULT_BITS};
query_application_interface(ApplicationQuery::DefaultManagerFlags, &flags, sizeof(flags));
Global::init(flags.manager_feature_flags);
}
RETRO_API void retro_deinit(void)
{
Global::deinit();
}
static void setup_variables()
{
application_internal_resolution = application_name + "_internal_resolution";
static const retro_variable variables[] = {
{ application_internal_resolution.c_str(), "Internal resolution; 1280x720|640x360|1280x1024|1920x1080" },
{ nullptr, nullptr },
};
environ_cb(RETRO_ENVIRONMENT_SET_VARIABLES, const_cast<retro_variable *>(variables));
}
static void query_variables()
{
retro_variable var = { application_internal_resolution.c_str(), nullptr };
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
{
unsigned new_width, new_height;
if (sscanf(var.value, "%ux%u", &new_width, &new_height) == 2)
{
current_width = new_width;
current_height = new_height;
}
}
}
RETRO_API void retro_set_environment(retro_environment_t cb)
{
environ_cb = cb;
retro_log_callback log_interface;
if (environ_cb(RETRO_ENVIRONMENT_GET_LOG_INTERFACE, &log_interface))
Granite::libretro_log = log_interface.log;
}
RETRO_API void retro_set_video_refresh(retro_video_refresh_t cb)
{
video_cb = cb;
}
RETRO_API void retro_set_audio_sample(retro_audio_sample_t)
{
}
RETRO_API void retro_set_audio_sample_batch(retro_audio_sample_batch_t cb)
{
audio_cb = cb;
}
RETRO_API void retro_set_input_poll(retro_input_poll_t cb)
{
input_poll_cb = cb;
}
RETRO_API void retro_set_input_state(retro_input_state_t cb)
{
input_state_cb = cb;
}
RETRO_API unsigned retro_api_version(void)
{
return RETRO_API_VERSION;
}
RETRO_API void retro_get_system_info(struct retro_system_info *info)
{
info->block_extract = false;
info->library_name = "Sample Scene Viewer";
info->library_version = "0.0";
info->need_fullpath = true;
info->valid_extensions = "gltf|glb|scene";
}
RETRO_API void retro_get_system_av_info(struct retro_system_av_info *info)
{
info->timing.fps = 60.0;
info->timing.sample_rate = 44100.0;
info->geometry.aspect_ratio = float(current_width) / current_height;
info->geometry.base_height = current_width;
info->geometry.base_width = current_height;
info->geometry.max_width = current_width;
info->geometry.max_height = current_height;
}
RETRO_API void retro_set_controller_port_device(unsigned, unsigned)
{
}
RETRO_API void retro_reset(void)
{
}
static void check_variables()
{
bool updated = false;
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE, &updated) && updated)
{
unsigned old_width = current_width;
unsigned old_height = current_height;
query_variables();
if (old_width != current_width || old_height != current_height)
{
retro_system_av_info av_info;
retro_get_system_av_info(&av_info);
libretro_set_swapchain_size(current_width, current_height);
if (!environ_cb(RETRO_ENVIRONMENT_SET_SYSTEM_AV_INFO, &av_info))
{
current_width = old_width;
current_height = old_height;
libretro_set_swapchain_size(current_width, current_height);
}
}
}
}
RETRO_API void retro_run(void)
{
if (!app)
{
// The application is dead, force a shutdown.
input_poll_cb();
environ_cb(RETRO_ENVIRONMENT_SHUTDOWN, nullptr);
return;
}
check_variables();
// Begin frame.
libretro_begin_frame(app->get_wsi(), last_frame_time);
// Run frame.
app->poll();
app->run_frame();
// Present.
libretro_end_frame(video_cb, app->get_wsi());
}
RETRO_API size_t retro_serialize_size(void)
{
return 0;
}
RETRO_API bool retro_serialize(void *, size_t)
{
return false;
}
RETRO_API bool retro_unserialize(const void *, size_t)
{
return false;
}
RETRO_API void retro_cheat_reset(void)
{
}
RETRO_API void retro_cheat_set(unsigned, bool, const char *)
{
}
static void context_destroy(void)
{
libretro_context_destroy(app);
}
static void context_reset(void)
{
retro_hw_render_interface_vulkan *vulkan;
if (!environ_cb(RETRO_ENVIRONMENT_GET_HW_RENDER_INTERFACE, &vulkan))
{
Granite::libretro_log(RETRO_LOG_ERROR, "Didn't get Vulkan HW interface.");
delete app;
app = nullptr;
return;
}
if (!libretro_context_reset(vulkan, *app))
{
Granite::libretro_log(RETRO_LOG_ERROR, "Failed to reset Vulkan context.");
delete app;
app = nullptr;
return;
}
}
static void frame_time_callback(retro_usec_t usecs)
{
last_frame_time = usecs;
}
RETRO_API bool retro_load_game(const struct retro_game_info *info)
{
char *argv[] = {
const_cast<char *>("libretro-granite"),
const_cast<char *>(info->path),
nullptr,
};
app = Granite::application_create(2, argv);
if (!app)
{
Granite::libretro_log(RETRO_LOG_ERROR, "Failed to load scene: %s\n", info->path);
return false;
}
current_width = app->get_default_width();
current_height = app->get_default_height();
if (!app->init_platform(std::make_unique<WSIPlatformLibretro>()))
{
Granite::libretro_log(RETRO_LOG_ERROR, "Failed to init platform.");
delete app;
return false;
}
application_name = app->get_name();
libretro_set_application_info(application_name.c_str(), app->get_version());
setup_variables();
query_variables();
libretro_set_swapchain_size(current_width, current_height);
hw_render.context_destroy = context_destroy;
hw_render.context_reset = context_reset;
hw_render.context_type = RETRO_HW_CONTEXT_VULKAN;
hw_render.version_major = 1;
hw_render.version_minor = 0;
if (!environ_cb(RETRO_ENVIRONMENT_SET_HW_RENDER, &hw_render))
{
Granite::libretro_log(RETRO_LOG_ERROR, "SET_HW_RENDER failed, this core cannot run.\n");
return false;
}
if (!libretro_load_game(environ_cb))
{
Granite::libretro_log(RETRO_LOG_ERROR, "Failed to set up Vulkan application, this core cannot run.\n");
return false;
}
retro_frame_time_callback frame_cb = {};
frame_cb.callback = frame_time_callback;
frame_cb.reference = (1000000 + 30) / 60;
last_frame_time = frame_cb.reference;
environ_cb(RETRO_ENVIRONMENT_SET_FRAME_TIME_CALLBACK, &frame_cb);
return true;
}
RETRO_API bool retro_load_game_special(unsigned, const struct retro_game_info *, size_t)
{
return false;
}
RETRO_API void retro_unload_game(void)
{
libretro_unload_game();
delete app;
app = nullptr;
}
RETRO_API unsigned retro_get_region(void)
{
return RETRO_REGION_NTSC;
}
RETRO_API void *retro_get_memory_data(unsigned)
{
return nullptr;
}
RETRO_API size_t retro_get_memory_size(unsigned)
{
return 0;
}
namespace Granite
{
void application_dummy()
{
}
// Alternatively, make sure this is linked in.
// Implementation is here to trick a linker to always let main() in static library work.
void application_setup_default_filesystem(const char *default_asset_directory)
{
auto *filesystem = GRANITE_FILESYSTEM();
if (filesystem)
Filesystem::setup_default_filesystem(filesystem, default_asset_directory);
}
}
@@ -0,0 +1,403 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "application_libretro_utils.hpp"
#include "application.hpp"
#include "application_events.hpp"
#include "thread_group.hpp"
#include "asset_manager.hpp"
#include "context.hpp"
namespace Granite
{
retro_log_printf_t libretro_log;
static retro_hw_render_interface_vulkan *vulkan_interface;
static retro_hw_render_context_negotiation_interface_vulkan vulkan_negotiation;
static Vulkan::ContextHandle vulkan_context;
static Vulkan::ImageViewHandle swapchain_unorm_view;
static Vulkan::ImageHandle swapchain_image;
static retro_vulkan_image swapchain_image_info;
static bool can_dupe = false;
static std::string application_name;
static unsigned application_version;
static unsigned swapchain_width;
static unsigned swapchain_height;
static unsigned swapchain_frame_index;
static Vulkan::Semaphore acquire_semaphore;
static VkApplicationInfo vulkan_app = {
VK_STRUCTURE_TYPE_APPLICATION_INFO,
nullptr,
nullptr, 0,
"Granite",
0,
VK_API_VERSION_1_1,
};
void libretro_set_swapchain_size(unsigned width, unsigned height)
{
swapchain_width = width;
swapchain_height = height;
}
void libretro_set_application_info(const char *name, unsigned version)
{
application_name = name;
application_version = version;
vulkan_app.pApplicationName = application_name.c_str();
vulkan_app.applicationVersion = application_version;
}
bool libretro_create_device(
struct retro_vulkan_context *context,
VkInstance instance,
VkPhysicalDevice gpu,
VkSurfaceKHR surface,
PFN_vkGetInstanceProcAddr get_instance_proc_addr,
const char **required_device_extensions,
unsigned num_required_device_extensions,
const char **, unsigned, // Deprecated.
const VkPhysicalDeviceFeatures *required_features)
{
if (!Vulkan::Context::init_loader(get_instance_proc_addr))
return false;
vulkan_context = Util::make_handle<Vulkan::Context>();
Vulkan::Context::SystemHandles system_handles;
system_handles.filesystem = GRANITE_FILESYSTEM();
system_handles.thread_group = GRANITE_THREAD_GROUP();
system_handles.asset_manager = GRANITE_ASSET_MANAGER();
system_handles.timeline_trace_file = system_handles.thread_group->get_timeline_trace_file();
vulkan_context->set_system_handles(system_handles);
vulkan_context->set_num_thread_indices(GRANITE_THREAD_GROUP()->get_num_threads() + 1);
if (!vulkan_context->init_device_from_instance(instance, gpu, surface, required_device_extensions, num_required_device_extensions,
required_features))
{
return false;
}
vulkan_context->release_device();
context->gpu = vulkan_context->get_gpu();
context->device = vulkan_context->get_device();
context->presentation_queue = vulkan_context->get_queue_info().queues[Vulkan::QUEUE_INDEX_GRAPHICS];
context->presentation_queue_family_index = vulkan_context->get_queue_info().family_indices[Vulkan::QUEUE_INDEX_GRAPHICS];
context->queue = vulkan_context->get_queue_info().queues[Vulkan::QUEUE_INDEX_GRAPHICS];
context->queue_family_index = vulkan_context->get_queue_info().family_indices[Vulkan::QUEUE_INDEX_GRAPHICS];
return true;
}
static VkInstance libretro_create_instance(
PFN_vkGetInstanceProcAddr get_instance_proc_addr,
const VkApplicationInfo *app,
retro_vulkan_create_instance_wrapper_t create_instance_wrapper,
void *opaque)
{
if (!Vulkan::Context::init_loader(get_instance_proc_addr))
return VK_NULL_HANDLE;
vulkan_context = Util::make_handle<Vulkan::Context>();
Vulkan::Context::SystemHandles system_handles;
system_handles.filesystem = GRANITE_FILESYSTEM();
system_handles.thread_group = GRANITE_THREAD_GROUP();
system_handles.asset_manager = GRANITE_ASSET_MANAGER();
system_handles.timeline_trace_file = system_handles.thread_group->get_timeline_trace_file();
vulkan_context->set_application_info(app);
vulkan_context->set_system_handles(system_handles);
vulkan_context->set_num_thread_indices(GRANITE_THREAD_GROUP()->get_num_threads() + 1);
struct Factory final : Vulkan::InstanceFactory
{
VkInstance create_instance(const VkInstanceCreateInfo *info) override
{
return wrapper(opaque, info);
}
retro_vulkan_create_instance_wrapper_t wrapper = nullptr;
void *opaque = nullptr;
} factory;
factory.wrapper = create_instance_wrapper;
factory.opaque = opaque;
vulkan_context->set_instance_factory(&factory);
if (!vulkan_context->init_instance(nullptr, 0))
{
vulkan_context.reset();
return VK_NULL_HANDLE;
}
vulkan_context->release_instance();
return vulkan_context->get_instance();
}
static bool libretro_create_device2(
struct retro_vulkan_context *context,
VkInstance instance,
VkPhysicalDevice gpu,
VkSurfaceKHR surface,
PFN_vkGetInstanceProcAddr get_instance_proc_addr,
retro_vulkan_create_device_wrapper_t create_device_wrapper,
void *opaque)
{
// We are guaranteed that create_instance has been called here.
if (!vulkan_context)
return false;
// Sanity check inputs.
if (vulkan_context->get_instance() != instance)
return false;
if (Vulkan::Context::get_instance_proc_addr() != get_instance_proc_addr)
return false;
struct Factory final : Vulkan::DeviceFactory
{
VkDevice create_device(VkPhysicalDevice gpu, const VkDeviceCreateInfo *info) override
{
return wrapper(gpu, opaque, info);
}
retro_vulkan_create_device_wrapper_t wrapper = nullptr;
void *opaque = nullptr;
} factory;
factory.wrapper = create_device_wrapper;
factory.opaque = opaque;
vulkan_context->set_device_factory(&factory);
if (!vulkan_context->init_device(gpu, surface, nullptr, 0))
return false;
vulkan_context->release_device();
context->gpu = vulkan_context->get_gpu();
context->device = vulkan_context->get_device();
context->presentation_queue = vulkan_context->get_queue_info().queues[Vulkan::QUEUE_INDEX_GRAPHICS];
context->presentation_queue_family_index = vulkan_context->get_queue_info().family_indices[Vulkan::QUEUE_INDEX_GRAPHICS];
context->queue = vulkan_context->get_queue_info().queues[Vulkan::QUEUE_INDEX_GRAPHICS];
context->queue_family_index = vulkan_context->get_queue_info().family_indices[Vulkan::QUEUE_INDEX_GRAPHICS];
return true;
}
void libretro_begin_frame(Vulkan::WSI &wsi, retro_usec_t frame_time)
{
// Setup the external frame.
vulkan_interface->wait_sync_index(vulkan_interface->handle);
wsi.set_external_frame(swapchain_frame_index, std::move(acquire_semaphore), double(frame_time) * 1e-6);
acquire_semaphore = {};
swapchain_frame_index ^= 1;
}
void libretro_end_frame(retro_video_refresh_t video_cb, Vulkan::WSI &wsi)
{
// Present to libretro frontend.
auto signal_semaphore = wsi.get_device().request_semaphore(VK_SEMAPHORE_TYPE_BINARY);
vulkan_interface->set_signal_semaphore(vulkan_interface->handle,
signal_semaphore->get_semaphore());
signal_semaphore->signal_external();
acquire_semaphore = wsi.consume_external_release_semaphore();
if (acquire_semaphore && acquire_semaphore->get_semaphore() != VK_NULL_HANDLE)
{
vulkan_interface->set_image(vulkan_interface->handle,
&swapchain_image_info,
1, &acquire_semaphore->get_semaphore(),
VK_QUEUE_FAMILY_IGNORED);
// Lets us recycle the semaphore.
acquire_semaphore->wait_external();
video_cb(RETRO_HW_FRAME_BUFFER_VALID, swapchain_width, swapchain_height, 0);
can_dupe = true;
}
else
{
vulkan_interface->set_image(vulkan_interface->handle,
&swapchain_image_info,
0, nullptr,
VK_QUEUE_FAMILY_IGNORED);
if (!can_dupe)
{
// Need something to show ... Just clear the image to black and present that.
// This should only happen if we don't render to swapchain the very first frame,
// so performance doesn't really matter.
auto &device = wsi.get_device();
auto cmd = device.request_command_buffer();
cmd->image_barrier(*swapchain_image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, VK_PIPELINE_STAGE_2_CLEAR_BIT,
VK_ACCESS_TRANSFER_WRITE_BIT);
cmd->clear_image(*swapchain_image, {});
cmd->image_barrier(*swapchain_image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
VK_IMAGE_LAYOUT_READ_ONLY_OPTIMAL,
VK_PIPELINE_STAGE_2_CLEAR_BIT, VK_ACCESS_TRANSFER_WRITE_BIT,
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_ACCESS_2_SHADER_SAMPLED_READ_BIT);
device.submit(cmd);
video_cb(RETRO_HW_FRAME_BUFFER_VALID, swapchain_width, swapchain_height, 0);
can_dupe = true;
}
else
{
video_cb(nullptr, swapchain_width, swapchain_height, 0);
}
}
// Mark video_cb has having done work in our frame context.
wsi.get_device().submit_external(Vulkan::CommandBuffer::Type::Generic);
acquire_semaphore = signal_semaphore;
}
bool libretro_context_reset(retro_hw_render_interface_vulkan *vulkan, Granite::Application &app)
{
vulkan_interface = vulkan;
if (vulkan->interface_type != RETRO_HW_RENDER_INTERFACE_VULKAN)
return false;
if (vulkan->interface_version != RETRO_HW_RENDER_INTERFACE_VULKAN_VERSION)
return false;
if (!app.init_wsi(std::move(vulkan_context)))
return false;
auto &device = app.get_wsi().get_device();
device.set_queue_lock([vulkan]() {
vulkan->lock_queue(vulkan->handle);
},
[vulkan]() {
vulkan->unlock_queue(vulkan->handle);
});
const unsigned num_swapchain_images = 2;
Vulkan::ImageCreateInfo info = Vulkan::ImageCreateInfo::render_target(swapchain_width, swapchain_height,
VK_FORMAT_R8G8B8A8_SRGB);
info.usage |= VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
info.flags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
info.initial_layout = VK_IMAGE_LAYOUT_UNDEFINED;
swapchain_image = device.create_image(info, nullptr);
swapchain_image->set_swapchain_layout(VK_IMAGE_LAYOUT_READ_ONLY_OPTIMAL);
can_dupe = false;
Vulkan::ImageViewCreateInfo view_info;
view_info.format = VK_FORMAT_R8G8B8A8_UNORM;
view_info.image = swapchain_image.get();
swapchain_unorm_view = device.create_image_view(view_info);
std::vector<Vulkan::ImageHandle> images;
for (unsigned i = 0; i < num_swapchain_images; i++)
images.push_back(swapchain_image);
device.init_frame_contexts(2);
if (!app.get_wsi().init_external_swapchain(std::move(images)))
return false;
// Setup the swapchain image info for the frontend.
swapchain_image_info.image_view = swapchain_unorm_view->get_view().view;
swapchain_image_info.image_layout = VK_IMAGE_LAYOUT_READ_ONLY_OPTIMAL;
swapchain_image_info.create_info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
swapchain_image_info.create_info.image = swapchain_unorm_view->get_image().get_image();
swapchain_image_info.create_info.format = VK_FORMAT_R8G8B8A8_UNORM;
swapchain_image_info.create_info.components.r = VK_COMPONENT_SWIZZLE_IDENTITY;
swapchain_image_info.create_info.components.g = VK_COMPONENT_SWIZZLE_IDENTITY;
swapchain_image_info.create_info.components.b = VK_COMPONENT_SWIZZLE_IDENTITY;
swapchain_image_info.create_info.components.a = VK_COMPONENT_SWIZZLE_IDENTITY;
swapchain_image_info.create_info.viewType = VK_IMAGE_VIEW_TYPE_2D;
swapchain_image_info.create_info.subresourceRange.levelCount = 1;
swapchain_image_info.create_info.subresourceRange.layerCount = 1;
swapchain_image_info.create_info.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
swapchain_frame_index = 0;
return true;
}
void libretro_context_destroy(Granite::Application *app)
{
swapchain_unorm_view.reset();
swapchain_image.reset();
acquire_semaphore.reset();
if (app)
app->teardown_wsi();
}
static const VkApplicationInfo *get_application_info(void)
{
return &vulkan_app;
}
bool libretro_load_game(retro_environment_t environ_cb)
{
vulkan_negotiation.interface_type = RETRO_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE_VULKAN;
if (!environ_cb(RETRO_ENVIRONMENT_GET_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE_SUPPORT, &vulkan_negotiation))
{
Granite::libretro_log(RETRO_LOG_WARN, "GET_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE_SUPPORT failed, assuming v1 only.\n");
vulkan_negotiation.interface_version = 1;
}
else if (vulkan_negotiation.interface_version == 0)
{
Granite::libretro_log(RETRO_LOG_ERROR, "Vulkan is not supported, this core cannot run.\n");
}
else
{
Granite::libretro_log(RETRO_LOG_INFO, "GET_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE_SUPPORT passed, exposing v2.\n");
vulkan_negotiation.interface_version = 2;
}
vulkan_negotiation.create_device = Granite::libretro_create_device;
vulkan_negotiation.create_device2 = Granite::libretro_create_device2;
vulkan_negotiation.create_instance = Granite::libretro_create_instance;
vulkan_negotiation.destroy_device = nullptr;
vulkan_negotiation.get_application_info = get_application_info;
if (!environ_cb(RETRO_ENVIRONMENT_SET_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE, &vulkan_negotiation))
{
Granite::libretro_log(RETRO_LOG_ERROR, "SET_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE failed, this core cannot run.\n");
return false;
}
auto *em = GRANITE_EVENT_MANAGER();
if (em)
{
em->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
em->enqueue_latched<ApplicationLifecycleEvent>(ApplicationLifecycle::Stopped);
em->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
em->enqueue_latched<ApplicationLifecycleEvent>(ApplicationLifecycle::Paused);
em->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
em->enqueue_latched<ApplicationLifecycleEvent>(ApplicationLifecycle::Running);
}
return true;
}
void libretro_unload_game()
{
auto *em = GRANITE_EVENT_MANAGER();
if (em)
{
em->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
em->enqueue_latched<ApplicationLifecycleEvent>(ApplicationLifecycle::Paused);
em->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
em->enqueue_latched<ApplicationLifecycleEvent>(ApplicationLifecycle::Stopped);
}
}
}
@@ -0,0 +1,71 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include "vulkan_headers.hpp"
#include "libretro.h"
#include "libretro_vulkan.h"
#include "application.hpp"
// Various utilities to make writing a libretro Vulkan interface easier.
// The heavy lifting of WSI interfacing with the libretro frontend is implemented here.
namespace Granite
{
extern retro_log_printf_t libretro_log;
bool libretro_create_device(
struct retro_vulkan_context *context,
VkInstance instance,
VkPhysicalDevice gpu,
VkSurfaceKHR surface,
PFN_vkGetInstanceProcAddr get_instance_proc_addr,
const char **required_device_extensions,
unsigned num_required_device_extensions,
const char **required_device_layers,
unsigned num_required_device_layers,
const VkPhysicalDeviceFeatures *required_features);
// Takes effect next time the swapchain is recreated, on context_reset.
void libretro_set_swapchain_size(unsigned width, unsigned height);
// Used in get_application_info.
void libretro_set_application_info(const char *name, unsigned version);
// Called on context_reset HW_RENDER callback.
bool libretro_context_reset(retro_hw_render_interface_vulkan *vulkan, Granite::Application &app);
// Called on context_destroy HW_RENDER callback.
void libretro_context_destroy(Granite::Application *app);
// Called at the start of the frame.
void libretro_begin_frame(Vulkan::WSI &wsi, retro_usec_t frame_time);
// Called at the end of the frame.
void libretro_end_frame(retro_video_refresh_t video_cb, Vulkan::WSI &wsi);
// Called on retro_load_game.
bool libretro_load_game(retro_environment_t environ_cb);
// Called on retro_unload_game.
void libretro_unload_game();
}
@@ -0,0 +1,36 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "../application_glue.hpp"
#include <stddef.h>
namespace Granite
{
int application_main(
bool (*query_application_interface)(ApplicationQuery, void *, size_t),
Application *(*create_application)(int, char **), int argc, char *argv[])
{
(void)query_application_interface;
(void)create_application;
return 0;
}
}
@@ -0,0 +1,856 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include <SDL3/SDL.h>
#include <SDL3/SDL_vulkan.h>
#include <atomic>
#include "application.hpp"
#include "application_wsi.hpp"
#include "application_events.hpp"
#include "input.hpp"
#include "input_sdl.hpp"
#include "cli_parser.hpp"
#include "global_managers_init.hpp"
#include "timeline_trace_file.hpp"
#include "path_utils.hpp"
#include "thread_group.hpp"
#include "thread_id.hpp"
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#endif
#ifdef __linux__
#include <dlfcn.h>
#endif
namespace Granite
{
static Key sdl_key_to_granite_key(SDL_Keycode key)
{
if (key >= 'a' && key <= 'z')
return Key(int(Granite::Key::A) + (key - 'a'));
else if (key >= 'A' && key <= 'Z')
return Key(int(Granite::Key::A) + (key - 'A'));
#define k(sdl, granite) case SDLK_##sdl: return Key::granite
switch (key)
{
k(LCTRL, LeftCtrl);
k(LALT, LeftAlt);
k(LSHIFT, LeftShift);
k(RETURN, Return);
k(SPACE, Space);
k(ESCAPE, Escape);
k(LEFT, Left);
k(RIGHT, Right);
k(UP, Up);
k(DOWN, Down);
k(0, _0);
k(1, _1);
k(2, _2);
k(3, _3);
k(4, _4);
k(5, _5);
k(6, _6);
k(7, _7);
k(8, _8);
k(9, _9);
default:
return Key::Unknown;
}
#undef k
}
struct WSIPlatformSDL : GraniteWSIPlatform
{
public:
struct Options
{
unsigned override_width = 0;
unsigned override_height = 0;
bool fullscreen = false;
#ifdef _WIN32
bool threaded = true;
#else
bool threaded = false;
#endif
};
explicit WSIPlatformSDL(const Options &options_)
: options(options_)
{
}
void run_gamepad_init()
{
Util::Timer tmp_timer;
tmp_timer.start();
if (!SDL_Init(SDL_INIT_GAMEPAD))
{
LOGE("Failed to init gamepad.\n");
return;
}
LOGI("SDL_Init(GAMEPAD) took %.3f seconds async.\n", tmp_timer.end());
LOGI("Pushing task to main thread.\n");
push_task_to_main_thread([this]() {
LOGI("Running task in main thread.\n");
if (!pad.init(get_input_tracker(), [](std::function<void ()> func) { func(); }))
LOGE("Failed to init gamepad tracker.\n");
gamepad_init_async.store(true, std::memory_order_release);
});
}
void kick_gamepad_init()
{
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
// Adding gamepad events will make main loop spin without waiting.
SDL_SetHint(SDL_HINT_AUTO_UPDATE_JOYSTICKS, "0");
// Enumerating gamepads can be extremely slow in some cases. Do this async.
// Gamepad interface is very async friendly.
gamepad_init_async = false;
if (auto *tg = GRANITE_THREAD_GROUP())
{
gamepad_init_task = tg->create_task([this]() { run_gamepad_init(); });
gamepad_init_task->set_desc("SDL init gamepad");
gamepad_init_task->set_task_class(TaskClass::Background);
gamepad_init_task->flush();
}
else
run_gamepad_init();
}
bool init(const std::string &name, unsigned width_, unsigned height_)
{
request_tear_down.store(false);
width = width_;
height = height_;
if (options.override_width)
width = options.override_width;
if (options.override_height)
height = options.override_height;
#ifdef __linux__
// RenderDoc doesn't support Wayland, and SDL3 uses Wayland by default.
// Opt in to X11 to avoid having to manually remember to pass down SDL_VIDEO_DRIVER=x11.
void *renderdoc_module = dlopen("librenderdoc.so", RTLD_NOW | RTLD_NOLOAD);
if (renderdoc_module)
{
LOGI("RenderDoc is loaded, disabling Wayland.\n");
setenv("SDL_VIDEO_DRIVER", "x11", 0);
}
#endif
Util::Timer tmp_timer;
tmp_timer.start();
if (!SDL_Init(SDL_INIT_EVENTS | SDL_INIT_VIDEO))
{
LOGE("Failed to init SDL.\n");
return false;
}
LOGI("SDL_Init took %.3f seconds.\n", tmp_timer.end());
kick_gamepad_init();
SDL_SetEventEnabled(SDL_EVENT_DROP_FILE, false);
SDL_SetEventEnabled(SDL_EVENT_DROP_TEXT, false);
if (!SDL_Vulkan_LoadLibrary(nullptr))
{
LOGE("Failed to load Vulkan library.\n");
return false;
}
if (!Vulkan::Context::init_loader(
reinterpret_cast<PFN_vkGetInstanceProcAddr>(SDL_Vulkan_GetVkGetInstanceProcAddr())))
{
LOGE("Failed to initialize Vulkan loader.\n");
return false;
}
wake_event_type = SDL_RegisterEvents(1);
application.name = name;
if (application.name.empty())
application.name = Path::basename(Path::get_executable_path());
window = SDL_CreateWindow(application.name.empty() ? "SDL Window" : application.name.c_str(),
int(width), int(height), SDL_WINDOW_RESIZABLE | SDL_WINDOW_VULKAN);
if (!window)
{
LOGE("Failed to create SDL window.\n");
return false;
}
if (options.fullscreen)
toggle_fullscreen();
application.info.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
application.info.pEngineName = "Granite";
application.info.pApplicationName = application.name.empty() ? "Granite" : application.name.c_str();
application.info.apiVersion = VK_API_VERSION_1_1;
return true;
}
const VkApplicationInfo *get_application_info() override
{
return &application.info;
}
void begin_drop_event() override
{
push_task_to_main_thread([]() {
SDL_SetEventEnabled(SDL_EVENT_DROP_FILE, true);
});
}
void show_message_box(const std::string &str, MessageType type) override
{
push_task_to_main_thread([this, str, type]() {
const char *title = nullptr;
Uint32 flags = 0;
switch (type)
{
case MessageType::Error:
flags = SDL_MESSAGEBOX_ERROR;
title = "Error";
break;
case MessageType::Warning:
flags = SDL_MESSAGEBOX_WARNING;
title = "Warning";
break;
case MessageType::Info:
flags = SDL_MESSAGEBOX_INFORMATION;
title = "Info";
break;
}
SDL_ShowSimpleMessageBox(flags, title, str.c_str(), window);
});
}
uintptr_t get_native_window() override
{
#ifdef _WIN32
SDL_PropertiesID props = SDL_GetWindowProperties(window);
SDL_LockProperties(props);
auto hwnd = static_cast<HWND>(SDL_GetPointerProperty(props, "SDL.window.win32.hwnd", nullptr));
SDL_UnlockProperties(props);
return reinterpret_cast<uintptr_t>(hwnd);
#else
return 0;
#endif
}
void toggle_fullscreen()
{
bool is_fullscreen = (SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN) != 0;
if (!is_fullscreen)
{
if (!SDL_SetWindowFullscreen(window, true))
{
LOGE("Failed to toggle fullscreen.\n");
}
#ifdef _WIN32
else
{
SDL_PropertiesID props = SDL_GetWindowProperties(window);
SDL_LockProperties(props);
auto hwnd = static_cast<HWND>(SDL_GetPointerProperty(props, "SDL.window.win32.hwnd", nullptr));
SDL_UnlockProperties(props);
push_task_to_async_thread([this, hwnd]() {
set_hmonitor(MonitorFromWindow(hwnd, MONITOR_DEFAULTTOPRIMARY));
});
}
#endif
}
else
{
#ifdef _WIN32
push_task_to_async_thread([this]() {
set_hmonitor(nullptr);
});
#endif
SDL_SetWindowFullscreen(window, false);
}
}
bool alive(Vulkan::WSI &) override
{
std::lock_guard<std::mutex> holder{get_input_tracker().get_lock()};
flush_deferred_input_events();
process_events_async_thread();
process_events_async_thread_non_pollable();
return !request_tear_down.load();
}
void poll_input() override
{
if (!options.threaded && !iterate_message_loop())
request_tear_down = true;
std::lock_guard<std::mutex> holder{get_input_tracker().get_lock()};
flush_deferred_input_events();
process_events_async_thread();
if (gamepad_init_async.load(std::memory_order_acquire))
pad.update(get_input_tracker());
get_input_tracker().dispatch_current_state(get_frame_timer().get_frame_time());
}
void poll_input_async(Granite::InputTrackerHandler *override_handler) override
{
std::lock_guard<std::mutex> holder{get_input_tracker().get_lock()};
begin_async_input_handling();
{
process_events_async_thread();
if (gamepad_init_async.load(std::memory_order_acquire))
pad.update(get_input_tracker());
}
end_async_input_handling();
get_input_tracker().dispatch_current_state(0.0, override_handler);
}
std::vector<const char *> get_instance_extensions() override
{
uint32_t count;
const char * const *ext = SDL_Vulkan_GetInstanceExtensions(&count);
return { ext, ext + count };
}
VkSurfaceKHR create_surface(VkInstance instance, VkPhysicalDevice) override
{
VkSurfaceKHR surface = VK_NULL_HANDLE;
if (!SDL_Vulkan_CreateSurface(window, instance, nullptr, &surface))
return VK_NULL_HANDLE;
int actual_width, actual_height;
SDL_GetWindowSizeInPixels(window, &actual_width, &actual_height);
width = unsigned(actual_width);
height = unsigned(actual_height);
return surface;
}
uint32_t get_surface_width() override
{
return width;
}
uint32_t get_surface_height() override
{
return height;
}
~WSIPlatformSDL()
{
if (gamepad_init_task)
gamepad_init_task->wait();
if (window)
SDL_DestroyWindow(window);
pad.close();
SDL_Quit();
}
void block_until_wsi_forward_progress(Vulkan::WSI &wsi) override
{
if (options.threaded)
{
get_frame_timer().enter_idle();
while (!resize && alive(wsi))
process_events_async_thread_blocking();
get_frame_timer().leave_idle();
}
else
{
WSIPlatform::block_until_wsi_forward_progress(wsi);
}
}
void notify_resize(unsigned width_, unsigned height_)
{
LOGI("Resize: %u x %u\n", width_, height_);
push_task_to_async_thread([=]() {
resize = true;
width = width_;
height = height_;
});
}
void notify_current_swapchain_dimensions(unsigned width_, unsigned height_) override
{
push_task_to_main_thread([=]() {
WSIPlatform::notify_current_swapchain_dimensions(width_, height_);
});
}
void set_window_title(const std::string &title) override
{
push_task_to_main_thread([=]() {
if (window)
SDL_SetWindowTitle(window, title.c_str());
});
}
bool process_sdl_event(const SDL_Event &e)
{
if (e.type == wake_event_type)
{
LOGI("Processing events main thread.\n");
process_events_main_thread();
return true;
}
const auto dispatcher = [this](std::function<void ()> func) {
push_task_to_async_thread(std::move(func));
};
if (pad.process_sdl_event(e, get_input_tracker(), dispatcher))
return true;
switch (e.type)
{
case SDL_EVENT_QUIT:
return false;
case SDL_EVENT_WINDOW_RESIZED:
if (e.window.windowID == SDL_GetWindowID(window))
notify_resize(e.window.data1, e.window.data2);
break;
case SDL_EVENT_MOUSE_BUTTON_DOWN:
case SDL_EVENT_MOUSE_BUTTON_UP:
if (e.button.windowID == SDL_GetWindowID(window))
{
MouseButton btn;
if (e.button.button == SDL_BUTTON_LEFT)
btn = MouseButton::Left;
else if (e.button.button == SDL_BUTTON_MIDDLE)
btn = MouseButton::Middle;
else if (e.button.button == SDL_BUTTON_RIGHT)
btn = MouseButton::Right;
else
break;
push_task_to_async_thread(
[this, btn, x = e.button.x, y = e.button.y,
pressed = e.type == SDL_EVENT_MOUSE_BUTTON_DOWN]() {
get_input_tracker().mouse_button_event(btn, x, y, pressed);
});
}
break;
case SDL_EVENT_WINDOW_MOUSE_ENTER:
if (e.window.windowID == SDL_GetWindowID(window))
{
float x, y;
SDL_GetMouseState(&x, &y);
push_task_to_async_thread([this, x, y]() {
get_input_tracker().mouse_enter(x, y);
});
}
break;
case SDL_EVENT_WINDOW_MOUSE_LEAVE:
if (e.window.windowID == SDL_GetWindowID(window))
{
push_task_to_async_thread([this]() {
get_input_tracker().mouse_leave();
});
}
break;
case SDL_EVENT_MOUSE_MOTION:
if (e.motion.windowID == SDL_GetWindowID(window))
{
push_task_to_async_thread([this, x = e.motion.x, y = e.motion.y]() {
get_input_tracker().mouse_move_event_absolute(x, y);
});
}
break;
case SDL_EVENT_KEY_DOWN:
case SDL_EVENT_KEY_UP:
if (e.key.windowID == SDL_GetWindowID(window))
{
KeyState state;
if (e.key.repeat)
state = KeyState::Repeat;
else if (e.type == SDL_EVENT_KEY_DOWN)
state = KeyState::Pressed;
else
state = KeyState::Released;
if (state == KeyState::Pressed && e.key.key == SDLK_ESCAPE)
{
return false;
}
else if (state == KeyState::Pressed && e.key.key == SDLK_RETURN &&
(e.key.mod & SDL_KMOD_ALT) != 0)
{
toggle_fullscreen();
}
else if (state == KeyState::Pressed && tolower(e.key.key) == 'v' &&
(e.key.mod & SDL_KMOD_LCTRL) != 0)
{
push_non_pollable_task_to_async_thread([c = clipboard]() mutable {
if (auto *manager = GRANITE_EVENT_MANAGER())
manager->enqueue<Vulkan::ApplicationWindowTextDropEvent>(std::move(c));
});
}
else
{
Key key = sdl_key_to_granite_key(e.key.key);
push_task_to_async_thread([=]() {
get_input_tracker().key_event(key, state);
});
}
}
break;
case SDL_EVENT_DROP_FILE:
if (e.drop.windowID == SDL_GetWindowID(window))
{
std::string str = e.drop.data;
push_non_pollable_task_to_async_thread([s = std::move(str)]() mutable {
if (auto *manager = GRANITE_EVENT_MANAGER())
manager->enqueue<Vulkan::ApplicationWindowFileDropEvent>(std::move(s));
});
}
break;
case SDL_EVENT_DROP_COMPLETE:
SDL_SetEventEnabled(SDL_EVENT_DROP_FILE, false);
break;
case SDL_EVENT_CLIPBOARD_UPDATE:
if (SDL_HasClipboardText())
{
const char *text = SDL_GetClipboardText();
if (text)
clipboard = text;
else
clipboard.clear();
}
else
clipboard.clear();
break;
default:
break;
}
return true;
}
void run_message_loop()
{
SDL_Event e;
while (async_loop_alive && SDL_WaitEvent(&e))
if (!process_sdl_event(e))
break;
}
bool iterate_message_loop()
{
SDL_Event e;
while (SDL_PollEvent(&e))
if (!process_sdl_event(e))
return false;
return true;
}
void run_loop(Application *app)
{
auto ctx = Global::create_thread_context();
process_events_main_thread();
if (options.threaded)
{
async_loop_alive = true;
threaded_main_loop = std::thread(&WSIPlatformSDL::thread_main, this, app, std::move(ctx));
run_message_loop();
notify_close();
if (threaded_main_loop.joinable())
threaded_main_loop.join();
}
else
thread_main(app, {});
}
static void dispatch_running_events()
{
auto *em = GRANITE_EVENT_MANAGER();
if (em)
{
em->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
em->enqueue_latched<ApplicationLifecycleEvent>(ApplicationLifecycle::Stopped);
em->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
em->enqueue_latched<ApplicationLifecycleEvent>(ApplicationLifecycle::Paused);
em->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
em->enqueue_latched<ApplicationLifecycleEvent>(ApplicationLifecycle::Running);
}
}
static void dispatch_stopped_events()
{
auto *em = GRANITE_EVENT_MANAGER();
if (em)
{
em->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
em->enqueue_latched<ApplicationLifecycleEvent>(ApplicationLifecycle::Paused);
em->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
em->enqueue_latched<ApplicationLifecycleEvent>(ApplicationLifecycle::Stopped);
}
}
void thread_main(Application *app, Global::GlobalManagersHandle ctx)
{
if (options.threaded)
{
// Set this up as an alternative main thread.
ThreadGroup::set_async_main_thread();
Global::set_thread_context(*ctx);
Util::register_thread_index(0);
ctx.reset();
}
{
GRANITE_SCOPED_TIMELINE_EVENT("sdl-dispatch-running-events");
dispatch_running_events();
}
{
{
GRANITE_SCOPED_TIMELINE_EVENT("sdl-start-audio-system");
Granite::Global::start_audio_system();
}
while (app->poll())
app->run_frame();
Granite::Global::stop_audio_system();
}
dispatch_stopped_events();
push_task_to_main_thread([this]() { async_loop_alive = false; });
}
void notify_close()
{
request_tear_down.store(true);
}
#ifdef _WIN32
void set_hmonitor(HMONITOR monitor)
{
current_hmonitor = monitor;
}
uintptr_t get_fullscreen_monitor() override
{
return reinterpret_cast<uintptr_t>(current_hmonitor);
}
#endif
template <typename Op>
void push_task_to_async_thread(Op &&op)
{
push_task_to_list(task_list_async, std::forward<Op>(op));
}
template <typename Op>
void push_non_pollable_task_to_async_thread(Op &&op)
{
push_non_pollable_task_to_list(task_list_async, std::forward<Op>(op));
}
private:
SDL_Window *window = nullptr;
unsigned width = 0;
unsigned height = 0;
uint32_t wake_event_type = 0;
Options options;
std::string clipboard;
TaskGroupHandle gamepad_init_task;
std::atomic<bool> gamepad_init_async;
struct
{
VkApplicationInfo info = {};
std::string name;
} application;
std::thread threaded_main_loop;
struct TaskList
{
std::mutex lock;
std::condition_variable cond;
std::vector<std::function<void ()>> list;
std::vector<std::function<void ()>> non_pollable_list;
} task_list_main, task_list_async;
static void process_events_for_list(TaskList &list, bool blocking)
{
std::unique_lock<std::mutex> holder{list.lock};
if (blocking)
while (list.list.empty())
list.cond.wait(holder, [&list]() { return !list.list.empty(); });
for (auto &task : list.list)
task();
list.list.clear();
}
template <typename Op>
void push_task_to_list(TaskList &list, Op &&op)
{
std::lock_guard<std::mutex> holder{list.lock};
list.list.emplace_back(std::forward<Op>(op));
list.cond.notify_one();
}
template <typename Op>
void push_non_pollable_task_to_list(TaskList &list, Op &&op)
{
std::lock_guard<std::mutex> holder{list.lock};
list.non_pollable_list.emplace_back(std::forward<Op>(op));
list.cond.notify_one();
}
void process_events_main_thread()
{
process_events_for_list(task_list_main, false);
}
void process_events_main_thread_blocking()
{
process_events_for_list(task_list_main, true);
}
void process_events_async_thread()
{
process_events_for_list(task_list_async, false);
}
void process_events_async_thread_non_pollable()
{
std::unique_lock<std::mutex> holder{task_list_async.lock};
for (auto &task : task_list_async.non_pollable_list)
task();
task_list_async.non_pollable_list.clear();
}
void process_events_async_thread_blocking()
{
process_events_for_list(task_list_async, true);
}
InputTrackerSDL pad;
template <typename Op>
void push_task_to_main_thread(Op &&op)
{
push_task_to_list(task_list_main, std::forward<Op>(op));
SDL_Event wake_event = {};
wake_event.type = wake_event_type;
SDL_PushEvent(&wake_event);
}
std::atomic_bool request_tear_down;
bool async_loop_alive = false;
#ifdef _WIN32
HMONITOR current_hmonitor = nullptr;
#endif
};
}
namespace Granite
{
int application_main(
bool (*query_application_interface)(ApplicationQuery, void *, size_t),
Application *(*create_application)(int, char **), int argc, char *argv[])
{
ApplicationQueryDefaultManagerFlags flags{Global::MANAGER_FEATURE_DEFAULT_BITS};
query_application_interface(ApplicationQuery::DefaultManagerFlags, &flags, sizeof(flags));
Global::init(flags.manager_feature_flags);
WSIPlatformSDL::Options options;
int exit_code;
Util::CLICallbacks cbs;
cbs.add("--fullscreen", [&](Util::CLIParser &) { options.fullscreen = true; });
cbs.add("--width", [&](Util::CLIParser &parser) { options.override_width = parser.next_uint(); });
cbs.add("--height", [&](Util::CLIParser &parser) { options.override_height = parser.next_uint(); });
cbs.add("--thread-main-loop", [&](Util::CLIParser &) { options.threaded = true; });
cbs.add("--no-thread-main-loop", [&](Util::CLIParser &) { options.threaded = false; });
cbs.error_handler = [&]() { LOGE("Failed to parse CLI arguments for SDL.\n"); };
if (!Util::parse_cli_filtered(std::move(cbs), argc, argv, exit_code))
return exit_code;
auto app = std::unique_ptr<Application>(create_application(argc, argv));
int ret;
if (app)
{
auto platform = std::make_unique<WSIPlatformSDL>(options);
auto *platform_handle = platform.get();
if (!platform->init(app->get_name(), app->get_default_width(), app->get_default_height()))
return 1;
if (!app->init_platform(std::move(platform)) || !app->init_wsi())
return 1;
platform_handle->run_loop(app.get());
app.reset();
ret = EXIT_SUCCESS;
}
else
{
ret = EXIT_FAILURE;
}
Global::deinit();
return ret;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,494 @@
/* Copyright (C) 2010-2020 The RetroArch team
*
* ---------------------------------------------------------------------------------------------
* The following license statement only applies to this libretro API header (libretro_vulkan.h)
* ---------------------------------------------------------------------------------------------
*
* Permission is hereby granted, free of charge,
* to any person obtaining a copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#ifndef LIBRETRO_VULKAN_H__
#define LIBRETRO_VULKAN_H__
#include <libretro.h>
#include <vulkan/vulkan.h>
#define RETRO_HW_RENDER_INTERFACE_VULKAN_VERSION 5
#define RETRO_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE_VULKAN_VERSION 2
struct retro_vulkan_image
{
VkImageView image_view;
VkImageLayout image_layout;
VkImageViewCreateInfo create_info;
};
typedef void (*retro_vulkan_set_image_t)(void *handle,
const struct retro_vulkan_image *image,
uint32_t num_semaphores,
const VkSemaphore *semaphores,
uint32_t src_queue_family);
typedef uint32_t (*retro_vulkan_get_sync_index_t)(void *handle);
typedef uint32_t (*retro_vulkan_get_sync_index_mask_t)(void *handle);
typedef void (*retro_vulkan_set_command_buffers_t)(void *handle,
uint32_t num_cmd,
const VkCommandBuffer *cmd);
typedef void (*retro_vulkan_wait_sync_index_t)(void *handle);
typedef void (*retro_vulkan_lock_queue_t)(void *handle);
typedef void (*retro_vulkan_unlock_queue_t)(void *handle);
typedef void (*retro_vulkan_set_signal_semaphore_t)(void *handle, VkSemaphore semaphore);
typedef const VkApplicationInfo *(*retro_vulkan_get_application_info_t)(void);
struct retro_vulkan_context
{
VkPhysicalDevice gpu;
VkDevice device;
VkQueue queue;
uint32_t queue_family_index;
VkQueue presentation_queue;
uint32_t presentation_queue_family_index;
};
/* This is only used in v1 of the negotiation interface.
* It is deprecated since it cannot express PDF2 features or optional extensions. */
typedef bool (*retro_vulkan_create_device_t)(
struct retro_vulkan_context *context,
VkInstance instance,
VkPhysicalDevice gpu,
VkSurfaceKHR surface,
PFN_vkGetInstanceProcAddr get_instance_proc_addr,
const char **required_device_extensions,
unsigned num_required_device_extensions,
const char **required_device_layers,
unsigned num_required_device_layers,
const VkPhysicalDeviceFeatures *required_features);
typedef void (*retro_vulkan_destroy_device_t)(void);
/* v2 CONTEXT_NEGOTIATION_INTERFACE only. */
typedef VkInstance (*retro_vulkan_create_instance_wrapper_t)(
void *opaque, const VkInstanceCreateInfo *create_info);
/* v2 CONTEXT_NEGOTIATION_INTERFACE only. */
typedef VkInstance (*retro_vulkan_create_instance_t)(
PFN_vkGetInstanceProcAddr get_instance_proc_addr,
const VkApplicationInfo *app,
retro_vulkan_create_instance_wrapper_t create_instance_wrapper,
void *opaque);
/* v2 CONTEXT_NEGOTIATION_INTERFACE only. */
typedef VkDevice (*retro_vulkan_create_device_wrapper_t)(
VkPhysicalDevice gpu, void *opaque,
const VkDeviceCreateInfo *create_info);
/* v2 CONTEXT_NEGOTIATION_INTERFACE only. */
typedef bool (*retro_vulkan_create_device2_t)(
struct retro_vulkan_context *context,
VkInstance instance,
VkPhysicalDevice gpu,
VkSurfaceKHR surface,
PFN_vkGetInstanceProcAddr get_instance_proc_addr,
retro_vulkan_create_device_wrapper_t create_device_wrapper,
void *opaque);
/* Note on thread safety:
* The Vulkan API is heavily designed around multi-threading, and
* the libretro interface for it should also be threading friendly.
* A core should be able to build command buffers and submit
* command buffers to the GPU from any thread.
*/
struct retro_hw_render_context_negotiation_interface_vulkan
{
/* Must be set to RETRO_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE_VULKAN. */
enum retro_hw_render_context_negotiation_interface_type interface_type;
/* Usually set to RETRO_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE_VULKAN_VERSION,
* but can be lower depending on GET_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE_SUPPORT. */
unsigned interface_version;
/* If non-NULL, returns a VkApplicationInfo struct that the frontend can use instead of
* its "default" application info.
* VkApplicationInfo::apiVersion also controls the target core Vulkan version for instance level functionality.
* Lifetime of the returned pointer must remain until the retro_vulkan_context is initialized.
*
* NOTE: For optimal compatibility with e.g. Android which is very slow to update its loader,
* a core version of 1.1 should be requested. Features beyond that can be requested with extensions.
* Vulkan 1.0 is only appropriate for legacy cores, but is still supported.
* A frontend is free to bump the instance creation apiVersion as necessary if the frontend requires more advanced core features.
*
* v2: This function must not be NULL, and must not return NULL.
* v1: It was not clearly defined if this function could return NULL.
* Frontends should be defensive and provide a default VkApplicationInfo
* if this function returns NULL or if this function is NULL.
*/
retro_vulkan_get_application_info_t get_application_info;
/* If non-NULL, the libretro core will choose one or more physical devices,
* create one or more logical devices and create one or more queues.
* The core must prepare a designated PhysicalDevice, Device, Queue and queue family index
* which the frontend will use for its internal operation.
*
* If gpu is not VK_NULL_HANDLE, the physical device provided to the frontend must be this PhysicalDevice if the call succeeds.
* The core is still free to use other physical devices for other purposes that are private to the core.
*
* The frontend will request certain extensions and layers for a device which is created.
* The core must ensure that the queue and queue_family_index support GRAPHICS and COMPUTE.
*
* If surface is not VK_NULL_HANDLE, the core must consider presentation when creating the queues.
* If presentation to "surface" is supported on the queue, presentation_queue must be equal to queue.
* If not, a second queue must be provided in presentation_queue and presentation_queue_index.
* If surface is not VK_NULL_HANDLE, the instance from frontend will have been created with supported for
* VK_KHR_surface extension.
*
* The core is free to set its own queue priorities.
* Device provided to frontend is owned by the frontend, but any additional device resources must be freed by core
* in destroy_device callback.
*
* If this function returns true, a PhysicalDevice, Device and Queues are initialized.
* If false, none of the above have been initialized and the frontend will attempt
* to fallback to "default" device creation, as if this function was never called.
*/
retro_vulkan_create_device_t create_device;
/* If non-NULL, this callback is called similar to context_destroy for HW_RENDER_INTERFACE.
* However, it will be called even if context_reset was not called.
* This can happen if the context never succeeds in being created.
* destroy_device will always be called before the VkInstance
* of the frontend is destroyed if create_device was called successfully so that the core has a chance of
* tearing down its own device resources.
*
* Only auxillary resources should be freed here, i.e. resources which are not part of retro_vulkan_context.
* v2: Auxillary instance resources created during create_instance can also be freed here.
*/
retro_vulkan_destroy_device_t destroy_device;
/* v2 API: If interface_version is < 2, fields below must be ignored.
* If the frontend does not support interface version 2, the v1 entry points will be used instead. */
/* If non-NULL, this is called to create an instance, otherwise a VkInstance is created by the frontend.
* v1 interface bug: The only way to enable instance features is through core versions signalled in VkApplicationInfo.
* The frontend may request that certain extensions and layers
* are enabled on the VkInstance. Application may add additional features.
* If app is non-NULL, apiVersion controls the minimum core version required by the application.
* Return a VkInstance or VK_NULL_HANDLE. The VkInstance is owned by the frontend.
*
* Rather than call vkCreateInstance directly, a core must call the CreateInstance wrapper provided with:
* VkInstance instance = create_instance_wrapper(opaque, &create_info);
* If the core wishes to create a private instance for whatever reason (relying on shared memory for example),
* it may call vkCreateInstance directly. */
retro_vulkan_create_instance_t create_instance;
/* If non-NULL and frontend recognizes negotiation interface >= 2, create_device2 takes precedence over create_device.
* Similar to create_device, but is extended to better understand new core versions and PDF2 feature enablement.
* Requirements for create_device2 are the same as create_device unless a difference is mentioned.
*
* v2 consideration:
* If the chosen gpu by frontend cannot be supported, a core must return false.
*
* NOTE: "Cannot be supported" is intentionally vaguely defined.
* Refusing to run on an iGPU for a very intensive core with desktop GPU as a minimum spec may be in the gray area.
* Not supporting optional features is not a good reason to reject a physical device, however.
*
* On device creation feature with explicit gpu, a frontend should fall back create_device2 with gpu == VK_NULL_HANDLE and let core
* decide on a supported device if possible.
*
* A core must assume that the explicitly provided GPU is the only guaranteed attempt it has to create a device.
* A fallback may not be attempted if there are particular reasons why only a specific physical device can work,
* but these situations should be esoteric and rare in nature, e.g. a libretro frontend is implemented with external memory
* and only LUID matching would work.
* Cores and frontends should ensure "best effort" when negotiating like this and appropriate logging is encouraged.
*
* v1 note: In the v1 version of create_device, it was never expected that create_device would fail like this,
* and frontends are not expected to attempt fall backs.
*
* Rather than call vkCreateDevice directly, a core must call the CreateDevice wrapper provided with:
* VkDevice device = create_device_wrapper(gpu, opaque, &create_info);
* If the core wishes to create a private device for whatever reason (relying on shared memory for example),
* it may call vkCreateDevice directly.
*
* This allows the frontend to add additional extensions that it requires as well as adjust the PDF2 pNext as required.
* It is also possible adjust the queue create infos in case the frontend desires to allocate some private queues.
*
* The get_instance_proc_addr provided in create_device2 must be the same as create_instance.
*
* NOTE: The frontend must not disable features requested by application.
* NOTE: The frontend must not add any robustness features as some API behavior may change (VK_EXT_descriptor_buffer comes to mind).
* I.e. robustBufferAccess and the like. (nullDescriptor from robustness2 is allowed to be enabled).
*/
retro_vulkan_create_device2_t create_device2;
};
struct retro_hw_render_interface_vulkan
{
/* Must be set to RETRO_HW_RENDER_INTERFACE_VULKAN. */
enum retro_hw_render_interface_type interface_type;
/* Must be set to RETRO_HW_RENDER_INTERFACE_VULKAN_VERSION. */
unsigned interface_version;
/* Opaque handle to the Vulkan backend in the frontend
* which must be passed along to all function pointers
* in this interface.
*
* The rationale for including a handle here (which libretro v1
* doesn't currently do in general) is:
*
* - Vulkan cores should be able to be freely threaded without lots of fuzz.
* This would break frontends which currently rely on TLS
* to deal with multiple cores loaded at the same time.
* - Fixing this in general is TODO for an eventual libretro v2.
*/
void *handle;
/* The Vulkan instance the context is using. */
VkInstance instance;
/* The physical device used. */
VkPhysicalDevice gpu;
/* The logical device used. */
VkDevice device;
/* Allows a core to fetch all its needed symbols without having to link
* against the loader itself. */
PFN_vkGetDeviceProcAddr get_device_proc_addr;
PFN_vkGetInstanceProcAddr get_instance_proc_addr;
/* The queue the core must use to submit data.
* This queue and index must remain constant throughout the lifetime
* of the context.
*
* This queue will be the queue that supports graphics and compute
* if the device supports compute.
*/
VkQueue queue;
unsigned queue_index;
/* Before calling retro_video_refresh_t with RETRO_HW_FRAME_BUFFER_VALID,
* set which image to use for this frame.
*
* If num_semaphores is non-zero, the frontend will wait for the
* semaphores provided to be signaled before using the results further
* in the pipeline.
*
* Semaphores provided by a single call to set_image will only be
* waited for once (waiting for a semaphore resets it).
* E.g. set_image, video_refresh, and then another
* video_refresh without set_image,
* but same image will only wait for semaphores once.
*
* For this reason, ownership transfer will only occur if semaphores
* are waited on for a particular frame in the frontend.
*
* Using semaphores is optional for synchronization purposes,
* but if not using
* semaphores, an image memory barrier in vkCmdPipelineBarrier
* should be used in the graphics_queue.
* Example:
*
* vkCmdPipelineBarrier(cmd,
* srcStageMask = VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT,
* dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
* image_memory_barrier = {
* srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
* dstAccessMask = VK_ACCESS_SHADER_READ_BIT,
* });
*
* The use of pipeline barriers instead of semaphores is encouraged
* as it is simpler and more fine-grained. A layout transition
* must generally happen anyways which requires a
* pipeline barrier.
*
* The image passed to set_image must have imageUsage flags set to at least
* VK_IMAGE_USAGE_TRANSFER_SRC_BIT and VK_IMAGE_USAGE_SAMPLED_BIT.
* The core will naturally want to use flags such as
* VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT and/or
* VK_IMAGE_USAGE_TRANSFER_DST_BIT depending
* on how the final image is created.
*
* The image must also have been created with MUTABLE_FORMAT bit set if
* 8-bit formats are used, so that the frontend can reinterpret sRGB
* formats as it sees fit.
*
* Images passed to set_image should be created with TILING_OPTIMAL.
* The image layout should be transitioned to either
* VK_IMAGE_LAYOUT_GENERIC or VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL.
* The actual image layout used must be set in image_layout.
*
* The image must be a 2D texture which may or not be layered
* and/or mipmapped.
*
* The image must be suitable for linear sampling.
* While the image_view is typically the only field used,
* the frontend may want to reinterpret the texture as sRGB vs.
* non-sRGB for example so the VkImageViewCreateInfo used to
* create the image view must also be passed in.
*
* The data in the pointer to the image struct will not be copied
* as the pNext field in create_info cannot be reliably deep-copied.
* The image pointer passed to set_image must be valid until
* retro_video_refresh_t has returned.
*
* If frame duping is used when passing NULL to retro_video_refresh_t,
* the frontend is free to either use the latest image passed to
* set_image or reuse the older pointer passed to set_image the
* frame RETRO_HW_FRAME_BUFFER_VALID was last used.
*
* Essentially, the lifetime of the pointer passed to
* retro_video_refresh_t should be extended if frame duping is used
* so that the frontend can reuse the older pointer.
*
* The image itself however, must not be touched by the core until
* wait_sync_index has been completed later. The frontend may perform
* layout transitions on the image, so even read-only access is not defined.
* The exception to read-only rule is if GENERAL layout is used for the image.
* In this case, the frontend is not allowed to perform any layout transitions,
* so concurrent reads from core and frontend are allowed.
*
* If frame duping is used, or if set_command_buffers is used,
* the frontend will not wait for any semaphores.
*
* The src_queue_family is used to specify which queue family
* the image is currently owned by. If using multiple queue families
* (e.g. async compute), the frontend will need to acquire ownership of the
* image before rendering with it and release the image afterwards.
*
* If src_queue_family is equal to the queue family (queue_index),
* no ownership transfer will occur.
* Similarly, if src_queue_family is VK_QUEUE_FAMILY_IGNORED,
* no ownership transfer will occur.
*
* The frontend will always release ownership back to src_queue_family.
* Waiting for frontend to complete with wait_sync_index() ensures that
* the frontend has released ownership back to the application.
* Note that in Vulkan, transfering ownership is a two-part process.
*
* Example frame:
* - core releases ownership from src_queue_index to queue_index with VkImageMemoryBarrier.
* - core calls set_image with src_queue_index.
* - Frontend will acquire the image with src_queue_index -> queue_index as well, completing the ownership transfer.
* - Frontend renders the frame.
* - Frontend releases ownership with queue_index -> src_queue_index.
* - Next time image is used, core must acquire ownership from queue_index ...
*
* Since the frontend releases ownership, we cannot necessarily dupe the frame because
* the core needs to make the roundtrip of ownership transfer.
*/
retro_vulkan_set_image_t set_image;
/* Get the current sync index for this frame which is obtained in
* frontend by calling e.g. vkAcquireNextImageKHR before calling
* retro_run().
*
* This index will correspond to which swapchain buffer is currently
* the active one.
*
* Knowing this index is very useful for maintaining safe asynchronous CPU
* and GPU operation without stalling.
*
* The common pattern for synchronization is to receive fences when
* submitting command buffers to Vulkan (vkQueueSubmit) and add this fence
* to a list of fences for frame number get_sync_index().
*
* Next time we receive the same get_sync_index(), we can wait for the
* fences from before, which will usually return immediately as the
* frontend will generally also avoid letting the GPU run ahead too much.
*
* After the fence has signaled, we know that the GPU has completed all
* GPU work related to work submitted in the frame we last saw get_sync_index().
*
* This means we can safely reuse or free resources allocated in this frame.
*
* In theory, even if we wait for the fences correctly, it is not technically
* safe to write to the image we earlier passed to the frontend since we're
* not waiting for the frontend GPU jobs to complete.
*
* The frontend will guarantee that the appropriate pipeline barrier
* in graphics_queue has been used such that
* VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT cannot
* start until the frontend is done with the image.
*/
retro_vulkan_get_sync_index_t get_sync_index;
/* Returns a bitmask of how many swapchain images we currently have
* in the frontend.
*
* If bit #N is set in the return value, get_sync_index can return N.
* Knowing this value is useful for preallocating per-frame management
* structures ahead of time.
*
* While this value will typically remain constant throughout the
* applications lifecycle, it may for example change if the frontend
* suddently changes fullscreen state and/or latency.
*
* If this value ever changes, it is safe to assume that the device
* is completely idle and all synchronization objects can be deleted
* right away as desired.
*/
retro_vulkan_get_sync_index_mask_t get_sync_index_mask;
/* Instead of submitting the command buffer to the queue first, the core
* can pass along its command buffer to the frontend, and the frontend
* will submit the command buffer together with the frontends command buffers.
*
* This has the advantage that the overhead of vkQueueSubmit can be
* amortized into a single call. For this mode, semaphores in set_image
* will be ignored, so vkCmdPipelineBarrier must be used to synchronize
* the core and frontend.
*
* The command buffers in set_command_buffers are only executed once,
* even if frame duping is used.
*
* If frame duping is used, set_image should be used for the frames
* which should be duped instead.
*
* Command buffers passed to the frontend with set_command_buffers
* must not actually be submitted to the GPU until retro_video_refresh_t
* is called.
*
* The frontend must submit the command buffer before submitting any
* other command buffers provided by set_command_buffers. */
retro_vulkan_set_command_buffers_t set_command_buffers;
/* Waits on CPU for device activity for the current sync index to complete.
* This is useful since the core will not have a relevant fence to sync with
* when the frontend is submitting the command buffers. */
retro_vulkan_wait_sync_index_t wait_sync_index;
/* If the core submits command buffers itself to any of the queues provided
* in this interface, the core must lock and unlock the frontend from
* racing on the VkQueue.
*
* Queue submission can happen on any thread.
* Even if queue submission happens on the same thread as retro_run(),
* the lock/unlock functions must still be called.
*
* NOTE: Queue submissions are heavy-weight. */
retro_vulkan_lock_queue_t lock_queue;
retro_vulkan_unlock_queue_t unlock_queue;
/* Sets a semaphore which is signaled when the image in set_image can safely be reused.
* The semaphore is consumed next call to retro_video_refresh_t.
* The semaphore will be signalled even for duped frames.
* The semaphore will be signalled only once, so set_signal_semaphore should be called every frame.
* The semaphore may be VK_NULL_HANDLE, which disables semaphore signalling for next call to retro_video_refresh_t.
*
* This is mostly useful to support use cases where you're rendering to a single image that
* is recycled in a ping-pong fashion with the frontend to save memory (but potentially less throughput).
*/
retro_vulkan_set_signal_semaphore_t set_signal_semaphore;
};
#endif
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,180 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include "application.hpp"
#include "render_context.hpp"
#include "scene_loader.hpp"
#include "animation_system.hpp"
#include "renderer.hpp"
#include "timer.hpp"
#include "event.hpp"
#include "font.hpp"
#include "ui_manager.hpp"
#include "render_graph.hpp"
#include "mesh_util.hpp"
#include "scene_renderer.hpp"
#include "lights/clusterer.hpp"
#include "lights/volumetric_fog.hpp"
#include "lights/volumetric_diffuse.hpp"
#include "camera_export.hpp"
#include "post/aa.hpp"
#include "post/temporal.hpp"
namespace Granite
{
class TaskComposer;
class SceneViewerApplication : public Application, public EventHandler
{
public:
struct CLIConfig
{
bool timestamp = false;
bool ocean = false;
int camera_index = -1;
};
SceneViewerApplication(const std::string &path,
const std::string &config_path, const std::string &quirks_path,
const CLIConfig &cli_config);
~SceneViewerApplication();
void render_frame(double frame_time, double elapsed_time) override;
void rescale_scene(float radius);
void loop_animations();
protected:
void update_scene(TaskComposer &composer, double frame_time, double elapsed_time);
void render_scene(TaskComposer &composer);
void post_frame() override;
RenderContext context;
RenderContext depth_contexts[NumShadowCascades];
RenderContext fallback_depth_context;
RendererSuite renderer_suite;
RendererSuite::Config renderer_suite_config;
FlatRenderer flat_renderer;
LightingParameters lighting;
LightingParameters fallback_lighting;
FPSCamera cam;
SceneLoader scene_loader;
std::unique_ptr<AnimationSystem> animation_system;
Camera *selected_camera = nullptr;
DirectionalLightComponent *selected_directional = nullptr;
DirectionalLightComponent default_directional_light;
SceneTransformManager scene_transform_manager;
void on_device_created(const Vulkan::DeviceCreatedEvent &e);
void on_device_destroyed(const Vulkan::DeviceCreatedEvent &e);
void on_swapchain_changed(const Vulkan::SwapchainParameterEvent &e);
void on_swapchain_destroyed(const Vulkan::SwapchainParameterEvent &e);
bool on_key_down(const KeyboardEvent &e);
RenderGraph graph;
bool need_shadow_map_update = true;
AABB shadow_scene_aabb;
std::unique_ptr<LightClusterer> cluster;
std::unique_ptr<VolumetricFog> volumetric_fog;
std::unique_ptr<VolumetricDiffuseLightManager> volumetric_diffuse;
RenderQueue queue;
void setup_shadow_map();
void update_shadow_scene_aabb();
void render_ui(Vulkan::CommandBuffer &cmd);
void add_main_pass(Vulkan::Device &device, const std::string &tag);
void add_main_pass_forward(Vulkan::Device &device, const std::string &tag);
void add_main_pass_deferred(Vulkan::Device &device, const std::string &tag);
void add_mv_pass(const std::string &tag, const std::string &depth, bool full_mv);
void add_shadow_pass(Vulkan::Device &device, const std::string &tag);
void add_shadow_pass_fallback(Vulkan::Device &device, const std::string &tag);
std::vector<RecordedCamera> recorded_cameras;
std::string get_name() override;
void bake_render_graph(const Vulkan::SwapchainParameterEvent &swap);
const Vulkan::SwapchainParameterEvent *pending_swapchain = nullptr;
private:
void read_config(const std::string &path);
void read_quirks(const std::string &path);
void read_lights();
struct Config
{
RendererType renderer_type = RendererType::GeneralDeferred;
unsigned msaa = 1;
float shadow_map_resolution = 2048.0f;
unsigned clustered_lights_shadow_resolution = 512;
SceneRendererFlags pcf_flags = SCENE_RENDERER_SHADOW_PCF_WIDE_BIT;
float resolution_scale = 1.0f;
bool resolution_scale_sharpen = true;
float lod_bias = 0.0f;
bool directional_light_shadows = true;
bool directional_light_cascaded_shadows = true;
bool directional_light_shadows_vsm = false;
bool clustered_lights_shadows = true;
bool clustered_lights_shadows_vsm = false;
bool hdr_bloom = true;
bool hdr_bloom_dynamic_exposure = true;
bool forward_depth_prepass = true;
bool rt_fp16 = false;
bool rescale_scene = false;
bool show_ui = true;
bool volumetric_fog = false;
bool volumetric_fog_regions = true;
bool volumetric_diffuse = false;
bool ssao = true;
bool debug_probes = false;
bool ssr = false;
PostAAType postaa_type = PostAAType::None;
};
Config config;
CLIConfig cli_config;
void export_lights();
void export_cameras();
enum { FrameWindowSize = 64, FrameWindowSizeMask = FrameWindowSize - 1 };
float last_frame_times[FrameWindowSize] = {};
unsigned last_frame_index = 0;
TemporalJitter jitter;
void capture_environment_probe();
RenderTextureResource *ssao_output = nullptr;
RenderTextureResource *shadows = nullptr;
RenderTextureResource *fallback_shadows = nullptr;
RenderTextureResource *hiz_main = nullptr;
RenderTextureResource *hiz_depth = nullptr;
Util::SmallVector<Vulkan::ImageViewHandle> hiz_depth_peel;
CullingPassesInfo culling_passes_info = {};
};
}
@@ -0,0 +1,18 @@
add_granite_internal_lib(granite-compiler compiler.cpp compiler.hpp)
target_include_directories(granite-compiler PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(granite-compiler
PUBLIC granite-application-global
PRIVATE SPIRV-Tools shaderc granite-path granite-util)
if (GRANITE_SHADER_COMPILER_OPTIMIZE)
target_compile_definitions(granite-compiler PRIVATE GRANITE_COMPILER_OPTIMIZE=1)
else()
target_compile_definitions(granite-compiler PRIVATE GRANITE_COMPILER_OPTIMIZE=0)
endif()
if (${CMAKE_BUILD_TYPE} MATCHES "Debug")
target_compile_definitions(granite-compiler PRIVATE GRANITE_COMPILER_DEBUG=1)
else()
target_compile_definitions(granite-compiler PRIVATE GRANITE_COMPILER_DEBUG=0)
endif()
@@ -0,0 +1,381 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "compiler.hpp"
#include "shaderc/shaderc.hpp"
#include "path_utils.hpp"
#include "logging.hpp"
#include "string_helpers.hpp"
#include "spirv-tools/libspirv.hpp"
namespace Granite
{
GLSLCompiler::GLSLCompiler(FilesystemInterface &iface_)
: iface(iface_)
{
}
static Stage stage_from_path(const std::string &path)
{
auto ext = Path::ext(path);
if (ext == "vert")
return Stage::Vertex;
else if (ext == "frag")
return Stage::Fragment;
else if (ext == "comp")
return Stage::Compute;
else if (ext == "task")
return Stage::Task;
else if (ext == "mesh")
return Stage::Mesh;
else
return Stage::Unknown;
}
static Stage convert_stage(const std::string &stage)
{
if (stage == "vertex")
return Stage::Vertex;
else if (stage == "compute")
return Stage::Compute;
else if (stage == "fragment")
return Stage::Fragment;
else if (stage == "task")
return Stage::Task;
else if (stage == "mesh")
return Stage::Mesh;
else
return Stage::Unknown;
}
bool GLSLCompiler::set_source_from_file(const std::string &path, Stage forced_stage)
{
if (!iface.load_text_file(path, source))
{
LOGE("Failed to load shader: %s\n", path.c_str());
return false;
}
source_path = path;
if (forced_stage != Stage::Unknown)
stage = forced_stage;
else
stage = stage_from_path(path);
return stage != Stage::Unknown;
}
bool GLSLCompiler::set_source_from_file_multistage(const std::string &path)
{
if (iface.load_text_file(path, source))
{
LOGE("Failed to load shader: %s\n", path.c_str());
return false;
}
source_path = path;
stage = Stage::Unknown;
return true;
}
void GLSLCompiler::set_include_directories(const std::vector<std::string> *include_directories_)
{
include_directories = include_directories_;
}
bool GLSLCompiler::find_include_path(const std::string &source_path_, const std::string &include_path,
std::string &included_path, std::string &included_source)
{
auto relpath = Path::relpath(source_path_, include_path);
if (iface.load_text_file(relpath, included_source))
{
included_path = relpath;
return true;
}
if (include_directories)
{
for (auto &include_dir : *include_directories)
{
auto path = Path::join(include_dir, include_path);
if (iface.load_text_file(path, included_source))
{
included_path = path;
return true;
}
}
}
return false;
}
bool GLSLCompiler::parse_variants(const std::string &source_, const std::string &path)
{
auto lines = Util::split(source_, "\n");
unsigned line_index = 1;
size_t offset;
for (auto &line : lines)
{
// This check, followed by the include statement check below isn't even remotely correct,
// but we only have to care about shaders we control here.
if ((offset = line.find("//")) != std::string::npos)
line = line.substr(0, offset);
if ((offset = line.find("#include \"")) != std::string::npos)
{
auto include_path = line.substr(offset + 10);
if (!include_path.empty() && include_path.back() == '"')
include_path.pop_back();
std::string included_source;
if (!find_include_path(path, include_path, include_path, included_source))
{
LOGE("Failed to include GLSL file: %s\n", include_path.c_str());
return false;
}
preprocessed_source += Util::join("#line ", 1, " \"", include_path, "\"\n");
if (!parse_variants(included_source, include_path))
return false;
preprocessed_source += Util::join("#line ", line_index + 1, " \"", path, "\"\n");
dependencies.insert(include_path);
}
else if (line.find("#pragma optimize off") == 0)
{
optimization = Optimization::ForceOff;
preprocessed_source += "// #pragma optimize off";
preprocessed_source += '\n';
}
else if (line.find("#pragma optimize on") == 0)
{
optimization = Optimization::ForceOn;
preprocessed_source += "// #pragma optimize on";
preprocessed_source += '\n';
}
else if (line.find("#pragma stage ") == 0)
{
if (!preprocessed_source.empty())
{
preprocessed_sections.push_back({ preprocessing_active_stage, std::move(preprocessed_source) });
preprocessed_source = {};
}
preprocessing_active_stage = convert_stage(line.substr(14));
preprocessed_source += Util::join("#line ", line_index + 1, " \"", path, "\"\n");
}
else if (line.find("#pragma ") == 0)
{
pragmas.push_back(line.substr(8));
preprocessed_source += "// ";
preprocessed_source += line;
preprocessed_source += '\n';
}
else
{
preprocessed_source += line;
preprocessed_source += '\n';
auto first_non_space = line.find_first_not_of(' ');
if (first_non_space != std::string::npos && line[first_non_space] == '#')
{
auto keywords = Util::split(line.substr(first_non_space + 1), " ");
if (keywords.size() == 1)
{
auto &word = keywords.front();
if (word == "endif")
preprocessed_source += Util::join("#line ", line_index + 1, " \"", path, "\"\n");
}
}
}
line_index++;
}
return true;
}
bool GLSLCompiler::preprocess()
{
// Use a custom preprocessor where we only resolve includes once.
// The builtin glslang preprocessor is not suitable for this task,
// since we need to defer resolving defines.
preprocessed_source.clear();
pragmas.clear();
preprocessed_sections.clear();
preprocessing_active_stage = Stage::Unknown;
bool ret = parse_variants(source, source_path);
if (ret && !preprocessed_source.empty())
{
preprocessed_sections.push_back({ preprocessing_active_stage, std::move(preprocessed_source) });
preprocessed_source = {};
}
return ret;
}
Util::Hash GLSLCompiler::get_source_hash() const
{
Util::Hasher h;
for (auto &section : preprocessed_sections)
{
h.u32(uint32_t(section.stage));
h.string(section.source);
}
h.string(preprocessed_source);
return h.get();
}
std::vector<uint32_t> GLSLCompiler::compile(std::string &error_message, const std::vector<std::pair<std::string, int>> *defines) const
{
shaderc::Compiler compiler;
shaderc::CompileOptions options;
if (preprocessed_sections.empty())
{
error_message = "Need to preprocess source first.";
return {};
}
if (defines)
for (auto &define : *defines)
options.AddMacroDefinition(define.first, std::to_string(define.second));
#if GRANITE_COMPILER_OPTIMIZE
if (optimization != Optimization::ForceOff)
options.SetOptimizationLevel(shaderc_optimization_level_performance);
else
options.SetOptimizationLevel(shaderc_optimization_level_zero);
#else
options.SetOptimizationLevel(shaderc_optimization_level_zero);
#endif
if (!strip && (GRANITE_COMPILER_DEBUG || optimization == Optimization::ForceOff))
options.SetGenerateDebugInfo();
shaderc_shader_kind kind;
switch (stage)
{
case Stage::Vertex:
kind = shaderc_glsl_vertex_shader;
options.AddMacroDefinition("STAGE_VERTEX", "1");
break;
case Stage::Fragment:
kind = shaderc_glsl_fragment_shader;
options.AddMacroDefinition("STAGE_FRAGMENT", "1");
break;
case Stage::Compute:
kind = shaderc_glsl_compute_shader;
options.AddMacroDefinition("STAGE_COMPUTE", "1");
break;
case Stage::Task:
kind = shaderc_glsl_task_shader;
options.AddMacroDefinition("STAGE_TASK", "1");
break;
case Stage::Mesh:
kind = shaderc_glsl_mesh_shader;
options.AddMacroDefinition("STAGE_MESH", "1");
break;
default:
error_message = "Invalid shader stage.";
return {};
}
auto env = shaderc_env_version_vulkan_1_1;
auto spv_version = shaderc_spirv_version_1_3;
if (target == Target::Vulkan12)
{
env = shaderc_env_version_vulkan_1_2;
spv_version = shaderc_spirv_version_1_5;
}
else if (target == Target::Vulkan13)
{
env = shaderc_env_version_vulkan_1_3;
spv_version = shaderc_spirv_version_1_6;
}
options.SetTargetEnvironment(shaderc_target_env_vulkan, env);
options.SetTargetSpirv(spv_version);
options.SetSourceLanguage(shaderc_source_language_glsl);
shaderc::SpvCompilationResult result;
if (preprocessed_sections.size() == 1)
{
if (preprocessed_sections.front().stage != Stage::Unknown && preprocessed_sections.front().stage != stage)
{
error_message = "No preprocessed sections available.";
return {};
}
result = compiler.CompileGlslToSpv(preprocessed_sections.front().source, kind, source_path.c_str(), options);
}
else
{
std::string combined_source;
for (auto &section : preprocessed_sections)
if (section.stage == Stage::Unknown || section.stage == stage)
combined_source += section.source;
if (combined_source.empty())
{
error_message = "No preprocessed sections available.";
return {};
}
result = compiler.CompileGlslToSpv(combined_source, kind, source_path.c_str(), options);
}
error_message.clear();
if (result.GetCompilationStatus() != shaderc_compilation_status_success)
{
error_message = result.GetErrorMessage();
return {};
}
std::vector<uint32_t> compiled_spirv(result.cbegin(), result.cend());
#if 0
spvtools::SpirvTools core(target == Target::Vulkan13 ? SPV_ENV_VULKAN_1_3 : SPV_ENV_VULKAN_1_1);
core.SetMessageConsumer([&error_message](spv_message_level_t, const char *, const spv_position_t&, const char *message) {
error_message = message;
});
spvtools::ValidatorOptions opts;
opts.SetScalarBlockLayout(true);
if (!core.Validate(compiled_spirv.data(), compiled_spirv.size(), opts))
{
error_message += "\nFailed to validate SPIR-V.\n";
return {};
}
#endif
return compiled_spirv;
}
}
@@ -0,0 +1,139 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include <string>
#include <vector>
#include <unordered_set>
#include <stdint.h>
#include "small_vector.hpp"
#include "global_managers.hpp"
#include "hash.hpp"
namespace Granite
{
enum class Stage
{
Vertex,
Fragment = 4, // Skip over tess/geom to match Vulkan ordering
Compute,
Task,
Mesh,
Unknown
};
enum class Target
{
Vulkan11,
Vulkan12,
Vulkan13
};
class GLSLCompiler
{
public:
explicit GLSLCompiler(FilesystemInterface &iface);
void set_target(Target target_)
{
target = target_;
}
void set_stage(Stage stage_)
{
stage = stage_;
}
void set_source(std::string source_, std::string path)
{
source = std::move(source_);
source_path = std::move(path);
}
void set_include_directories(const std::vector<std::string> *include_directories);
bool set_source_from_file(const std::string &path, Stage stage = Stage::Unknown);
bool set_source_from_file_multistage(const std::string &path);
bool preprocess();
Util::Hash get_source_hash() const;
std::vector<uint32_t> compile(std::string &error_message, const std::vector<std::pair<std::string, int>> *defines = nullptr) const;
const std::unordered_set<std::string> &get_dependencies() const
{
return dependencies;
}
enum class Optimization
{
ForceOff,
ForceOn,
Default
};
void set_optimization(Optimization opt)
{
optimization = opt;
}
void set_strip(bool strip_)
{
strip = strip_;
}
const std::vector<std::string> &get_user_pragmas() const
{
return pragmas;
}
private:
FilesystemInterface &iface;
std::string source;
std::string source_path;
const std::vector<std::string> *include_directories = nullptr;
Stage stage = Stage::Unknown;
std::unordered_set<std::string> dependencies;
struct Section
{
Stage stage;
std::string source;
};
Util::SmallVector<Section> preprocessed_sections;
std::string preprocessed_source;
Stage preprocessing_active_stage = Stage::Unknown;
std::vector<std::pair<size_t, size_t>> preprocessed_lines;
std::vector<std::string> pragmas;
Target target = Target::Vulkan11;
bool parse_variants(const std::string &source, const std::string &path);
Optimization optimization = Optimization::Default;
bool strip = false;
bool find_include_path(const std::string &source_path, const std::string &include_path,
std::string &included_path, std::string &included_source);
};
}
@@ -0,0 +1,3 @@
add_granite_internal_lib(granite-ecs ecs.hpp ecs.cpp)
target_include_directories(granite-ecs PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(granite-ecs PUBLIC granite-util)
+138
View File
@@ -0,0 +1,138 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "ecs.hpp"
namespace Granite
{
Entity *EntityPool::create_entity()
{
Util::Hasher hasher;
hasher.u64(++cookie);
auto *entity = entity_pool.allocate(this, hasher.get());
entity->pool_offset = entities.size();
entities.push_back(entity);
return entity;
}
void EntityPool::free_component(Entity &entity, ComponentType id, ComponentNode *component)
{
auto *c = component_types.find(id);
assert(c);
c->free_component(component->get());
component_nodes.free(component);
auto *component_groups = component_to_groups.find(id);
if (component_groups)
{
for (auto &group : *component_groups)
{
auto *g = groups.find(group.get_hash());
if (g)
g->remove_entity(entity);
}
}
}
void EntityPool::delete_entity(Entity *entity)
{
{
auto &components = entity->get_components();
auto &list = components.inner_list();
auto itr = list.begin();
while (itr != list.end())
{
auto *component = itr.get();
itr = list.erase(itr);
free_component(*entity, component->get_hash(), component);
}
}
auto offset = entity->pool_offset;
assert(offset < entities.size());
entities[offset] = entities.back();
entities[offset]->pool_offset = offset;
entities.pop_back();
entity_pool.free(entity);
}
EntityPool::~EntityPool()
{
{
auto &list = component_types.inner_list();
auto itr = list.begin();
while (itr != list.end())
{
auto *to_free = itr.get();
itr = list.erase(itr);
delete to_free;
}
}
reset_groups();
free_groups();
}
void EntityDeleter::operator()(Entity *entity)
{
entity->get_pool()->delete_entity(entity);
}
void EntityPool::free_groups()
{
auto &list = groups.inner_list();
auto itr = list.begin();
while (itr != list.end())
{
auto *to_free = itr.get();
itr = list.erase(itr);
delete to_free;
}
groups.clear();
}
void EntityPool::reset_groups()
{
for (auto &group : groups)
group.reset();
}
void EntityPool::reset_groups_for_component_type(ComponentType id)
{
auto *component_groups = component_to_groups.find(id);
if (component_groups)
{
for (auto &group : *component_groups)
{
auto *g = groups.find(group.get_hash());
if (g)
g->reset();
}
}
}
void ComponentSet::insert(ComponentType type)
{
set.emplace_yield(type);
}
}
+456
View File
@@ -0,0 +1,456 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include <tuple>
#include <vector>
#include <memory>
#include <algorithm>
#include "object_pool.hpp"
#include "intrusive.hpp"
#include "intrusive_hash_map.hpp"
#include "compile_time_hash.hpp"
#include "enum_cast.hpp"
#include <assert.h>
namespace Granite
{
struct ComponentBase
{
};
template <typename T, typename Tup>
inline T *get_component(Tup &t)
{
return std::get<T *>(t);
}
template <typename T>
inline T *get(const std::tuple<T *> &t)
{
return std::get<0>(t);
}
template <typename... Ts>
using ComponentGroupVector = std::vector<std::tuple<Ts *...>>;
class Entity;
#define GRANITE_COMPONENT_TYPE_HASH(x) ::Util::compile_time_fnv1(#x)
using ComponentType = uint64_t;
#define GRANITE_COMPONENT_TYPE_DECL(x) \
enum class ComponentTypeWrapper : ::Granite::ComponentType { \
type_id = GRANITE_COMPONENT_TYPE_HASH(x) \
}; \
static inline constexpr ::Granite::ComponentType get_component_id_hash() { \
return ::Granite::ComponentType(ComponentTypeWrapper::type_id); \
}
struct ComponentSetKey : Util::IntrusiveHashMapEnabled<ComponentSetKey>
{
};
class ComponentSet : public Util::IntrusiveHashMapEnabled<ComponentSet>
{
public:
void insert(ComponentType type);
Util::IntrusiveList<ComponentSetKey>::Iterator begin()
{
return set.begin();
}
Util::IntrusiveList<ComponentSetKey>::Iterator end()
{
return set.end();
}
private:
Util::IntrusiveHashMap<ComponentSetKey> set;
};
using ComponentNode = Util::IntrusivePODWrapper<ComponentBase *>;
using ComponentHashMap = Util::IntrusiveHashMapHolder<ComponentNode>;
using ComponentGroupHashMap = Util::IntrusiveHashMap<ComponentSet>;
struct ComponentIDMapping
{
template <typename T>
constexpr static Util::Hash get_id()
{
enum class Result : Util::Hash { result = T::get_component_id_hash() };
return Util::Hash(Result::result);
}
template <typename... Ts>
constexpr static Util::Hash get_group_id()
{
enum class Result : Util::Hash { result = Util::compile_time_fnv1_merged(Ts::get_component_id_hash()...) };
return Util::Hash(Result::result);
}
};
class EntityGroupBase : public Util::IntrusiveHashMapEnabled<EntityGroupBase>
{
public:
virtual ~EntityGroupBase() = default;
virtual void add_entity(Entity &entity) = 0;
virtual void remove_entity(const Entity &entity) = 0;
virtual void reset() = 0;
};
class EntityPool;
struct EntityDeleter
{
void operator()(Entity *entity);
};
class Entity : public Util::IntrusiveListEnabled<Entity>
{
public:
friend class EntityPool;
Entity(EntityPool *pool_, Util::Hash hash_)
: pool(pool_), hash(hash_)
{
}
bool has_component(ComponentType id) const
{
auto itr = components.find(id);
return itr != nullptr;
}
template <typename T>
bool has_component() const
{
return has_component(ComponentIDMapping::get_id<T>());
}
template <typename T>
T *get_component()
{
auto *t = components.find(ComponentIDMapping::get_id<T>());
if (t)
return static_cast<T *>(t->get());
else
return nullptr;
}
template <typename T>
const T *get_component() const
{
auto *t = components.find(ComponentIDMapping::get_id<T>());
if (t)
return static_cast<const T *>(t->get());
else
return nullptr;
}
template <typename T, typename... Ts>
T *allocate_component(Ts&&... ts);
template <typename T>
void free_component();
ComponentHashMap &get_components()
{
return components;
}
EntityPool *get_pool()
{
return pool;
}
Util::Hash get_hash() const
{
return hash;
}
bool mark_for_destruction()
{
bool ret = !marked;
marked = true;
return ret;
}
private:
EntityPool *pool;
Util::Hash hash;
size_t pool_offset = 0;
ComponentHashMap components;
bool marked = false;
};
template <typename... Ts>
class EntityGroup : public EntityGroupBase
{
public:
void add_entity(Entity &entity) override final
{
if (has_all_components<Ts...>(entity))
{
entity_to_index[entity.get_hash()].get() = entities.size();
groups.push_back(std::make_tuple(entity.get_component<Ts>()...));
entities.push_back(&entity);
}
}
void remove_entity(const Entity &entity) override final
{
size_t offset;
if (entity_to_index.find_and_consume_pod(entity.get_hash(), offset))
{
entities[offset] = entities.back();
groups[offset] = groups.back();
entity_to_index[entities[offset]->get_hash()].get() = offset;
entity_to_index.erase(entity.get_hash());
entities.pop_back();
groups.pop_back();
}
}
const ComponentGroupVector<Ts...> &get_groups() const
{
return groups;
}
const std::vector<Entity *> &get_entities() const
{
return entities;
}
void reset() override final
{
groups.clear();
entities.clear();
entity_to_index.clear();
}
private:
ComponentGroupVector<Ts...> groups;
std::vector<Entity *> entities;
Util::IntrusiveHashMap<Util::IntrusivePODWrapper<size_t>> entity_to_index;
template <typename... Us>
struct HasAllComponents;
template <typename U, typename... Us>
struct HasAllComponents<U, Us...>
{
static bool has_component(const Entity &entity)
{
return entity.has_component(ComponentIDMapping::get_id<U>()) &&
HasAllComponents<Us...>::has_component(entity);
}
};
template <typename U>
struct HasAllComponents<U>
{
static bool has_component(const Entity &entity)
{
return entity.has_component(ComponentIDMapping::get_id<U>());
}
};
template <typename... Us>
bool has_all_components(const Entity &entity)
{
return HasAllComponents<Us...>::has_component(entity);
}
};
class ComponentAllocatorBase : public Util::IntrusiveHashMapEnabled<ComponentAllocatorBase>
{
public:
virtual ~ComponentAllocatorBase() = default;
virtual void free_component(ComponentBase *component) = 0;
};
template <typename T>
struct ComponentAllocator : public ComponentAllocatorBase
{
Util::ObjectPool<T> pool;
void free_component(ComponentBase *component) override final
{
pool.free(static_cast<T *>(component));
}
};
class EntityPool
{
public:
~EntityPool();
EntityPool() = default;
void operator=(const EntityPool &) = delete;
EntityPool(const EntityPool &) = delete;
Entity *create_entity();
void delete_entity(Entity *entity);
template <typename... Ts>
EntityGroup<Ts...> *get_component_group_holder()
{
ComponentType group_id = ComponentIDMapping::get_group_id<Ts...>();
auto *t = groups.find(group_id);
if (!t)
{
register_group<Ts...>(group_id);
t = new EntityGroup<Ts...>();
t->set_hash(group_id);
groups.insert_yield(t);
auto *group = static_cast<EntityGroup<Ts...> *>(t);
for (auto &entity : entities)
group->add_entity(*entity);
}
return static_cast<EntityGroup<Ts...> *>(t);
}
template <typename... Ts>
const ComponentGroupVector<Ts...> &get_component_group()
{
auto *group = get_component_group_holder<Ts...>();
return group->get_groups();
}
template <typename... Ts>
const std::vector<Entity *> &get_component_entities()
{
auto *group = get_component_group_holder<Ts...>();
return group->get_entities();
}
template <typename T, typename... Ts>
T *allocate_component(Entity &entity, Ts&&... ts)
{
constexpr ComponentType id = ComponentIDMapping::get_id<T>();
auto *t = component_types.find(id);
if (!t)
{
t = new ComponentAllocator<T>();
t->set_hash(id);
component_types.insert_yield(t);
}
auto *allocator = static_cast<ComponentAllocator<T> *>(t);
auto *existing = entity.components.find(id);
if (existing)
{
auto *comp = static_cast<T *>(existing->get());
// In-place modify. Destroy old data, and in-place construct.
// Do not need to fiddle with data structures internally.
comp->~T();
return new (comp) T(std::forward<Ts>(ts)...);
}
else
{
auto *comp = allocator->pool.allocate(std::forward<Ts>(ts)...);
auto *node = component_nodes.allocate(comp);
node->set_hash(id);
entity.components.insert_replace(node);
auto *component_groups = component_to_groups.find(id);
if (component_groups)
for (auto &group : *component_groups)
groups.find(group.get_hash())->add_entity(entity);
return comp;
}
}
void free_component(Entity &entity, ComponentType id, ComponentNode *component);
void reset_groups();
void reset_groups_for_component_type(ComponentType id);
private:
Util::ObjectPool<Entity> entity_pool;
Util::IntrusiveHashMapHolder<EntityGroupBase> groups;
Util::IntrusiveHashMapHolder<ComponentAllocatorBase> component_types;
Util::ObjectPool<ComponentNode> component_nodes;
ComponentGroupHashMap component_to_groups;
std::vector<Entity *> entities;
uint64_t cookie = 0;
template <typename... Us>
struct GroupRegisters;
template <typename U, typename... Us>
struct GroupRegisters<U, Us...>
{
static void register_group(ComponentGroupHashMap &groups,
ComponentType group_id)
{
groups.emplace_yield(ComponentIDMapping::get_id<U>())->insert(group_id);
GroupRegisters<Us...>::register_group(groups, group_id);
}
};
template <typename U>
struct GroupRegisters<U>
{
static void register_group(ComponentGroupHashMap &groups,
ComponentType group_id)
{
groups.emplace_yield(ComponentIDMapping::get_id<U>())->insert(group_id);
}
};
template <typename U, typename... Us>
void register_group(ComponentType group_id)
{
GroupRegisters<U, Us...>::register_group(component_to_groups, group_id);
}
void free_groups();
};
template <typename T, typename... Ts>
T *Entity::allocate_component(Ts&&... ts)
{
return pool->allocate_component<T>(*this, std::forward<Ts>(ts)...);
}
template <typename T>
void Entity::free_component()
{
auto id = ComponentIDMapping::get_id<T>();
auto *t = components.find(id);
if (t)
{
components.erase(t);
pool->free_component(*this, t->get_hash(), t);
}
}
}
@@ -0,0 +1,3 @@
add_granite_internal_lib(granite-event event.hpp event.cpp)
target_include_directories(granite-event PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(granite-event PUBLIC granite-util granite-application-global)
@@ -0,0 +1,213 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "event.hpp"
#include <algorithm>
#include <assert.h>
namespace Granite
{
EventManager::~EventManager()
{
dispatch();
for (auto &event_type : latched_events)
{
for (auto &handler : event_type.handlers)
{
dispatch_down_events(event_type.queued_events, handler);
// Before the event manager dies, make sure no stale EventHandler objects try to unregister themselves.
handler.unregister_key->release_manager_reference();
}
}
}
void EventManager::dispatch()
{
for (auto &event_type : events)
{
auto &handlers = event_type.handlers;
auto &queued_events = event_type.queued_events;
auto itr = remove_if(begin(handlers), end(handlers), [&](const Handler &handler) {
for (auto &event : queued_events)
{
if (!handler.mem_fn(handler.handler, *event))
{
handler.unregister_key->release_manager_reference();
return true;
}
}
return false;
});
handlers.erase(itr, end(handlers));
queued_events.clear();
}
}
void EventManager::dispatch_event(std::vector<Handler> &handlers, const Event &e)
{
auto itr = remove_if(begin(handlers), end(handlers), [&](const Handler &handler) -> bool {
bool to_remove = !handler.mem_fn(handler.handler, e);
if (to_remove)
handler.unregister_key->release_manager_reference();
return to_remove;
});
handlers.erase(itr, end(handlers));
}
void EventManager::dispatch_up_events(std::vector<std::unique_ptr<Event>> &up_events, const LatchHandler &handler)
{
for (auto &event : up_events)
handler.up_fn(handler.handler, *event);
}
void EventManager::dispatch_down_events(std::vector<std::unique_ptr<Event>> &down_events, const LatchHandler &handler)
{
for (auto &event : down_events)
handler.down_fn(handler.handler, *event);
}
void EventManager::LatchEventTypeData::flush_recursive_handlers()
{
handlers.insert(end(handlers), begin(recursive_handlers), end(recursive_handlers));
recursive_handlers.clear();
}
void EventManager::EventTypeData::flush_recursive_handlers()
{
handlers.insert(end(handlers), begin(recursive_handlers), end(recursive_handlers));
recursive_handlers.clear();
}
void EventManager::dispatch_up_event(LatchEventTypeData &event_type, const Event &event)
{
event_type.dispatching = true;
for (auto &handler : event_type.handlers)
handler.up_fn(handler.handler, event);
event_type.flush_recursive_handlers();
event_type.dispatching = false;
}
void EventManager::dispatch_down_event(LatchEventTypeData &event_type, const Event &event)
{
event_type.dispatching = true;
for (auto &handler : event_type.handlers)
handler.down_fn(handler.handler, event);
event_type.flush_recursive_handlers();
event_type.dispatching = false;
}
void EventManager::unregister_handler(EventHandler *handler)
{
for (auto &event_type : events)
{
auto itr = remove_if(begin(event_type.handlers), end(event_type.handlers), [&](const Handler &h) -> bool {
bool to_remove = h.unregister_key == handler;
if (to_remove)
h.unregister_key->release_manager_reference();
return to_remove;
});
if (itr != end(event_type.handlers) && event_type.dispatching)
throw std::logic_error("Unregistering handlers while dispatching events.");
if (itr != end(event_type.handlers))
event_type.handlers.erase(itr, end(event_type.handlers));
}
}
void EventManager::unregister_latch_handler(EventHandler *handler)
{
for (auto &event_type : latched_events)
{
auto itr = remove_if(begin(event_type.handlers), end(event_type.handlers), [&](const LatchHandler &h) -> bool {
bool to_remove = h.unregister_key == handler;
if (to_remove)
h.unregister_key->release_manager_reference();
return to_remove;
});
if (itr != end(event_type.handlers))
event_type.handlers.erase(itr, end(event_type.handlers));
}
}
void EventManager::dequeue_latched(uint64_t cookie)
{
for (auto &event_type : latched_events)
{
auto &queued_events = event_type.queued_events;
if (event_type.enqueueing)
throw std::logic_error("Dequeueing latched while queueing events.");
event_type.enqueueing = true;
auto itr = remove_if(begin(queued_events), end(queued_events), [&](const std::unique_ptr<Event> &event) {
bool signal = event->get_cookie() == cookie;
if (signal)
dispatch_down_event(event_type, *event);
return signal;
});
event_type.enqueueing = false;
queued_events.erase(itr, end(queued_events));
}
}
void EventManager::dequeue_all_latched(EventType type)
{
auto &event_type = latched_events[type];
if (event_type.enqueueing)
throw std::logic_error("Dequeueing latched while queueing events.");
event_type.enqueueing = true;
for (auto &event : event_type.queued_events)
dispatch_down_event(event_type, *event);
event_type.queued_events.clear();
event_type.enqueueing = false;
}
void EventHandler::release_manager_reference()
{
assert(event_manager_ref_count > 0);
assert(event_manager);
if (--event_manager_ref_count == 0)
event_manager = nullptr;
}
void EventHandler::add_manager_reference(EventManager *manager)
{
assert(!event_manager_ref_count || manager == event_manager);
event_manager = manager;
event_manager_ref_count++;
}
EventHandler::~EventHandler()
{
if (event_manager)
event_manager->unregister_handler(this);
// Splitting the branch is significant since event manager can release its last reference in between.
if (event_manager)
event_manager->unregister_latch_handler(this);
assert(event_manager_ref_count == 0 && !event_manager);
}
}

Some files were not shown because too many files have changed in this diff Show More