Files
netris-nestri/apps/nescapture/src/discovery.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

166 lines
4.9 KiB
Rust

// ─────────────────────────────────────────────────────────────────────────────
// discovery.rs — Phase 6: per-draw logging for shader hash discovery
//
// When HUDLESS_DISCOVER=1 is set, every vkCmdDraw* call is logged to:
// /tmp/hudless_discover_$EXE.log
//
// Log format:
// frame=0001 draw=00042 vert=0x1a2b3c4d5e6f7890 frag=0xaabbccddeeff0011 verts=6 blend=true depth=false
//
// HUD suspect heuristics:
// - blend=true && depth_test=false → strong suspect (UI quads)
// - verts <= 6 → two triangles = one quad
// - Same pipeline reused many times per frame at low vertex count
// ─────────────────────────────────────────────────────────────────────────────
use crate::state::{CB_STATE, DEVICE_STATE};
use ash::vk::{self, Handle};
use std::fs::OpenOptions;
use std::io::Write;
use std::sync::Mutex;
static LOG_FILE: Mutex<Option<std::fs::File>> = Mutex::new(None);
pub fn is_discovery_mode() -> bool {
std::env::var("NESCAPTURE_DISCOVER")
.map(|v| v == "1")
.unwrap_or(false)
}
fn ensure_log_file() -> bool {
let mut guard = LOG_FILE.lock().unwrap();
if guard.is_some() {
return true;
}
let exe_name = std::env::var("NESCAPTURE_GAME_NAME").unwrap_or_else(|_| {
std::fs::read_link("/proc/self/exe")
.ok()
.and_then(|p| p.file_name().map(|n| n.to_string_lossy().into_owned()))
.unwrap_or_else(|| "unknown".to_string())
});
let path = format!("/tmp/hudless_discover_{}.log", exe_name);
match OpenOptions::new().create(true).append(true).open(&path) {
Ok(f) => {
log::info!("discovery logging to {}", path);
*guard = Some(f);
true
}
Err(e) => {
log::warn!("failed to open discovery log {}: {}", path, e);
false
}
}
}
pub fn log_draw(
frame: u64,
draw: u32,
vert_hash: Option<u64>,
frag_hash: Option<u64>,
vert_count: u32,
blend_enabled: bool,
depth_test_enabled: bool,
) {
if !ensure_log_file() {
return;
}
let mut guard = match LOG_FILE.lock() {
Ok(g) => g,
Err(_) => return,
};
let file = match guard.as_mut() {
Some(f) => f,
None => return,
};
let vert_str = vert_hash
.map(|h| format!("{:#018x}", h))
.unwrap_or_else(|| "none".to_string());
let frag_str = frag_hash
.map(|h| format!("{:#018x}", h))
.unwrap_or_else(|| "none".to_string());
let is_hud_suspect = blend_enabled && !depth_test_enabled && vert_count <= 6;
let suspect_marker = if is_hud_suspect { " [SUSPECT]" } else { "" };
let line = format!(
"frame={:04} draw={:05} vert={} frag={} verts={:<5} blend={:<5} depth={}{}\n",
frame,
draw,
vert_str,
frag_str,
vert_count,
blend_enabled,
depth_test_enabled,
suspect_marker
);
let _ = file.write_all(line.as_bytes());
let _ = file.flush();
}
pub unsafe fn record_draw(cb: vk::CommandBuffer, vert_count: u32) {
if !is_discovery_mode() {
return;
}
let cb_key = cb.as_raw();
let device_key = match CB_STATE.get(&cb_key) {
Some(r) => r.device_key,
None => return,
};
let ds = match DEVICE_STATE.get(&device_key) {
Some(s) => s,
None => return,
};
let frame = ds.frame_counter.load(std::sync::atomic::Ordering::Relaxed);
let (vert_hash, frag_hash, blend_enabled, depth_test_enabled) = {
let state = match CB_STATE.get_mut(&cb_key) {
Some(s) => s,
None => return,
};
let draw = state.draw_counter + 1;
let vert_hash = state.active_vert_hash;
let frag_hash = state.active_frag_hash;
let (blend_enabled, depth_test_enabled) = if let Some(active_frag) = frag_hash {
let mut blend = false;
let mut depth = false;
for entry in ds.pipeline_state.iter() {
if let Some(ph) = ds.pipeline_registry.get(entry.key()) {
if ph.frag_hash == Some(active_frag) {
blend = entry.value().blend_enabled;
depth = entry.value().depth_test_enabled;
break;
}
}
}
(blend, depth)
} else {
(false, false)
};
drop(state);
log_draw(
frame,
draw,
vert_hash,
frag_hash,
vert_count,
blend_enabled,
depth_test_enabled,
);
(vert_hash, frag_hash, blend_enabled, depth_test_enabled)
};
}