fix(ui): redesign overlay as top dock with 3 transparent cards and unified settings

This commit is contained in:
katR 2026-09-05 11:45:37 +05:30
parent 10a2ea5343
commit 98dc90e85c
9 changed files with 896 additions and 1182 deletions

View file

@ -14,7 +14,7 @@ RequestExecutionLevel user
!define MUI_FINISHPAGE_RUN "$INSTDIR\vrec-ui.exe"
!define MUI_FINISHPAGE_RUN_PARAMETERS "--menu"
!define MUI_FINISHPAGE_RUN_TEXT "Launch vrec Studio Overlay"
!define MUI_FINISHPAGE_RUN_TEXT "Launch vrec"
!insertmacro MUI_PAGE_FINISH
!insertmacro MUI_UNPAGE_CONFIRM
@ -28,8 +28,7 @@ Section "vrec" SecMain
; Create Start Menu and Desktop shortcuts
CreateDirectory "$SMPROGRAMS\vrec"
CreateShortcut "$SMPROGRAMS\vrec\vrec Studio.lnk" "$INSTDIR\vrec-ui.exe" "--menu"
CreateShortcut "$SMPROGRAMS\vrec\vrec Background Engine.lnk" "$INSTDIR\vrec-daemon.exe" ""
CreateShortcut "$SMPROGRAMS\vrec\vrec.lnk" "$INSTDIR\vrec-ui.exe" "--menu"
CreateShortcut "$DESKTOP\vrec.lnk" "$INSTDIR\vrec-ui.exe" "--menu"
; Register uninstaller with Windows Control Panel / Settings

View file

@ -1,3 +1,5 @@
#![cfg_attr(target_os = "windows", windows_subsystem = "windows")]
use vrec::capture::{Frame, FrameSource};
use vrec::encoder::VideoEncoder;
use vrec::ring::Packet;
@ -99,9 +101,20 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
#[cfg(windows)]
let listener = {
let l = std::net::TcpListener::bind("127.0.0.1:42069")?;
match std::net::TcpListener::bind("127.0.0.1:42069") {
Ok(l) => {
println!("Daemon listening on TCP IPC: 127.0.0.1:42069");
l
}
Err(e) => {
if vrec::ipc::query_status().is_ok() {
println!("Another instance of vrec-daemon is already running. Exiting cleanly.");
return Ok(());
}
std::thread::sleep(std::time::Duration::from_millis(500));
std::net::TcpListener::bind("127.0.0.1:42069")?
}
}
};
let cmd_tx_ipc = cmd_tx.clone();
@ -114,8 +127,15 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
for stream in listener.incoming() {
match stream {
Ok(mut stream) => {
let _ = stream.set_read_timeout(Some(std::time::Duration::from_millis(1000)));
let _ = stream.set_write_timeout(Some(std::time::Duration::from_millis(1000)));
let cmd_tx_ipc = cmd_tx_ipc.clone();
let is_recording_state_ipc = Arc::clone(&is_recording_state_ipc);
let record_start_state_ipc = Arc::clone(&record_start_state_ipc);
let replay_enabled_state_ipc = Arc::clone(&replay_enabled_state_ipc);
let audio_muted_state_ipc = Arc::clone(&audio_muted_state_ipc);
std::thread::spawn(move || {
let _ = stream.set_read_timeout(Some(std::time::Duration::from_millis(1500)));
let _ = stream.set_write_timeout(Some(std::time::Duration::from_millis(1500)));
let mut len_buf = [0u8; 4];
if stream.read_exact(&mut len_buf).is_ok() {
let len = u32::from_le_bytes(len_buf) as usize;
@ -153,10 +173,17 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let mut cfg = vrec::config::VrecConfig::load();
cfg.show_cursor = !cfg.show_cursor;
let _ = cfg.save();
println!("Cursor display toggled to: {}. Restarting daemon for new capture session...", cfg.show_cursor);
#[cfg(target_os = "linux")]
{
let session_type = env::var("XDG_SESSION_TYPE").unwrap_or_default();
if session_type.to_lowercase() == "wayland" {
println!("Cursor display toggled to: {}. Restarting Wayland session...", cfg.show_cursor);
let _ = cmd_tx_ipc.send(Command::StopRecording);
std::thread::sleep(std::time::Duration::from_millis(150));
std::process::exit(0);
}
}
println!("Cursor display toggled to: {}", cfg.show_cursor);
},
Command::CycleAudioMode => {
let mut cfg = vrec::config::VrecConfig::load();
@ -183,6 +210,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
}
}
}
});
}
Err(err) => {
eprintln!("Socket error: {}", err);
@ -253,7 +281,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
Frame::Raw { width, height, .. } => (*width, *height),
Frame::DmaBuf { width, height, .. } => (*width, *height),
#[cfg(target_os = "windows")]
Frame::D3D11Texture { .. } => (1920, 1080),
Frame::D3D11Texture { width, height, .. } => (*width, *height),
};
let _ = frame_tx.send(first_frame);

View file

@ -1,3 +1,5 @@
#![cfg_attr(target_os = "windows", windows_subsystem = "windows")]
use vrec::ipc::{Command, send_command, query_status};
use vrec::overlay::show_notification;
use std::env;
@ -35,7 +37,8 @@ fn ensure_wayland_env() {
}
fn get_daemon_cmd() -> std::process::Command {
if let Ok(mut path) = std::env::current_exe() {
#[allow(unused_mut)]
let mut cmd = if let Ok(mut path) = std::env::current_exe() {
path.pop();
#[cfg(target_os = "windows")]
let candidate = path.join("vrec-daemon.exe");
@ -43,33 +46,102 @@ fn get_daemon_cmd() -> std::process::Command {
let candidate = path.join("vrec-daemon");
if candidate.exists() {
return std::process::Command::new(candidate);
}
}
std::process::Command::new(candidate)
} else {
#[cfg(target_os = "windows")]
return std::process::Command::new("vrec-daemon.exe");
{
std::process::Command::new("vrec-daemon.exe")
}
#[cfg(not(target_os = "windows"))]
return std::process::Command::new("vrec-daemon");
{
std::process::Command::new("vrec-daemon")
}
}
} else {
#[cfg(target_os = "windows")]
{
std::process::Command::new("vrec-daemon.exe")
}
#[cfg(not(target_os = "windows"))]
{
std::process::Command::new("vrec-daemon")
}
};
#[cfg(target_os = "windows")]
{
use std::os::windows::process::CommandExt;
cmd.creation_flags(0x08000000); // CREATE_NO_WINDOW
}
cmd
}
fn get_ui_cmd() -> std::process::Command {
#[allow(unused_mut)]
let mut cmd = if let Ok(path) = std::env::current_exe() {
std::process::Command::new(path)
} else {
#[cfg(target_os = "windows")]
{
std::process::Command::new("vrec-ui.exe")
}
#[cfg(not(target_os = "windows"))]
{
std::process::Command::new("vrec-ui")
}
};
#[cfg(target_os = "windows")]
{
use std::os::windows::process::CommandExt;
cmd.creation_flags(0x08000000); // CREATE_NO_WINDOW
}
cmd
}
fn ensure_daemon_running() {
if query_status().is_err() {
println!("vrec-daemon not running. Auto-launching vrec-daemon...");
let _ = get_daemon_cmd()
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn();
for _ in 0..20 {
std::thread::sleep(std::time::Duration::from_millis(150));
for _ in 0..15 {
std::thread::sleep(std::time::Duration::from_millis(100));
if query_status().is_ok() {
println!("vrec-daemon successfully connected.");
break;
}
}
}
}
fn ensure_hotkeys_running() {
#[cfg(target_os = "windows")]
{
unsafe {
use windows::Win32::Foundation::{CloseHandle, GetLastError, WIN32_ERROR};
use windows::Win32::System::Threading::CreateMutexW;
if let Ok(handle) = CreateMutexW(None, false, windows::core::w!("Global\\vrec_hotkeys_single_instance")) {
if GetLastError() == WIN32_ERROR(183) { // ERROR_ALREADY_EXISTS
let _ = CloseHandle(handle);
return;
}
let _ = CloseHandle(handle);
}
}
let _ = get_ui_cmd()
.arg("--hotkeys")
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn();
}
}
fn send_with_notification(cmd: Command, success_msg: &str) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
ensure_daemon_running();
match send_command(cmd) {
@ -135,6 +207,7 @@ fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
}
"--menu" => {
ensure_daemon_running();
ensure_hotkeys_running();
vrec::overlay_egui::run_egui_overlay();
return Ok(());
}
@ -197,10 +270,23 @@ fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
}
} else {
ensure_daemon_running();
ensure_hotkeys_running();
vrec::overlay_egui::run_egui_overlay();
return Ok(());
}
#[cfg(target_os = "windows")]
let _mutex = unsafe {
use windows::Win32::Foundation::{GetLastError, WIN32_ERROR};
use windows::Win32::System::Threading::CreateMutexW;
let handle = CreateMutexW(None, true, windows::core::w!("Global\\vrec_hotkeys_single_instance"));
if GetLastError() == WIN32_ERROR(183) {
return Ok(());
}
handle.ok()
};
println!("Starting vrec UI/Hotkey process...");
let session_type = env::var("XDG_SESSION_TYPE").unwrap_or_else(|_| "x11".to_string());
@ -223,7 +309,13 @@ fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
println!("X11 session detected.");
}
let manager = GlobalHotKeyManager::new()?;
let manager = match GlobalHotKeyManager::new() {
Ok(m) => m,
Err(e) => {
eprintln!("Failed to create global hotkey manager: {}", e);
return Ok(());
}
};
let hotkey_menu = HotKey::new(Some(Modifiers::ALT), Code::KeyZ);
let hotkey_save = HotKey::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::KeyR);
let hotkey_record = HotKey::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::F9);
@ -241,7 +333,7 @@ fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
if let Ok(event) = receiver.try_recv() {
if event.id == hotkey_menu.id() {
ensure_daemon_running();
vrec::overlay_egui::run_egui_overlay();
let _ = get_ui_cmd().arg("--menu").spawn();
} else if event.id == hotkey_save.id() {
let _ = send_with_notification(Command::SaveReplay, "Replay saved");
} else if event.id == hotkey_record.id() {

View file

@ -35,6 +35,8 @@ pub enum Frame {
#[cfg(target_os = "windows")]
D3D11Texture {
handle: usize,
width: u32,
height: u32,
},
}

View file

@ -16,8 +16,8 @@ pub struct WindowsCapture {
context: ID3D11DeviceContext,
duplication: IDXGIOutputDuplication,
gpu_texture: ID3D11Texture2D,
width: u32,
height: u32,
pub width: u32,
pub height: u32,
}
#[cfg(target_os = "windows")]
@ -30,14 +30,40 @@ impl WindowsCapture {
// Initialize COM library on capture thread
let _ = CoInitializeEx(None, COINIT_MULTITHREADED);
// Create D3D11 hardware device
// Create DXGI Factory to find the display adapter with active outputs
let factory: IDXGIFactory1 = CreateDXGIFactory1()?;
let mut chosen_adapter: Option<IDXGIAdapter1> = None;
let mut chosen_output: Option<IDXGIOutput1> = None;
let mut a_idx = 0;
while let Ok(adapter) = factory.EnumAdapters1(a_idx) {
let mut o_idx = 0;
while let Ok(output) = adapter.EnumOutputs(o_idx) {
if let Ok(output1) = output.cast::<IDXGIOutput1>() {
chosen_adapter = Some(adapter);
chosen_output = Some(output1);
break;
}
o_idx += 1;
}
if chosen_adapter.is_some() {
break;
}
a_idx += 1;
}
let (adapter, output1) = match (chosen_adapter, chosen_output) {
(Some(a), Some(o)) => (a, o),
_ => return Err("No active display output found for Windows desktop capture".into()),
};
let mut device: Option<ID3D11Device> = None;
let mut context: Option<ID3D11DeviceContext> = None;
let mut feature_level = D3D_FEATURE_LEVEL_11_0;
D3D11CreateDevice(
None,
D3D_DRIVER_TYPE_HARDWARE,
Some(&adapter),
D3D_DRIVER_TYPE_UNKNOWN,
HMODULE(std::ptr::null_mut()),
D3D11_CREATE_DEVICE_BGRA_SUPPORT,
Some(&[D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0]),
@ -50,21 +76,14 @@ impl WindowsCapture {
let device = device.ok_or("Failed to create D3D11 device")?;
let context = context.ok_or("Failed to create D3D11 context")?;
// Retrieve DXGI Output for the primary display
let dxgi_device: IDXGIDevice = device.cast()?;
let adapter = dxgi_device.GetAdapter()?;
let output = adapter.EnumOutputs(0)?;
let output1: IDXGIOutput1 = output.cast()?;
let desc = output.GetDesc()?;
let width = (desc.DesktopCoordinates.right - desc.DesktopCoordinates.left) as u32;
let height = (desc.DesktopCoordinates.bottom - desc.DesktopCoordinates.top) as u32;
let desc = output1.GetDesc()?;
let width = (desc.DesktopCoordinates.right - desc.DesktopCoordinates.left).unsigned_abs();
let height = (desc.DesktopCoordinates.bottom - desc.DesktopCoordinates.top).unsigned_abs();
// Initialize Desktop Duplication
let duplication = output1.DuplicateOutput(&device)?;
// Allocate a dedicated VRAM-resident GPU texture for 100% zero-copy capture
// (Frame stays on GPU die, zero PCIe bus readback, 0% CPU consumption)
let gpu_desc = D3D11_TEXTURE2D_DESC {
Width: width,
Height: height,
@ -99,10 +118,17 @@ impl WindowsCapture {
unsafe {
let dxgi_device: IDXGIDevice = self.device.cast()?;
let adapter = dxgi_device.GetAdapter()?;
let output = adapter.EnumOutputs(0)?;
let output1: IDXGIOutput1 = output.cast()?;
self.duplication = output1.DuplicateOutput(&self.device)?;
Ok(())
let mut o_idx = 0;
while let Ok(output) = adapter.EnumOutputs(o_idx) {
if let Ok(output1) = output.cast::<IDXGIOutput1>() {
if let Ok(dup) = output1.DuplicateOutput(&self.device) {
self.duplication = dup;
return Ok(());
}
}
o_idx += 1;
}
Err("Failed to reinitialize desktop duplication".into())
}
}
}
@ -120,24 +146,26 @@ impl FrameSource for WindowsCapture {
if let Some(resource) = desktop_resource {
let texture: ID3D11Texture2D = resource.cast()?;
// Copy directly on the GPU from the desktop buffer to our persistent VRAM texture
// Copy directly on the GPU from desktop buffer to persistent VRAM texture
self.context.CopyResource(&self.gpu_texture, &texture);
// Immediately release the desktop frame back to the DWM compositor
let _ = self.duplication.ReleaseFrame();
// Pass the VRAM texture handle directly to the hardware encoder (Zero CPU copy!)
return Ok(Frame::D3D11Texture {
handle: self.gpu_texture.as_raw() as usize,
width: self.width,
height: self.height,
});
}
let _ = self.duplication.ReleaseFrame();
}
Err(e) if e.code() == DXGI_ERROR_WAIT_TIMEOUT => {
// Display is idle (no new pixels), yield current texture without spinning CPU
std::thread::sleep(std::time::Duration::from_millis(8));
return Ok(Frame::D3D11Texture {
handle: self.gpu_texture.as_raw() as usize,
width: self.width,
height: self.height,
});
}
Err(e) if e.code() == DXGI_ERROR_ACCESS_LOST => {
@ -154,6 +182,8 @@ impl FrameSource for WindowsCapture {
Ok(Frame::D3D11Texture {
handle: self.gpu_texture.as_raw() as usize,
width: self.width,
height: self.height,
})
}
}

View file

@ -336,7 +336,7 @@ impl VaapiEncoder {
av_frame_free(&mut drm_frame);
}
#[cfg(target_os = "windows")]
Frame::D3D11Texture { handle } => {
Frame::D3D11Texture { handle, .. } => {
println!("D3D11: Mock encoding D3D11 texture (Windows zero-copy). Handle: {}", handle);
}
}

View file

@ -87,11 +87,12 @@ pub fn send_command(cmd: Command) -> Result<(), Box<dyn std::error::Error + Send
#[cfg(windows)]
{
use std::net::TcpStream;
match TcpStream::connect("127.0.0.1:42069") {
use std::net::{SocketAddr, TcpStream};
let addr: SocketAddr = "127.0.0.1:42069".parse().unwrap();
match TcpStream::connect_timeout(&addr, Duration::from_millis(300)) {
Ok(mut stream) => {
let _ = stream.set_write_timeout(Some(Duration::from_millis(1000)));
let _ = stream.set_read_timeout(Some(Duration::from_millis(1000)));
let _ = stream.set_write_timeout(Some(Duration::from_millis(600)));
let _ = stream.set_read_timeout(Some(Duration::from_millis(600)));
if stream.write_all(&len_buf).is_ok() && stream.write_all(&payload).is_ok() {
return Ok(());
}
@ -128,14 +129,15 @@ pub fn query_status() -> Result<DaemonStatus, Box<dyn std::error::Error + Send +
#[cfg(windows)]
let stream_res = {
use std::net::TcpStream;
TcpStream::connect("127.0.0.1:42069")
use std::net::{SocketAddr, TcpStream};
let addr: SocketAddr = "127.0.0.1:42069".parse().unwrap();
TcpStream::connect_timeout(&addr, Duration::from_millis(300))
};
match stream_res {
Ok(mut stream) => {
let _ = stream.set_write_timeout(Some(Duration::from_millis(1000)));
let _ = stream.set_read_timeout(Some(Duration::from_millis(1000)));
let _ = stream.set_write_timeout(Some(Duration::from_millis(600)));
let _ = stream.set_read_timeout(Some(Duration::from_millis(600)));
if stream.write_all(&len_buf).is_err() || stream.write_all(&payload).is_err() {
continue;

View file

@ -16,7 +16,31 @@ pub fn show_notification_overlay() {
}
pub fn show_notification(message: &str) {
#[cfg(unix)]
#[cfg(target_os = "windows")]
{
let msg = message.to_string();
std::thread::spawn(move || {
use std::os::windows::process::CommandExt;
let script = format!(
"[reflection.assembly]::loadwithpartialname('System.Windows.Forms') | Out-Null; \
$notify = new-object system.windows.forms.notifyicon; \
$notify.icon = [System.Drawing.SystemIcons]::Information; \
$notify.visible = $true; \
$notify.showballoontip(2000, 'vrec', '{}', [system.windows.forms.tooltipicon]::Info); \
Start-Sleep -Seconds 2; \
$notify.dispose()",
msg.replace('\'', "''")
);
let _ = std::process::Command::new("powershell")
.args(["-NoProfile", "-WindowStyle", "Hidden", "-Command", &script])
.creation_flags(0x08000000)
.output();
});
return;
}
#[cfg(not(target_os = "windows"))]
{
if std::env::var("WAYLAND_DISPLAY").is_err() && std::env::var("DISPLAY").is_err() {
return;
}
@ -45,14 +69,6 @@ pub fn show_notification(message: &str) {
window.set_anchor(gtk_layer_shell::Edge::Top, true);
}
#[cfg(not(target_os = "linux"))]
{
window.set_decorated(false);
window.set_keep_above(true);
window.set_skip_taskbar_hint(true);
window.set_position(gtk::WindowPosition::Center);
}
let css_provider = CssProvider::new();
let css = r#"
window {
@ -90,6 +106,7 @@ pub fn show_notification(message: &str) {
app.run_with_args(&[] as &[&str]);
}
}
pub fn show_menu_overlay() {
#[cfg(unix)]

File diff suppressed because it is too large Load diff