From 0989e389c3bcafe26dd42513b2fa0238b335c18c Mon Sep 17 00:00:00 2001 From: Ava Chaney Date: Sun, 2 Jul 2023 23:49:29 -0700 Subject: [PATCH] game hangs on exit, but this is progress! --- .gitignore | 1 + Cargo.lock | 87 +++- components/llrt/Cargo.toml | 4 +- components/llrt/src/hooking/swapchain.rs | 615 ++++++++++++++++++++++- components/llrt/src/lib.rs | 8 +- components/llrt/src/webview.rs | 522 +++++++++++++++++++ components/macros/src/lib.rs | 10 +- 7 files changed, 1232 insertions(+), 15 deletions(-) create mode 100644 components/llrt/src/webview.rs diff --git a/.gitignore b/.gitignore index 7b40a09..a6a2d73 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .bonelessrc.json +deps/cef_binary* deps/cef-sys/cef_binary* local/ dist/ diff --git a/Cargo.lock b/Cargo.lock index 1939471..82a648c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1081,7 +1081,6 @@ name = "grebuloff-llrt" version = "0.1.0" dependencies = [ "anyhow", - "cef-sys", "chrono", "deno_ast", "deno_core", @@ -1102,6 +1101,7 @@ dependencies = [ "serde", "serde_json", "tokio", + "webview2-com", "windows", ] @@ -3145,6 +3145,45 @@ version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" +[[package]] +name = "webview2-com" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79e563ffe8e84d42e43ffacbace8780c0244fc8910346f334613559d92e203ad" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-implement", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac1345798ecd8122468840bcdf1b95e5dc6d2206c5e4b0eafa078d061f59c9bc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[package]] +name = "webview2-com-sys" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d39576804304cf9ead192467ef47f7859a1a12fec3bd459d5ba34b8cd65ed5" +dependencies = [ + "regex", + "serde", + "serde_json", + "thiserror", + "windows", + "windows-bindgen", + "windows-metadata", +] + [[package]] name = "which" version = "4.4.0" @@ -3199,9 +3238,49 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" dependencies = [ + "windows-implement", + "windows-interface", "windows-targets", ] +[[package]] +name = "windows-bindgen" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fe21a77bc54b7312dbd66f041605e098990c98be48cd52967b85b5e60e75ae6" +dependencies = [ + "windows-metadata", + "windows-tokens", +] + +[[package]] +name = "windows-implement" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2ee588991b9e7e6c8338edf3333fbe4da35dc72092643958ebb43f0ab2c49c" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "windows-interface" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6fb8df20c9bcaa8ad6ab513f7b40104840c8867d5751126e4df3b08388d0cc7" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "windows-metadata" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "422ee0e5f0e2cc372bb6addbfff9a8add712155cd743df9c15f6ab000f31432d" + [[package]] name = "windows-sys" version = "0.48.0" @@ -3226,6 +3305,12 @@ dependencies = [ "windows_x86_64_msvc", ] +[[package]] +name = "windows-tokens" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b34c9a3b28cb41db7385546f7f9a8179348dffc89923dde66857b1ba5312f6b4" + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.0" diff --git a/components/llrt/Cargo.toml b/components/llrt/Cargo.toml index e004427..b688eaa 100644 --- a/components/llrt/Cargo.toml +++ b/components/llrt/Cargo.toml @@ -20,7 +20,7 @@ tokio = { version = "1.28.2", features = ["net", "rt", "rt-multi-thread", "time" rmp = "0.8.11" rmp-serde = "1.1.1" serde = { version = "1.0.163", features = ["derive"] } -windows = { version = "0.48.0", features = ["Win32_Foundation", "Win32_System_Threading", "Win32_UI_WindowsAndMessaging", "Win32_System_LibraryLoader", "Win32_System_ProcessStatus"] } +windows = { version = "0.48.0", features = ["Win32_Foundation", "Win32_System_Threading", "Win32_UI_WindowsAndMessaging", "Win32_System_LibraryLoader", "Win32_System_ProcessStatus", "Win32_Graphics_Gdi", "Win32_Graphics_Dxgi", "Win32_Graphics_Dxgi_Common", "Win32_Graphics_Direct3D11", "Win32_Graphics_Direct3D"] } serde_json = "1.0.96" anyhow = "1.0.71" include_dir = "0.7.3" @@ -30,7 +30,7 @@ itertools = "0.10.5" rustc-hash = "1.1.0" deno_core = "0.191.0" retour = { version = "0.3.0", features = ["static-detour"] } -cef-sys = { path = "../../deps/cef-sys" } +webview2-com = "0.25.0" [build-dependencies] chrono = "0.4.26" diff --git a/components/llrt/src/hooking/swapchain.rs b/components/llrt/src/hooking/swapchain.rs index 9e92149..52a5fbe 100644 --- a/components/llrt/src/hooking/swapchain.rs +++ b/components/llrt/src/hooking/swapchain.rs @@ -5,12 +5,28 @@ use ffxiv_client_structs::generated::ffxiv::client::graphics::kernel::{ }; use grebuloff_macros::{function_hook, vtable_functions, VTable}; use log::{debug, trace}; -use std::ffi::c_void; +use std::{ + cell::{OnceCell, RefCell}, + mem::MaybeUninit, + ptr::addr_of_mut, +}; +use windows::Win32::{ + Foundation::{HWND, RECT}, + Graphics::{ + Direct3D::D3D_PRIMITIVE_TOPOLOGY, + Direct3D11::*, + Dxgi::{Common::DXGI_FORMAT, IDXGISwapChain, DXGI_SWAP_CHAIN_DESC}, + }, +}; + +thread_local! { + static RENDER_DATA: RefCell> = RefCell::new(OnceCell::new()); +} #[derive(VTable)] struct ResolvedSwapChain { #[vtable_base] - base: *mut *mut c_void, + base: *mut *mut IDXGISwapChain, } vtable_functions!(impl ResolvedSwapChain { @@ -45,7 +61,7 @@ unsafe fn resolve_swap_chain() -> ResolvedSwapChain { debug!("device: {:p}", device); let swap_chain = (*device).swap_chain; debug!("swap chain: {:p}", swap_chain); - let dxgi_swap_chain = (*swap_chain).dxgiswap_chain as *mut *mut *mut c_void; + let dxgi_swap_chain = (*swap_chain).dxgiswap_chain as *mut *mut *mut IDXGISwapChain; debug!("dxgi swap chain: {:p}", *dxgi_swap_chain); ResolvedSwapChain { @@ -61,7 +77,596 @@ pub unsafe fn hook_swap_chain() -> Result<()> { Ok(()) } +/// Stores data that is used for rendering our UI overlay. +struct RenderData { + /// Used to sanity-check that we're rendering into the correct context. + sc_addr: *const IDXGISwapChain, + /// The render target view for the swap chain's back buffer. + rtv: ID3D11RenderTargetView, + buffer_width: u32, + buffer_height: u32, + window_handle: HWND, +} + #[function_hook] -unsafe fn present(this: *mut c_void, sync_interval: u32, present_flags: u32) -> i32 { - original.call(this, sync_interval, present_flags) +unsafe extern "stdcall" fn present( + this: IDXGISwapChain, + sync_interval: u32, + present_flags: u32, +) -> i32 { + debug!("in present, this: {:p}", &this as *const _); + let device: ID3D11Device2 = this.GetDevice().unwrap(); + + RENDER_DATA.with(move |cell| { + let mut cell = cell.borrow_mut(); + let mut data = match cell.get_mut() { + Some(data) => { + // ensure we're rendering into the correct context + if data.sc_addr != &this as *const _ { + debug!("IDXGISwapChain::Present called with a different swap chain than before, executing original function"); + return original.call(this, sync_interval, present_flags); + } + + data + }, + None => { + // initialize our render data for this thread (the render thread) + trace!("initializing RenderData on IDXGISwapChain::Present"); + + // init render target view + let back_buffer: ID3D11Texture2D = this.GetBuffer(0).expect("failed to get back buffer"); + let mut rtv = None; + + device + .CreateRenderTargetView(&back_buffer, None, Some(&mut rtv)) + .expect("failed to create render target view (CreateRenderTargetView not ok)"); + + let rtv = rtv.expect("failed to create render target view (was null)"); + + let mut desc = MaybeUninit::::zeroed(); + this.GetDesc(desc.as_mut_ptr()).expect("failed to get DXGI_SWAP_CHAIN_DESC"); + let desc = desc.assume_init(); + + // // create vertex buffer + // let vertex_buffer = { + // let buffer_desc = D3D11_BUFFER_DESC { + // ByteWidth: 4 * std::mem::size_of::() as u32, + // Usage: D3D11_USAGE_DEFAULT, + // BindFlags: D3D11_BIND_INDEX_BUFFER, + // ..Default::default() + // }; + + // let subresource_desc = D3D11_SUBRESOURCE_DATA { + // pSysMem: verts.as_ptr() as *const _, + // ..Default::default() + // }; + + // let mut buffer = MaybeUninit::>::zeroed(); + // device + // .CreateBuffer( + // &buffer_desc, + // Some(&subresource_desc), + // Some(buffer.as_mut_ptr()), + // ) + // .expect("CreateBuffer failed"); + // let buffer = buffer.assume_init(); + + // buffer + // }; + // set the cell with the initialized data + cell.set(RenderData { + sc_addr: &this as *const _, + rtv, + buffer_width: desc.BufferDesc.Width, + buffer_height: desc.BufferDesc.Height, + window_handle: desc.OutputWindow, + }) + .unwrap_unchecked(); + + cell.get_mut().unwrap_unchecked() // SAFETY: we just set the cell + } + }; + + let context = device.GetImmediateContext().unwrap(); + debug!("in present, context: {:p}", &context as *const _); + + // use a new scope here to ensure the state backup is dropped at the end, + // thus restoring the original render state before we call the original function + { + let _ = RenderStateBackup::new(device.GetImmediateContext().unwrap()); + + trace!("time to draw!"); + context.OMSetRenderTargets(Some(&[Some(data.rtv.clone())]), None); + + // set up orthographic projection matrix into our constant buffer + { + let colour: [f32; 4] = [0.0, 0.2, 0.4, 1.0]; + context.ClearRenderTargetView(&data.rtv, colour.as_ptr()); + } + + trace!("resetting render targets"); + context.OMSetRenderTargets(None, None); + } + + // call the original function + original.call(this, sync_interval, present_flags) + }) +} + +// let (mut out, out_ptr, mut out_count) = temp_array!(D3D11_VIEWPORT, D3D11_VIEWPORT_AND_SCISSORRECT_OBJECT_COUNT_PER_PIPELINE); +macro_rules! temp_array { + ($type: ident, $capacity: ident) => {{ + let mut out: MaybeUninit<[$type; $capacity as usize]> = MaybeUninit::zeroed(); + let out_ptr = out.as_mut_ptr(); + let out_count = $capacity as u32; + + (out, out_ptr, out_count) + }}; + + (Option<$opt_type: ident>, $capacity: ident) => {{ + let mut out: MaybeUninit<[Option<$opt_type>; $capacity as usize]> = MaybeUninit::zeroed(); + let out_ptr = out.as_mut_ptr(); + let out_count = $capacity as u32; + + (out, out_ptr, out_count) + }}; +} + +// reconcile_array!(out, out_count); +macro_rules! reconcile_array { + ($out: ident, $out_count: ident) => {{ + let mut vec = Vec::with_capacity($out_count as usize); + vec.extend_from_slice(&$out.assume_init()[0..$out_count as usize]); + vec + }}; +} + +macro_rules! backup_shaders { + ( + $context: ident, $obj_ptr: ident, + ($shader_field: ident, $class_inst_field: ident) => $get_shader: ident, + $constant_buf_field: ident => $get_constant_buf: ident, + $resource_field: ident => $get_shader_resources: ident, + $samplers_field: ident => $get_samplers: ident$(,)? + ) => {{ + // save shader + { + let (out, mut out_ptr, mut out_count) = + temp_array!(Option, D3D11_SHADER_MAX_INTERFACES); + + $context.$get_shader( + addr_of_mut!((*$obj_ptr).$shader_field), + Some(out_ptr as *mut _), + Some(&mut out_count), + ); + + addr_of_mut!((*$obj_ptr).$class_inst_field).write(reconcile_array!(out, out_count)); + } + + // save constant buffers + { + let (out, mut out_ptr, mut out_count) = temp_array!( + Option, + D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT + ); + + $context.$get_constant_buf(0, Some(&mut *out_ptr)); + + addr_of_mut!((*$obj_ptr).$constant_buf_field).write(reconcile_array!(out, out_count)); + } + + // save resources + { + let (out, mut out_ptr, mut out_count) = temp_array!( + Option, + D3D11_COMMONSHADER_INPUT_RESOURCE_SLOT_COUNT + ); + + $context.$get_shader_resources(0, Some(&mut *out_ptr)); + + addr_of_mut!((*$obj_ptr).$resource_field).write(reconcile_array!(out, out_count)); + } + + // save samplers + { + let (out, mut out_ptr, mut out_count) = temp_array!( + Option, + D3D11_COMMONSHADER_SAMPLER_SLOT_COUNT + ); + + $context.$get_samplers(0, Some(&mut *out_ptr)); + + addr_of_mut!((*$obj_ptr).$samplers_field).write(reconcile_array!(out, out_count)); + } + }}; +} + +macro_rules! restore_shaders { + ( + $context: expr, + ($shader_field: expr, $class_inst_field: expr) => $set_shader: ident, + $constant_buf_field: expr => $set_constant_buf: ident, + $resource_field: expr => $set_shader_resources: ident, + $samplers_field: expr => $set_samplers: ident$(,)? + ) => {{ + $context.$set_shader($shader_field.as_ref(), Some($class_inst_field.as_slice())); + $context.$set_constant_buf(0, Some($constant_buf_field.as_slice())); + $context.$set_shader_resources(0, Some($resource_field.as_slice())); + $context.$set_samplers(0, Some($samplers_field.as_slice())); + }}; +} + +struct RenderStateBackup { + context: ID3D11DeviceContext, + + // ### IA ### + ia_input_layout: Option, + ia_vertex_buffers: Vec>, + ia_vertex_buffer_strides: Vec, + ia_vertex_buffer_offsets: Vec, + ia_index_buffer: Option, + ia_index_buffer_format: DXGI_FORMAT, + ia_index_buffer_offset: u32, + ia_primitive_topology: D3D_PRIMITIVE_TOPOLOGY, + + // ### RS ### + rs_state: Option, + rs_viewport: Vec, + rs_scissor_rect: Vec, + + // ### OM ### + om_blend_state: Option, + om_blend_factor: f32, + om_sample_mask: u32, + om_depth_stencil_state: Option, + om_depth_stencil_ref: u32, + om_render_targets: Vec>, + om_depth_stencil_view: Option, + + // ### VS ### + vs_shader: Option, + vs_class_instances: Vec>, + vs_constant_buffers: Vec>, + vs_shader_resources: Vec>, + vs_samplers: Vec>, + + // ### HS ### + hs_shader: Option, + hs_class_instances: Vec>, + hs_constant_buffers: Vec>, + hs_shader_resources: Vec>, + hs_samplers: Vec>, + + // ### DS ### + ds_shader: Option, + ds_class_instances: Vec>, + ds_constant_buffers: Vec>, + ds_shader_resources: Vec>, + ds_samplers: Vec>, + + // ### GS ### + gs_shader: Option, + gs_class_instances: Vec>, + gs_constant_buffers: Vec>, + gs_shader_resources: Vec>, + gs_samplers: Vec>, + + // ### PS ### + ps_shader: Option, + ps_class_instances: Vec>, + ps_constant_buffers: Vec>, + ps_shader_resources: Vec>, + ps_samplers: Vec>, + + // ### CS ### + cs_shader: Option, + cs_class_instances: Vec>, + cs_constant_buffers: Vec>, + cs_shader_resources: Vec>, + cs_samplers: Vec>, +} + +impl RenderStateBackup { + #[allow(const_item_mutation)] + pub unsafe fn new(context: ID3D11DeviceContext) -> Self { + // why are some of these using `as *mut _`? + // this is why: https://github.com/microsoft/windows-rs/issues/1567 + let mut obj = MaybeUninit::::zeroed(); + let obj_ptr = obj.as_mut_ptr(); + + Self::backup_ia(&context, obj_ptr); + Self::backup_rs(&context, obj_ptr); + Self::backup_om(&context, obj_ptr); + Self::backup_vs(&context, obj_ptr); + Self::backup_hs(&context, obj_ptr); + Self::backup_ds(&context, obj_ptr); + Self::backup_gs(&context, obj_ptr); + Self::backup_ps(&context, obj_ptr); + Self::backup_cs(&context, obj_ptr); + + // save the context + addr_of_mut!((*obj_ptr).context).write(context); + + obj.assume_init() + } + + unsafe fn backup_ia(context: &ID3D11DeviceContext, obj_ptr: *mut Self) { + // save input layout + { + addr_of_mut!((*obj_ptr).ia_input_layout).write(context.IAGetInputLayout().ok()); + } + + // save index buffer + { + context.IAGetIndexBuffer( + Some(addr_of_mut!((*obj_ptr).ia_index_buffer)), + Some(addr_of_mut!((*obj_ptr).ia_index_buffer_format)), + Some(addr_of_mut!((*obj_ptr).ia_index_buffer_offset)), + ); + } + + // save primitive topology + { + addr_of_mut!((*obj_ptr).ia_primitive_topology).write(context.IAGetPrimitiveTopology()); + } + + // save vertex buffers + { + let (buf_out, mut buf_out_ptr, mut buf_out_count) = temp_array!( + Option, + D3D11_IA_VERTEX_INPUT_RESOURCE_SLOT_COUNT + ); + let (stride_out, mut stride_out_ptr, mut stride_out_count) = + temp_array!(u32, D3D11_IA_VERTEX_INPUT_RESOURCE_SLOT_COUNT); + let (offset_out, mut offset_out_ptr, mut offset_out_count) = + temp_array!(u32, D3D11_IA_VERTEX_INPUT_RESOURCE_SLOT_COUNT); + + context.IAGetVertexBuffers( + 0, + D3D11_IA_VERTEX_INPUT_RESOURCE_SLOT_COUNT, + Some(addr_of_mut!(buf_out_ptr) as *mut _), + Some(addr_of_mut!(stride_out_ptr) as *mut _), + Some(addr_of_mut!(offset_out_ptr) as *mut _), + ); + + addr_of_mut!((*obj_ptr).ia_vertex_buffers) + .write(reconcile_array!(buf_out, buf_out_count)); + addr_of_mut!((*obj_ptr).ia_vertex_buffer_strides) + .write(reconcile_array!(stride_out, stride_out_count)); + addr_of_mut!((*obj_ptr).ia_vertex_buffer_offsets) + .write(reconcile_array!(offset_out, offset_out_count)); + } + } + + unsafe fn backup_rs(context: &ID3D11DeviceContext, obj_ptr: *mut Self) { + // save rasterizer state + { + addr_of_mut!((*obj_ptr).rs_state).write(context.RSGetState().ok()); + } + + // save viewport + { + let (out, mut out_ptr, mut out_count) = temp_array!( + D3D11_VIEWPORT, + D3D11_VIEWPORT_AND_SCISSORRECT_OBJECT_COUNT_PER_PIPELINE + ); + + context.RSGetViewports(&mut out_count, Some(addr_of_mut!(out_ptr) as *mut _)); + + addr_of_mut!((*obj_ptr).rs_viewport).write(reconcile_array!(out, out_count)); + } + + // save scissor rects + { + let (out, mut out_ptr, mut out_count) = temp_array!( + RECT, + D3D11_VIEWPORT_AND_SCISSORRECT_OBJECT_COUNT_PER_PIPELINE + ); + + context.RSGetScissorRects(&mut out_count, Some(addr_of_mut!(out_ptr) as *mut _)); + + addr_of_mut!((*obj_ptr).rs_scissor_rect).write(reconcile_array!(out, out_count)); + } + } + + unsafe fn backup_om(context: &ID3D11DeviceContext, obj_ptr: *mut Self) { + // save blend state + { + context.OMGetBlendState( + Some(addr_of_mut!((*obj_ptr).om_blend_state)), + Some(addr_of_mut!((*obj_ptr).om_blend_factor)), + Some(addr_of_mut!((*obj_ptr).om_sample_mask)), + ); + } + + // save depth stencil state + { + context.OMGetDepthStencilState( + Some(addr_of_mut!((*obj_ptr).om_depth_stencil_state)), + Some(addr_of_mut!((*obj_ptr).om_depth_stencil_ref)), + ); + } + + // save render targets + { + context.OMGetRenderTargets( + Some(&mut *addr_of_mut!((*obj_ptr).om_render_targets)), + Some(addr_of_mut!((*obj_ptr).om_depth_stencil_view)), + ); + } + } + + unsafe fn backup_vs(context: &ID3D11DeviceContext, obj_ptr: *mut Self) { + backup_shaders!( + context, obj_ptr, + (vs_shader, vs_class_instances) => VSGetShader, + vs_constant_buffers => VSGetConstantBuffers, + vs_shader_resources => VSGetShaderResources, + vs_samplers => VSGetSamplers, + ); + } + + unsafe fn backup_hs(context: &ID3D11DeviceContext, obj_ptr: *mut Self) { + backup_shaders!( + context, obj_ptr, + (hs_shader, hs_class_instances) => HSGetShader, + hs_constant_buffers => HSGetConstantBuffers, + hs_shader_resources => HSGetShaderResources, + hs_samplers => HSGetSamplers, + ); + } + + unsafe fn backup_ds(context: &ID3D11DeviceContext, obj_ptr: *mut Self) { + backup_shaders!( + context, obj_ptr, + (ds_shader, ds_class_instances) => DSGetShader, + ds_constant_buffers => DSGetConstantBuffers, + ds_shader_resources => DSGetShaderResources, + ds_samplers => DSGetSamplers, + ); + } + + unsafe fn backup_gs(context: &ID3D11DeviceContext, obj_ptr: *mut Self) { + backup_shaders!( + context, obj_ptr, + (gs_shader, gs_class_instances) => GSGetShader, + gs_constant_buffers => GSGetConstantBuffers, + gs_shader_resources => GSGetShaderResources, + gs_samplers => GSGetSamplers, + ); + } + + unsafe fn backup_ps(context: &ID3D11DeviceContext, obj_ptr: *mut Self) { + backup_shaders!( + context, obj_ptr, + (ps_shader, ps_class_instances) => PSGetShader, + ps_constant_buffers => PSGetConstantBuffers, + ps_shader_resources => PSGetShaderResources, + ps_samplers => PSGetSamplers, + ); + } + + unsafe fn backup_cs(context: &ID3D11DeviceContext, obj_ptr: *mut Self) { + backup_shaders!( + context, obj_ptr, + (cs_shader, cs_class_instances) => CSGetShader, + cs_constant_buffers => CSGetConstantBuffers, + cs_shader_resources => CSGetShaderResources, + cs_samplers => CSGetSamplers, + ); + } + + unsafe fn restore_ia(&self) { + self.context.IASetInputLayout(self.ia_input_layout.as_ref()); + self.context + .IASetPrimitiveTopology(self.ia_primitive_topology); + self.context.IASetVertexBuffers( + 0, + self.ia_vertex_buffers.len() as u32, + Some(self.ia_vertex_buffers.as_ptr()), + Some(self.ia_vertex_buffer_strides.as_ptr()), + Some(self.ia_vertex_buffer_offsets.as_ptr()), + ); + } + + unsafe fn restore_rs(&self) { + self.context.RSSetState(self.rs_state.as_ref()); + self.context + .RSSetViewports(Some(self.rs_viewport.as_slice())); + self.context + .RSSetScissorRects(Some(self.rs_scissor_rect.as_slice())); + } + + unsafe fn restore_om(&self) { + self.context.OMSetBlendState( + self.om_blend_state.as_ref(), + Some(&self.om_blend_factor), + self.om_sample_mask, + ); + self.context.OMSetDepthStencilState( + self.om_depth_stencil_state.as_ref(), + self.om_depth_stencil_ref, + ); + self.context.OMSetRenderTargets( + Some(&self.om_render_targets), + self.om_depth_stencil_view.as_ref(), + ); + } + + unsafe fn restore_vs(&self) { + restore_shaders!( + self.context, + (self.vs_shader, self.vs_class_instances) => VSSetShader, + self.vs_constant_buffers => VSSetConstantBuffers, + self.vs_shader_resources => VSSetShaderResources, + self.vs_samplers => VSSetSamplers, + ); + } + + unsafe fn restore_hs(&self) { + restore_shaders!( + self.context, + (self.hs_shader, self.hs_class_instances) => HSSetShader, + self.hs_constant_buffers => HSSetConstantBuffers, + self.hs_shader_resources => HSSetShaderResources, + self.hs_samplers => HSSetSamplers, + ); + } + + unsafe fn restore_ds(&self) { + restore_shaders!( + self.context, + (self.ds_shader, self.ds_class_instances) => DSSetShader, + self.ds_constant_buffers => DSSetConstantBuffers, + self.ds_shader_resources => DSSetShaderResources, + self.ds_samplers => DSSetSamplers, + ); + } + + unsafe fn restore_gs(&self) { + restore_shaders!( + self.context, + (self.gs_shader, self.gs_class_instances) => GSSetShader, + self.gs_constant_buffers => GSSetConstantBuffers, + self.gs_shader_resources => GSSetShaderResources, + self.gs_samplers => GSSetSamplers, + ); + } + + unsafe fn restore_ps(&self) { + restore_shaders!( + self.context, + (self.ps_shader, self.ps_class_instances) => PSSetShader, + self.ps_constant_buffers => PSSetConstantBuffers, + self.ps_shader_resources => PSSetShaderResources, + self.ps_samplers => PSSetSamplers, + ); + } + + unsafe fn restore_cs(&self) { + restore_shaders!( + self.context, + (self.cs_shader, self.cs_class_instances) => CSSetShader, + self.cs_constant_buffers => CSSetConstantBuffers, + self.cs_shader_resources => CSSetShaderResources, + self.cs_samplers => CSSetSamplers, + ); + } +} + +/// Restores the render state that was backed up in the constructor. +impl Drop for RenderStateBackup { + fn drop(&mut self) { + unsafe { + self.restore_ia(); + self.restore_rs(); + self.restore_om(); + self.restore_vs(); + self.restore_hs(); + self.restore_ds(); + self.restore_gs(); + self.restore_ps(); + self.restore_cs(); + } + } } diff --git a/components/llrt/src/lib.rs b/components/llrt/src/lib.rs index f1dbe01..e55f472 100644 --- a/components/llrt/src/lib.rs +++ b/components/llrt/src/lib.rs @@ -2,6 +2,7 @@ mod dalamud; mod hooking; mod resolvers; mod runtime; +mod webview; #[macro_use] extern crate retour; @@ -168,12 +169,7 @@ async fn init_async() -> Result<()> { // https://github.com/robmikh/screenshot-rs/tree/main - example code for win10 capture api in rust // https://github.com/jnschulze/flutter-webview-windows - example where this technique is used // - // task::spawn_blocking(|| { - // info!("webview2 init"); - // let webview = WebView::new(); - // webview.run().unwrap(); - // }) - // .await?; + // task::spawn_blocking(|| webview::init_ui_host().unwrap()).await?; // run the main loop // this is the last thing that should be called in init_async diff --git a/components/llrt/src/webview.rs b/components/llrt/src/webview.rs new file mode 100644 index 0000000..00057e2 --- /dev/null +++ b/components/llrt/src/webview.rs @@ -0,0 +1,522 @@ +use anyhow::{bail, Result}; +use log::info; +use serde::Deserialize; +use serde_json::{Number, Value}; +use std::{ + cell::OnceCell, + collections::HashMap, + mem, ptr, + sync::{mpsc, Arc, Mutex}, +}; +use webview2_com::{Microsoft::Web::WebView2::Win32::*, *}; +use windows::{ + core::*, + Win32::{ + Foundation::{E_POINTER, HWND, LPARAM, LRESULT, RECT, SIZE, WPARAM}, + System::{Com::*, LibraryLoader, Threading, WinRT::EventRegistrationToken}, + UI::WindowsAndMessaging::{ + self, GetForegroundWindow, MSG, WNDCLASSW, WS_EX_LAYERED, WS_OVERLAPPEDWINDOW, + }, + }, +}; + +static mut WEBVIEW_INSTANCE: OnceCell = OnceCell::new(); + +pub fn init_ui_host() -> Result<()> { + info!("initializing ui host"); + + unsafe { + CoInitializeEx(None, COINIT_APARTMENTTHREADED)?; + } + + let webview = WebView::create(None, true)?; + if !unsafe { WEBVIEW_INSTANCE.set(webview.clone()) }.is_ok() { + bail!("failed to set webview instance"); + } + + webview.init(r#"console.log(`hello world!`);"#).unwrap(); + + // Off we go.... + webview.run() +} + +struct Window(HWND); + +impl Drop for Window { + fn drop(&mut self) { + unsafe { + WindowsAndMessaging::DestroyWindow(self.0); + } + } +} + +#[derive(Clone)] +pub struct FrameWindow { + window: Arc, + size: Arc>, +} + +impl FrameWindow { + fn new() -> Self { + let hwnd = { + let window_class = WNDCLASSW { + lpfnWndProc: Some(window_proc), + lpszClassName: w!("GrebuloffUIHost"), + ..Default::default() + }; + + unsafe { + WindowsAndMessaging::RegisterClassW(&window_class); + + WindowsAndMessaging::CreateWindowExW( + WS_EX_LAYERED, + w!("GrebuloffUIHost"), + w!("GrebuloffUIHost"), + WS_OVERLAPPEDWINDOW, + WindowsAndMessaging::CW_USEDEFAULT, + WindowsAndMessaging::CW_USEDEFAULT, + WindowsAndMessaging::CW_USEDEFAULT, + WindowsAndMessaging::CW_USEDEFAULT, + None, + None, + LibraryLoader::GetModuleHandleW(None).unwrap_or_default(), + None, + ) + } + }; + + FrameWindow { + window: Arc::new(hwnd), + size: Arc::new(Mutex::new(SIZE { cx: 0, cy: 0 })), + } + } +} + +struct WebViewController(ICoreWebView2Controller); + +type WebViewSender = mpsc::Sender>; +type WebViewReceiver = mpsc::Receiver>; +type BindingCallback = Box) -> Result>; +type BindingsMap = HashMap; + +#[derive(Clone)] +pub struct WebView { + controller: Arc, + webview: Arc, + tx: WebViewSender, + rx: Arc, + thread_id: u32, + bindings: Arc>, + frame: Option, + parent: Arc, +} + +impl Drop for WebViewController { + fn drop(&mut self) { + unsafe { self.0.Close() }.unwrap(); + } +} + +#[derive(Debug, Deserialize)] +struct InvokeMessage { + id: u64, + method: String, + params: Vec, +} + +impl WebView { + pub fn create(parent: Option, debug: bool) -> Result { + let (parent, frame) = match parent { + Some(hwnd) => (hwnd, None), + None => { + let frame = FrameWindow::new(); + (*frame.window, Some(frame)) + } + }; + + let environment = { + let (tx, rx) = mpsc::channel(); + + CreateCoreWebView2EnvironmentCompletedHandler::wait_for_async_operation( + Box::new(|environmentcreatedhandler| unsafe { + CreateCoreWebView2Environment(&environmentcreatedhandler) + .map_err(webview2_com::Error::WindowsError) + }), + Box::new(move |error_code, environment| { + error_code?; + tx.send(environment.ok_or_else(|| windows::core::Error::from(E_POINTER))) + .expect("send over mpsc channel"); + Ok(()) + }), + ) + .unwrap(); + + rx.recv()? + }?; + + let controller = { + let (tx, rx) = mpsc::channel(); + + CreateCoreWebView2ControllerCompletedHandler::wait_for_async_operation( + Box::new(move |handler| unsafe { + environment + .CreateCoreWebView2Controller(parent, &handler) + .map_err(webview2_com::Error::WindowsError) + }), + Box::new(move |error_code, controller| { + error_code?; + tx.send(controller.ok_or_else(|| windows::core::Error::from(E_POINTER))) + .expect("send over mpsc channel"); + Ok(()) + }), + ) + .unwrap(); + + rx.recv()? + }?; + + let size = get_window_size(unsafe { GetForegroundWindow() }); + let mut client_rect = RECT::default(); + unsafe { + WindowsAndMessaging::GetClientRect(parent, std::mem::transmute(&mut client_rect)); + controller + .cast::() + .unwrap() + .SetDefaultBackgroundColor(COREWEBVIEW2_COLOR { + R: 0, + G: 0, + B: 0, + A: 0, + }) + .expect("Failed to set background to transparent"); + controller.SetBounds(RECT { + left: 0, + top: 0, + right: size.cx, + bottom: size.cy, + })?; + controller.SetIsVisible(true)?; + } + + let webview = unsafe { controller.CoreWebView2()? }; + + if !debug { + unsafe { + let settings = webview.Settings()?; + settings.SetAreDefaultContextMenusEnabled(false)?; + settings.SetAreDevToolsEnabled(false)?; + } + } + + if let Some(frame) = frame.as_ref() { + *frame.size.lock().unwrap() = size; + } + + let (tx, rx) = mpsc::channel(); + let rx = Arc::new(rx); + let thread_id = unsafe { Threading::GetCurrentThreadId() }; + + let webview = WebView { + controller: Arc::new(WebViewController(controller)), + webview: Arc::new(webview), + tx, + rx, + thread_id, + bindings: Arc::new(Mutex::new(HashMap::new())), + frame, + parent: Arc::new(parent), + }; + + // Inject the invoke handler. + webview + .init(r#"window.external = { invoke: s => window.chrome.webview.postMessage(s) };"#)?; + + let bindings = webview.bindings.clone(); + let bound = webview.clone(); + unsafe { + let mut _token = EventRegistrationToken::default(); + webview.webview.add_WebMessageReceived( + &WebMessageReceivedEventHandler::create(Box::new(move |_webview, args| { + if let Some(args) = args { + let mut message = PWSTR(ptr::null_mut()); + if args.WebMessageAsJson(&mut message).is_ok() { + let message = CoTaskMemPWSTR::from(message); + if let Ok(value) = + serde_json::from_str::(&message.to_string()) + { + if let Ok(mut bindings) = bindings.try_lock() { + if let Some(f) = bindings.get_mut(&value.method) { + match (*f)(value.params) { + Ok(result) => bound.resolve(value.id, 0, result), + Err(err) => bound.resolve( + value.id, + 1, + Value::String(format!("{err:#?}")), + ), + } + .unwrap(); + } + } + } + } + } + Ok(()) + })), + &mut _token, + )?; + } + + Ok(webview) + } + + pub fn run(self) -> Result<()> { + let webview = self.webview.as_ref(); + let (tx, rx) = mpsc::channel(); + + let handler = NavigationCompletedEventHandler::create(Box::new(move |_sender, _args| { + tx.send(()).expect("send over mpsc channel"); + Ok(()) + })); + let mut token = EventRegistrationToken::default(); + unsafe { + webview.add_NavigationCompleted(&handler, &mut token)?; + let url = CoTaskMemPWSTR::from("http://localhost:3000/"); + webview.Navigate(*url.as_ref().as_pcwstr())?; + let result = webview2_com::wait_with_pump(rx); + webview.remove_NavigationCompleted(token)?; + result.unwrap(); + } + + if let Some(frame) = self.frame.as_ref() { + let hwnd = *frame.window; + unsafe { + WindowsAndMessaging::ShowWindow(hwnd, WindowsAndMessaging::SW_SHOW); + } + } + + let mut msg = MSG::default(); + let h_wnd = HWND::default(); + + loop { + while let Ok(f) = self.rx.try_recv() { + (f)(self.clone()); + } + + unsafe { + let result = WindowsAndMessaging::GetMessageW(&mut msg, h_wnd, 0, 0).0; + + match result { + -1 => break Err(windows::core::Error::from_win32().into()), + 0 => break Ok(()), + _ => match msg.message { + WindowsAndMessaging::WM_APP => (), + _ => { + WindowsAndMessaging::TranslateMessage(&msg); + WindowsAndMessaging::DispatchMessageW(&msg); + } + }, + } + } + } + } + + pub fn terminate(self) -> Result<()> { + self.dispatch(|_webview| unsafe { + WindowsAndMessaging::PostQuitMessage(0); + })?; + + Ok(()) + } + + pub fn set_size(&self, width: i32, height: i32) -> Result<&Self> { + if let Some(frame) = self.frame.as_ref() { + *frame.size.lock().expect("lock size") = SIZE { + cx: width, + cy: height, + }; + unsafe { + self.controller.0.SetBounds(RECT { + left: 0, + top: 0, + right: width, + bottom: height, + })?; + + WindowsAndMessaging::SetWindowPos( + *frame.window, + None, + 0, + 0, + width, + height, + WindowsAndMessaging::SWP_NOACTIVATE + | WindowsAndMessaging::SWP_NOZORDER + | WindowsAndMessaging::SWP_NOMOVE, + ); + } + } + Ok(self) + } + + pub fn get_window(&self) -> HWND { + *self.parent + } + + pub fn init(&self, js: &str) -> Result<&Self> { + let webview = self.webview.clone(); + let js = String::from(js); + AddScriptToExecuteOnDocumentCreatedCompletedHandler::wait_for_async_operation( + Box::new(move |handler| unsafe { + let js = CoTaskMemPWSTR::from(js.as_str()); + webview + .AddScriptToExecuteOnDocumentCreated(*js.as_ref().as_pcwstr(), &handler) + .map_err(webview2_com::Error::WindowsError) + }), + Box::new(|error_code, _id| error_code), + ) + .unwrap(); + Ok(self) + } + + pub fn eval(&self, js: &str) -> Result<&Self> { + let webview = self.webview.clone(); + let js = String::from(js); + ExecuteScriptCompletedHandler::wait_for_async_operation( + Box::new(move |handler| unsafe { + let js = CoTaskMemPWSTR::from(js.as_str()); + webview + .ExecuteScript(*js.as_ref().as_pcwstr(), &handler) + .map_err(webview2_com::Error::WindowsError) + }), + Box::new(|error_code, _result| error_code), + ) + .unwrap(); + Ok(self) + } + + pub fn dispatch(&self, f: F) -> Result<&Self> + where + F: FnOnce(WebView) + Send + 'static, + { + self.tx.send(Box::new(f)).expect("send the fn"); + + unsafe { + WindowsAndMessaging::PostThreadMessageW( + self.thread_id, + WindowsAndMessaging::WM_APP, + WPARAM::default(), + LPARAM::default(), + ); + } + Ok(self) + } + + pub fn bind(&self, name: &str, f: F) -> Result<&Self> + where + F: FnMut(Vec) -> Result + 'static, + { + self.bindings + .lock() + .unwrap() + .insert(String::from(name), Box::new(f)); + + let js = String::from( + r#" + (function() { + var name = '"#, + ) + name + + r#"'; + var RPC = window._rpc = (window._rpc || {nextSeq: 1}); + window[name] = function() { + var seq = RPC.nextSeq++; + var promise = new Promise(function(resolve, reject) { + RPC[seq] = { + resolve: resolve, + reject: reject, + }; + }); + window.external.invoke({ + id: seq, + method: name, + params: Array.prototype.slice.call(arguments), + }); + return promise; + } + })()"#; + + self.init(&js) + } + + pub fn resolve(&self, id: u64, status: i32, result: Value) -> Result<&Self> { + let result = result.to_string(); + + self.dispatch(move |webview| { + let method = match status { + 0 => "resolve", + _ => "reject", + }; + let js = format!( + r#" + window._rpc[{id}].{method}({result}); + window._rpc[{id}] = undefined;"# + ); + + webview.eval(&js).expect("eval return script"); + }) + } +} + +extern "system" fn window_proc(hwnd: HWND, msg: u32, w_param: WPARAM, l_param: LPARAM) -> LRESULT { + let webview = match unsafe { WEBVIEW_INSTANCE.get() } { + Some(webview) => webview, + None => return unsafe { WindowsAndMessaging::DefWindowProcW(hwnd, msg, w_param, l_param) }, + }; + + let frame = webview + .frame + .as_ref() + .expect("should only be called for owned windows"); + + match msg { + WindowsAndMessaging::WM_SIZE => { + let size = get_window_size(hwnd); + unsafe { + webview + .controller + .0 + .SetBounds(RECT { + left: 0, + top: 0, + right: size.cx, + bottom: size.cy, + }) + .unwrap(); + } + *frame.size.lock().expect("lock size") = size; + LRESULT::default() + } + + WindowsAndMessaging::WM_CLOSE => { + unsafe { + WindowsAndMessaging::DestroyWindow(hwnd); + } + LRESULT::default() + } + + WindowsAndMessaging::WM_DESTROY => { + // webview.terminate().expect("window is gone"); + LRESULT::default() + } + + _ => unsafe { WindowsAndMessaging::DefWindowProcW(hwnd, msg, w_param, l_param) }, + } +} + +fn get_window_size(hwnd: HWND) -> SIZE { + let mut client_rect = RECT::default(); + unsafe { WindowsAndMessaging::GetClientRect(hwnd, std::mem::transmute(&mut client_rect)) }; + SIZE { + cx: client_rect.right - client_rect.left, + cy: client_rect.bottom - client_rect.top, + } +} diff --git a/components/macros/src/lib.rs b/components/macros/src/lib.rs index 55519c4..b08af50 100644 --- a/components/macros/src/lib.rs +++ b/components/macros/src/lib.rs @@ -258,12 +258,20 @@ pub fn function_hook( }; let fn_body = impl_fn.block.stmts; + // preserve calling convention, if specified on the function, otherwise default to C + let abi = impl_fn + .sig + .abi + .as_ref() + .map(|abi| quote! { #abi }) + .unwrap_or_else(|| quote! { extern "C" }); + quote! { #[doc = "Auto-generated function hook."] #(#doc)* #[allow(non_upper_case_globals)] static_detour! { - static #hook_name: unsafe extern "C" #fn_type; + static #hook_name: unsafe #abi #fn_type; } #[doc = "Auto-generated function hook."]