warlocks-stave/src/bin/gui_demo.rs

342 lines
13 KiB
Rust

/// Warlock's Stave — egui GUI Demo
///
/// Auto-steps through a realistic x86-64 instruction stream, feeding each
/// frame through the core analysis pipeline (branch prediction, dictionary
/// scanning, heatmap accumulation) and rendering the results on the 4-quarter
/// hex grid workspace.
///
/// Run: cargo run --bin gui_demo
///
/// Controls:
/// Space — toggle auto-step
/// Right — single step forward
/// R — reset demo state
/// Esc / Q — quit
use eframe::egui::{self, Color32, RichText};
use stave_core::stave_ui::{
AxialCoordinate, VisualCellMetrics, WarlockAppWorkspace,
};
use stave_core::{BranchEvaluator, DictionaryRule, MemoryScanner};
// ---------------------------------------------------------------------------
// Instruction stream: realistic x86-64 bytes with hex + ASM labels
// ---------------------------------------------------------------------------
const STREAM: &[(&[u8], &str, &str)] = &[
(&[0x48, 0x31, 0xC0], "48 31 C0 ", "xor rax, rax"),
(&[0x48, 0x89, 0xC3], "48 89 C3 ", "mov rbx, rax"),
(&[0x0F, 0x84, 0x10, 0x00, 0x00, 0x00], "0F 84 10 00 00 00 ", "je +0x10"),
(&[0xE9, 0x05, 0x00, 0x00, 0x00], "E9 05 00 00 00 ", "jmp +0x05"),
(&[0x48, 0x83, 0xC4, 0x28], "48 83 C4 28 ", "add rsp, 0x28"),
(&[0x48, 0x8D, 0x05, 0x00, 0x00, 0x00, 0x00], "48 8D 05 00 00 00 00 ", "lea rax, [rip]"),
(&[0x0F, 0x85, 0x20, 0x00, 0x00, 0x00], "0F 85 20 00 00 00 ", "jne +0x20"),
(&[0x48, 0x31, 0xFF], "48 31 FF ", "xor rdi, rdi"),
// This one contains "http://" — dictionary scanner will flag it
(&[0x68, 0x74, 0x74, 0x70, 0x3A, 0x2F, 0x2F], "68 74 74 70 3A 2F 2F ", "push 0x2F2F7074_7474_7868"),
(&[0xC3], "C3 ", "ret"),
// Loop back: second pass re-visits cells (heatmap intensity increases)
(&[0x48, 0x31, 0xC0], "48 31 C0 ", "xor rax, rax"),
(&[0x0F, 0x84, 0x10, 0x00, 0x00, 0x00], "0F 84 10 00 00 00 ", "je +0x10"),
(&[0x48, 0x89, 0xE5], "48 89 E5 ", "mov rbp, rsp"),
(&[0x48, 0x83, 0xEC, 0x20], "48 83 EC 20 ", "sub rsp, 0x20"),
(&[0xE8, 0x00, 0x00, 0x00, 0x00], "E8 00 00 00 00 ", "call +0x00"),
];
/// Hex positions for each instruction — laid out in a visible cluster.
/// When the stream loops, cells get revisited and their intensity grows.
const HEX_POS: &[(i32, i32)] = &[
( 0, 0), ( 1, 0), ( 2, 0), ( 3, 0), ( 2, -1),
( 1, -1), ( 0, -1), (-1, -1), (-1, 0), ( 0, 1),
// Second pass — overlaps first pass cells
( 0, 0), ( 2, 0), ( 1, 1), ( 2, 1), ( 3, 1),
];
const COLOR_NEON: Color32 = Color32::from_rgb(0, 230, 180);
const COLOR_ACCENT: Color32 = Color32::from_rgb(0, 150, 255);
const COLOR_DIM: Color32 = Color32::from_rgb(120, 120, 130);
const COLOR_BG: Color32 = Color32::from_rgb(10, 10, 12);
// ---------------------------------------------------------------------------
// Demo application state
// ---------------------------------------------------------------------------
struct GuiDemo {
workspace: WarlockAppWorkspace,
step_index: usize,
auto_step: bool,
interval_ms: u64,
last_step: std::time::Instant,
total_steps: u64,
branch_log: Vec<(u64, String, String)>,
dict_log: Vec<(u64, String)>,
}
impl GuiDemo {
fn new(_cc: &eframe::CreationContext<'_>) -> Self {
let mut ws = WarlockAppWorkspace::new();
// Clear seed data — we'll populate from the instruction stream
ws.cell_database.clear();
ws.register_states.clear();
ws.disassembly_buffer.clear();
Self {
workspace: ws,
step_index: 0,
auto_step: true,
interval_ms: 350,
last_step: std::time::Instant::now(),
total_steps: 0,
branch_log: Vec::new(),
dict_log: Vec::new(),
}
}
/// Feed one instruction through the analysis pipeline.
fn do_step(&mut self) {
let idx = self.step_index % STREAM.len();
let (bytes, hex, asm) = STREAM[idx];
let rip = 0x7FF0 + idx as u64 * 16;
// Alternate EFLAGS to exercise conditional / not-taken paths
let eflags: u32 = match idx % 4 {
0 => 0x202, // ZF=1
1 => 0x246, // ZF=1, SF=1
2 => 0x282, // ZF=0 (je won't be taken)
_ => 0x242, // ZF=1, SF=1
};
// --- Branch evaluation ---
let branch = BranchEvaluator::evaluate_execution_flow(bytes, rip, eflags);
let branch_str = format!("{:?}", branch);
// --- Dictionary scan ---
let dict_hits = MemoryScanner::scan_buffer(
bytes,
&DictionaryRule::get_default_hunting_library(),
);
// --- Update heatmap cell ---
let pos = HEX_POS[idx];
let coord = AxialCoordinate { q: pos.0, r: pos.1 };
let entry = self
.workspace
.cell_database
.entry(coord)
.or_insert(VisualCellMetrics {
total_hits: 0,
is_breakpoint: asm.starts_with("je") || asm.starts_with("jne"),
address_reference: rip,
});
entry.total_hits += 1;
// --- Disassembly buffer ---
self.workspace
.disassembly_buffer
.push((rip, hex.to_string(), asm.to_string()));
if self.workspace.disassembly_buffer.len() > 60 {
self.workspace.disassembly_buffer.remove(0);
}
// --- Register states (simulated) ---
let rip_val = rip + bytes.len() as u64;
let rax_val = match asm {
"xor rax, rax" => 0,
_ => 0xDEAD_0000_0000_0000 + self.total_steps as u64,
};
let rsp_val = 0x7FFF_FFFF_E000 - self.total_steps as u64 * 0x28;
self.workspace
.register_states
.insert("RIP".to_string(), (rip_val, true));
self.workspace
.register_states
.insert("RAX".to_string(), (rax_val, asm.contains("rax")));
self.workspace
.register_states
.insert("RBX".to_string(), (0x0000_7FFF_E000, asm.contains("rbx")));
self.workspace
.register_states
.insert("RSP".to_string(), (rsp_val, asm.contains("rsp")));
self.workspace
.register_states
.insert("RDI".to_string(), (0, asm.contains("rdi")));
self.workspace
.register_states
.insert("RBP".to_string(), (rsp_val, asm.contains("rbp")));
self.workspace
.register_states
.insert("EFLAGS".to_string(), (eflags as u64, true));
// --- Logging ---
if !branch_str.contains("NonControlFlow") {
self.branch_log
.push((rip, asm.to_string(), branch_str));
if self.branch_log.len() > 30 {
self.branch_log.remove(0);
}
}
for dm in &dict_hits {
self.dict_log
.push((rip, format!("[{}] {}", dm.label, asm)));
if self.dict_log.len() > 20 {
self.dict_log.remove(0);
}
}
self.step_index += 1;
self.total_steps += 1;
}
/// Render a compact control bar above the workspace.
fn render_controls(&mut self, ctx: &egui::Context) {
egui::TopBottomPanel::top("controls").show(ctx, |ui| {
ui.set_height(36.0);
ui.horizontal(|ui| {
ui.style_mut().visuals.panel_fill = COLOR_BG;
// Play / Pause
let label = if self.auto_step { "Pause" } else { "Play" };
if ui.button(label).clicked() {
self.auto_step = !self.auto_step;
}
// Single step
if ui.button("Step").clicked() {
self.do_step();
}
// Speed slider
ui.label(RichText::new("Speed:").color(COLOR_DIM));
ui.add(egui::Slider::new(&mut self.interval_ms, 50..=1000).suffix("ms"));
// Reset
if ui.button("Reset").clicked() {
self.step_index = 0;
self.total_steps = 0;
self.workspace.cell_database.clear();
self.workspace.register_states.clear();
self.workspace.disassembly_buffer.clear();
self.branch_log.clear();
self.dict_log.clear();
}
ui.separator();
// Status
ui.label(
RichText::new(format!(
"Frame {} | Cells {} | Dict hits {}",
self.total_steps,
self.workspace.cell_database.len(),
self.dict_log.len(),
))
.color(COLOR_NEON),
);
});
});
}
/// Render the live log panel below the workspace.
fn render_log_panel(&self, ctx: &egui::Context) {
egui::TopBottomPanel::bottom("log").show(ctx, |ui| {
ui.set_height(100.0);
ui.style_mut().visuals.panel_fill = COLOR_BG;
ui.vertical(|ui| {
ui.horizontal(|ui| {
ui.label(
RichText::new("BRANCH EVALUATOR LOG")
.strong()
.color(COLOR_ACCENT),
);
ui.separator();
ui.label(
RichText::new("DICTIONARY SCANNER LOG")
.strong()
.color(Color32::from_rgb(255, 200, 50)),
);
});
egui::ScrollArea::vertical()
.max_height(72.0)
.show(ui, |ui| {
ui.horizontal(|ui| {
// Branch log (left half)
ui.vertical(|ui| {
for (rip, asm, outcome) in &self.branch_log {
ui.monospace(
RichText::new(format!(
" {:#010X} {} -> {}",
rip, asm, outcome
))
.color(Color32::from_rgb(180, 220, 255)),
);
}
});
ui.separator();
// Dict log (right half)
ui.vertical(|ui| {
for (rip, msg) in &self.dict_log {
ui.monospace(
RichText::new(format!(
" {:#010X} {}",
rip, msg
))
.color(Color32::from_rgb(255, 220, 100)),
);
}
});
});
});
});
});
}
}
impl eframe::App for GuiDemo {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
// Auto-step timer
if self.auto_step {
if self.last_step.elapsed().as_millis() as u64 >= self.interval_ms {
self.do_step();
self.last_step = std::time::Instant::now();
}
ctx.request_repaint_after(std::time::Duration::from_millis(50));
}
// Keyboard shortcuts
ctx.input(|i| {
if i.key_pressed(egui::Key::Space) {
self.auto_step = !self.auto_step;
}
if i.key_pressed(egui::Key::ArrowRight) {
self.do_step();
}
if i.key_pressed(egui::Key::R) {
self.step_index = 0;
self.total_steps = 0;
self.workspace.cell_database.clear();
self.workspace.register_states.clear();
self.workspace.disassembly_buffer.clear();
self.branch_log.clear();
self.dict_log.clear();
}
});
// Render: controls (top) → workspace (center) → log (bottom)
self.render_controls(ctx);
self.workspace.update_workspace_layout(ctx);
self.render_log_panel(ctx);
}
}
fn main() -> eframe::Result<()> {
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_title("Warlock's Stave — GUI Demo")
.with_inner_size([1280.0, 860.0])
.with_min_inner_size([800.0, 500.0]),
..Default::default()
};
eframe::run_native(
"Warlock's Stave",
options,
Box::new(|cc| Box::new(GuiDemo::new(cc))),
)
}