Files
netris-nestri/apps/nescapture/src/config.rs
Wanjohi 6164e0c636 feat(nescapture): open the capture layer
A Vulkan implicit layer that captures frames from inside the workload's own
process and encodes them on the GPU they were drawn on. Fourth and last of this
batch, imported as a tree from `nestrilabs/nescapture` on the same terms.

Filed under `apps/` rather than `crates/` despite building a cdylib. The rule
here is what a thing *is*, not what it compiles to: this is a finished artefact
that gets installed into an image beside its layer manifest, not a library
another crate in this tree depends on. `crates/` is for the latter, and putting
this there would make the distinction useless the first time someone looked.

Wired to the workspace, `nesprotocol` by path. Its description named the
transport component; that reads better as what it actually is — where the frames
go — so it says that instead.

Whole workspace builds and tests: 21 across four members.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 18:04:02 +03:00

142 lines
4.8 KiB
Rust

// ─────────────────────────────────────────────────────────────────────────────
// config.rs — Phase 3: per-game shader hash configuration
//
// Loads a TOML config file that maps game executables to sets of shader hashes:
// hud_fragment_shaders — fragment shaders used for HUD/UI elements
// hud_vertex_shaders — vertex shaders used for HUD/UI elements
// skip_fragment_shaders — fragment shaders to silently drop (motion blur, etc.)
//
// Environment variables:
// HUDLESS_CONFIG=/path/to/config.toml — explicit config path
// HUDLESS_GAME_NAME=Game.exe — override game name detection
//
// Config format:
// [game."Control_DX11.exe"]
// hud_fragment_shaders = ["0xaabbccddeeff0011"]
// hud_vertex_shaders = ["0x1a2b3c4d5e6f7890"]
// skip_fragment_shaders = ["0x1122334455667788"]
// ─────────────────────────────────────────────────────────────────────────────
use serde::Deserialize;
use std::collections::HashSet;
use std::fs;
use std::path::Path;
/// Parsed shader hash sets for a single game.
#[derive(Clone, Debug, Default)]
pub struct ShaderHashSet {
pub hud_fragment_shaders: HashSet<u64>,
pub hud_vertex_shaders: HashSet<u64>,
pub skip_fragment_shaders: HashSet<u64>,
}
impl ShaderHashSet {
/// Check if the given vert/frag hash pair matches a HUD shader.
///
/// A HUD match occurs when:
/// - frag_hash is in hud_fragment_shaders, OR
/// - vert_hash is in hud_vertex_shaders (if frag not set)
pub fn is_hud_shader(&self, vert_hash: Option<u64>, frag_hash: Option<u64>) -> bool {
match frag_hash {
Some(fh) => self.hud_fragment_shaders.contains(&fh),
None => vert_hash.is_some_and(|vh| self.hud_vertex_shaders.contains(&vh)),
}
}
/// Check if the given frag_hash matches a skip shader.
pub fn is_skip_shader(&self, frag_hash: Option<u64>) -> bool {
if let Some(fh) = frag_hash {
return self.skip_fragment_shaders.contains(&fh);
}
false
}
}
/// TOML deserialization structure.
#[derive(Deserialize)]
struct ConfigFile {
game: Option<std::collections::HashMap<String, GameConfig>>,
}
#[derive(Deserialize)]
struct GameConfig {
hud_fragment_shaders: Option<Vec<String>>,
hud_vertex_shaders: Option<Vec<String>>,
skip_fragment_shaders: Option<Vec<String>>,
}
/// Parse a hex string like "0xaabbccddeeff0011" into a u64.
fn parse_hex(s: &str) -> Option<u64> {
let trimmed = s.trim().trim_start_matches("0x").trim_start_matches("0X");
u64::from_str_radix(trimmed, 16).ok()
}
/// Load the config from the given path and return the ShaderHashSet for the
/// current game.
pub fn load_config(path: &Path) -> Option<ShaderHashSet> {
let content = match fs::read_to_string(path) {
Ok(c) => c,
Err(e) => {
log::warn!("failed to read config {:?}: {}", path, e);
return None;
}
};
let config: ConfigFile = match toml::from_str(&content) {
Ok(c) => c,
Err(e) => {
log::warn!("failed to parse config {:?}: {}", path, e);
return None;
}
};
let game_name = std::env::var("NESCAPTURE_GAME_NAME").ok()?;
let games = config.game.as_ref()?;
let game_config = games.get(&game_name)?;
let mut set = ShaderHashSet::default();
if let Some(hashes) = &game_config.hud_fragment_shaders {
for h in hashes {
if let Some(v) = parse_hex(h) {
set.hud_fragment_shaders.insert(v);
}
}
}
if let Some(hashes) = &game_config.hud_vertex_shaders {
for h in hashes {
if let Some(v) = parse_hex(h) {
set.hud_vertex_shaders.insert(v);
}
}
}
if let Some(hashes) = &game_config.skip_fragment_shaders {
for h in hashes {
if let Some(v) = parse_hex(h) {
set.skip_fragment_shaders.insert(v);
}
}
}
log::info!(
"loaded config for '{}' — {} hud_frag, {} hud_vert, {} skip_frag",
game_name,
set.hud_fragment_shaders.len(),
set.hud_vertex_shaders.len(),
set.skip_fragment_shaders.len(),
);
Some(set)
}
/// Resolve the config file path from HUDLESS_CONFIG env var, or fall back to
/// a default location relative to the game executable.
pub fn resolve_config_path() -> Option<std::path::PathBuf> {
if let Ok(path) = std::env::var("NESCAPTURE_CONFIG") {
return Some(std::path::PathBuf::from(path));
}
None
}