Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eb8a659319 | |||
| fa4df1de9e | |||
| ef862454b0 | |||
| f77eec1299 | |||
| 575975687c | |||
| 49ba1cd11b | |||
| e71cb9b7bd | |||
| 9724fb4a4e | |||
| 767f028bdf | |||
| 4c3b11445c |
Generated
+11
@@ -2759,6 +2759,7 @@ name = "pf-client-core"
|
|||||||
version = "0.11.0"
|
version = "0.11.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"ash",
|
||||||
"async-channel",
|
"async-channel",
|
||||||
"ffmpeg-next",
|
"ffmpeg-next",
|
||||||
"mdns-sd",
|
"mdns-sd",
|
||||||
@@ -2766,6 +2767,7 @@ dependencies = [
|
|||||||
"pf-ffvk",
|
"pf-ffvk",
|
||||||
"pipewire",
|
"pipewire",
|
||||||
"punktfunk-core",
|
"punktfunk-core",
|
||||||
|
"pyrowave-sys",
|
||||||
"rustls",
|
"rustls",
|
||||||
"sdl3",
|
"sdl3",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -3120,6 +3122,7 @@ dependencies = [
|
|||||||
"pf-driver-proto",
|
"pf-driver-proto",
|
||||||
"pipewire",
|
"pipewire",
|
||||||
"punktfunk-core",
|
"punktfunk-core",
|
||||||
|
"pyrowave-sys",
|
||||||
"quinn",
|
"quinn",
|
||||||
"rand 0.8.6",
|
"rand 0.8.6",
|
||||||
"rcgen",
|
"rcgen",
|
||||||
@@ -3190,6 +3193,14 @@ dependencies = [
|
|||||||
"winresource",
|
"winresource",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyrowave-sys"
|
||||||
|
version = "0.11.0"
|
||||||
|
dependencies = [
|
||||||
|
"bindgen",
|
||||||
|
"cmake",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quick-error"
|
name = "quick-error"
|
||||||
version = "1.2.3"
|
version = "1.2.3"
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ members = [
|
|||||||
"crates/pf-console-ui",
|
"crates/pf-console-ui",
|
||||||
"crates/pf-ffvk",
|
"crates/pf-ffvk",
|
||||||
"crates/pf-driver-proto",
|
"crates/pf-driver-proto",
|
||||||
|
"crates/pyrowave-sys",
|
||||||
"clients/probe",
|
"clients/probe",
|
||||||
"clients/linux",
|
"clients/linux",
|
||||||
"clients/session",
|
"clients/session",
|
||||||
|
|||||||
+3
-2
@@ -10,7 +10,7 @@
|
|||||||
"name": "MIT OR Apache-2.0",
|
"name": "MIT OR Apache-2.0",
|
||||||
"identifier": "MIT OR Apache-2.0"
|
"identifier": "MIT OR Apache-2.0"
|
||||||
},
|
},
|
||||||
"version": "0.9.1"
|
"version": "0.11.0"
|
||||||
},
|
},
|
||||||
"paths": {
|
"paths": {
|
||||||
"/api/v1/clients": {
|
"/api/v1/clients": {
|
||||||
@@ -2047,7 +2047,8 @@
|
|||||||
"enum": [
|
"enum": [
|
||||||
"h264",
|
"h264",
|
||||||
"h265",
|
"h265",
|
||||||
"av1"
|
"av1",
|
||||||
|
"pyrowave"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"ApiDisplayInfo": {
|
"ApiDisplayInfo": {
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ path = "src/main.rs"
|
|||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["ui"]
|
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
|
# 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,
|
# (`--no-default-features`) is the ~15 MB-smaller power-user build: same streaming,
|
||||||
# stats on stdout only.
|
# stats on stdout only.
|
||||||
|
|||||||
@@ -40,6 +40,11 @@ tracing = "0.1"
|
|||||||
[target.'cfg(target_os = "linux")'.dependencies]
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
pipewire = "0.9"
|
pipewire = "0.9"
|
||||||
sdl3 = { version = "0.18", features = ["hidapi"] }
|
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]
|
[target.'cfg(windows)'.dependencies]
|
||||||
wasapi = "0.23"
|
wasapi = "0.23"
|
||||||
@@ -57,3 +62,8 @@ windows = { git = "https://github.com/microsoft/windows-rs", rev = "a4f7b2cb7c63
|
|||||||
# method itself is feature-gated behind this.
|
# method itself is feature-gated behind this.
|
||||||
"Win32_Security",
|
"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"]
|
||||||
|
|||||||
@@ -33,7 +33,11 @@ pub mod session;
|
|||||||
pub mod trust;
|
pub mod trust;
|
||||||
#[cfg(any(target_os = "linux", windows))]
|
#[cfg(any(target_os = "linux", windows))]
|
||||||
pub mod video;
|
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)]
|
#[cfg(windows)]
|
||||||
pub mod video_d3d11;
|
pub mod video_d3d11;
|
||||||
|
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
|
||||||
|
pub mod video_pyrowave;
|
||||||
|
|
||||||
pub mod wol;
|
pub mod wol;
|
||||||
|
|||||||
@@ -211,6 +211,21 @@ fn pump(
|
|||||||
frame_tx: async_channel::Sender<DecodedFrame>,
|
frame_tx: async_channel::Sender<DecodedFrame>,
|
||||||
stop: Arc<AtomicBool>,
|
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(
|
let connector = match NativeClient::connect(
|
||||||
¶ms.host,
|
¶ms.host,
|
||||||
params.port,
|
params.port,
|
||||||
@@ -220,8 +235,9 @@ fn pump(
|
|||||||
params.bitrate_kbps,
|
params.bitrate_kbps,
|
||||||
params.video_caps,
|
params.video_caps,
|
||||||
params.audio_channels,
|
params.audio_channels,
|
||||||
crate::video::decodable_codecs(), // codecs FFmpeg can decode (HEVC/H.264/AV1)
|
// FFmpeg's codecs plus CODEC_PYROWAVE when the presenter device passed the probe.
|
||||||
params.preferred_codec, // the user's soft codec preference (0 = auto)
|
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
|
// 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).
|
// A/B run can pin an exact peak (PUNKTFUNK_CLIENT_PEAK_NITS=600).
|
||||||
punktfunk_core::client::display_hdr_env_override().or(params.display_hdr),
|
punktfunk_core::client::display_hdr_env_override().or(params.display_hdr),
|
||||||
@@ -262,7 +278,24 @@ fn pump(
|
|||||||
welcome_codec = connector.codec,
|
welcome_codec = connector.codec,
|
||||||
"negotiated video codec"
|
"negotiated video codec"
|
||||||
);
|
);
|
||||||
let mut decoder = match Decoder::new(codec_id, ¶ms.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, ¶ms.decoder, params.vulkan.as_ref())
|
||||||
|
};
|
||||||
|
#[cfg(not(all(target_os = "linux", feature = "pyrowave")))]
|
||||||
|
let built = Decoder::new(codec_id, ¶ms.decoder, params.vulkan.as_ref());
|
||||||
|
let mut decoder = match built {
|
||||||
Ok(d) => d,
|
Ok(d) => d,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
let _ = ev_tx.send_blocking(SessionEvent::Ended(Some(format!("video decoder: {e}"))));
|
let _ = ev_tx.send_blocking(SessionEvent::Ended(Some(format!("video decoder: {e}"))));
|
||||||
@@ -413,6 +446,8 @@ fn pump(
|
|||||||
DecodedImage::VkFrame(_) => "vulkan",
|
DecodedImage::VkFrame(_) => "vulkan",
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
DecodedImage::D3d11(_) => "d3d11va",
|
DecodedImage::D3d11(_) => "d3d11va",
|
||||||
|
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
|
||||||
|
DecodedImage::PyroWave(_) => "pyrowave",
|
||||||
};
|
};
|
||||||
if total_frames == 1 {
|
if total_frames == 1 {
|
||||||
let (w, h, path) = match &image {
|
let (w, h, path) = match &image {
|
||||||
@@ -422,6 +457,8 @@ fn pump(
|
|||||||
DecodedImage::VkFrame(v) => (v.width, v.height, "vulkan-video"),
|
DecodedImage::VkFrame(v) => (v.width, v.height, "vulkan-video"),
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
DecodedImage::D3d11(d) => (d.width, d.height, "d3d11va"),
|
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");
|
tracing::info!(width = w, height = h, path, "first frame decoded");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,6 +68,11 @@ pub enum DecodedImage {
|
|||||||
/// (Intel's Windows driver foremost). See `crate::video_d3d11`.
|
/// (Intel's Windows driver foremost). See `crate::video_d3d11`.
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
D3d11(crate::video_d3d11::D3d11Frame),
|
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
|
/// 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,
|
DecodedImage::VkFrame(f) => f.keyframe,
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
DecodedImage::D3d11(f) => f.keyframe,
|
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),
|
DecodedImage::VkFrame(f) => (f.width, f.height),
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
DecodedImage::D3d11(f) => (f.width, f.height),
|
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),
|
Vaapi(VaapiDecoder),
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
D3d11va(crate::video_d3d11::D3d11vaDecoder),
|
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),
|
Software(SoftwareDecoder),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -360,6 +374,21 @@ pub fn decodable_codecs() -> u8 {
|
|||||||
bits
|
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
|
/// libavcodec logs reference-frame recovery to the process stderr very verbosely
|
||||||
/// (`First slice in a frame missing`, `Could not find ref with POC …`, `Error
|
/// (`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
|
/// 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
|
/// 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.
|
/// (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 {
|
pub fn take_keyframe_request(&mut self) -> bool {
|
||||||
std::mem::take(&mut self.want_keyframe)
|
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)),
|
Backend::Vaapi(v) => v.decode(au).map(|f| f.map(DecodedImage::Dmabuf)),
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
Backend::D3d11va(d) => d.decode(au).map(|f| f.map(DecodedImage::D3d11)),
|
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)),
|
Backend::Software(s) => return Ok(s.decode(au)?.map(DecodedImage::Cpu)),
|
||||||
};
|
};
|
||||||
match result {
|
match result {
|
||||||
@@ -1077,6 +1126,24 @@ pub struct VulkanDecodeDevice {
|
|||||||
/// features). The bundle now exists even without it — Windows D3D11 interop rides the
|
/// 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`.
|
/// same struct — so consumers gate the FFmpeg-Vulkan decoder on THIS, not on `Some`.
|
||||||
pub video_decode: bool,
|
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`:
|
/// 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.
|
/// D3D11 shared-texture frames can reach the screen. Always `false` off Windows.
|
||||||
pub d3d11_import: bool,
|
pub d3d11_import: bool,
|
||||||
@@ -1598,6 +1665,14 @@ mod tests {
|
|||||||
f_sampler_ycbcr: true,
|
f_sampler_ycbcr: true,
|
||||||
f_timeline_semaphore: true,
|
f_timeline_semaphore: true,
|
||||||
f_synchronization2: 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,
|
video_decode: true,
|
||||||
d3d11_import: false,
|
d3d11_import: false,
|
||||||
adapter_luid: None,
|
adapter_luid: None,
|
||||||
|
|||||||
@@ -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 (≤ 1–2 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.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -43,3 +43,8 @@ windows-sys = { version = "0.61", features = [
|
|||||||
"Win32_UI_Shell",
|
"Win32_UI_Shell",
|
||||||
"Win32_UI_WindowsAndMessaging",
|
"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.
@@ -32,6 +32,32 @@ impl CscPass {
|
|||||||
/// `attachment_format` = the video image's format: R8G8B8A8 for SDR, a 10-bit
|
/// `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).
|
/// 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> {
|
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
|
// One color attachment: the presenter's video image. Content is fully
|
||||||
// overwritten (DONT_CARE load), and the pass ends in TRANSFER_SRC so the
|
// overwritten (DONT_CARE load), and the pass ends in TRANSFER_SRC so the
|
||||||
// existing letterbox blit consumes it with no extra barrier.
|
// existing letterbox blit consumes it with no extra barrier.
|
||||||
@@ -89,20 +115,16 @@ impl CscPass {
|
|||||||
}?;
|
}?;
|
||||||
|
|
||||||
let samplers = [sampler];
|
let samplers = [sampler];
|
||||||
let bindings = [
|
let bindings: Vec<vk::DescriptorSetLayoutBinding> = (0..plane_bindings)
|
||||||
|
.map(|b| {
|
||||||
vk::DescriptorSetLayoutBinding::default()
|
vk::DescriptorSetLayoutBinding::default()
|
||||||
.binding(0)
|
.binding(b)
|
||||||
.descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
|
.descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
|
||||||
.descriptor_count(1)
|
.descriptor_count(1)
|
||||||
.stage_flags(vk::ShaderStageFlags::FRAGMENT)
|
.stage_flags(vk::ShaderStageFlags::FRAGMENT)
|
||||||
.immutable_samplers(&samplers),
|
.immutable_samplers(&samplers)
|
||||||
vk::DescriptorSetLayoutBinding::default()
|
})
|
||||||
.binding(1)
|
.collect();
|
||||||
.descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
|
|
||||||
.descriptor_count(1)
|
|
||||||
.stage_flags(vk::ShaderStageFlags::FRAGMENT)
|
|
||||||
.immutable_samplers(&samplers),
|
|
||||||
];
|
|
||||||
let set_layout = unsafe {
|
let set_layout = unsafe {
|
||||||
device.create_descriptor_set_layout(
|
device.create_descriptor_set_layout(
|
||||||
&vk::DescriptorSetLayoutCreateInfo::default().bindings(&bindings),
|
&vk::DescriptorSetLayoutCreateInfo::default().bindings(&bindings),
|
||||||
@@ -124,7 +146,7 @@ impl CscPass {
|
|||||||
|
|
||||||
let pool_sizes = [vk::DescriptorPoolSize::default()
|
let pool_sizes = [vk::DescriptorPoolSize::default()
|
||||||
.ty(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
|
.ty(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
|
||||||
.descriptor_count(2)];
|
.descriptor_count(plane_bindings)];
|
||||||
let desc_pool = unsafe {
|
let desc_pool = unsafe {
|
||||||
device.create_descriptor_pool(
|
device.create_descriptor_pool(
|
||||||
&vk::DescriptorPoolCreateInfo::default()
|
&vk::DescriptorPoolCreateInfo::default()
|
||||||
@@ -145,7 +167,7 @@ impl CscPass {
|
|||||||
device,
|
device,
|
||||||
render_pass,
|
render_pass,
|
||||||
pipeline_layout,
|
pipeline_layout,
|
||||||
include_bytes!("../shaders/nv12_csc.frag.spv"),
|
frag_spv,
|
||||||
false, // opaque — the CSC output IS the video
|
false, // opaque — the CSC output IS the video
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
@@ -184,6 +206,26 @@ impl CscPass {
|
|||||||
unsafe { device.update_descriptor_sets(&writes, &[]) };
|
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) {
|
pub fn destroy(&self, device: &ash::Device) {
|
||||||
unsafe {
|
unsafe {
|
||||||
device.destroy_pipeline(self.pipeline, None);
|
device.destroy_pipeline(self.pipeline, None);
|
||||||
|
|||||||
@@ -968,6 +968,28 @@ fn run_inner(mut opts: SessionOpts, mut mode: ModeCtl) -> Result<Option<Outcome>
|
|||||||
image,
|
image,
|
||||||
} = f;
|
} = f;
|
||||||
let did_present = match image {
|
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) => {
|
DecodedImage::Cpu(c) => {
|
||||||
st.hdr = c.color.is_pq();
|
st.hdr = c.color.is_pq();
|
||||||
presenter.present(&window, FrameInput::Cpu(&c), overlay_frame.as_ref())?
|
presenter.present(&window, FrameInput::Cpu(&c), overlay_frame.as_ref())?
|
||||||
|
|||||||
@@ -40,6 +40,10 @@ pub enum FrameInput<'a> {
|
|||||||
/// D3D11VA hand-off — a shareable NT-handle texture to import (`d3d11.rs`).
|
/// D3D11VA hand-off — a shareable NT-handle texture to import (`d3d11.rs`).
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
D3d11(pf_client_core::video::D3d11Frame),
|
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.
|
/// The dmabuf/CSC machinery, present only when the device carries the import extensions.
|
||||||
@@ -321,6 +325,10 @@ pub struct Presenter {
|
|||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
hw_win: Option<HwCtxWin>,
|
hw_win: Option<HwCtxWin>,
|
||||||
csc: CscPass,
|
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.
|
/// FFmpeg Vulkan Video decode handles — `None` when the stack can't do it.
|
||||||
video_export: Option<pf_client_core::video::VulkanDecodeDevice>,
|
video_export: Option<pf_client_core::video::VulkanDecodeDevice>,
|
||||||
/// The console-UI composite quad (§6.1's presenter half).
|
/// 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_f12)
|
||||||
.push_next(&mut have_f13);
|
.push_next(&mut have_f13);
|
||||||
unsafe { instance.get_physical_device_features2(pdev, &mut have_f2) };
|
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
|
let features_ok = have_f11.sampler_ycbcr_conversion == vk::TRUE
|
||||||
&& have_f12.timeline_semaphore == vk::TRUE
|
&& have_f12.timeline_semaphore == vk::TRUE
|
||||||
&& have_f13.synchronization2 == 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.
|
// The decode queue family + which codec operations it can run.
|
||||||
let decode_family: Option<(u32, vk::VideoCodecOperationFlagsKHR)> = {
|
let decode_family: Option<(u32, vk::VideoCodecOperationFlagsKHR)> = {
|
||||||
@@ -575,13 +597,18 @@ impl Presenter {
|
|||||||
let mut en_f11 = vk::PhysicalDeviceVulkan11Features::default()
|
let mut en_f11 = vk::PhysicalDeviceVulkan11Features::default()
|
||||||
.sampler_ycbcr_conversion(have_f11.sampler_ycbcr_conversion == vk::TRUE);
|
.sampler_ycbcr_conversion(have_f11.sampler_ycbcr_conversion == vk::TRUE);
|
||||||
let mut en_f12 = vk::PhysicalDeviceVulkan12Features::default()
|
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()
|
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()
|
let mut en_f2 = vk::PhysicalDeviceFeatures2::default()
|
||||||
.push_next(&mut en_f11)
|
.push_next(&mut en_f11)
|
||||||
.push_next(&mut en_f12)
|
.push_next(&mut en_f12)
|
||||||
.push_next(&mut en_f13);
|
.push_next(&mut en_f13);
|
||||||
|
en_f2.features.shader_int16 = if pyrowave_ok { vk::TRUE } else { vk::FALSE };
|
||||||
|
|
||||||
let priorities = [1.0f32];
|
let priorities = [1.0f32];
|
||||||
let mut queue_info = vec![vk::DeviceQueueCreateInfo::default()
|
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),
|
ext_mem_win32: ash::khr::external_memory_win32::Device::new(&instance, &device),
|
||||||
});
|
});
|
||||||
let csc = CscPass::new(&device, vk::Format::R8G8B8A8_UNORM)?;
|
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
|
// 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
|
// 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).
|
// 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());
|
let queue_lock = std::sync::Arc::new(pf_client_core::video::QueueLock::new());
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
let export_worthy = video_ok || win_capable;
|
let export_worthy = video_ok || win_capable || pyrowave_ok;
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
let export_worthy = video_ok;
|
let export_worthy = video_ok || pyrowave_ok;
|
||||||
let video_export = if export_worthy {
|
let video_export = if export_worthy {
|
||||||
let qf_props = unsafe { instance.get_physical_device_queue_family_properties(pdev) };
|
let qf_props = unsafe { instance.get_physical_device_queue_family_properties(pdev) };
|
||||||
let mut device_extensions: Vec<CString> =
|
let mut device_extensions: Vec<CString> =
|
||||||
@@ -678,6 +712,14 @@ impl Presenter {
|
|||||||
f_sampler_ycbcr: have_f11.sampler_ycbcr_conversion == vk::TRUE,
|
f_sampler_ycbcr: have_f11.sampler_ycbcr_conversion == vk::TRUE,
|
||||||
f_timeline_semaphore: have_f12.timeline_semaphore == vk::TRUE,
|
f_timeline_semaphore: have_f12.timeline_semaphore == vk::TRUE,
|
||||||
f_synchronization2: have_f13.synchronization2 == 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,
|
video_decode: video_ok,
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
d3d11_import: win_capable,
|
d3d11_import: win_capable,
|
||||||
@@ -742,6 +784,8 @@ impl Presenter {
|
|||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
hw_win,
|
hw_win,
|
||||||
csc,
|
csc,
|
||||||
|
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
|
||||||
|
csc_planar,
|
||||||
video_export,
|
video_export,
|
||||||
overlay_pipe,
|
overlay_pipe,
|
||||||
retired_hw: None,
|
retired_hw: None,
|
||||||
@@ -1017,6 +1061,10 @@ impl Presenter {
|
|||||||
vk::Format::R8G8B8A8_UNORM
|
vk::Format::R8G8B8A8_UNORM
|
||||||
};
|
};
|
||||||
self.csc.destroy(&self.device); // fence-safe: only our cmd bufs reference it
|
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)?;
|
self.csc = CscPass::new(&self.device, self.video_format)?;
|
||||||
if let Some(v) = self.video.take() {
|
if let Some(v) = self.video.take() {
|
||||||
unsafe {
|
unsafe {
|
||||||
@@ -1075,6 +1123,8 @@ impl Presenter {
|
|||||||
FrameInput::VkFrame(v) => Some(v.color.is_pq()),
|
FrameInput::VkFrame(v) => Some(v.color.is_pq()),
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
FrameInput::D3d11(d) => Some(d.color.is_pq()),
|
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 {
|
if let Some(pq) = frame_pq {
|
||||||
// A PQ stream we can only tone-map (no HDR10 surface) is the silent failure behind
|
// A PQ stream we can only tone-map (no HDR10 surface) is the silent failure behind
|
||||||
@@ -1103,6 +1153,8 @@ impl Presenter {
|
|||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
let mut win_frame: Option<crate::d3d11::HwFrame> = None;
|
let mut win_frame: Option<crate::d3d11::HwFrame> = None;
|
||||||
let mut vk_frame: Option<(VkVideoFrame, [vk::ImageView; 2])> = 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 {
|
let cpu_frame = match input {
|
||||||
FrameInput::Redraw => None,
|
FrameInput::Redraw => None,
|
||||||
FrameInput::Cpu(f) => Some(f),
|
FrameInput::Cpu(f) => Some(f),
|
||||||
@@ -1129,6 +1181,11 @@ impl Presenter {
|
|||||||
vk_frame = Some((v, views));
|
vk_frame = Some((v, views));
|
||||||
None
|
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
|
// 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]);
|
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 {
|
if let Some(o) = overlay {
|
||||||
// Point the composite at this overlay image (same fence-wait safety).
|
// Point the composite at this overlay image (same fence-wait safety).
|
||||||
let infos = [vk::DescriptorImageInfo::default()
|
let infos = [vk::DescriptorImageInfo::default()
|
||||||
@@ -1325,6 +1398,18 @@ impl Presenter {
|
|||||||
vk_sync = Some(sync);
|
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).
|
// 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) {
|
if let (Some(f), Some(v), Some(s)) = (cpu_frame, &self.video, &self.staging) {
|
||||||
barrier(
|
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
|
/// 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).
|
/// exact sampling contract (the frames pool was created MUTABLE_FORMAT for this).
|
||||||
/// 8-bit NV12 (R8 + R8G8) and 10-bit P010/X6 (R10X6 + R10X6G10X6).
|
/// 8-bit NV12 (R8 + R8G8) and 10-bit P010/X6 (R10X6 + R10X6G10X6).
|
||||||
@@ -1929,6 +2089,10 @@ impl Drop for Presenter {
|
|||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
self.hw.take();
|
self.hw.take();
|
||||||
self.csc.destroy(&self.device);
|
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);
|
self.overlay_pipe.destroy(&self.device);
|
||||||
for s in self.render_sems.drain(..) {
|
for s in self.render_sems.drain(..) {
|
||||||
self.device.destroy_semaphore(s, None);
|
self.device.destroy_semaphore(s, None);
|
||||||
|
|||||||
@@ -928,6 +928,10 @@ pub const PUNKTFUNK_CODEC_H264: u8 = 0x01;
|
|||||||
pub const PUNKTFUNK_CODEC_HEVC: u8 = 0x02;
|
pub const PUNKTFUNK_CODEC_HEVC: u8 = 0x02;
|
||||||
/// Codec bit: AV1. (Mirrors `quic::CODEC_AV1`.)
|
/// Codec bit: AV1. (Mirrors `quic::CODEC_AV1`.)
|
||||||
pub const PUNKTFUNK_CODEC_AV1: u8 = 0x04;
|
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).
|
// Keep the ABI cap bits in lockstep with the wire constants (compile-time guard against drift).
|
||||||
#[cfg(feature = "quic")]
|
#[cfg(feature = "quic")]
|
||||||
@@ -938,6 +942,7 @@ const _: () = {
|
|||||||
assert!(PUNKTFUNK_CODEC_H264 == crate::quic::CODEC_H264);
|
assert!(PUNKTFUNK_CODEC_H264 == crate::quic::CODEC_H264);
|
||||||
assert!(PUNKTFUNK_CODEC_HEVC == crate::quic::CODEC_HEVC);
|
assert!(PUNKTFUNK_CODEC_HEVC == crate::quic::CODEC_HEVC);
|
||||||
assert!(PUNKTFUNK_CODEC_AV1 == crate::quic::CODEC_AV1);
|
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).
|
// Keep the ABI gamepad constants in lockstep with the wire enum (compile-time guard against drift).
|
||||||
|
|||||||
@@ -151,15 +151,28 @@ pub const CODEC_H264: u8 = 0x01;
|
|||||||
pub const CODEC_HEVC: u8 = 0x02;
|
pub const CODEC_HEVC: u8 = 0x02;
|
||||||
/// [`Hello::video_codecs`] bit: the client can decode AV1.
|
/// [`Hello::video_codecs`] bit: the client can decode AV1.
|
||||||
pub const CODEC_AV1: u8 = 0x04;
|
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; 100–400 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`]
|
/// 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
|
/// 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
|
/// 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
|
/// ([`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
|
/// 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
|
/// compatibility / software floor). [`CODEC_PYROWAVE`] is intentionally NOT in that ladder — it can
|
||||||
/// share nothing — the caller then refuses the session with a clear error rather than emitting a
|
/// only be returned via the `preferred` path (plan §3: opt-in, pinned, honest). Returns the
|
||||||
/// stream the client can't decode.
|
/// 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> {
|
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.
|
// An older client (no codec byte) decodes HEVC — the only codec every pre-negotiation build sent.
|
||||||
let client = if client_codecs == 0 {
|
let client = if client_codecs == 0 {
|
||||||
|
|||||||
@@ -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.
|
// 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);
|
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 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).
|
// a build that ignores the trailing byte (back-compat: extra bytes are skipped).
|
||||||
let h = Hello {
|
let h = Hello {
|
||||||
|
|||||||
@@ -80,6 +80,9 @@ log = "0.4"
|
|||||||
# crate vendors libopus (cmake-built from source — no system lib, no vcpkg), so it builds on Windows
|
# 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.
|
# 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]
|
[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"
|
opus = "0.3"
|
||||||
# Software H.264 encoder — the GPU-less encode path on both Linux and Windows (and a fallback when no
|
# 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) —
|
# 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
|
# 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`.
|
# PUNKTFUNK_VULKAN_ENCODE (opt-in for now). Build the AMD/Intel RFI host with `--features vulkan-encode`.
|
||||||
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
|
# Build-time icon/version-info embedding (build.rs; Windows dev/CI hosts only — Linux packaging
|
||||||
# builds of this crate never execute the winresource block).
|
# builds of this crate never execute the winresource block).
|
||||||
|
|||||||
@@ -37,6 +37,10 @@ pub enum Codec {
|
|||||||
H264,
|
H264,
|
||||||
H265,
|
H265,
|
||||||
Av1,
|
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
|
/// Chroma subsampling the encoder emits, negotiated with the client (the `PUNKTFUNK_444` gate + the
|
||||||
@@ -73,6 +77,7 @@ impl Codec {
|
|||||||
match bit {
|
match bit {
|
||||||
punktfunk_core::quic::CODEC_H264 => Codec::H264,
|
punktfunk_core::quic::CODEC_H264 => Codec::H264,
|
||||||
punktfunk_core::quic::CODEC_AV1 => Codec::Av1,
|
punktfunk_core::quic::CODEC_AV1 => Codec::Av1,
|
||||||
|
punktfunk_core::quic::CODEC_PYROWAVE => Codec::PyroWave,
|
||||||
_ => Codec::H265,
|
_ => Codec::H265,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -83,6 +88,7 @@ impl Codec {
|
|||||||
Codec::H264 => punktfunk_core::quic::CODEC_H264,
|
Codec::H264 => punktfunk_core::quic::CODEC_H264,
|
||||||
Codec::H265 => punktfunk_core::quic::CODEC_HEVC,
|
Codec::H265 => punktfunk_core::quic::CODEC_HEVC,
|
||||||
Codec::Av1 => punktfunk_core::quic::CODEC_AV1,
|
Codec::Av1 => punktfunk_core::quic::CODEC_AV1,
|
||||||
|
Codec::PyroWave => punktfunk_core::quic::CODEC_PYROWAVE,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,6 +103,29 @@ impl Codec {
|
|||||||
/// still lands on HEVC for an auto client, exactly the pre-probe behaviour. Fed to
|
/// 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.
|
/// [`punktfunk_core::quic::resolve_codec`] against the client's advertised codecs.
|
||||||
pub fn host_wire_caps() -> u8 {
|
pub fn host_wire_caps() -> u8 {
|
||||||
|
// 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"
|
||||||
|
) {
|
||||||
|
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
|
/// The static GPU superset (H.264 | HEVC | AV1) — mirrors the GameStream
|
||||||
/// `SERVER_CODEC_MODE_SUPPORT` advertisement for the unprobed backends.
|
/// `SERVER_CODEC_MODE_SUPPORT` advertisement for the unprobed backends.
|
||||||
const GPU_SUPERSET: u8 = punktfunk_core::quic::CODEC_H264
|
const GPU_SUPERSET: u8 = punktfunk_core::quic::CODEC_H264
|
||||||
@@ -140,6 +169,8 @@ impl Codec {
|
|||||||
_ => punktfunk_core::quic::CODEC_HEVC,
|
_ => punktfunk_core::quic::CODEC_HEVC,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
})();
|
||||||
|
base | pyro
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lowercase stats/console label (`"h264"` / `"hevc"` / `"av1"`) — the codec string seeded into
|
/// Lowercase stats/console label (`"h264"` / `"hevc"` / `"av1"`) — the codec string seeded into
|
||||||
@@ -149,6 +180,7 @@ impl Codec {
|
|||||||
Codec::H264 => "h264",
|
Codec::H264 => "h264",
|
||||||
Codec::H265 => "hevc",
|
Codec::H265 => "hevc",
|
||||||
Codec::Av1 => "av1",
|
Codec::Av1 => "av1",
|
||||||
|
Codec::PyroWave => "pyrowave",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,6 +191,9 @@ impl Codec {
|
|||||||
Codec::H264 => "h264_nvenc",
|
Codec::H264 => "h264_nvenc",
|
||||||
Codec::H265 => "hevc_nvenc",
|
Codec::H265 => "hevc_nvenc",
|
||||||
Codec::Av1 => "av1_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::H264 => "h264_vaapi",
|
||||||
Codec::H265 => "hevc_vaapi",
|
Codec::H265 => "hevc_vaapi",
|
||||||
Codec::Av1 => "av1_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::H264 => "h264_amf",
|
||||||
Codec::H265 => "hevc_amf",
|
Codec::H265 => "hevc_amf",
|
||||||
Codec::Av1 => "av1_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::H264 => "h264_qsv",
|
||||||
Codec::H265 => "hevc_qsv",
|
Codec::H265 => "hevc_qsv",
|
||||||
Codec::Av1 => "av1_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 {
|
pub fn max_dimension(self) -> u32 {
|
||||||
match self {
|
match self {
|
||||||
Codec::H264 => 4096,
|
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::H264 => 480_000_000,
|
||||||
Codec::H265 => 800_000_000,
|
Codec::H265 => 800_000_000,
|
||||||
Codec::Av1 => 1_200_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")]
|
#[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);
|
// 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
|
// 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
|
// 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:
|
// 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
|
// `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
|
// 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!(
|
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")]
|
#[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)
|
let _ = cuda; // always false on Windows (no Cuda payload)
|
||||||
// NVIDIA → NVENC (direct SDK), AMD → AMF, Intel → QSV (both libavcodec), else → software
|
// 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.
|
// 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() {
|
match crate::config::config().encoder_pref.as_str() {
|
||||||
"nvenc" | "nvidia" | "cuda" => false,
|
"nvenc" | "nvidia" | "cuda" => false,
|
||||||
"vaapi" | "amd" | "intel" => true,
|
"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(),
|
_ => linux_auto_is_vaapi(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1274,6 +1369,20 @@ mod vulkan_video;
|
|||||||
#[cfg(all(target_os = "linux", feature = "vulkan-encode"))]
|
#[cfg(all(target_os = "linux", feature = "vulkan-encode"))]
|
||||||
#[path = "encode/linux/vk_av1_encode.rs"]
|
#[path = "encode/linux/vk_av1_encode.rs"]
|
||||||
mod vk_av1_encode;
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|||||||
@@ -224,6 +224,8 @@ fn codec_guid(codec: Codec) -> nv::GUID {
|
|||||||
Codec::H264 => nv::NV_ENC_CODEC_H264_GUID,
|
Codec::H264 => nv::NV_ENC_CODEC_H264_GUID,
|
||||||
Codec::H265 => nv::NV_ENC_CODEC_HEVC_GUID,
|
Codec::H265 => nv::NV_ENC_CODEC_HEVC_GUID,
|
||||||
Codec::Av1 => nv::NV_ENC_CODEC_AV1_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::Av1 => {}
|
||||||
Codec::H264 => {}
|
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
|
// Chroma + bit depth. 4:4:4 (HEVC Range Extensions, chromaFormatIDC=3) engages on a YUV444
|
||||||
@@ -549,6 +552,7 @@ impl NvencCudaEncoder {
|
|||||||
.set_inputPixelBitDepthMinus8(0);
|
.set_inputPixelBitDepthMinus8(0);
|
||||||
}
|
}
|
||||||
Codec::H264 => {}
|
Codec::H264 => {}
|
||||||
|
Codec::PyroWave => unreachable!("PyroWave never opens the direct-NVENC backend"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -596,6 +600,7 @@ impl NvencCudaEncoder {
|
|||||||
av1.matrixCoefficients = mat;
|
av1.matrixCoefficients = mat;
|
||||||
av1.colorRange = 0;
|
av1.colorRange = 0;
|
||||||
}
|
}
|
||||||
|
Codec::PyroWave => unreachable!("PyroWave never opens the direct-NVENC backend"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -616,6 +621,7 @@ impl NvencCudaEncoder {
|
|||||||
Codec::Av1 => {
|
Codec::Av1 => {
|
||||||
cfg.encodeCodecConfig.av1Config.maxNumRefFramesInDPB = RFI_DPB;
|
cfg.encodeCodecConfig.av1Config.maxNumRefFramesInDPB = RFI_DPB;
|
||||||
}
|
}
|
||||||
|
Codec::PyroWave => unreachable!("PyroWave never opens the direct-NVENC backend"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(cfg)
|
Ok(cfg)
|
||||||
@@ -1014,6 +1020,9 @@ impl Encoder for NvencCudaEncoder {
|
|||||||
pic.codecPicParams.h264PicParams.seiPayloadArrayCnt = sei.len() as u32;
|
pic.codecPicParams.h264PicParams.seiPayloadArrayCnt = sei.len() as u32;
|
||||||
}
|
}
|
||||||
Codec::Av1 => {}
|
Codec::Av1 => {}
|
||||||
|
Codec::PyroWave => {
|
||||||
|
unreachable!("PyroWave never opens the direct-NVENC backend")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(api().encode_picture)(self.encoder, &mut pic)
|
(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::H264 => 0,
|
||||||
Codec::H265 => 1,
|
Codec::H265 => 1,
|
||||||
Codec::Av1 => 2,
|
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`.
|
//! The AV1 encode structs our pinned `ash 0.38` predates are vendored in `vk_av1_encode.rs`.
|
||||||
#![allow(clippy::too_many_arguments)]
|
#![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 crate::encode::{Codec, EncodedFrame, Encoder, EncoderCaps};
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
use ash::vk;
|
use ash::vk;
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use std::ffi::c_void;
|
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;
|
const NV12: vk::Format = vk::Format::G8_B8R8_2PLANE_420_UNORM;
|
||||||
/// Max resident dmabuf imports (comfortably above any PipeWire pool depth; imports alias existing
|
/// Max resident dmabuf imports (comfortably above any PipeWire pool depth; imports alias existing
|
||||||
@@ -700,81 +701,7 @@ impl VulkanVideoEncoder {
|
|||||||
cw: u32,
|
cw: u32,
|
||||||
ch: u32,
|
ch: u32,
|
||||||
) -> Result<(vk::Image, vk::DeviceMemory, vk::ImageView)> {
|
) -> Result<(vk::Image, vk::DeviceMemory, vk::ImageView)> {
|
||||||
let fmt = fourcc_to_vk(d.fourcc)
|
super::vk_util::import_rgb_dmabuf(&self.device, &self.ext_fd, &self.mem_props, d, cw, ch)
|
||||||
.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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Import a dmabuf, reusing a cached per-buffer import when the same underlying buffer recurs
|
/// 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 ----------
|
// ---------- 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 {
|
fn align_up(v: u64, a: u64) -> u64 {
|
||||||
v.div_ceil(a) * a
|
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(
|
unsafe fn make_video_image(
|
||||||
device: &ash::Device,
|
device: &ash::Device,
|
||||||
mp: &vk::PhysicalDeviceMemoryProperties,
|
mp: &vk::PhysicalDeviceMemoryProperties,
|
||||||
|
|||||||
@@ -913,6 +913,7 @@ fn codec_props(codec: Codec) -> CodecProps {
|
|||||||
intra_refresh: None,
|
intra_refresh: None,
|
||||||
ltr: 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
|
// 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
|
// (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
|
// The static KEY_FRAME_ALIGNED header-insertion mode already puts a sequence
|
||||||
// header OBU on every key frame; there is no per-surface twin.
|
// header OBU on every key frame; there is no per-surface twin.
|
||||||
Codec::Av1 => {}
|
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).
|
// 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::H264 => nv::NV_ENC_CODEC_H264_GUID,
|
||||||
Codec::H265 => nv::NV_ENC_CODEC_HEVC_GUID,
|
Codec::H265 => nv::NV_ENC_CODEC_HEVC_GUID,
|
||||||
Codec::Av1 => nv::NV_ENC_CODEC_AV1_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.
|
// H.264 has no tier; the preset default level is already autoselect.
|
||||||
Codec::H264 => {}
|
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
|
// 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);
|
cfg.encodeCodecConfig.hevcConfig.set_pixelBitDepthMinus8(2);
|
||||||
// 10 - 8
|
// 10 - 8
|
||||||
}
|
}
|
||||||
|
Codec::PyroWave => unreachable!("PyroWave never opens the direct-NVENC backend"),
|
||||||
Codec::Av1 => {
|
Codec::Av1 => {
|
||||||
cfg.encodeCodecConfig.av1Config.set_pixelBitDepthMinus8(2);
|
cfg.encodeCodecConfig.av1Config.set_pixelBitDepthMinus8(2);
|
||||||
// The input rides at its real depth; NVENC upconverts (mirrors the HEVC path).
|
// The input rides at its real depth; NVENC upconverts (mirrors the HEVC path).
|
||||||
@@ -864,6 +867,7 @@ impl NvencD3d11Encoder {
|
|||||||
av1.matrixCoefficients = mat;
|
av1.matrixCoefficients = mat;
|
||||||
av1.colorRange = 0; // studio/limited swing
|
av1.colorRange = 0; // studio/limited swing
|
||||||
}
|
}
|
||||||
|
Codec::PyroWave => unreachable!("PyroWave never opens the direct-NVENC backend"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -885,6 +889,7 @@ impl NvencD3d11Encoder {
|
|||||||
Codec::Av1 => {
|
Codec::Av1 => {
|
||||||
cfg.encodeCodecConfig.av1Config.maxNumRefFramesInDPB = RFI_DPB;
|
cfg.encodeCodecConfig.av1Config.maxNumRefFramesInDPB = RFI_DPB;
|
||||||
}
|
}
|
||||||
|
Codec::PyroWave => unreachable!("PyroWave never opens the direct-NVENC backend"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(cfg)
|
Ok(cfg)
|
||||||
@@ -1456,6 +1461,9 @@ impl Encoder for NvencD3d11Encoder {
|
|||||||
}
|
}
|
||||||
// AV1 mastering/CLL ride METADATA OBUs, not SEI — separate follow-up.
|
// AV1 mastering/CLL ride METADATA OBUs, not SEI — separate follow-up.
|
||||||
Codec::Av1 => {}
|
Codec::Av1 => {}
|
||||||
|
Codec::PyroWave => {
|
||||||
|
unreachable!("PyroWave never opens the direct-NVENC backend")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(api().encode_picture)(self.encoder, &mut pic)
|
(api().encode_picture)(self.encoder, &mut pic)
|
||||||
|
|||||||
@@ -873,6 +873,8 @@ fn parse_spike(args: &[String]) -> Result<Options> {
|
|||||||
Codec::H264 => "h264",
|
Codec::H264 => "h264",
|
||||||
Codec::H265 => "h265",
|
Codec::H265 => "h265",
|
||||||
Codec::Av1 => "obu",
|
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}"))
|
PathBuf::from(format!("/tmp/punktfunk-spike.{ext}"))
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -310,6 +310,8 @@ enum ApiCodec {
|
|||||||
H264,
|
H264,
|
||||||
H265,
|
H265,
|
||||||
Av1,
|
Av1,
|
||||||
|
/// PyroWave — the opt-in wired-LAN intra-only wavelet codec.
|
||||||
|
PyroWave,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Codec> for ApiCodec {
|
impl From<Codec> for ApiCodec {
|
||||||
@@ -318,6 +320,7 @@ impl From<Codec> for ApiCodec {
|
|||||||
Codec::H264 => ApiCodec::H264,
|
Codec::H264 => ApiCodec::H264,
|
||||||
Codec::H265 => ApiCodec::H265,
|
Codec::H265 => ApiCodec::H265,
|
||||||
Codec::Av1 => ApiCodec::Av1,
|
Codec::Av1 => ApiCodec::Av1,
|
||||||
|
Codec::PyroWave => ApiCodec::PyroWave,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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 }
|
||||||
@@ -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");
|
||||||
|
}
|
||||||
@@ -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
@@ -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
@@ -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()
|
||||||
|
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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();
|
||||||
|
};
|
||||||
|
}
|
||||||
+100
@@ -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; } }
|
||||||
+2
@@ -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)
|
||||||
+55
@@ -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;
|
||||||
|
};
|
||||||
|
}
|
||||||
+188
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
+81
@@ -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;
|
||||||
|
};
|
||||||
|
}
|
||||||
+258
@@ -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;
|
||||||
|
};
|
||||||
|
}
|
||||||
+27
@@ -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})
|
||||||
+308
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
+127
@@ -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())
|
||||||
+137
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
+34
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+133
@@ -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);
|
||||||
|
};
|
||||||
|
}
|
||||||
+53
@@ -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()
|
||||||
+20
@@ -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'
|
||||||
|
}
|
||||||
crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/android/external_layers/README.txt
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
Place {arm64-v8a,armeabi-v7a}/libVkLayer_*.so here to have it bundled in the APK.
|
||||||
crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/android/gradle/AndroidManifest.xml
Vendored
+28
@@ -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>
|
||||||
Vendored
+100
@@ -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$$
|
||||||
|
}
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
android.useAndroidX=true
|
||||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 4.9 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 2.9 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 7.9 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
include 'granite'
|
||||||
|
project(':granite').projectDir = file('$$GRANITE_ANDROID_ACTIVITY_PATH$$')
|
||||||
|
include '$$APP$$'
|
||||||
+5
@@ -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$$'
|
||||||
+26
@@ -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
|
||||||
|
}
|
||||||
+146
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
<resources>
|
||||||
|
</resources>
|
||||||
+7
@@ -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>
|
||||||
+1363
File diff suppressed because it is too large
Load Diff
+686
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+33
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+441
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+403
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+71
@@ -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();
|
||||||
|
}
|
||||||
+36
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+856
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+3964
File diff suppressed because it is too large
Load Diff
Vendored
+494
@@ -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
|
||||||
+1618
File diff suppressed because it is too large
Load Diff
+180
@@ -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 §ion : 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 §ion : 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)
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
Reference in New Issue
Block a user