Compare commits

...
Author SHA1 Message Date
Ludovic MarieandClaude Opus 5.5 1538d95440 Pipeline cache: owned devices
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-02 21:11:13 +01:00
Ludovic MarieandClaude Opus 5.5 7d3d0f143e Pipeline cache: the unsafe call in horizon-hdr
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-02 21:03:33 +01:00
Ludovic MarieandClaude Opus 5.5 d86ce5a443 Keep Vulkan's compiled pipelines on disk between runs
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-02 20:57:04 +01:00
32 changed files with 526 additions and 53 deletions

No files matched your search

+15 -11
View File
@@ -157,6 +157,7 @@ pub mod pack_uniforms;
mod pack_weather;
mod page_runs;
pub mod panorama;
pub(crate) mod pipeline_cache;
mod readback;
mod resident;
mod reuse;
@@ -1771,8 +1772,8 @@ async fn open_backend(
if let Err(e) = check_layers(&adapter, layers) {
return Err(NoDevice::Failed(format!("{name}: {e}")));
}
// Mappable primary buffers: section pages that no longer fit in
// video memory go to system memory. Shader packs' 16-bit normalized buffers (render
// Vulkan's pipeline cache (gfx/pipeline_cache.rs). Mappable primary buffers: section
// pages that no longer fit in video memory go to system memory. Shader packs' 16-bit normalized buffers (render
// targets with the adapter's own format features) and R11F_G11F_B10F: a float
// stand-in neither clamps nor drops NaN. Subgroups: shader packs' compute programs
// (`MC_GL_KHR_shader_subgroup_*`); storage buffers in vertex shaders, clearing and
@@ -1789,7 +1790,8 @@ async fn open_backend(
| wgpu::Features::SUBGROUP
| wgpu::Features::VERTEX_WRITABLE_STORAGE
| wgpu::Features::CLEAR_TEXTURE
| wgpu::Features::TEXTURE_ATOMIC));
| wgpu::Features::TEXTURE_ATOMIC
| wgpu::Features::PIPELINE_CACHE));
let driver = format!("{} {}", info.driver, info.driver_info);
startup_step(format_args!(
"requesting the device (driver {}) with features [{features}] and the adapter's \
@@ -2027,6 +2029,8 @@ impl Renderer {
open_device(&window, offscreen, api, layers).await?
}
};
// Before the first pipeline: Vulkan reads back what it compiled last run.
pipeline_cache::open(&adapter, &device, &crate::logging::game_home());
// Computed once: the sprite arrays, the shaders' constants and the
// shader pack atlas all cut the sprites by it.
let split = check_layers(&adapter, layers)?;
@@ -2271,7 +2275,7 @@ impl Renderer {
})],
}),
multiview_mask: None,
cache: None,
cache: crate::gfx::pipeline_cache::get(&device),
})
};
let pipeline = |label: &str,
@@ -2463,7 +2467,7 @@ impl Renderer {
})],
}),
multiview_mask: None,
cache: None,
cache: crate::gfx::pipeline_cache::get(&device),
});
let lines = pipeline(
"Block outline",
@@ -3124,7 +3128,7 @@ impl Renderer {
})],
}),
multiview_mask: None,
cache: None,
cache: crate::gfx::pipeline_cache::get(&device),
})
};
let entity_pipeline = |label, cull_mode, depth_write, compare, bias, blend, clouds| {
@@ -3296,7 +3300,7 @@ impl Renderer {
})],
}),
multiview_mask: None,
cache: None,
cache: crate::gfx::pipeline_cache::get(&device),
});
// RenderPipelines.ENERGY_SWIRL: BlendFunction.ADDITIVE (one, one), no culling; no depth
// writes either: vanilla sorts the quads far to near, so every face passes the test.
@@ -3396,7 +3400,7 @@ impl Renderer {
})],
}),
multiview_mask: None,
cache: None,
cache: crate::gfx::pipeline_cache::get(&device),
};
let additive_pipeline = kept.create_render_pipeline(&additive_descriptor);
// `RenderPipelines.DRAGON_RAYS` uses DepthStencilState.DEFAULT:
@@ -3492,7 +3496,7 @@ impl Renderer {
})],
}),
multiview_mask: None,
cache: None,
cache: crate::gfx::pipeline_cache::get(&device),
});
drop(entity_step);
// WorldBorderRenderer: misc/forcefield.png as a MipmappedTexture
@@ -3662,7 +3666,7 @@ impl Renderer {
})],
}),
multiview_mask: None,
cache: None,
cache: crate::gfx::pipeline_cache::get(&device),
});
let screen_shader = kept.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Horizon screen effects"),
@@ -3734,7 +3738,7 @@ impl Renderer {
})],
}),
multiview_mask: None,
cache: None,
cache: crate::gfx::pipeline_cache::get(&device),
})
});
let screen_capacity = 64;
@@ -417,7 +417,7 @@ fn pipeline(device: &wgpu::Device) -> Option<(wgpu::ComputePipeline, wgpu::BindG
module: &module,
entry_point: Some("animate"),
compilation_options: Default::default(),
cache: None,
cache: crate::gfx::pipeline_cache::get(device),
});
let layout = pipeline.get_bind_group_layout(0);
let internal = pollster::block_on(internal.pop());
@@ -1013,7 +1013,7 @@ mod tests {
targets: &[Some(wgpu::TextureFormat::Rgba8Unorm.into())],
}),
multiview_mask: None,
cache: None,
cache: crate::gfx::pipeline_cache::get(&device),
})
};
let mut flat = flat::Flat::new(&device, &terrain_layout, 4 * GRAIN, 0, pipeline);
+1 -1
View File
@@ -308,7 +308,7 @@ impl Cull {
targets,
}),
multiview_mask: None,
cache: None,
cache: crate::gfx::pipeline_cache::get(device),
});
Self {
pipeline,
@@ -67,7 +67,7 @@ impl EntityOutline {
targets: &[Some(FORMAT.into())],
}),
multiview_mask: None,
cache: None,
cache: crate::gfx::pipeline_cache::get(device),
});
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Entity outline"),
@@ -119,7 +119,7 @@ impl EntityOutline {
targets: &[Some(target)],
}),
multiview_mask: None,
cache: None,
cache: crate::gfx::pipeline_cache::get(device),
})
};
// BlendFunction.ENTITY_OUTLINE_BLIT over the colour only (write mask 7).
@@ -239,7 +239,7 @@ impl EntitySkin {
module: &module,
entry_point: Some("skin"),
compilation_options: Default::default(),
cache: None,
cache: crate::gfx::pipeline_cache::get(device),
});
let layout = pipeline.get_bind_group_layout(0);
let internal = pollster::block_on(internal.pop());
+2 -2
View File
@@ -396,7 +396,7 @@ impl Flat {
module: &module,
entry_point: Some(entry),
compilation_options: Default::default(),
cache: None,
cache: crate::gfx::pipeline_cache::get(device),
})
};
let flatten = compute_pipeline(&compute_layout, "flatten");
@@ -1172,7 +1172,7 @@ mod tests {
module: &module,
entry_point: Some(entry),
compilation_options: Default::default(),
cache: None,
cache: crate::gfx::pipeline_cache::get(&device),
})
};
let (flatten, flatten_seen) = (pipeline("flatten"), pipeline("flatten_seen"));
@@ -290,7 +290,7 @@ impl Interpolation {
module: &module,
entry_point: Some(entry),
compilation_options: Default::default(),
cache: None,
cache: crate::gfx::pipeline_cache::get(device),
})
};
let view = |t: wgpu::Texture| t.create_view(&Default::default());
@@ -77,7 +77,7 @@ impl ObjectMotion {
targets: &[Some(FORMAT.into())],
}),
multiview_mask: None,
cache: None,
cache: crate::gfx::pipeline_cache::get(device),
});
Self {
pipeline,
@@ -175,7 +175,7 @@ impl OpticalFlow {
module: &module,
entry_point: Some(entry),
compilation_options: Default::default(),
cache: None,
cache: crate::gfx::pipeline_cache::get(device),
})
};
let pipelines = Pipelines {
+2 -2
View File
@@ -224,7 +224,7 @@ impl Hdr {
})],
}),
multiview_mask: None,
cache: None,
cache: crate::gfx::pipeline_cache::get(device),
})
};
let alpha_pass = |label, blend| {
@@ -251,7 +251,7 @@ impl Hdr {
})],
}),
multiview_mask: None,
cache: None,
cache: crate::gfx::pipeline_cache::get(device),
})
};
let clear_alpha = alpha_pass("Horizon HDR UI mask", None);
@@ -89,7 +89,7 @@ impl EmissionStats {
})],
}),
multiview_mask: None,
cache: None,
cache: crate::gfx::pipeline_cache::get(device),
});
let params = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Horizon HDR emission threshold parameters"),
@@ -418,7 +418,7 @@ impl NativeMaterial {
},
}),
multiview_mask: None,
cache: None,
cache: crate::gfx::pipeline_cache::get(device),
})
};
let blend = Some(wgpu::BlendState::ALPHA_BLENDING);
@@ -743,7 +743,7 @@ impl NativePost {
})],
}),
multiview_mask: None,
cache: None,
cache: crate::gfx::pipeline_cache::get(device),
});
if let Some(e) = pollster::block_on(scope.pop()) {
return Err(format!("{}: {e}", stage.file));
@@ -394,7 +394,7 @@ fn entity_pipeline(
targets: &[],
}),
multiview_mask: None,
cache: None,
cache: crate::gfx::pipeline_cache::get(device),
})
}
@@ -565,7 +565,7 @@ impl NativeShadow {
targets: if translucent { &color_target } else { &[] },
}),
multiview_mask: None,
cache: None,
cache: crate::gfx::pipeline_cache::get(device),
})
};
let instanced = layout(None);
@@ -87,7 +87,7 @@ fn pipeline(
})],
}),
multiview_mask: None,
cache: None,
cache: crate::gfx::pipeline_cache::get(device),
})
}
+2 -2
View File
@@ -299,7 +299,7 @@ impl Oit {
targets,
}),
multiview_mask: None,
cache: None,
cache: crate::gfx::pipeline_cache::get(device),
})
};
let terrain_pipeline = |label, fragment, stage| {
@@ -468,7 +468,7 @@ impl Oit {
targets: &stage_targets(stage),
}),
multiview_mask: None,
cache: None,
cache: crate::gfx::pipeline_cache::get(device),
})
};
let cloud_pipelines = [
@@ -141,7 +141,7 @@ impl Features {
targets: &stage_targets(stage),
}),
multiview_mask: None,
cache: None,
cache: crate::gfx::pipeline_cache::get(device),
})
})
};
@@ -100,7 +100,7 @@ impl CenterDepth {
targets: &[Some(format.into())],
}),
multiview_mask: None,
cache: None,
cache: crate::gfx::pipeline_cache::get(device),
});
let texel = device.create_texture(&wgpu::TextureDescriptor {
label: Some("Shader pack centre depth"),
@@ -823,7 +823,7 @@ fn helper(
targets,
}),
multiview_mask: None,
cache: None,
cache: crate::gfx::pipeline_cache::get(device),
});
(pipeline, layout)
}
@@ -1499,7 +1499,7 @@ fn build_pass(
targets: &targets,
}),
multiview_mask: None,
cache: None,
cache: crate::gfx::pipeline_cache::get(device),
});
(layout, pipeline)
});
@@ -1633,7 +1633,7 @@ fn build_compute(
module: &module,
entry_point: Some("main"),
compilation_options: Default::default(),
cache: None,
cache: crate::gfx::pipeline_cache::get(device),
});
(layout, pipeline)
});
@@ -282,7 +282,7 @@ impl Panorama {
})],
}),
multiview_mask: None,
cache: None,
cache: crate::gfx::pipeline_cache::get(device),
});
Self {
pipeline,
@@ -0,0 +1,449 @@
//! The pipelines a Vulkan driver compiled, kept on disk between runs.
//!
//! A Vulkan driver turns each pipeline's shaders into the GPU's own code when
//! the pipeline is created; a `VkPipelineCache` made from an earlier run's
//! bytes lets it skip that. Metal keeps its own cache per user, and DX12's
//! compiled shaders are kept by wgpu-hal's `dx12::disk_cache`
//! ([`super::dx12_shader_cache`]), so only Vulkan has a file here.
//!
//! One file per adapter, driver and Horizon build, in the game folder:
//! `cache/wgpu_pipeline_cache_vulkan_{vendor}_{device}-{hash}.bin`, the hash
//! covering the driver's name and version and the build ([`file_name`]). A
//! save replaces the file at once (a temporary file, then a rename) and
//! removes the same adapter's files of other drivers or builds. The file is
//! [`MAGIC`], a checksum, then the bytes `PipelineCache::get_data` gave; wgpu
//! checks its own header in them (the adapter, the driver's cache UUID,
//! wgpu's version) when the cache is created. A file that is missing, short,
//! fails the checksum or wgpu's checks is ignored: the pipelines compile as
//! they would without it, and the next save replaces it.
//! `HORIZON_PIPELINE_CACHE=0` turns the cache off.
use std::{
fs, io,
path::{Path, PathBuf},
sync::OnceLock,
time::Duration,
};
const MAGIC: &[u8; 8] = b"HZVKPC1\n";
/// A cache past this is not saved: Horizon's pipelines take a few MB.
const MAX_BYTES: usize = 256 << 20;
/// When the cache is first saved (the menus' and the world's pipelines are
/// made by then), and how often after that if it grew.
const FIRST_SAVE: Duration = Duration::from_secs(10);
const SAVE_EVERY: Duration = Duration::from_secs(60);
struct Open {
device: wgpu::Device,
cache: wgpu::PipelineCache,
path: PathBuf,
}
static OPEN: OnceLock<Open> = OnceLock::new();
/// The cache to create `device`'s pipelines with, once [`open`] made one.
pub fn get(device: &wgpu::Device) -> Option<&'static wgpu::PipelineCache> {
OPEN.get()
.filter(|open| open.device == *device)
.map(|open| &open.cache)
}
/// Makes the pipeline cache of this run's device from the file an earlier
/// run saved in `home`, and saves it there from a background thread as it
/// grows. Does nothing off Vulkan, when `HORIZON_PIPELINE_CACHE=0`, or after
/// the first call.
pub fn open(adapter: &wgpu::Adapter, device: &wgpu::Device, home: &Path) {
if OPEN.get().is_some()
|| !device.features().contains(wgpu::Features::PIPELINE_CACHE)
|| !enabled(std::env::var("HORIZON_PIPELINE_CACHE").ok().as_deref())
{
return;
}
let Some(name) = file_name(&adapter.get_info(), &build()) else {
return;
};
let path = home.join("cache").join(name);
let data = read(&path);
let Some(cache) = cache_from(device, data.as_deref()) else {
return;
};
let loaded = cache.get_data().map_or(0, |data| data.len());
log::info!(
"Horizon: pipeline cache {} ({})",
path.display(),
match &data {
Some(data) => format!("{} KiB read", data.len() >> 10),
None => "new".to_owned(),
}
);
if OPEN
.set(Open {
device: device.clone(),
cache,
path,
})
.is_err()
{
return;
}
let spawned = std::thread::Builder::new()
.name("Pipeline cache".into())
.spawn(move || keep_saved(OPEN.get().expect("set above"), loaded));
if let Err(e) = spawned {
log::warn!("Horizon: pipeline cache not saved: {e}");
}
}
/// `HORIZON_PIPELINE_CACHE`: on unless `0`, `off`, `false` or `no`.
fn enabled(env: Option<&str>) -> bool {
!env.is_some_and(|v| {
matches!(
v.trim().to_ascii_lowercase().as_str(),
"0" | "off" | "false" | "no"
)
})
}
/// The cache file's name for `info`'s adapter and driver and this `build`:
/// wgpu's key (vendor and device), then a hash of the driver's name and
/// version and the build. None off Vulkan.
fn file_name(info: &wgpu::AdapterInfo, build: &str) -> Option<String> {
let key = wgpu::util::pipeline_cache_key(info)?;
let mut hash = FNV_OFFSET;
for part in [info.driver.as_str(), info.driver_info.as_str(), build] {
hash = fnv1a(hash, part.as_bytes());
hash = fnv1a(hash, &[0]);
}
Some(format!("{key}-{hash:016x}.bin"))
}
/// This build: its version and commit, or for a build from source the
/// executable's time, so a rebuilt game starts a new file.
fn build() -> String {
let mut build = horizon_core::release::VERSION.to_owned();
if let Some(nightly) = horizon_core::release::NIGHTLY {
build += nightly;
}
match option_env!("HORIZON_COMMIT") {
Some(commit) => build += commit,
None => {
let built = std::env::current_exe()
.and_then(|exe| exe.metadata()?.modified())
.ok()
.and_then(|time| time.duration_since(std::time::UNIX_EPOCH).ok());
if let Some(built) = built {
build += &format!(" {}", built.as_nanos());
}
}
}
build
}
/// The cache made from `data`, or if wgpu refuses `data` an empty one; None
/// (logged) if the device makes neither.
fn cache_from(device: &wgpu::Device, data: Option<&[u8]>) -> Option<wgpu::PipelineCache> {
create(device, data).or_else(|| data.and_then(|_| create(device, None)))
}
/// The cache, or None (logged) if the device refused to make it: with
/// `fallback`, wgpu still refuses data saved for another adapter.
fn create(device: &wgpu::Device, data: Option<&[u8]>) -> Option<wgpu::PipelineCache> {
let validation = device.push_error_scope(wgpu::ErrorFilter::Validation);
let memory = device.push_error_scope(wgpu::ErrorFilter::OutOfMemory);
let internal = device.push_error_scope(wgpu::ErrorFilter::Internal);
// `data` is what `get_data` gave in an earlier run, behind a checksum ([`read`]).
let cache = horizon_hdr::pipeline_cache(device, data);
let error = [internal, memory, validation]
.into_iter()
.find_map(|scope| pollster::block_on(scope.pop()));
match error {
Some(e) => {
log::warn!(
"Horizon: pipeline cache {}: {e}",
if data.is_some() {
"file not used"
} else {
"off"
}
);
None
}
None => Some(cache),
}
}
/// Saves `open`'s cache whenever it grew past `saved` bytes.
fn keep_saved(open: &Open, mut saved: usize) {
std::thread::sleep(FIRST_SAVE);
loop {
if let Some(data) = open.cache.get_data()
&& data.len() > saved
&& data.len() <= MAX_BYTES
{
match save(&open.path, &data) {
Ok(()) => saved = data.len(),
Err(e) => log::warn!("Horizon: pipeline cache {}: {e}", open.path.display()),
}
}
std::thread::sleep(SAVE_EVERY);
}
}
/// The cache bytes saved at `path`, or None if there are none or they are
/// not whole.
fn read(path: &Path) -> Option<Vec<u8>> {
let mut bytes = match fs::read(path) {
Ok(bytes) => bytes,
Err(e) if e.kind() == io::ErrorKind::NotFound => return None,
Err(e) => {
log::warn!("Horizon: pipeline cache {} not read: {e}", path.display());
return None;
}
};
let whole = bytes.strip_prefix(MAGIC.as_slice()).and_then(|rest| {
let (check, data) = rest.split_first_chunk::<8>()?;
(u64::from_le_bytes(*check) == fnv1a(FNV_OFFSET, data)).then_some(())
});
if whole.is_none() {
log::warn!(
"Horizon: pipeline cache {} ignored: not a whole cache",
path.display()
);
return None;
}
bytes.drain(..MAGIC.len() + 8);
Some(bytes)
}
/// Writes `data` to `path` at once (a temporary file renamed over it), then
/// removes the files of the same adapter's other drivers and builds.
fn save(path: &Path, data: &[u8]) -> io::Result<()> {
let dir = path.parent().unwrap_or(Path::new("."));
fs::create_dir_all(dir)?;
let mut bytes = Vec::with_capacity(MAGIC.len() + 8 + data.len());
bytes.extend_from_slice(MAGIC);
bytes.extend_from_slice(&fnv1a(FNV_OFFSET, data).to_le_bytes());
bytes.extend_from_slice(data);
let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("cache");
let temp = dir.join(format!("{name}.{}.tmp", std::process::id()));
fs::write(&temp, &bytes)?;
if let Err(e) = fs::rename(&temp, path) {
let _ = fs::remove_file(&temp);
return Err(e);
}
// `{key}-{hash}.bin`: the other hashes of this key are stale.
if let Some((key, _)) = name.rsplit_once('-')
&& let Ok(entries) = fs::read_dir(dir)
{
for entry in entries.flatten() {
let other = entry.file_name();
let Some(other) = other.to_str() else {
continue;
};
if other != name
&& other.ends_with(".bin")
&& other.rsplit_once('-').is_some_and(|(k, _)| k == key)
{
let _ = fs::remove_file(entry.path());
}
}
}
Ok(())
}
const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
fn fnv1a(mut hash: u64, bytes: &[u8]) -> u64 {
for &b in bytes {
hash ^= u64::from(b);
hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
}
hash
}
#[cfg(test)]
mod tests {
use super::*;
fn vulkan(vendor: u32, device: u32, driver_info: &str) -> wgpu::AdapterInfo {
wgpu::AdapterInfo {
name: "GPU".into(),
vendor,
device,
driver: "NVIDIA".into(),
driver_info: driver_info.into(),
..wgpu::AdapterInfo::new(wgpu::DeviceType::DiscreteGpu, wgpu::Backend::Vulkan)
}
}
fn scratch(name: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!(
"horizon-pipeline-cache-{name}-{}",
std::process::id()
));
let _ = fs::remove_dir_all(&dir);
dir
}
#[test]
fn the_file_is_named_by_adapter_driver_and_build() {
let nvidia = vulkan(0x10de, 0x2684, "580.95");
let name = file_name(&nvidia, "0.4.1").unwrap();
assert!(
name.starts_with("wgpu_pipeline_cache_vulkan_4318_9860-") && name.ends_with(".bin"),
"{name}"
);
assert_eq!(file_name(&nvidia, "0.4.1").unwrap(), name);
let others = [
file_name(&vulkan(0x10de, 0x2684, "581.08"), "0.4.1").unwrap(),
file_name(&nvidia, "0.4.2").unwrap(),
file_name(&vulkan(0x10de, 0x2704, "580.95"), "0.4.1").unwrap(),
file_name(&vulkan(0x1002, 0x2684, "580.95"), "0.4.1").unwrap(),
];
for other in &others {
assert_ne!(other, &name);
}
// A new driver or build keeps the adapter's part, so its save
// replaces the old file.
for other in &others[..2] {
assert_eq!(
other.rsplit_once('-').unwrap().0,
name.rsplit_once('-').unwrap().0
);
}
for backend in [wgpu::Backend::Metal, wgpu::Backend::Dx12, wgpu::Backend::Gl] {
let info = wgpu::AdapterInfo {
backend,
..nvidia.clone()
};
assert_eq!(file_name(&info, "0.4.1"), None, "{backend:?}");
}
}
#[test]
fn the_cache_turns_off_with_its_variable() {
for off in ["0", "off", "OFF", "false", "no", " 0 "] {
assert!(!enabled(Some(off)), "{off}");
}
for on in [None, Some("1"), Some(""), Some("on")] {
assert!(enabled(on), "{on:?}");
}
}
#[test]
fn a_saved_cache_reads_back_and_a_damaged_one_is_ignored() {
let dir = scratch("damaged");
let path = dir.join("wgpu_pipeline_cache_vulkan_1_2-00.bin");
assert_eq!(read(&path), None, "no file yet");
let data: Vec<u8> = (0..5000u32).map(|i| (i * 7 % 251) as u8).collect();
save(&path, &data).unwrap();
assert_eq!(read(&path).as_deref(), Some(data.as_slice()));
assert!(
fs::read_dir(&dir).unwrap().count() == 1,
"no temporary file left"
);
let whole = fs::read(&path).unwrap();
let mut flipped = whole.clone();
flipped[MAGIC.len() + 8 + 1234] ^= 0x10;
let mut other_magic = whole.clone();
other_magic[0] = b'X';
for damaged in [
whole[..whole.len() - 1].to_vec(),
whole[..MAGIC.len() + 4].to_vec(),
Vec::new(),
flipped,
other_magic,
] {
fs::write(&path, &damaged).unwrap();
assert_eq!(read(&path), None, "{} bytes", damaged.len());
}
fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn a_save_removes_the_adapters_files_of_other_builds() {
let dir = scratch("stale");
let old = dir.join("wgpu_pipeline_cache_vulkan_4318_9860-0000000000000001.bin");
let other_gpu = dir.join("wgpu_pipeline_cache_vulkan_4098_9860-0000000000000001.bin");
let new = dir.join("wgpu_pipeline_cache_vulkan_4318_9860-0000000000000002.bin");
save(&old, b"old").unwrap();
save(&other_gpu, b"other").unwrap();
fs::write(dir.join("dx12-shaders.bin"), b"dx12").unwrap();
save(&new, b"new").unwrap();
assert!(!old.exists());
assert_eq!(read(&new).as_deref(), Some(b"new".as_slice()));
assert_eq!(read(&other_gpu).as_deref(), Some(b"other".as_slice()));
assert!(dir.join("dx12-shaders.bin").exists());
fs::remove_dir_all(&dir).unwrap();
}
/// On a Vulkan device (lavapipe on CI's Linux runners): a cache saved
/// from one device makes the next one's, and bytes wgpu rejects make an
/// empty cache instead of an error. Skipped without Vulkan.
#[test]
fn a_vulkan_cache_round_trips_and_bad_bytes_fall_back() {
let mut descriptor = wgpu::InstanceDescriptor::new_without_display_handle();
descriptor.backends = wgpu::Backends::VULKAN;
let instance = wgpu::Instance::new(descriptor);
let Ok(adapter) = pollster::block_on(instance.request_adapter(&Default::default())) else {
return;
};
if !adapter.features().contains(wgpu::Features::PIPELINE_CACHE) {
return;
}
let device = || {
pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
required_features: wgpu::Features::PIPELINE_CACHE,
..Default::default()
}))
.unwrap()
.0
};
let first = device();
let cache = create(&first, None).unwrap();
let shader = first.create_shader_module(wgpu::ShaderModuleDescriptor {
label: None,
source: wgpu::ShaderSource::Wgsl("@compute @workgroup_size(1) fn main() {}".into()),
});
first.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: None,
layout: None,
module: &shader,
entry_point: None,
compilation_options: Default::default(),
cache: Some(&cache),
});
let data = cache.get_data().unwrap();
let dir = scratch("vulkan");
let path = dir.join(file_name(&adapter.get_info(), "test").unwrap());
save(&path, &data).unwrap();
let second = device();
let read_back = read(&path).unwrap();
assert_eq!(read_back, data);
let cache = create(&second, Some(&read_back)).expect("the saved cache is used");
assert!(cache.get_data().unwrap().len() >= data.len() / 2);
// wgpu's header: the adapter's key at 17..32 (a refusal even with
// `fallback`), the driver's validation key at 32..48 (outdated), then
// the data's length (truncated).
let mut other_gpu = read_back.clone();
other_gpu[20] ^= 0x5a;
assert!(create(&second, Some(&other_gpu)).is_none());
let mut other_driver = read_back.clone();
other_driver[40] ^= 0x5a;
for bad in [
&other_gpu[..],
&other_driver[..],
&read_back[..read_back.len() / 2],
b"junk",
] {
let cache = cache_from(&second, Some(bad)).expect("an empty cache instead");
assert!(
cache.get_data().unwrap().len() < data.len(),
"{} bytes",
bad.len()
);
}
fs::remove_dir_all(&dir).unwrap();
}
}
@@ -146,7 +146,7 @@ impl Stars {
})],
}),
multiview_mask: None,
cache: None,
cache: crate::gfx::pipeline_cache::get(device),
});
Self {
pipeline,
@@ -123,7 +123,7 @@ impl GpuTimer {
module: &module,
entry_point: Some("mark"),
compilation_options: Default::default(),
cache: None,
cache: crate::gfx::pipeline_cache::get(device),
}),
module,
end: None,
@@ -300,7 +300,7 @@ pub(super) fn end_pipeline(
})],
}),
multiview_mask: None,
cache: None,
cache: crate::gfx::pipeline_cache::get(device),
})
}
@@ -70,7 +70,7 @@ impl Weather {
})],
}),
multiview_mask: None,
cache: None,
cache: crate::gfx::pipeline_cache::get(device),
});
let uniform = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Weather time"),
@@ -304,7 +304,7 @@ pub(super) fn native_pipeline(
})],
}),
multiview_mask: None,
cache: None,
cache: crate::gfx::pipeline_cache::get(device),
});
if let Some(e) = pollster::block_on(scope.pop()) {
return Err(format!("{}: {e}", stage.file));
@@ -472,7 +472,7 @@ struct Case { animation: vec4<f32>, bases: vec4<u32>, time: vec4<f32>, uv: vec2<
module: &shader,
entry_point: Some("main"),
compilation_options: Default::default(),
cache: None,
cache: crate::gfx::pipeline_cache::get(&device),
});
let input = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: None,
@@ -143,7 +143,7 @@ impl Post {
})],
}),
multiview_mask: None,
cache: None,
cache: crate::gfx::pipeline_cache::get(device),
})
};
// The OpenGL backend's GLSL translator does not support textureLoad
@@ -205,7 +205,7 @@ impl Post {
})],
}),
multiview_mask: None,
cache: None,
cache: crate::gfx::pipeline_cache::get(device),
});
let internal = pollster::block_on(internal.pop());
match internal.or(pollster::block_on(validation.pop())) {
+1 -1
View File
@@ -2281,7 +2281,7 @@ impl HudRenderer {
})],
}),
multiview_mask: None,
cache: None,
cache: crate::gfx::pipeline_cache::get(device),
})
};
let pipeline = create("HUD", wgpu::BlendState::ALPHA_BLENDING, "fs_main", false);
@@ -408,7 +408,7 @@ impl CitGlintPass {
})],
}),
multiview_mask: None,
cache: None,
cache: crate::gfx::pipeline_cache::get(device),
})
})
.collect();
@@ -196,7 +196,7 @@ impl PackText {
})],
}),
multiview_mask: None,
cache: None,
cache: crate::gfx::pipeline_cache::get(device),
});
if let Some(e) = pollster::block_on(scope.pop()) {
log::warn!("the resource pack's text shader is not used: {e}");
@@ -110,7 +110,7 @@ impl MenuBlur {
depth_stencil: None,
multisample: Default::default(),
multiview_mask: None,
cache: None,
cache: crate::gfx::pipeline_cache::get(device),
});
// The post chain's "bilinear": true inputs, clamped like render targets.
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
@@ -429,7 +429,7 @@ impl Translated {
depth_stencil: None,
multisample: Default::default(),
multiview_mask: None,
cache: None,
cache: crate::gfx::pipeline_cache::get(device),
});
self.pipelines.insert(format, pipeline);
}
@@ -773,7 +773,7 @@ impl PostEffects {
depth_stencil: None,
multisample: Default::default(),
multiview_mask: None,
cache: None,
cache: crate::gfx::pipeline_cache::get(device),
})
})
}
+21 -1
View File
@@ -1,7 +1,27 @@
//! HDR metadata for the swapchain: the Vulkan calls the game crate, which
//! forbids unsafe code, can't make itself. wgpu sets no HDR metadata, and
//! without it a compositor may clip or tone-map everything above 1.0 (80 nits)
//! of an scRGB swapchain.
//! of an scRGB swapchain. Also the pipeline cache made from an earlier run's
//! bytes ([`pipeline_cache`]).
/// A pipeline cache from `data`, the bytes `PipelineCache::get_data` gave for
/// this adapter in an earlier run (None: an empty cache). wgpu checks their
/// header (adapter, driver's cache UUID, wgpu's version) and makes an empty
/// cache from bytes that are outdated or cut short; bytes of another adapter
/// are a validation error on `device`.
pub fn pipeline_cache(device: &wgpu::Device, data: Option<&[u8]>) -> wgpu::PipelineCache {
// SAFETY: wgpu asks for bytes `get_data` returned for a compatible adapter;
// the caller keeps them unchanged on disk behind a checksum, wgpu rejects a
// header of another adapter or driver, and `fallback` turns outdated ones
// into an empty cache.
unsafe {
device.create_pipeline_cache(&wgpu::PipelineCacheDescriptor {
label: Some("Horizon pipeline cache"),
data,
fallback: true,
})
}
}
#[cfg(target_os = "linux")]
pub mod wayland;