Add native self-hosted instance connection to fluxer_desktop
Trimmed monorepo checkout (fluxer_desktop + packages/voice_engine_v2 + tools/ci) with a "Connect to a Different Server" menu item and popout that lets the desktop app switch to any self-hosted Fluxer instance, plus fixes for well-known discovery on single-domain self-hosted deployments and a false-positive ERR_ABORTED on same-origin client redirects during the switch. Defaults to chat.fluxr.chat and uses an isolated userData directory from the official build.
This commit is contained in:
+156
@@ -0,0 +1,156 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "d3d10-present-fixture"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"windows",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
|
||||
dependencies = [
|
||||
"windows-collections",
|
||||
"windows-core",
|
||||
"windows-future",
|
||||
"windows-numerics",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-collections"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-future"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-link",
|
||||
"windows-threading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.59.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-numerics"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-threading"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
@@ -0,0 +1,29 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
[package]
|
||||
name = "d3d10-present-fixture"
|
||||
version = "0.0.0"
|
||||
edition = "2024"
|
||||
license = "AGPL-3.0-or-later"
|
||||
publish = false
|
||||
|
||||
# Empty workspace table keeps this fixture out of any parent Cargo workspace,
|
||||
# matching the sibling crates (hook/, vulkan-layer/) in this package.
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
|
||||
[[bin]]
|
||||
name = "d3d10-present-fixture"
|
||||
path = "src/main.rs"
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
windows = {version = "0.62.2", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_Graphics_Direct3D",
|
||||
"Win32_Graphics_Direct3D10",
|
||||
"Win32_Graphics_Dxgi",
|
||||
"Win32_Graphics_Dxgi_Common",
|
||||
"Win32_Graphics_Gdi",
|
||||
"Win32_System_LibraryLoader",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
]}
|
||||
windows-core = "0.62.2"
|
||||
@@ -0,0 +1,389 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn main() {
|
||||
eprintln!("d3d10-present-fixture is only functional on Windows");
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn main() -> windows_core::Result<()> {
|
||||
windows_impl::run()
|
||||
}
|
||||
|
||||
#[cfg_attr(not(windows), allow(dead_code))]
|
||||
mod pattern {
|
||||
pub const BARS: [(u8, u8, u8); 8] = [
|
||||
(255, 255, 255),
|
||||
(255, 255, 0),
|
||||
(0, 255, 255),
|
||||
(0, 255, 0),
|
||||
(255, 0, 255),
|
||||
(255, 0, 0),
|
||||
(0, 0, 255),
|
||||
(0, 0, 0),
|
||||
];
|
||||
|
||||
pub const COUNTER_BLOCK: u32 = 16;
|
||||
|
||||
pub fn bar_colour(x: u32, width: u32) -> (u8, u8, u8) {
|
||||
let width = width.max(1);
|
||||
let index = ((x as u64 * BARS.len() as u64) / width as u64) as usize;
|
||||
BARS[index.min(BARS.len() - 1)]
|
||||
}
|
||||
|
||||
pub fn counter_colour(frame_index: u64) -> (u8, u8, u8) {
|
||||
let low = (frame_index & 0x00FF_FFFF) as u32;
|
||||
(
|
||||
(low & 0xFF) as u8,
|
||||
((low >> 8) & 0xFF) as u8,
|
||||
((low >> 16) & 0xFF) as u8,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
mod cli {
|
||||
pub struct Options {
|
||||
pub frames: Option<u64>,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub borderless: bool,
|
||||
}
|
||||
|
||||
impl Default for Options {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
frames: None,
|
||||
width: 1280,
|
||||
height: 720,
|
||||
borderless: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Options {
|
||||
pub fn from_env() -> Self {
|
||||
let mut options = Options::default();
|
||||
let mut args = std::env::args().skip(1);
|
||||
while let Some(arg) = args.next() {
|
||||
match arg.as_str() {
|
||||
"--frames" => {
|
||||
if let Some(value) = args.next().and_then(|v| v.parse().ok()) {
|
||||
options.frames = Some(value);
|
||||
}
|
||||
}
|
||||
"--width" => {
|
||||
if let Some(value) = args.next().and_then(|v| v.parse().ok()) {
|
||||
options.width = value;
|
||||
}
|
||||
}
|
||||
"--height" => {
|
||||
if let Some(value) = args.next().and_then(|v| v.parse().ok()) {
|
||||
options.height = value;
|
||||
}
|
||||
}
|
||||
"--borderless" => options.borderless = true,
|
||||
"--windowed" => options.borderless = false,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
options
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
mod windows_impl {
|
||||
use super::{cli::Options, pattern};
|
||||
use windows::Win32::Foundation::{HMODULE, HWND, LPARAM, LRESULT, WPARAM};
|
||||
use windows::Win32::Graphics::Direct3D10::{
|
||||
D3D10_DRIVER_TYPE_HARDWARE, D3D10_MAPPED_TEXTURE2D, D3D10_SDK_VERSION, D3D10CreateDevice,
|
||||
ID3D10Device, ID3D10RenderTargetView, ID3D10Resource, ID3D10Texture2D,
|
||||
};
|
||||
use windows::Win32::Graphics::Dxgi::Common::{
|
||||
DXGI_FORMAT, DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_R8G8B8A8_UNORM, DXGI_MODE_DESC,
|
||||
DXGI_RATIONAL, DXGI_SAMPLE_DESC,
|
||||
};
|
||||
use windows::Win32::Graphics::Dxgi::{
|
||||
DXGI_PRESENT, DXGI_SWAP_CHAIN_DESC, DXGI_SWAP_EFFECT_DISCARD,
|
||||
DXGI_USAGE_RENDER_TARGET_OUTPUT, IDXGIDevice, IDXGIFactory, IDXGISwapChain,
|
||||
};
|
||||
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::{
|
||||
CS_HREDRAW, CS_VREDRAW, CW_USEDEFAULT, CreateWindowExW, DefWindowProcW, DispatchMessageW,
|
||||
GetClientRect, MSG, PM_REMOVE, PeekMessageW, PostQuitMessage, RegisterClassExW, SW_SHOW,
|
||||
ShowWindow, TranslateMessage, WM_DESTROY, WM_QUIT, WNDCLASSEXW, WS_OVERLAPPEDWINDOW,
|
||||
WS_POPUP, WS_VISIBLE,
|
||||
};
|
||||
use windows_core::{BOOL, Interface, Result, w};
|
||||
|
||||
extern "system" fn wnd_proc(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
|
||||
unsafe {
|
||||
match msg {
|
||||
WM_DESTROY => {
|
||||
PostQuitMessage(0);
|
||||
LRESULT(0)
|
||||
}
|
||||
_ => DefWindowProcW(hwnd, msg, wparam, lparam),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn create_window(width: u32, height: u32, borderless: bool) -> Result<HWND> {
|
||||
unsafe {
|
||||
let instance = GetModuleHandleW(None)?;
|
||||
let class_name = w!("FluxerD3D10Fixture");
|
||||
let wc = WNDCLASSEXW {
|
||||
cbSize: size_of::<WNDCLASSEXW>() as u32,
|
||||
style: CS_HREDRAW | CS_VREDRAW,
|
||||
lpfnWndProc: Some(wnd_proc),
|
||||
hInstance: instance.into(),
|
||||
lpszClassName: class_name,
|
||||
..Default::default()
|
||||
};
|
||||
RegisterClassExW(&wc);
|
||||
let style = if borderless {
|
||||
WS_POPUP | WS_VISIBLE
|
||||
} else {
|
||||
WS_OVERLAPPEDWINDOW | WS_VISIBLE
|
||||
};
|
||||
let hwnd = CreateWindowExW(
|
||||
Default::default(),
|
||||
class_name,
|
||||
w!("Fluxer D3D10 Present Fixture"),
|
||||
style,
|
||||
CW_USEDEFAULT,
|
||||
CW_USEDEFAULT,
|
||||
width as i32,
|
||||
height as i32,
|
||||
None,
|
||||
None,
|
||||
Some(instance.into()),
|
||||
None,
|
||||
)?;
|
||||
let _ = ShowWindow(hwnd, SW_SHOW);
|
||||
Ok(hwnd)
|
||||
}
|
||||
}
|
||||
|
||||
struct Renderer {
|
||||
device: ID3D10Device,
|
||||
swap_chain: IDXGISwapChain,
|
||||
rtv: ID3D10RenderTargetView,
|
||||
width: u32,
|
||||
height: u32,
|
||||
staging: ID3D10Texture2D,
|
||||
format: DXGI_FORMAT,
|
||||
}
|
||||
|
||||
impl Renderer {
|
||||
unsafe fn new(hwnd: HWND, width: u32, height: u32) -> Result<Self> {
|
||||
unsafe {
|
||||
let mut device: Option<ID3D10Device> = None;
|
||||
D3D10CreateDevice(
|
||||
None,
|
||||
D3D10_DRIVER_TYPE_HARDWARE,
|
||||
HMODULE::default(),
|
||||
0,
|
||||
D3D10_SDK_VERSION,
|
||||
Some(&mut device),
|
||||
)?;
|
||||
let device = device.expect("D3D10CreateDeviceAndSwapChain yielded no device");
|
||||
|
||||
let dxgi_device: IDXGIDevice = device.cast()?;
|
||||
let adapter = dxgi_device.GetAdapter()?;
|
||||
let factory: IDXGIFactory = adapter.GetParent()?;
|
||||
let (swap_chain, format) =
|
||||
Self::make_swap_chain(&factory, &device, hwnd, width, height)?;
|
||||
|
||||
let rtv = Self::make_rtv(&device, &swap_chain)?;
|
||||
let staging = Self::make_staging(&device, width, height, format)?;
|
||||
|
||||
Ok(Self {
|
||||
device,
|
||||
swap_chain,
|
||||
rtv,
|
||||
width,
|
||||
height,
|
||||
staging,
|
||||
format,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn make_swap_chain(
|
||||
factory: &IDXGIFactory,
|
||||
device: &ID3D10Device,
|
||||
hwnd: HWND,
|
||||
width: u32,
|
||||
height: u32,
|
||||
) -> Result<(IDXGISwapChain, DXGI_FORMAT)> {
|
||||
unsafe {
|
||||
let mut last = windows_core::HRESULT(0);
|
||||
for format in [DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_R8G8B8A8_UNORM] {
|
||||
let desc = DXGI_SWAP_CHAIN_DESC {
|
||||
BufferDesc: DXGI_MODE_DESC {
|
||||
Width: width,
|
||||
Height: height,
|
||||
RefreshRate: DXGI_RATIONAL {
|
||||
Numerator: 0,
|
||||
Denominator: 0,
|
||||
},
|
||||
Format: format,
|
||||
..Default::default()
|
||||
},
|
||||
SampleDesc: DXGI_SAMPLE_DESC {
|
||||
Count: 1,
|
||||
Quality: 0,
|
||||
},
|
||||
BufferUsage: DXGI_USAGE_RENDER_TARGET_OUTPUT,
|
||||
BufferCount: 1,
|
||||
OutputWindow: hwnd,
|
||||
Windowed: BOOL(1),
|
||||
SwapEffect: DXGI_SWAP_EFFECT_DISCARD,
|
||||
Flags: 0,
|
||||
};
|
||||
let mut swap_chain: Option<IDXGISwapChain> = None;
|
||||
let hr = factory.CreateSwapChain(device, &desc, &mut swap_chain);
|
||||
if hr.is_ok() {
|
||||
return Ok((
|
||||
swap_chain.expect("CreateSwapChain yielded no swapchain"),
|
||||
format,
|
||||
));
|
||||
}
|
||||
last = hr;
|
||||
}
|
||||
last.ok()?;
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn make_rtv(
|
||||
device: &ID3D10Device,
|
||||
swap_chain: &IDXGISwapChain,
|
||||
) -> Result<ID3D10RenderTargetView> {
|
||||
unsafe {
|
||||
let back_buffer: ID3D10Texture2D = swap_chain.GetBuffer(0)?;
|
||||
let resource: ID3D10Resource = back_buffer.cast()?;
|
||||
let mut rtv: Option<ID3D10RenderTargetView> = None;
|
||||
device.CreateRenderTargetView(&resource, None, Some(&mut rtv))?;
|
||||
Ok(rtv.expect("CreateRenderTargetView yielded no view"))
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn make_staging(
|
||||
device: &ID3D10Device,
|
||||
width: u32,
|
||||
height: u32,
|
||||
format: DXGI_FORMAT,
|
||||
) -> Result<ID3D10Texture2D> {
|
||||
use windows::Win32::Graphics::Direct3D10::{
|
||||
D3D10_CPU_ACCESS_WRITE, D3D10_TEXTURE2D_DESC, D3D10_USAGE_STAGING,
|
||||
};
|
||||
unsafe {
|
||||
let desc = D3D10_TEXTURE2D_DESC {
|
||||
Width: width,
|
||||
Height: height,
|
||||
MipLevels: 1,
|
||||
ArraySize: 1,
|
||||
Format: format,
|
||||
SampleDesc: DXGI_SAMPLE_DESC {
|
||||
Count: 1,
|
||||
Quality: 0,
|
||||
},
|
||||
Usage: D3D10_USAGE_STAGING,
|
||||
BindFlags: 0,
|
||||
CPUAccessFlags: D3D10_CPU_ACCESS_WRITE.0 as u32,
|
||||
MiscFlags: 0,
|
||||
};
|
||||
device.CreateTexture2D(&desc, None)
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn render_frame(&self, frame_index: u64) -> Result<()> {
|
||||
use windows::Win32::Graphics::Direct3D10::D3D10_MAP_WRITE;
|
||||
unsafe {
|
||||
let (cr, cg, cb) =
|
||||
pattern::BARS[(frame_index % pattern::BARS.len() as u64) as usize];
|
||||
let clear = [cr as f32 / 255.0, cg as f32 / 255.0, cb as f32 / 255.0, 1.0];
|
||||
self.device.ClearRenderTargetView(&self.rtv, &clear);
|
||||
|
||||
let mapped: D3D10_MAPPED_TEXTURE2D = self.staging.Map(0, D3D10_MAP_WRITE, 0)?;
|
||||
let row_pitch = mapped.RowPitch as usize;
|
||||
let base = mapped.pData as *mut u8;
|
||||
let counter = pattern::counter_colour(frame_index);
|
||||
let luma = (frame_index & 0xFF) as u8;
|
||||
for y in 0..self.height {
|
||||
let row = base.add(y as usize * row_pitch);
|
||||
for x in 0..self.width {
|
||||
let (r, g, b) = if x < pattern::COUNTER_BLOCK && y < pattern::COUNTER_BLOCK
|
||||
{
|
||||
counter
|
||||
} else {
|
||||
pattern::bar_colour(x, self.width)
|
||||
};
|
||||
let px = row.add(x as usize * 4);
|
||||
if self.format == DXGI_FORMAT_R8G8B8A8_UNORM {
|
||||
*px = r;
|
||||
*px.add(1) = g;
|
||||
*px.add(2) = b.saturating_add(luma / 4);
|
||||
} else {
|
||||
*px = b.saturating_add(luma / 4);
|
||||
*px.add(1) = g;
|
||||
*px.add(2) = r;
|
||||
}
|
||||
*px.add(3) = 255;
|
||||
}
|
||||
}
|
||||
self.staging.Unmap(0);
|
||||
|
||||
let back_buffer: ID3D10Texture2D = self.swap_chain.GetBuffer(0)?;
|
||||
let dst: ID3D10Resource = back_buffer.cast()?;
|
||||
let src: ID3D10Resource = self.staging.cast()?;
|
||||
self.device.CopyResource(&dst, &src);
|
||||
self.swap_chain.Present(1, DXGI_PRESENT(0)).ok()?;
|
||||
let _ = &self.device;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run() -> Result<()> {
|
||||
let options = Options::from_env();
|
||||
unsafe {
|
||||
let hwnd = create_window(options.width, options.height, options.borderless)?;
|
||||
|
||||
let mut rect = Default::default();
|
||||
let _ = GetClientRect(hwnd, &mut rect);
|
||||
let width = ((rect.right - rect.left).max(1)) as u32;
|
||||
let height = ((rect.bottom - rect.top).max(1)) as u32;
|
||||
|
||||
let renderer = Renderer::new(hwnd, width, height)?;
|
||||
|
||||
println!("HWND={}", hwnd.0 as isize);
|
||||
use std::io::Write;
|
||||
let _ = std::io::stdout().flush();
|
||||
|
||||
let mut frame_index: u64 = 0;
|
||||
let mut msg = MSG::default();
|
||||
loop {
|
||||
while PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).as_bool() {
|
||||
if msg.message == WM_QUIT {
|
||||
return Ok(());
|
||||
}
|
||||
let _ = TranslateMessage(&msg);
|
||||
DispatchMessageW(&msg);
|
||||
}
|
||||
|
||||
renderer.render_frame(frame_index)?;
|
||||
frame_index += 1;
|
||||
|
||||
if let Some(limit) = options.frames {
|
||||
if frame_index >= limit {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "d3d11-present-fixture"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"windows",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
|
||||
dependencies = [
|
||||
"windows-collections",
|
||||
"windows-core",
|
||||
"windows-future",
|
||||
"windows-numerics",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-collections"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-future"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-link",
|
||||
"windows-threading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.59.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-numerics"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-threading"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
@@ -0,0 +1,29 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
[package]
|
||||
name = "d3d11-present-fixture"
|
||||
version = "0.0.0"
|
||||
edition = "2024"
|
||||
license = "AGPL-3.0-or-later"
|
||||
publish = false
|
||||
|
||||
# Empty workspace table keeps this fixture out of any parent Cargo workspace,
|
||||
# matching the sibling crates (hook/, vulkan-layer/) in this package.
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
|
||||
[[bin]]
|
||||
name = "d3d11-present-fixture"
|
||||
path = "src/main.rs"
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
windows = {version = "0.62.2", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_Graphics_Direct3D",
|
||||
"Win32_Graphics_Direct3D11",
|
||||
"Win32_Graphics_Dxgi",
|
||||
"Win32_Graphics_Dxgi_Common",
|
||||
"Win32_Graphics_Gdi",
|
||||
"Win32_System_LibraryLoader",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
]}
|
||||
windows-core = "0.62.2"
|
||||
@@ -0,0 +1,496 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn main() {
|
||||
eprintln!("d3d11-present-fixture is only functional on Windows");
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn main() -> windows_core::Result<()> {
|
||||
windows_impl::run()
|
||||
}
|
||||
|
||||
#[cfg_attr(not(windows), allow(dead_code))]
|
||||
mod pattern {
|
||||
pub const BARS: [(u8, u8, u8); 8] = [
|
||||
(255, 255, 255),
|
||||
(255, 255, 0),
|
||||
(0, 255, 255),
|
||||
(0, 255, 0),
|
||||
(255, 0, 255),
|
||||
(255, 0, 0),
|
||||
(0, 0, 255),
|
||||
(0, 0, 0),
|
||||
];
|
||||
|
||||
pub const COUNTER_BLOCK: u32 = 16;
|
||||
|
||||
pub fn bar_colour(x: u32, width: u32) -> (u8, u8, u8) {
|
||||
let width = width.max(1);
|
||||
let index = ((x as u64 * BARS.len() as u64) / width as u64) as usize;
|
||||
BARS[index.min(BARS.len() - 1)]
|
||||
}
|
||||
|
||||
pub fn counter_colour(frame_index: u64) -> (u8, u8, u8) {
|
||||
let low = (frame_index & 0x00FF_FFFF) as u32;
|
||||
(
|
||||
(low & 0xFF) as u8,
|
||||
((low >> 8) & 0xFF) as u8,
|
||||
((low >> 16) & 0xFF) as u8,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(not(windows), allow(dead_code))]
|
||||
mod cli {
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum PresentFormat {
|
||||
Bgra8,
|
||||
Rgba8,
|
||||
R10G10B10A2,
|
||||
Rgba16Float,
|
||||
}
|
||||
|
||||
impl PresentFormat {
|
||||
fn parse(value: &str) -> Option<Self> {
|
||||
match value {
|
||||
"bgra8" | "b8g8r8a8" => Some(Self::Bgra8),
|
||||
"rgba8" | "r8g8b8a8" => Some(Self::Rgba8),
|
||||
"r10g10b10a2" | "rgb10a2" | "10bit" => Some(Self::R10G10B10A2),
|
||||
"rgba16f" | "r16g16b16a16f" | "fp16" => Some(Self::Rgba16Float),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Options {
|
||||
pub frames: Option<u64>,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub borderless: bool,
|
||||
pub format: PresentFormat,
|
||||
}
|
||||
|
||||
impl Default for Options {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
frames: None,
|
||||
width: 1280,
|
||||
height: 720,
|
||||
borderless: false,
|
||||
format: PresentFormat::Bgra8,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Options {
|
||||
pub fn from_env() -> Self {
|
||||
let mut options = Options::default();
|
||||
let mut args = std::env::args().skip(1);
|
||||
while let Some(arg) = args.next() {
|
||||
match arg.as_str() {
|
||||
"--frames" => {
|
||||
if let Some(value) = args.next().and_then(|v| v.parse().ok()) {
|
||||
options.frames = Some(value);
|
||||
}
|
||||
}
|
||||
"--width" => {
|
||||
if let Some(value) = args.next().and_then(|v| v.parse().ok()) {
|
||||
options.width = value;
|
||||
}
|
||||
}
|
||||
"--height" => {
|
||||
if let Some(value) = args.next().and_then(|v| v.parse().ok()) {
|
||||
options.height = value;
|
||||
}
|
||||
}
|
||||
"--borderless" => options.borderless = true,
|
||||
"--windowed" => options.borderless = false,
|
||||
"--format" => {
|
||||
if let Some(format) = args.next().and_then(|v| PresentFormat::parse(&v)) {
|
||||
options.format = format;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
options
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::PresentFormat;
|
||||
|
||||
#[test]
|
||||
fn parses_explicit_format_modes() {
|
||||
assert_eq!(PresentFormat::parse("bgra8"), Some(PresentFormat::Bgra8));
|
||||
assert_eq!(PresentFormat::parse("rgba8"), Some(PresentFormat::Rgba8));
|
||||
assert_eq!(
|
||||
PresentFormat::parse("r10g10b10a2"),
|
||||
Some(PresentFormat::R10G10B10A2)
|
||||
);
|
||||
assert_eq!(
|
||||
PresentFormat::parse("fp16"),
|
||||
Some(PresentFormat::Rgba16Float)
|
||||
);
|
||||
assert_eq!(PresentFormat::parse("unknown"), None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
mod windows_impl {
|
||||
use super::{
|
||||
cli::{Options, PresentFormat},
|
||||
pattern,
|
||||
};
|
||||
use windows::Win32::Foundation::{HMODULE, HWND, LPARAM, LRESULT, WPARAM};
|
||||
use windows::Win32::Graphics::Direct3D::D3D_DRIVER_TYPE_HARDWARE;
|
||||
use windows::Win32::Graphics::Direct3D11::{
|
||||
D3D11_CREATE_DEVICE_BGRA_SUPPORT, D3D11_MAPPED_SUBRESOURCE, D3D11_SDK_VERSION,
|
||||
D3D11CreateDevice, ID3D11Device, ID3D11DeviceContext, ID3D11RenderTargetView,
|
||||
ID3D11Resource, ID3D11Texture2D,
|
||||
};
|
||||
use windows::Win32::Graphics::Dxgi::Common::{
|
||||
DXGI_ALPHA_MODE_IGNORE, DXGI_FORMAT, DXGI_FORMAT_B8G8R8A8_UNORM,
|
||||
DXGI_FORMAT_R8G8B8A8_UNORM, DXGI_FORMAT_R10G10B10A2_UNORM, DXGI_FORMAT_R16G16B16A16_FLOAT,
|
||||
DXGI_SAMPLE_DESC,
|
||||
};
|
||||
use windows::Win32::Graphics::Dxgi::{
|
||||
DXGI_SCALING_STRETCH, DXGI_SWAP_CHAIN_DESC1, DXGI_SWAP_EFFECT_FLIP_DISCARD,
|
||||
DXGI_USAGE_RENDER_TARGET_OUTPUT, IDXGIDevice, IDXGIFactory2, IDXGISwapChain1,
|
||||
};
|
||||
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::{
|
||||
CS_HREDRAW, CS_VREDRAW, CW_USEDEFAULT, CreateWindowExW, DefWindowProcW, DispatchMessageW,
|
||||
GetClientRect, MSG, PM_REMOVE, PeekMessageW, PostQuitMessage, RegisterClassExW, SW_SHOW,
|
||||
ShowWindow, TranslateMessage, WM_DESTROY, WM_QUIT, WNDCLASSEXW, WS_OVERLAPPEDWINDOW,
|
||||
WS_POPUP, WS_VISIBLE,
|
||||
};
|
||||
use windows_core::{Interface, Result, w};
|
||||
|
||||
impl PresentFormat {
|
||||
fn dxgi(self) -> DXGI_FORMAT {
|
||||
match self {
|
||||
PresentFormat::Bgra8 => DXGI_FORMAT_B8G8R8A8_UNORM,
|
||||
PresentFormat::Rgba8 => DXGI_FORMAT_R8G8B8A8_UNORM,
|
||||
PresentFormat::R10G10B10A2 => DXGI_FORMAT_R10G10B10A2_UNORM,
|
||||
PresentFormat::Rgba16Float => DXGI_FORMAT_R16G16B16A16_FLOAT,
|
||||
}
|
||||
}
|
||||
|
||||
fn bytes_per_pixel(self) -> usize {
|
||||
match self {
|
||||
PresentFormat::Bgra8 | PresentFormat::Rgba8 | PresentFormat::R10G10B10A2 => 4,
|
||||
PresentFormat::Rgba16Float => 8,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extern "system" fn wnd_proc(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
|
||||
unsafe {
|
||||
match msg {
|
||||
WM_DESTROY => {
|
||||
PostQuitMessage(0);
|
||||
LRESULT(0)
|
||||
}
|
||||
_ => DefWindowProcW(hwnd, msg, wparam, lparam),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn create_window(width: u32, height: u32, borderless: bool) -> Result<HWND> {
|
||||
unsafe {
|
||||
let instance = GetModuleHandleW(None)?;
|
||||
let class_name = w!("FluxerD3D11Fixture");
|
||||
let wc = WNDCLASSEXW {
|
||||
cbSize: size_of::<WNDCLASSEXW>() as u32,
|
||||
style: CS_HREDRAW | CS_VREDRAW,
|
||||
lpfnWndProc: Some(wnd_proc),
|
||||
hInstance: instance.into(),
|
||||
lpszClassName: class_name,
|
||||
..Default::default()
|
||||
};
|
||||
RegisterClassExW(&wc);
|
||||
let style = if borderless {
|
||||
WS_POPUP | WS_VISIBLE
|
||||
} else {
|
||||
WS_OVERLAPPEDWINDOW | WS_VISIBLE
|
||||
};
|
||||
let hwnd = CreateWindowExW(
|
||||
Default::default(),
|
||||
class_name,
|
||||
w!("Fluxer D3D11 Present Fixture"),
|
||||
style,
|
||||
CW_USEDEFAULT,
|
||||
CW_USEDEFAULT,
|
||||
width as i32,
|
||||
height as i32,
|
||||
None,
|
||||
None,
|
||||
Some(instance.into()),
|
||||
None,
|
||||
)?;
|
||||
let _ = ShowWindow(hwnd, SW_SHOW);
|
||||
Ok(hwnd)
|
||||
}
|
||||
}
|
||||
|
||||
struct Renderer {
|
||||
device: ID3D11Device,
|
||||
context: ID3D11DeviceContext,
|
||||
swap_chain: IDXGISwapChain1,
|
||||
rtv: ID3D11RenderTargetView,
|
||||
width: u32,
|
||||
height: u32,
|
||||
staging: ID3D11Texture2D,
|
||||
format: PresentFormat,
|
||||
}
|
||||
|
||||
impl Renderer {
|
||||
unsafe fn new(hwnd: HWND, width: u32, height: u32, format: PresentFormat) -> Result<Self> {
|
||||
unsafe {
|
||||
let mut device: Option<ID3D11Device> = None;
|
||||
let mut context: Option<ID3D11DeviceContext> = None;
|
||||
D3D11CreateDevice(
|
||||
None,
|
||||
D3D_DRIVER_TYPE_HARDWARE,
|
||||
HMODULE::default(),
|
||||
D3D11_CREATE_DEVICE_BGRA_SUPPORT,
|
||||
None,
|
||||
D3D11_SDK_VERSION,
|
||||
Some(&mut device),
|
||||
None,
|
||||
Some(&mut context),
|
||||
)?;
|
||||
let device = device.expect("D3D11CreateDevice yielded no device");
|
||||
let context = context.expect("D3D11CreateDevice yielded no context");
|
||||
|
||||
let dxgi_device: IDXGIDevice = device.cast()?;
|
||||
let adapter = dxgi_device.GetAdapter()?;
|
||||
let factory: IDXGIFactory2 = adapter.GetParent()?;
|
||||
|
||||
let desc = DXGI_SWAP_CHAIN_DESC1 {
|
||||
Width: width,
|
||||
Height: height,
|
||||
Format: format.dxgi(),
|
||||
Stereo: false.into(),
|
||||
SampleDesc: DXGI_SAMPLE_DESC {
|
||||
Count: 1,
|
||||
Quality: 0,
|
||||
},
|
||||
BufferUsage: DXGI_USAGE_RENDER_TARGET_OUTPUT,
|
||||
BufferCount: 2,
|
||||
Scaling: DXGI_SCALING_STRETCH,
|
||||
SwapEffect: DXGI_SWAP_EFFECT_FLIP_DISCARD,
|
||||
AlphaMode: DXGI_ALPHA_MODE_IGNORE,
|
||||
Flags: 0,
|
||||
};
|
||||
let swap_chain =
|
||||
factory.CreateSwapChainForHwnd(&device, hwnd, &desc, None, None)?;
|
||||
|
||||
let rtv = Self::make_rtv(&device, &swap_chain)?;
|
||||
let staging = Self::make_staging(&device, width, height, format)?;
|
||||
|
||||
Ok(Self {
|
||||
device,
|
||||
context,
|
||||
swap_chain,
|
||||
rtv,
|
||||
width,
|
||||
height,
|
||||
staging,
|
||||
format,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn make_rtv(
|
||||
device: &ID3D11Device,
|
||||
swap_chain: &IDXGISwapChain1,
|
||||
) -> Result<ID3D11RenderTargetView> {
|
||||
unsafe {
|
||||
let back_buffer: ID3D11Texture2D = swap_chain.GetBuffer(0)?;
|
||||
let resource: ID3D11Resource = back_buffer.cast()?;
|
||||
let mut rtv: Option<ID3D11RenderTargetView> = None;
|
||||
device.CreateRenderTargetView(&resource, None, Some(&mut rtv))?;
|
||||
Ok(rtv.expect("CreateRenderTargetView yielded no view"))
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn make_staging(
|
||||
device: &ID3D11Device,
|
||||
width: u32,
|
||||
height: u32,
|
||||
format: PresentFormat,
|
||||
) -> Result<ID3D11Texture2D> {
|
||||
use windows::Win32::Graphics::Direct3D11::{
|
||||
D3D11_CPU_ACCESS_WRITE, D3D11_TEXTURE2D_DESC, D3D11_USAGE_STAGING,
|
||||
};
|
||||
unsafe {
|
||||
let desc = D3D11_TEXTURE2D_DESC {
|
||||
Width: width,
|
||||
Height: height,
|
||||
MipLevels: 1,
|
||||
ArraySize: 1,
|
||||
Format: format.dxgi(),
|
||||
SampleDesc: DXGI_SAMPLE_DESC {
|
||||
Count: 1,
|
||||
Quality: 0,
|
||||
},
|
||||
Usage: D3D11_USAGE_STAGING,
|
||||
BindFlags: 0,
|
||||
CPUAccessFlags: D3D11_CPU_ACCESS_WRITE.0 as u32,
|
||||
MiscFlags: 0,
|
||||
};
|
||||
let mut staging: Option<ID3D11Texture2D> = None;
|
||||
device.CreateTexture2D(&desc, None, Some(&mut staging))?;
|
||||
Ok(staging.expect("CreateTexture2D yielded no staging texture"))
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn render_frame(&self, frame_index: u64) -> Result<()> {
|
||||
use windows::Win32::Graphics::Direct3D11::D3D11_MAP_WRITE;
|
||||
unsafe {
|
||||
let mut mapped = D3D11_MAPPED_SUBRESOURCE::default();
|
||||
self.context
|
||||
.Map(&self.staging, 0, D3D11_MAP_WRITE, 0, Some(&mut mapped))?;
|
||||
let row_pitch = mapped.RowPitch as usize;
|
||||
let base = mapped.pData as *mut u8;
|
||||
let counter = pattern::counter_colour(frame_index);
|
||||
let luma = (frame_index & 0xFF) as u8;
|
||||
let bpp = self.format.bytes_per_pixel();
|
||||
for y in 0..self.height {
|
||||
let row = base.add(y as usize * row_pitch);
|
||||
for x in 0..self.width {
|
||||
let (r, g, b) = if x < pattern::COUNTER_BLOCK && y < pattern::COUNTER_BLOCK
|
||||
{
|
||||
counter
|
||||
} else {
|
||||
pattern::bar_colour(x, self.width)
|
||||
};
|
||||
let px = row.add(x as usize * bpp);
|
||||
write_pixel(self.format, px, r, g, b.saturating_add(luma / 4));
|
||||
}
|
||||
}
|
||||
self.context.Unmap(&self.staging, 0);
|
||||
|
||||
let back_buffer: ID3D11Texture2D = self.swap_chain.GetBuffer(0)?;
|
||||
let dst: ID3D11Resource = back_buffer.cast()?;
|
||||
let src: ID3D11Resource = self.staging.cast()?;
|
||||
self.context.CopyResource(&dst, &src);
|
||||
let _ = &self.rtv;
|
||||
self.swap_chain.Present(1, Default::default()).ok()?;
|
||||
let _ = &self.device;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn write_pixel(format: PresentFormat, px: *mut u8, r: u8, g: u8, b: u8) {
|
||||
unsafe {
|
||||
match format {
|
||||
PresentFormat::Bgra8 => {
|
||||
*px = b;
|
||||
*px.add(1) = g;
|
||||
*px.add(2) = r;
|
||||
*px.add(3) = 255;
|
||||
}
|
||||
PresentFormat::Rgba8 => {
|
||||
*px = r;
|
||||
*px.add(1) = g;
|
||||
*px.add(2) = b;
|
||||
*px.add(3) = 255;
|
||||
}
|
||||
PresentFormat::R10G10B10A2 => {
|
||||
let packed = scale8_to_10(r)
|
||||
| (scale8_to_10(g) << 10)
|
||||
| (scale8_to_10(b) << 20)
|
||||
| (0x3 << 30);
|
||||
std::ptr::copy_nonoverlapping(packed.to_le_bytes().as_ptr(), px, 4);
|
||||
}
|
||||
PresentFormat::Rgba16Float => {
|
||||
let highlight = if r == 255 && g == 255 && b > 240 {
|
||||
1.25
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
let values = [
|
||||
f32_to_f16((r as f32 / 255.0) * highlight),
|
||||
f32_to_f16((g as f32 / 255.0) * highlight),
|
||||
f32_to_f16((b as f32 / 255.0) * highlight),
|
||||
f32_to_f16(1.0),
|
||||
];
|
||||
for (index, value) in values.iter().enumerate() {
|
||||
std::ptr::copy_nonoverlapping(
|
||||
value.to_le_bytes().as_ptr(),
|
||||
px.add(index * 2),
|
||||
2,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn scale8_to_10(value: u8) -> u32 {
|
||||
(value as u32 * 1023 + 127) / 255
|
||||
}
|
||||
|
||||
fn f32_to_f16(value: f32) -> u16 {
|
||||
let value = value.clamp(0.0, 65504.0);
|
||||
if value == 0.0 {
|
||||
return 0;
|
||||
}
|
||||
let bits = value.to_bits();
|
||||
let exp = ((bits >> 23) & 0xFF) as i32 - 127 + 15;
|
||||
let mant = bits & 0x7F_FFFF;
|
||||
if exp <= 0 {
|
||||
return 0;
|
||||
}
|
||||
if exp >= 31 {
|
||||
return 0x7BFF;
|
||||
}
|
||||
((exp as u16) << 10) | (((mant + 0x1000) >> 13) as u16)
|
||||
}
|
||||
|
||||
pub fn run() -> Result<()> {
|
||||
let options = Options::from_env();
|
||||
unsafe {
|
||||
let hwnd = create_window(options.width, options.height, options.borderless)?;
|
||||
|
||||
let mut rect = Default::default();
|
||||
let _ = GetClientRect(hwnd, &mut rect);
|
||||
let width = ((rect.right - rect.left).max(1)) as u32;
|
||||
let height = ((rect.bottom - rect.top).max(1)) as u32;
|
||||
|
||||
let renderer = Renderer::new(hwnd, width, height, options.format)?;
|
||||
|
||||
println!("HWND={}", hwnd.0 as isize);
|
||||
use std::io::Write;
|
||||
let _ = std::io::stdout().flush();
|
||||
|
||||
let mut frame_index: u64 = 0;
|
||||
let mut msg = MSG::default();
|
||||
loop {
|
||||
while PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).as_bool() {
|
||||
if msg.message == WM_QUIT {
|
||||
return Ok(());
|
||||
}
|
||||
let _ = TranslateMessage(&msg);
|
||||
DispatchMessageW(&msg);
|
||||
}
|
||||
|
||||
renderer.render_frame(frame_index)?;
|
||||
frame_index += 1;
|
||||
|
||||
if let Some(limit) = options.frames {
|
||||
if frame_index >= limit {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "d3d12-present-fixture"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"windows",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
|
||||
dependencies = [
|
||||
"windows-collections",
|
||||
"windows-core",
|
||||
"windows-future",
|
||||
"windows-numerics",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-collections"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-future"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-link",
|
||||
"windows-threading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.59.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-numerics"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-threading"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
@@ -0,0 +1,31 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
[package]
|
||||
name = "d3d12-present-fixture"
|
||||
version = "0.0.0"
|
||||
edition = "2024"
|
||||
license = "AGPL-3.0-or-later"
|
||||
publish = false
|
||||
|
||||
# Empty workspace table keeps this fixture out of any parent Cargo workspace,
|
||||
# matching the sibling crates (hook/, vulkan-layer/) in this package.
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
|
||||
[[bin]]
|
||||
name = "d3d12-present-fixture"
|
||||
path = "src/main.rs"
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
windows = {version = "0.62.2", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_Graphics_Direct3D",
|
||||
"Win32_Graphics_Direct3D12",
|
||||
"Win32_Graphics_Dxgi",
|
||||
"Win32_Graphics_Dxgi_Common",
|
||||
"Win32_Graphics_Gdi",
|
||||
"Win32_Security",
|
||||
"Win32_System_LibraryLoader",
|
||||
"Win32_System_Threading",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
]}
|
||||
windows-core = "0.62.2"
|
||||
@@ -0,0 +1,582 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn main() {
|
||||
eprintln!("d3d12-present-fixture is only functional on Windows");
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn main() -> windows_core::Result<()> {
|
||||
windows_impl::run()
|
||||
}
|
||||
|
||||
#[cfg_attr(not(windows), allow(dead_code))]
|
||||
mod pattern {
|
||||
pub const BARS: [(u8, u8, u8); 8] = [
|
||||
(255, 255, 255),
|
||||
(255, 255, 0),
|
||||
(0, 255, 255),
|
||||
(0, 255, 0),
|
||||
(255, 0, 255),
|
||||
(255, 0, 0),
|
||||
(0, 0, 255),
|
||||
(0, 0, 0),
|
||||
];
|
||||
|
||||
pub const COUNTER_BLOCK: u32 = 16;
|
||||
|
||||
pub fn bar_colour(x: u32, width: u32) -> (u8, u8, u8) {
|
||||
let width = width.max(1);
|
||||
let index = ((x as u64 * BARS.len() as u64) / width as u64) as usize;
|
||||
BARS[index.min(BARS.len() - 1)]
|
||||
}
|
||||
|
||||
pub fn counter_colour(frame_index: u64) -> (u8, u8, u8) {
|
||||
let low = (frame_index & 0x00FF_FFFF) as u32;
|
||||
(
|
||||
(low & 0xFF) as u8,
|
||||
((low >> 8) & 0xFF) as u8,
|
||||
((low >> 16) & 0xFF) as u8,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(not(windows), allow(dead_code))]
|
||||
mod cli {
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum PresentFormat {
|
||||
Bgra8,
|
||||
Rgba8,
|
||||
R10G10B10A2,
|
||||
Rgba16Float,
|
||||
}
|
||||
|
||||
impl PresentFormat {
|
||||
fn parse(value: &str) -> Option<Self> {
|
||||
match value {
|
||||
"bgra8" | "b8g8r8a8" => Some(Self::Bgra8),
|
||||
"rgba8" | "r8g8b8a8" => Some(Self::Rgba8),
|
||||
"r10g10b10a2" | "rgb10a2" | "10bit" => Some(Self::R10G10B10A2),
|
||||
"rgba16f" | "r16g16b16a16f" | "fp16" => Some(Self::Rgba16Float),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Options {
|
||||
pub frames: Option<u64>,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub borderless: bool,
|
||||
pub backbuffers: u32,
|
||||
pub format: PresentFormat,
|
||||
}
|
||||
|
||||
impl Default for Options {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
frames: None,
|
||||
width: 1280,
|
||||
height: 720,
|
||||
borderless: false,
|
||||
backbuffers: 2,
|
||||
format: PresentFormat::Bgra8,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Options {
|
||||
pub fn from_env() -> Self {
|
||||
let mut options = Options::default();
|
||||
let mut args = std::env::args().skip(1);
|
||||
while let Some(arg) = args.next() {
|
||||
match arg.as_str() {
|
||||
"--frames" => {
|
||||
if let Some(value) = args.next().and_then(|v| v.parse().ok()) {
|
||||
options.frames = Some(value);
|
||||
}
|
||||
}
|
||||
"--width" => {
|
||||
if let Some(value) = args.next().and_then(|v| v.parse().ok()) {
|
||||
options.width = value;
|
||||
}
|
||||
}
|
||||
"--height" => {
|
||||
if let Some(value) = args.next().and_then(|v| v.parse().ok()) {
|
||||
options.height = value;
|
||||
}
|
||||
}
|
||||
"--backbuffers" => {
|
||||
if let Some(value) = args.next().and_then(|v| v.parse().ok()) {
|
||||
options.backbuffers = value;
|
||||
}
|
||||
}
|
||||
"--borderless" => options.borderless = true,
|
||||
"--windowed" => options.borderless = false,
|
||||
"--format" => {
|
||||
if let Some(format) = args.next().and_then(|v| PresentFormat::parse(&v)) {
|
||||
options.format = format;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
options.backbuffers = options.backbuffers.max(2);
|
||||
options
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::PresentFormat;
|
||||
|
||||
#[test]
|
||||
fn parses_explicit_format_modes() {
|
||||
assert_eq!(PresentFormat::parse("bgra8"), Some(PresentFormat::Bgra8));
|
||||
assert_eq!(PresentFormat::parse("rgba8"), Some(PresentFormat::Rgba8));
|
||||
assert_eq!(
|
||||
PresentFormat::parse("r10g10b10a2"),
|
||||
Some(PresentFormat::R10G10B10A2)
|
||||
);
|
||||
assert_eq!(
|
||||
PresentFormat::parse("fp16"),
|
||||
Some(PresentFormat::Rgba16Float)
|
||||
);
|
||||
assert_eq!(PresentFormat::parse("unknown"), None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
mod windows_impl {
|
||||
use super::cli::{Options, PresentFormat};
|
||||
use super::pattern;
|
||||
use windows::Win32::Foundation::RECT;
|
||||
use windows::Win32::Foundation::{CloseHandle, HANDLE, HWND, LPARAM, LRESULT, WPARAM};
|
||||
use windows::Win32::Graphics::Direct3D::{D3D_DRIVER_TYPE_HARDWARE, D3D_FEATURE_LEVEL_11_0};
|
||||
use windows::Win32::Graphics::Direct3D12::{
|
||||
D3D12_COMMAND_LIST_TYPE_DIRECT, D3D12_COMMAND_QUEUE_DESC, D3D12_COMMAND_QUEUE_FLAG_NONE,
|
||||
D3D12_CPU_DESCRIPTOR_HANDLE, D3D12_DESCRIPTOR_HEAP_DESC, D3D12_DESCRIPTOR_HEAP_FLAG_NONE,
|
||||
D3D12_DESCRIPTOR_HEAP_TYPE_RTV, D3D12_FENCE_FLAG_NONE, D3D12_RESOURCE_BARRIER,
|
||||
D3D12_RESOURCE_BARRIER_0, D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES,
|
||||
D3D12_RESOURCE_BARRIER_FLAG_NONE, D3D12_RESOURCE_BARRIER_TYPE_TRANSITION,
|
||||
D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_RENDER_TARGET,
|
||||
D3D12_RESOURCE_TRANSITION_BARRIER, D3D12CreateDevice, ID3D12CommandAllocator,
|
||||
ID3D12CommandQueue, ID3D12DescriptorHeap, ID3D12Device, ID3D12Fence,
|
||||
ID3D12GraphicsCommandList, ID3D12PipelineState, ID3D12Resource,
|
||||
};
|
||||
use windows::Win32::Graphics::Dxgi::Common::{
|
||||
DXGI_ALPHA_MODE_IGNORE, DXGI_FORMAT, DXGI_FORMAT_B8G8R8A8_UNORM,
|
||||
DXGI_FORMAT_R8G8B8A8_UNORM, DXGI_FORMAT_R10G10B10A2_UNORM, DXGI_FORMAT_R16G16B16A16_FLOAT,
|
||||
DXGI_SAMPLE_DESC,
|
||||
};
|
||||
use windows::Win32::Graphics::Dxgi::{
|
||||
CreateDXGIFactory2, DXGI_ADAPTER_FLAG, DXGI_ADAPTER_FLAG_SOFTWARE,
|
||||
DXGI_CREATE_FACTORY_FLAGS, DXGI_SCALING_STRETCH, DXGI_SWAP_CHAIN_DESC1,
|
||||
DXGI_SWAP_EFFECT_FLIP_DISCARD, DXGI_USAGE_RENDER_TARGET_OUTPUT, IDXGIAdapter1,
|
||||
IDXGIFactory4, IDXGISwapChain3,
|
||||
};
|
||||
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
|
||||
use windows::Win32::System::Threading::{CreateEventW, INFINITE, WaitForSingleObject};
|
||||
use windows::Win32::UI::WindowsAndMessaging::{
|
||||
CS_HREDRAW, CS_VREDRAW, CW_USEDEFAULT, CreateWindowExW, DefWindowProcW, DispatchMessageW,
|
||||
GetClientRect, MSG, PM_REMOVE, PeekMessageW, PostQuitMessage, RegisterClassExW, SW_SHOW,
|
||||
ShowWindow, TranslateMessage, WM_DESTROY, WM_QUIT, WNDCLASSEXW, WS_OVERLAPPEDWINDOW,
|
||||
WS_POPUP, WS_VISIBLE,
|
||||
};
|
||||
use windows_core::{Interface, Result, w};
|
||||
|
||||
impl PresentFormat {
|
||||
fn dxgi(self) -> DXGI_FORMAT {
|
||||
match self {
|
||||
PresentFormat::Bgra8 => DXGI_FORMAT_B8G8R8A8_UNORM,
|
||||
PresentFormat::Rgba8 => DXGI_FORMAT_R8G8B8A8_UNORM,
|
||||
PresentFormat::R10G10B10A2 => DXGI_FORMAT_R10G10B10A2_UNORM,
|
||||
PresentFormat::Rgba16Float => DXGI_FORMAT_R16G16B16A16_FLOAT,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extern "system" fn wnd_proc(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
|
||||
unsafe {
|
||||
match msg {
|
||||
WM_DESTROY => {
|
||||
PostQuitMessage(0);
|
||||
LRESULT(0)
|
||||
}
|
||||
_ => DefWindowProcW(hwnd, msg, wparam, lparam),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn create_window(width: u32, height: u32, borderless: bool) -> Result<HWND> {
|
||||
unsafe {
|
||||
let instance = GetModuleHandleW(None)?;
|
||||
let class_name = w!("FluxerD3D12Fixture");
|
||||
let wc = WNDCLASSEXW {
|
||||
cbSize: size_of::<WNDCLASSEXW>() as u32,
|
||||
style: CS_HREDRAW | CS_VREDRAW,
|
||||
lpfnWndProc: Some(wnd_proc),
|
||||
hInstance: instance.into(),
|
||||
lpszClassName: class_name,
|
||||
..Default::default()
|
||||
};
|
||||
RegisterClassExW(&wc);
|
||||
let style = if borderless {
|
||||
WS_POPUP | WS_VISIBLE
|
||||
} else {
|
||||
WS_OVERLAPPEDWINDOW | WS_VISIBLE
|
||||
};
|
||||
let hwnd = CreateWindowExW(
|
||||
Default::default(),
|
||||
class_name,
|
||||
w!("Fluxer D3D12 Present Fixture"),
|
||||
style,
|
||||
CW_USEDEFAULT,
|
||||
CW_USEDEFAULT,
|
||||
width as i32,
|
||||
height as i32,
|
||||
None,
|
||||
None,
|
||||
Some(instance.into()),
|
||||
None,
|
||||
)?;
|
||||
let _ = ShowWindow(hwnd, SW_SHOW);
|
||||
Ok(hwnd)
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn create_device(factory: &IDXGIFactory4) -> Result<ID3D12Device> {
|
||||
unsafe {
|
||||
let mut index = 0u32;
|
||||
while let Ok(adapter) = factory.EnumAdapters1(index) {
|
||||
let adapter: IDXGIAdapter1 = adapter;
|
||||
let desc = adapter.GetDesc1()?;
|
||||
let is_software =
|
||||
(DXGI_ADAPTER_FLAG(desc.Flags as i32).0 & DXGI_ADAPTER_FLAG_SOFTWARE.0) != 0;
|
||||
if !is_software {
|
||||
let mut device: Option<ID3D12Device> = None;
|
||||
if D3D12CreateDevice(&adapter, D3D_FEATURE_LEVEL_11_0, &mut device).is_ok() {
|
||||
if let Some(device) = device {
|
||||
return Ok(device);
|
||||
}
|
||||
}
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
let mut device: Option<ID3D12Device> = None;
|
||||
let _ = D3D_DRIVER_TYPE_HARDWARE;
|
||||
D3D12CreateDevice(None, D3D_FEATURE_LEVEL_11_0, &mut device)?;
|
||||
device.ok_or_else(windows_core::Error::from_thread)
|
||||
}
|
||||
}
|
||||
|
||||
struct Renderer {
|
||||
device: ID3D12Device,
|
||||
queue: ID3D12CommandQueue,
|
||||
swap_chain: IDXGISwapChain3,
|
||||
rtv_heap: ID3D12DescriptorHeap,
|
||||
rtv_descriptor_size: usize,
|
||||
render_targets: Vec<ID3D12Resource>,
|
||||
allocators: Vec<ID3D12CommandAllocator>,
|
||||
list: ID3D12GraphicsCommandList,
|
||||
fence: ID3D12Fence,
|
||||
fence_event: HANDLE,
|
||||
fence_value: u64,
|
||||
frame_fence_values: Vec<u64>,
|
||||
width: u32,
|
||||
height: u32,
|
||||
format: PresentFormat,
|
||||
}
|
||||
|
||||
impl Renderer {
|
||||
unsafe fn new(
|
||||
hwnd: HWND,
|
||||
width: u32,
|
||||
height: u32,
|
||||
backbuffers: u32,
|
||||
format: PresentFormat,
|
||||
) -> Result<Self> {
|
||||
unsafe {
|
||||
let factory: IDXGIFactory4 = CreateDXGIFactory2(DXGI_CREATE_FACTORY_FLAGS(0))?;
|
||||
let device = create_device(&factory)?;
|
||||
|
||||
let queue_desc = D3D12_COMMAND_QUEUE_DESC {
|
||||
Type: D3D12_COMMAND_LIST_TYPE_DIRECT,
|
||||
Priority: 0,
|
||||
Flags: D3D12_COMMAND_QUEUE_FLAG_NONE,
|
||||
NodeMask: 0,
|
||||
};
|
||||
let queue: ID3D12CommandQueue = device.CreateCommandQueue(&queue_desc)?;
|
||||
|
||||
let desc = DXGI_SWAP_CHAIN_DESC1 {
|
||||
Width: width,
|
||||
Height: height,
|
||||
Format: format.dxgi(),
|
||||
Stereo: false.into(),
|
||||
SampleDesc: DXGI_SAMPLE_DESC {
|
||||
Count: 1,
|
||||
Quality: 0,
|
||||
},
|
||||
BufferUsage: DXGI_USAGE_RENDER_TARGET_OUTPUT,
|
||||
BufferCount: backbuffers,
|
||||
Scaling: DXGI_SCALING_STRETCH,
|
||||
SwapEffect: DXGI_SWAP_EFFECT_FLIP_DISCARD,
|
||||
AlphaMode: DXGI_ALPHA_MODE_IGNORE,
|
||||
Flags: 0,
|
||||
};
|
||||
let swap_chain1 =
|
||||
factory.CreateSwapChainForHwnd(&queue, hwnd, &desc, None, None)?;
|
||||
let swap_chain: IDXGISwapChain3 = swap_chain1.cast()?;
|
||||
|
||||
let rtv_heap_desc = D3D12_DESCRIPTOR_HEAP_DESC {
|
||||
Type: D3D12_DESCRIPTOR_HEAP_TYPE_RTV,
|
||||
NumDescriptors: backbuffers,
|
||||
Flags: D3D12_DESCRIPTOR_HEAP_FLAG_NONE,
|
||||
NodeMask: 0,
|
||||
};
|
||||
let rtv_heap: ID3D12DescriptorHeap = device.CreateDescriptorHeap(&rtv_heap_desc)?;
|
||||
let rtv_descriptor_size = device
|
||||
.GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV)
|
||||
as usize;
|
||||
|
||||
let heap_start = rtv_heap.GetCPUDescriptorHandleForHeapStart();
|
||||
let mut render_targets = Vec::with_capacity(backbuffers as usize);
|
||||
let mut allocators = Vec::with_capacity(backbuffers as usize);
|
||||
for i in 0..backbuffers {
|
||||
let back_buffer: ID3D12Resource = swap_chain.GetBuffer(i)?;
|
||||
let handle = D3D12_CPU_DESCRIPTOR_HANDLE {
|
||||
ptr: heap_start.ptr + i as usize * rtv_descriptor_size,
|
||||
};
|
||||
device.CreateRenderTargetView(&back_buffer, None, handle);
|
||||
render_targets.push(back_buffer);
|
||||
|
||||
let allocator: ID3D12CommandAllocator =
|
||||
device.CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT)?;
|
||||
allocators.push(allocator);
|
||||
}
|
||||
|
||||
let list: ID3D12GraphicsCommandList = device.CreateCommandList(
|
||||
0,
|
||||
D3D12_COMMAND_LIST_TYPE_DIRECT,
|
||||
&allocators[0],
|
||||
None::<&ID3D12PipelineState>,
|
||||
)?;
|
||||
list.Close()?;
|
||||
|
||||
let fence: ID3D12Fence = device.CreateFence(0, D3D12_FENCE_FLAG_NONE)?;
|
||||
let fence_event = CreateEventW(None, false, false, None)?;
|
||||
|
||||
Ok(Self {
|
||||
device,
|
||||
queue,
|
||||
swap_chain,
|
||||
rtv_heap,
|
||||
rtv_descriptor_size,
|
||||
render_targets,
|
||||
frame_fence_values: vec![0; backbuffers as usize],
|
||||
allocators,
|
||||
list,
|
||||
fence,
|
||||
fence_event,
|
||||
fence_value: 0,
|
||||
width,
|
||||
height,
|
||||
format,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn transition(
|
||||
resource: &ID3D12Resource,
|
||||
before: windows::Win32::Graphics::Direct3D12::D3D12_RESOURCE_STATES,
|
||||
after: windows::Win32::Graphics::Direct3D12::D3D12_RESOURCE_STATES,
|
||||
) -> D3D12_RESOURCE_BARRIER {
|
||||
D3D12_RESOURCE_BARRIER {
|
||||
Type: D3D12_RESOURCE_BARRIER_TYPE_TRANSITION,
|
||||
Flags: D3D12_RESOURCE_BARRIER_FLAG_NONE,
|
||||
Anonymous: D3D12_RESOURCE_BARRIER_0 {
|
||||
Transition: std::mem::ManuallyDrop::new(D3D12_RESOURCE_TRANSITION_BARRIER {
|
||||
pResource: std::mem::ManuallyDrop::new(Some(resource.clone())),
|
||||
Subresource: D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES,
|
||||
StateBefore: before,
|
||||
StateAfter: after,
|
||||
}),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn render_frame(&mut self, frame_index: u64) -> Result<()> {
|
||||
unsafe {
|
||||
let buffer_index = self.swap_chain.GetCurrentBackBufferIndex() as usize;
|
||||
|
||||
self.wait_for_fence_value(self.frame_fence_values[buffer_index])?;
|
||||
|
||||
let allocator = &self.allocators[buffer_index];
|
||||
let back_buffer = &self.render_targets[buffer_index];
|
||||
allocator.Reset()?;
|
||||
self.list.Reset(allocator, None::<&ID3D12PipelineState>)?;
|
||||
|
||||
let to_rt = Self::transition(
|
||||
back_buffer,
|
||||
D3D12_RESOURCE_STATE_PRESENT,
|
||||
D3D12_RESOURCE_STATE_RENDER_TARGET,
|
||||
);
|
||||
self.list.ResourceBarrier(&[to_rt]);
|
||||
|
||||
let rtv = D3D12_CPU_DESCRIPTOR_HANDLE {
|
||||
ptr: self.rtv_heap.GetCPUDescriptorHandleForHeapStart().ptr
|
||||
+ buffer_index * self.rtv_descriptor_size,
|
||||
};
|
||||
self.list.OMSetRenderTargets(1, Some(&rtv), false, None);
|
||||
|
||||
self.paint_pattern(rtv, frame_index);
|
||||
|
||||
let to_present = Self::transition(
|
||||
back_buffer,
|
||||
D3D12_RESOURCE_STATE_RENDER_TARGET,
|
||||
D3D12_RESOURCE_STATE_PRESENT,
|
||||
);
|
||||
self.list.ResourceBarrier(&[to_present]);
|
||||
|
||||
self.list.Close()?;
|
||||
|
||||
let command_list: windows::Win32::Graphics::Direct3D12::ID3D12CommandList =
|
||||
self.list.cast()?;
|
||||
self.queue.ExecuteCommandLists(&[Some(command_list)]);
|
||||
|
||||
self.swap_chain.Present(1, Default::default()).ok()?;
|
||||
|
||||
self.fence_value += 1;
|
||||
let signal = self.fence_value;
|
||||
self.queue.Signal(&self.fence, signal)?;
|
||||
self.frame_fence_values[buffer_index] = signal;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn paint_pattern(&self, rtv: D3D12_CPU_DESCRIPTOR_HANDLE, frame_index: u64) {
|
||||
unsafe {
|
||||
let width = self.width.max(1);
|
||||
let height = self.height as i32;
|
||||
let luma = (frame_index & 0xFF) as u8;
|
||||
|
||||
for bar in 0..pattern::BARS.len() as u32 {
|
||||
let x0 = ((bar as u64 * width as u64) / pattern::BARS.len() as u64) as i32;
|
||||
let x1 =
|
||||
(((bar + 1) as u64 * width as u64) / pattern::BARS.len() as u64) as i32;
|
||||
if x1 <= x0 {
|
||||
continue;
|
||||
}
|
||||
let (r, g, b) = pattern::bar_colour(x0 as u32, width);
|
||||
let colour = rgba(self.format, r, g, b.saturating_add(luma / 4));
|
||||
let rect = RECT {
|
||||
left: x0,
|
||||
top: 0,
|
||||
right: x1,
|
||||
bottom: height,
|
||||
};
|
||||
self.list.ClearRenderTargetView(rtv, &colour, Some(&[rect]));
|
||||
}
|
||||
|
||||
let (cr, cg, cb) = pattern::counter_colour(frame_index);
|
||||
let block = pattern::COUNTER_BLOCK as i32;
|
||||
let counter_rect = RECT {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: block.min(self.width as i32),
|
||||
bottom: block.min(height),
|
||||
};
|
||||
let counter_colour = rgba(self.format, cr, cg, cb);
|
||||
self.list
|
||||
.ClearRenderTargetView(rtv, &counter_colour, Some(&[counter_rect]));
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn wait_for_fence_value(&self, value: u64) -> Result<()> {
|
||||
unsafe {
|
||||
if value == 0 || self.fence.GetCompletedValue() >= value {
|
||||
return Ok(());
|
||||
}
|
||||
self.fence.SetEventOnCompletion(value, self.fence_event)?;
|
||||
WaitForSingleObject(self.fence_event, INFINITE);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn flush(&mut self) -> Result<()> {
|
||||
unsafe {
|
||||
self.fence_value += 1;
|
||||
let signal = self.fence_value;
|
||||
self.queue.Signal(&self.fence, signal)?;
|
||||
self.wait_for_fence_value(signal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Renderer {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
let _ = self.flush();
|
||||
let _ = CloseHandle(self.fence_event);
|
||||
let _ = &self.device;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn rgba(format: PresentFormat, r: u8, g: u8, b: u8) -> [f32; 4] {
|
||||
let highlight =
|
||||
if matches!(format, PresentFormat::Rgba16Float) && r == 255 && g == 255 && b > 240 {
|
||||
1.25
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
[
|
||||
(r as f32 / 255.0) * highlight,
|
||||
(g as f32 / 255.0) * highlight,
|
||||
(b as f32 / 255.0) * highlight,
|
||||
1.0,
|
||||
]
|
||||
}
|
||||
|
||||
pub fn run() -> Result<()> {
|
||||
let options = Options::from_env();
|
||||
unsafe {
|
||||
let hwnd = create_window(options.width, options.height, options.borderless)?;
|
||||
|
||||
let mut rect = Default::default();
|
||||
let _ = GetClientRect(hwnd, &mut rect);
|
||||
let width = ((rect.right - rect.left).max(1)) as u32;
|
||||
let height = ((rect.bottom - rect.top).max(1)) as u32;
|
||||
|
||||
let mut renderer =
|
||||
Renderer::new(hwnd, width, height, options.backbuffers, options.format)?;
|
||||
|
||||
println!("HWND={}", hwnd.0 as isize);
|
||||
use std::io::Write;
|
||||
let _ = std::io::stdout().flush();
|
||||
|
||||
let mut frame_index: u64 = 0;
|
||||
let mut msg = MSG::default();
|
||||
loop {
|
||||
while PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).as_bool() {
|
||||
if msg.message == WM_QUIT {
|
||||
return Ok(());
|
||||
}
|
||||
let _ = TranslateMessage(&msg);
|
||||
DispatchMessageW(&msg);
|
||||
}
|
||||
|
||||
renderer.render_frame(frame_index)?;
|
||||
frame_index += 1;
|
||||
|
||||
if let Some(limit) = options.frames {
|
||||
if frame_index >= limit {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "d3d9-present-fixture"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"windows",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
|
||||
dependencies = [
|
||||
"windows-collections",
|
||||
"windows-core",
|
||||
"windows-future",
|
||||
"windows-numerics",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-collections"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-future"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-link",
|
||||
"windows-threading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.59.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-numerics"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-threading"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
@@ -0,0 +1,26 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
[package]
|
||||
name = "d3d9-present-fixture"
|
||||
version = "0.0.0"
|
||||
edition = "2024"
|
||||
license = "AGPL-3.0-or-later"
|
||||
publish = false
|
||||
|
||||
# Empty workspace table keeps this fixture out of any parent Cargo workspace,
|
||||
# matching the sibling crates (hook/, vulkan-layer/) in this package.
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
|
||||
[[bin]]
|
||||
name = "d3d9-present-fixture"
|
||||
path = "src/main.rs"
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
windows = {version = "0.62.2", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_Graphics_Direct3D9",
|
||||
"Win32_Graphics_Gdi",
|
||||
"Win32_System_LibraryLoader",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
]}
|
||||
windows-core = "0.62.2"
|
||||
@@ -0,0 +1,417 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn main() {
|
||||
eprintln!("d3d9-present-fixture is only functional on Windows");
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn main() -> windows_core::Result<()> {
|
||||
windows_impl::run()
|
||||
}
|
||||
|
||||
#[cfg_attr(not(windows), allow(dead_code))]
|
||||
mod pattern {
|
||||
pub const BARS: [(u8, u8, u8); 8] = [
|
||||
(255, 255, 255),
|
||||
(255, 255, 0),
|
||||
(0, 255, 255),
|
||||
(0, 255, 0),
|
||||
(255, 0, 255),
|
||||
(255, 0, 0),
|
||||
(0, 0, 255),
|
||||
(0, 0, 0),
|
||||
];
|
||||
|
||||
pub const COUNTER_BLOCK: u32 = 16;
|
||||
|
||||
pub fn bar_colour(x: u32, width: u32) -> (u8, u8, u8) {
|
||||
let width = width.max(1);
|
||||
let index = ((x as u64 * BARS.len() as u64) / width as u64) as usize;
|
||||
BARS[index.min(BARS.len() - 1)]
|
||||
}
|
||||
|
||||
pub fn counter_colour(frame_index: u64) -> (u8, u8, u8) {
|
||||
let low = (frame_index & 0x00FF_FFFF) as u32;
|
||||
(
|
||||
(low & 0xFF) as u8,
|
||||
((low >> 8) & 0xFF) as u8,
|
||||
((low >> 16) & 0xFF) as u8,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
mod cli {
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum PresentMode {
|
||||
Device,
|
||||
SwapChain,
|
||||
}
|
||||
|
||||
pub struct Options {
|
||||
pub frames: Option<u64>,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub borderless: bool,
|
||||
pub multisample: bool,
|
||||
pub present_mode: PresentMode,
|
||||
}
|
||||
|
||||
impl Default for Options {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
frames: None,
|
||||
width: 1280,
|
||||
height: 720,
|
||||
borderless: false,
|
||||
multisample: false,
|
||||
present_mode: PresentMode::Device,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Options {
|
||||
pub fn from_env() -> Self {
|
||||
let mut options = Options::default();
|
||||
let mut args = std::env::args().skip(1);
|
||||
while let Some(arg) = args.next() {
|
||||
match arg.as_str() {
|
||||
"--frames" => {
|
||||
if let Some(value) = args.next().and_then(|v| v.parse().ok()) {
|
||||
options.frames = Some(value);
|
||||
}
|
||||
}
|
||||
"--width" => {
|
||||
if let Some(value) = args.next().and_then(|v| v.parse().ok()) {
|
||||
options.width = value;
|
||||
}
|
||||
}
|
||||
"--height" => {
|
||||
if let Some(value) = args.next().and_then(|v| v.parse().ok()) {
|
||||
options.height = value;
|
||||
}
|
||||
}
|
||||
"--borderless" => options.borderless = true,
|
||||
"--windowed" => options.borderless = false,
|
||||
"--multisample" => options.multisample = true,
|
||||
"--no-multisample" => options.multisample = false,
|
||||
"--swapchain-present" => options.present_mode = PresentMode::SwapChain,
|
||||
"--device-present" => options.present_mode = PresentMode::Device,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
options
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
mod windows_impl {
|
||||
use super::{
|
||||
cli::{Options, PresentMode},
|
||||
pattern,
|
||||
};
|
||||
use std::ptr::null;
|
||||
use windows::Win32::Foundation::{HWND, LPARAM, LRESULT, WPARAM};
|
||||
use windows::Win32::Graphics::Direct3D9::{
|
||||
D3D_SDK_VERSION, D3DADAPTER_DEFAULT, D3DCLEAR_TARGET, D3DCREATE_HARDWARE_VERTEXPROCESSING,
|
||||
D3DCREATE_SOFTWARE_VERTEXPROCESSING, D3DDEVTYPE_HAL, D3DFMT_X8R8G8B8,
|
||||
D3DMULTISAMPLE_2_SAMPLES, D3DMULTISAMPLE_NONE, D3DPRESENT_PARAMETERS, D3DRECT,
|
||||
D3DSWAPEFFECT_DISCARD, Direct3DCreate9, IDirect3D9, IDirect3DDevice9,
|
||||
};
|
||||
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::{
|
||||
CS_HREDRAW, CS_VREDRAW, CW_USEDEFAULT, CreateWindowExW, DefWindowProcW, DispatchMessageW,
|
||||
GetClientRect, MSG, PM_REMOVE, PeekMessageW, PostQuitMessage, RegisterClassExW, SW_SHOW,
|
||||
ShowWindow, TranslateMessage, WM_DESTROY, WM_QUIT, WNDCLASSEXW, WS_OVERLAPPEDWINDOW,
|
||||
WS_POPUP, WS_VISIBLE,
|
||||
};
|
||||
use windows_core::{BOOL, Result, w};
|
||||
|
||||
const D3DERR_DEVICELOST: i32 = 0x8876_0868u32 as i32;
|
||||
|
||||
fn d3dcolor_xrgb(r: u8, g: u8, b: u8) -> u32 {
|
||||
0xFF00_0000 | ((r as u32) << 16) | ((g as u32) << 8) | (b as u32)
|
||||
}
|
||||
|
||||
extern "system" fn wnd_proc(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
|
||||
unsafe {
|
||||
match msg {
|
||||
WM_DESTROY => {
|
||||
PostQuitMessage(0);
|
||||
LRESULT(0)
|
||||
}
|
||||
_ => DefWindowProcW(hwnd, msg, wparam, lparam),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn create_window(width: u32, height: u32, borderless: bool) -> Result<HWND> {
|
||||
unsafe {
|
||||
let instance = GetModuleHandleW(None)?;
|
||||
let class_name = w!("FluxerD3D9Fixture");
|
||||
let wc = WNDCLASSEXW {
|
||||
cbSize: size_of::<WNDCLASSEXW>() as u32,
|
||||
style: CS_HREDRAW | CS_VREDRAW,
|
||||
lpfnWndProc: Some(wnd_proc),
|
||||
hInstance: instance.into(),
|
||||
lpszClassName: class_name,
|
||||
..Default::default()
|
||||
};
|
||||
RegisterClassExW(&wc);
|
||||
let style = if borderless {
|
||||
WS_POPUP | WS_VISIBLE
|
||||
} else {
|
||||
WS_OVERLAPPEDWINDOW | WS_VISIBLE
|
||||
};
|
||||
let hwnd = CreateWindowExW(
|
||||
Default::default(),
|
||||
class_name,
|
||||
w!("Fluxer D3D9 Present Fixture"),
|
||||
style,
|
||||
CW_USEDEFAULT,
|
||||
CW_USEDEFAULT,
|
||||
width as i32,
|
||||
height as i32,
|
||||
None,
|
||||
None,
|
||||
Some(instance.into()),
|
||||
None,
|
||||
)?;
|
||||
let _ = ShowWindow(hwnd, SW_SHOW);
|
||||
Ok(hwnd)
|
||||
}
|
||||
}
|
||||
|
||||
fn present_params(
|
||||
hwnd: HWND,
|
||||
width: u32,
|
||||
height: u32,
|
||||
multisample: bool,
|
||||
) -> D3DPRESENT_PARAMETERS {
|
||||
D3DPRESENT_PARAMETERS {
|
||||
BackBufferWidth: width,
|
||||
BackBufferHeight: height,
|
||||
BackBufferFormat: D3DFMT_X8R8G8B8,
|
||||
BackBufferCount: 1,
|
||||
MultiSampleType: if multisample {
|
||||
D3DMULTISAMPLE_2_SAMPLES
|
||||
} else {
|
||||
D3DMULTISAMPLE_NONE
|
||||
},
|
||||
MultiSampleQuality: 0,
|
||||
SwapEffect: D3DSWAPEFFECT_DISCARD,
|
||||
hDeviceWindow: hwnd,
|
||||
Windowed: BOOL(1),
|
||||
EnableAutoDepthStencil: BOOL(0),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
struct Renderer {
|
||||
_d3d9: IDirect3D9,
|
||||
device: IDirect3DDevice9,
|
||||
hwnd: HWND,
|
||||
width: u32,
|
||||
height: u32,
|
||||
device_lost: bool,
|
||||
multisample: bool,
|
||||
present_mode: PresentMode,
|
||||
}
|
||||
|
||||
impl Renderer {
|
||||
unsafe fn new(
|
||||
hwnd: HWND,
|
||||
width: u32,
|
||||
height: u32,
|
||||
multisample: bool,
|
||||
present_mode: PresentMode,
|
||||
) -> Result<Self> {
|
||||
unsafe {
|
||||
let d3d9 = Direct3DCreate9(D3D_SDK_VERSION)
|
||||
.ok_or_else(|| windows_core::Error::from_thread())?;
|
||||
|
||||
let mut params = present_params(hwnd, width, height, multisample);
|
||||
|
||||
let mut device: Option<IDirect3DDevice9> = None;
|
||||
let mut created = d3d9
|
||||
.CreateDevice(
|
||||
D3DADAPTER_DEFAULT,
|
||||
D3DDEVTYPE_HAL,
|
||||
hwnd,
|
||||
D3DCREATE_HARDWARE_VERTEXPROCESSING as u32,
|
||||
&mut params,
|
||||
&mut device,
|
||||
)
|
||||
.is_ok();
|
||||
if !created {
|
||||
created = d3d9
|
||||
.CreateDevice(
|
||||
D3DADAPTER_DEFAULT,
|
||||
D3DDEVTYPE_HAL,
|
||||
hwnd,
|
||||
D3DCREATE_SOFTWARE_VERTEXPROCESSING as u32,
|
||||
&mut params,
|
||||
&mut device,
|
||||
)
|
||||
.is_ok();
|
||||
}
|
||||
let device = if created {
|
||||
device.ok_or_else(|| windows_core::Error::from_thread())?
|
||||
} else {
|
||||
return Err(windows_core::Error::from_thread());
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
_d3d9: d3d9,
|
||||
device,
|
||||
hwnd,
|
||||
width,
|
||||
height,
|
||||
device_lost: false,
|
||||
multisample,
|
||||
present_mode,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn try_reset(&mut self) {
|
||||
unsafe {
|
||||
let mut params =
|
||||
present_params(self.hwnd, self.width, self.height, self.multisample);
|
||||
if self.device.Reset(&mut params).is_ok() {
|
||||
self.device_lost = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn render_frame(&mut self, frame_index: u64) -> Result<()> {
|
||||
unsafe {
|
||||
if self.device_lost {
|
||||
self.try_reset();
|
||||
if self.device_lost {
|
||||
return self.present();
|
||||
}
|
||||
}
|
||||
|
||||
let width = self.width.max(1);
|
||||
let height = self.height as i32;
|
||||
|
||||
let bars = pattern::BARS.len() as u32;
|
||||
for bar in 0..bars {
|
||||
let x1 = ((bar as u64 * width as u64) / bars as u64) as i32;
|
||||
let x2 = (((bar + 1) as u64 * width as u64) / bars as u64) as i32;
|
||||
if x2 <= x1 {
|
||||
continue;
|
||||
}
|
||||
let mid = ((x1 + x2) / 2).max(0) as u32;
|
||||
let (r, g, b) = pattern::bar_colour(mid, width);
|
||||
let rect = D3DRECT {
|
||||
x1,
|
||||
y1: 0,
|
||||
x2,
|
||||
y2: height,
|
||||
};
|
||||
self.device.Clear(
|
||||
1,
|
||||
&rect,
|
||||
D3DCLEAR_TARGET as u32,
|
||||
d3dcolor_xrgb(r, g, b),
|
||||
1.0,
|
||||
0,
|
||||
)?;
|
||||
}
|
||||
|
||||
let block = pattern::COUNTER_BLOCK as i32;
|
||||
let block_x2 = block.min(width as i32);
|
||||
let block_y2 = block.min(height);
|
||||
if block_x2 > 0 && block_y2 > 0 {
|
||||
let (r, g, b) = pattern::counter_colour(frame_index);
|
||||
let rect = D3DRECT {
|
||||
x1: 0,
|
||||
y1: 0,
|
||||
x2: block_x2,
|
||||
y2: block_y2,
|
||||
};
|
||||
self.device.Clear(
|
||||
1,
|
||||
&rect,
|
||||
D3DCLEAR_TARGET as u32,
|
||||
d3dcolor_xrgb(r, g, b),
|
||||
1.0,
|
||||
0,
|
||||
)?;
|
||||
}
|
||||
|
||||
self.present()
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn present(&mut self) -> Result<()> {
|
||||
unsafe {
|
||||
let result = match self.present_mode {
|
||||
PresentMode::Device => {
|
||||
self.device.Present(null(), null(), HWND::default(), null())
|
||||
}
|
||||
PresentMode::SwapChain => self.device.GetSwapChain(0).and_then(|swap_chain| {
|
||||
swap_chain.Present(null(), null(), HWND::default(), null(), 0)
|
||||
}),
|
||||
};
|
||||
match result {
|
||||
Ok(()) => Ok(()),
|
||||
Err(err) if err.code().0 == D3DERR_DEVICELOST => {
|
||||
self.device_lost = true;
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run() -> Result<()> {
|
||||
let options = Options::from_env();
|
||||
unsafe {
|
||||
let hwnd = create_window(options.width, options.height, options.borderless)?;
|
||||
|
||||
let mut rect = Default::default();
|
||||
let _ = GetClientRect(hwnd, &mut rect);
|
||||
let width = ((rect.right - rect.left).max(1)) as u32;
|
||||
let height = ((rect.bottom - rect.top).max(1)) as u32;
|
||||
|
||||
let mut renderer = Renderer::new(
|
||||
hwnd,
|
||||
width,
|
||||
height,
|
||||
options.multisample,
|
||||
options.present_mode,
|
||||
)?;
|
||||
|
||||
println!("HWND={}", hwnd.0 as isize);
|
||||
use std::io::Write;
|
||||
let _ = std::io::stdout().flush();
|
||||
|
||||
let mut frame_index: u64 = 0;
|
||||
let mut msg = MSG::default();
|
||||
loop {
|
||||
while PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).as_bool() {
|
||||
if msg.message == WM_QUIT {
|
||||
return Ok(());
|
||||
}
|
||||
let _ = TranslateMessage(&msg);
|
||||
DispatchMessageW(&msg);
|
||||
}
|
||||
|
||||
renderer.render_frame(frame_index)?;
|
||||
frame_index += 1;
|
||||
|
||||
if let Some(limit) = options.frames {
|
||||
if frame_index >= limit {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "i686-present-fixture"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"windows",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
|
||||
dependencies = [
|
||||
"windows-collections",
|
||||
"windows-core",
|
||||
"windows-future",
|
||||
"windows-numerics",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-collections"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-future"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-link",
|
||||
"windows-threading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.59.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-numerics"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-threading"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
@@ -0,0 +1,40 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
[package]
|
||||
name = "i686-present-fixture"
|
||||
version = "0.0.0"
|
||||
edition = "2024"
|
||||
license = "AGPL-3.0-or-later"
|
||||
publish = false
|
||||
|
||||
# Empty workspace table keeps this fixture out of any parent Cargo workspace,
|
||||
# matching the sibling crates (hook/, vulkan-layer/) in this package.
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
|
||||
[[bin]]
|
||||
name = "i686-present-fixture"
|
||||
path = "src/main.rs"
|
||||
|
||||
# This fixture is a deterministic Direct3D 11 present fixture whose ONLY purpose
|
||||
# is to be built for the 32-bit (i686 / WOW64) target so the harness can drive
|
||||
# the cross-bitness injection paths (an x64 host injecting the 32-bit hook into a
|
||||
# 32-bit game). The source is target-arch-agnostic -- what makes it "i686" is
|
||||
# building it for `i686-pc-windows-msvc`:
|
||||
#
|
||||
# cargo build --release --manifest-path test-apps/i686-present-fixture/Cargo.toml \
|
||||
# --target i686-pc-windows-msvc
|
||||
#
|
||||
# It also compile-checks for x86_64-pc-windows-msvc so the standard cargo-check
|
||||
# matrix stays uniform across fixtures.
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
windows = {version = "0.62.2", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_Graphics_Direct3D",
|
||||
"Win32_Graphics_Direct3D11",
|
||||
"Win32_Graphics_Dxgi",
|
||||
"Win32_Graphics_Dxgi_Common",
|
||||
"Win32_Graphics_Gdi",
|
||||
"Win32_System_LibraryLoader",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
]}
|
||||
windows-core = "0.62.2"
|
||||
@@ -0,0 +1,358 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn main() {
|
||||
eprintln!("i686-present-fixture is only functional on Windows");
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn main() -> windows_core::Result<()> {
|
||||
windows_impl::run()
|
||||
}
|
||||
|
||||
#[cfg_attr(not(windows), allow(dead_code))]
|
||||
mod pattern {
|
||||
pub const BARS: [(u8, u8, u8); 8] = [
|
||||
(255, 255, 255),
|
||||
(255, 255, 0),
|
||||
(0, 255, 255),
|
||||
(0, 255, 0),
|
||||
(255, 0, 255),
|
||||
(255, 0, 0),
|
||||
(0, 0, 255),
|
||||
(0, 0, 0),
|
||||
];
|
||||
|
||||
pub const COUNTER_BLOCK: u32 = 16;
|
||||
|
||||
pub fn bar_colour(x: u32, width: u32) -> (u8, u8, u8) {
|
||||
let width = width.max(1);
|
||||
let index = ((x as u64 * BARS.len() as u64) / width as u64) as usize;
|
||||
BARS[index.min(BARS.len() - 1)]
|
||||
}
|
||||
|
||||
pub fn counter_colour(frame_index: u64) -> (u8, u8, u8) {
|
||||
let low = (frame_index & 0x00FF_FFFF) as u32;
|
||||
(
|
||||
(low & 0xFF) as u8,
|
||||
((low >> 8) & 0xFF) as u8,
|
||||
((low >> 16) & 0xFF) as u8,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
mod cli {
|
||||
pub struct Options {
|
||||
pub frames: Option<u64>,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub borderless: bool,
|
||||
}
|
||||
|
||||
impl Default for Options {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
frames: None,
|
||||
width: 1280,
|
||||
height: 720,
|
||||
borderless: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Options {
|
||||
pub fn from_env() -> Self {
|
||||
let mut options = Options::default();
|
||||
let mut args = std::env::args().skip(1);
|
||||
while let Some(arg) = args.next() {
|
||||
match arg.as_str() {
|
||||
"--frames" => {
|
||||
if let Some(value) = args.next().and_then(|v| v.parse().ok()) {
|
||||
options.frames = Some(value);
|
||||
}
|
||||
}
|
||||
"--width" => {
|
||||
if let Some(value) = args.next().and_then(|v| v.parse().ok()) {
|
||||
options.width = value;
|
||||
}
|
||||
}
|
||||
"--height" => {
|
||||
if let Some(value) = args.next().and_then(|v| v.parse().ok()) {
|
||||
options.height = value;
|
||||
}
|
||||
}
|
||||
"--borderless" => options.borderless = true,
|
||||
"--windowed" => options.borderless = false,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
options
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
mod windows_impl {
|
||||
use super::{cli::Options, pattern};
|
||||
use windows::Win32::Foundation::{HMODULE, HWND, LPARAM, LRESULT, WPARAM};
|
||||
use windows::Win32::Graphics::Direct3D::D3D_DRIVER_TYPE_HARDWARE;
|
||||
use windows::Win32::Graphics::Direct3D11::{
|
||||
D3D11_CREATE_DEVICE_BGRA_SUPPORT, D3D11_MAPPED_SUBRESOURCE, D3D11_SDK_VERSION,
|
||||
D3D11CreateDevice, ID3D11Device, ID3D11DeviceContext, ID3D11RenderTargetView,
|
||||
ID3D11Resource, ID3D11Texture2D,
|
||||
};
|
||||
use windows::Win32::Graphics::Dxgi::Common::{
|
||||
DXGI_ALPHA_MODE_IGNORE, DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_SAMPLE_DESC,
|
||||
};
|
||||
use windows::Win32::Graphics::Dxgi::{
|
||||
DXGI_SCALING_STRETCH, DXGI_SWAP_CHAIN_DESC1, DXGI_SWAP_EFFECT_FLIP_DISCARD,
|
||||
DXGI_USAGE_RENDER_TARGET_OUTPUT, IDXGIDevice, IDXGIFactory2, IDXGISwapChain1,
|
||||
};
|
||||
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::{
|
||||
CS_HREDRAW, CS_VREDRAW, CW_USEDEFAULT, CreateWindowExW, DefWindowProcW, DispatchMessageW,
|
||||
GetClientRect, MSG, PM_REMOVE, PeekMessageW, PostQuitMessage, RegisterClassExW, SW_SHOW,
|
||||
ShowWindow, TranslateMessage, WM_DESTROY, WM_QUIT, WNDCLASSEXW, WS_OVERLAPPEDWINDOW,
|
||||
WS_POPUP, WS_VISIBLE,
|
||||
};
|
||||
use windows_core::{Interface, Result, w};
|
||||
|
||||
extern "system" fn wnd_proc(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
|
||||
unsafe {
|
||||
match msg {
|
||||
WM_DESTROY => {
|
||||
PostQuitMessage(0);
|
||||
LRESULT(0)
|
||||
}
|
||||
_ => DefWindowProcW(hwnd, msg, wparam, lparam),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn create_window(width: u32, height: u32, borderless: bool) -> Result<HWND> {
|
||||
unsafe {
|
||||
let instance = GetModuleHandleW(None)?;
|
||||
let class_name = w!("FluxerI686Fixture");
|
||||
let wc = WNDCLASSEXW {
|
||||
cbSize: size_of::<WNDCLASSEXW>() as u32,
|
||||
style: CS_HREDRAW | CS_VREDRAW,
|
||||
lpfnWndProc: Some(wnd_proc),
|
||||
hInstance: instance.into(),
|
||||
lpszClassName: class_name,
|
||||
..Default::default()
|
||||
};
|
||||
RegisterClassExW(&wc);
|
||||
let style = if borderless {
|
||||
WS_POPUP | WS_VISIBLE
|
||||
} else {
|
||||
WS_OVERLAPPEDWINDOW | WS_VISIBLE
|
||||
};
|
||||
let hwnd = CreateWindowExW(
|
||||
Default::default(),
|
||||
class_name,
|
||||
w!("Fluxer i686 (WOW64) Present Fixture"),
|
||||
style,
|
||||
CW_USEDEFAULT,
|
||||
CW_USEDEFAULT,
|
||||
width as i32,
|
||||
height as i32,
|
||||
None,
|
||||
None,
|
||||
Some(instance.into()),
|
||||
None,
|
||||
)?;
|
||||
let _ = ShowWindow(hwnd, SW_SHOW);
|
||||
Ok(hwnd)
|
||||
}
|
||||
}
|
||||
|
||||
struct Renderer {
|
||||
device: ID3D11Device,
|
||||
context: ID3D11DeviceContext,
|
||||
swap_chain: IDXGISwapChain1,
|
||||
rtv: ID3D11RenderTargetView,
|
||||
width: u32,
|
||||
height: u32,
|
||||
staging: ID3D11Texture2D,
|
||||
}
|
||||
|
||||
impl Renderer {
|
||||
unsafe fn new(hwnd: HWND, width: u32, height: u32) -> Result<Self> {
|
||||
unsafe {
|
||||
let mut device: Option<ID3D11Device> = None;
|
||||
let mut context: Option<ID3D11DeviceContext> = None;
|
||||
D3D11CreateDevice(
|
||||
None,
|
||||
D3D_DRIVER_TYPE_HARDWARE,
|
||||
HMODULE::default(),
|
||||
D3D11_CREATE_DEVICE_BGRA_SUPPORT,
|
||||
None,
|
||||
D3D11_SDK_VERSION,
|
||||
Some(&mut device),
|
||||
None,
|
||||
Some(&mut context),
|
||||
)?;
|
||||
let device = device.expect("D3D11CreateDevice yielded no device");
|
||||
let context = context.expect("D3D11CreateDevice yielded no context");
|
||||
|
||||
let dxgi_device: IDXGIDevice = device.cast()?;
|
||||
let adapter = dxgi_device.GetAdapter()?;
|
||||
let factory: IDXGIFactory2 = adapter.GetParent()?;
|
||||
|
||||
let desc = DXGI_SWAP_CHAIN_DESC1 {
|
||||
Width: width,
|
||||
Height: height,
|
||||
Format: DXGI_FORMAT_B8G8R8A8_UNORM,
|
||||
Stereo: false.into(),
|
||||
SampleDesc: DXGI_SAMPLE_DESC {
|
||||
Count: 1,
|
||||
Quality: 0,
|
||||
},
|
||||
BufferUsage: DXGI_USAGE_RENDER_TARGET_OUTPUT,
|
||||
BufferCount: 2,
|
||||
Scaling: DXGI_SCALING_STRETCH,
|
||||
SwapEffect: DXGI_SWAP_EFFECT_FLIP_DISCARD,
|
||||
AlphaMode: DXGI_ALPHA_MODE_IGNORE,
|
||||
Flags: 0,
|
||||
};
|
||||
let swap_chain =
|
||||
factory.CreateSwapChainForHwnd(&device, hwnd, &desc, None, None)?;
|
||||
|
||||
let rtv = Self::make_rtv(&device, &swap_chain)?;
|
||||
let staging = Self::make_staging(&device, width, height)?;
|
||||
|
||||
Ok(Self {
|
||||
device,
|
||||
context,
|
||||
swap_chain,
|
||||
rtv,
|
||||
width,
|
||||
height,
|
||||
staging,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn make_rtv(
|
||||
device: &ID3D11Device,
|
||||
swap_chain: &IDXGISwapChain1,
|
||||
) -> Result<ID3D11RenderTargetView> {
|
||||
unsafe {
|
||||
let back_buffer: ID3D11Texture2D = swap_chain.GetBuffer(0)?;
|
||||
let resource: ID3D11Resource = back_buffer.cast()?;
|
||||
let mut rtv: Option<ID3D11RenderTargetView> = None;
|
||||
device.CreateRenderTargetView(&resource, None, Some(&mut rtv))?;
|
||||
Ok(rtv.expect("CreateRenderTargetView yielded no view"))
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn make_staging(
|
||||
device: &ID3D11Device,
|
||||
width: u32,
|
||||
height: u32,
|
||||
) -> Result<ID3D11Texture2D> {
|
||||
use windows::Win32::Graphics::Direct3D11::{
|
||||
D3D11_CPU_ACCESS_WRITE, D3D11_TEXTURE2D_DESC, D3D11_USAGE_STAGING,
|
||||
};
|
||||
unsafe {
|
||||
let desc = D3D11_TEXTURE2D_DESC {
|
||||
Width: width,
|
||||
Height: height,
|
||||
MipLevels: 1,
|
||||
ArraySize: 1,
|
||||
Format: DXGI_FORMAT_B8G8R8A8_UNORM,
|
||||
SampleDesc: DXGI_SAMPLE_DESC {
|
||||
Count: 1,
|
||||
Quality: 0,
|
||||
},
|
||||
Usage: D3D11_USAGE_STAGING,
|
||||
BindFlags: 0,
|
||||
CPUAccessFlags: D3D11_CPU_ACCESS_WRITE.0 as u32,
|
||||
MiscFlags: 0,
|
||||
};
|
||||
let mut staging: Option<ID3D11Texture2D> = None;
|
||||
device.CreateTexture2D(&desc, None, Some(&mut staging))?;
|
||||
Ok(staging.expect("CreateTexture2D yielded no staging texture"))
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn render_frame(&self, frame_index: u64) -> Result<()> {
|
||||
use windows::Win32::Graphics::Direct3D11::D3D11_MAP_WRITE;
|
||||
unsafe {
|
||||
let mut mapped = D3D11_MAPPED_SUBRESOURCE::default();
|
||||
self.context
|
||||
.Map(&self.staging, 0, D3D11_MAP_WRITE, 0, Some(&mut mapped))?;
|
||||
let row_pitch = mapped.RowPitch as usize;
|
||||
let base = mapped.pData as *mut u8;
|
||||
let counter = pattern::counter_colour(frame_index);
|
||||
let luma = (frame_index & 0xFF) as u8;
|
||||
for y in 0..self.height {
|
||||
let row = base.add(y as usize * row_pitch);
|
||||
for x in 0..self.width {
|
||||
let (r, g, b) = if x < pattern::COUNTER_BLOCK && y < pattern::COUNTER_BLOCK
|
||||
{
|
||||
counter
|
||||
} else {
|
||||
pattern::bar_colour(x, self.width)
|
||||
};
|
||||
let px = row.add(x as usize * 4);
|
||||
*px = b.saturating_add(luma / 4);
|
||||
*px.add(1) = g;
|
||||
*px.add(2) = r;
|
||||
*px.add(3) = 255;
|
||||
}
|
||||
}
|
||||
self.context.Unmap(&self.staging, 0);
|
||||
|
||||
let back_buffer: ID3D11Texture2D = self.swap_chain.GetBuffer(0)?;
|
||||
let dst: ID3D11Resource = back_buffer.cast()?;
|
||||
let src: ID3D11Resource = self.staging.cast()?;
|
||||
self.context.CopyResource(&dst, &src);
|
||||
let _ = &self.rtv;
|
||||
self.swap_chain.Present(1, Default::default()).ok()?;
|
||||
let _ = &self.device;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run() -> Result<()> {
|
||||
let options = Options::from_env();
|
||||
unsafe {
|
||||
let hwnd = create_window(options.width, options.height, options.borderless)?;
|
||||
|
||||
let mut rect = Default::default();
|
||||
let _ = GetClientRect(hwnd, &mut rect);
|
||||
let width = ((rect.right - rect.left).max(1)) as u32;
|
||||
let height = ((rect.bottom - rect.top).max(1)) as u32;
|
||||
|
||||
let renderer = Renderer::new(hwnd, width, height)?;
|
||||
|
||||
println!("HWND={}", hwnd.0 as isize);
|
||||
use std::io::Write;
|
||||
let _ = std::io::stdout().flush();
|
||||
|
||||
let mut frame_index: u64 = 0;
|
||||
let mut msg = MSG::default();
|
||||
loop {
|
||||
while PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).as_bool() {
|
||||
if msg.message == WM_QUIT {
|
||||
return Ok(());
|
||||
}
|
||||
let _ = TranslateMessage(&msg);
|
||||
DispatchMessageW(&msg);
|
||||
}
|
||||
|
||||
renderer.render_frame(frame_index)?;
|
||||
frame_index += 1;
|
||||
|
||||
if let Some(limit) = options.frames {
|
||||
if frame_index >= limit {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "opengl-swapbuffers-fixture"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"windows",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
|
||||
dependencies = [
|
||||
"windows-collections",
|
||||
"windows-core",
|
||||
"windows-future",
|
||||
"windows-numerics",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-collections"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-future"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-link",
|
||||
"windows-threading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.59.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-numerics"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-threading"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
[package]
|
||||
name = "opengl-swapbuffers-fixture"
|
||||
version = "0.0.0"
|
||||
edition = "2024"
|
||||
license = "AGPL-3.0-or-later"
|
||||
publish = false
|
||||
|
||||
# Empty workspace table keeps this fixture out of any parent Cargo workspace,
|
||||
# matching the sibling crates (hook/, vulkan-layer/) in this package.
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
|
||||
[[bin]]
|
||||
name = "opengl-swapbuffers-fixture"
|
||||
path = "src/main.rs"
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
windows = {version = "0.62.2", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_Graphics_Gdi",
|
||||
"Win32_Graphics_OpenGL",
|
||||
"Win32_System_LibraryLoader",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
]}
|
||||
windows-core = "0.62.2"
|
||||
+484
@@ -0,0 +1,484 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn main() {
|
||||
eprintln!("opengl-swapbuffers-fixture is only functional on Windows");
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn main() -> windows_core::Result<()> {
|
||||
windows_impl::run()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
mod pattern {
|
||||
pub const BARS: [(u8, u8, u8); 8] = [
|
||||
(255, 255, 255),
|
||||
(255, 255, 0),
|
||||
(0, 255, 255),
|
||||
(0, 255, 0),
|
||||
(255, 0, 255),
|
||||
(255, 0, 0),
|
||||
(0, 0, 255),
|
||||
(0, 0, 0),
|
||||
];
|
||||
|
||||
pub const COUNTER_BLOCK: u32 = 16;
|
||||
|
||||
pub fn bar_colour(x: u32, width: u32) -> (u8, u8, u8) {
|
||||
let width = width.max(1);
|
||||
let index = ((x as u64 * BARS.len() as u64) / width as u64) as usize;
|
||||
BARS[index.min(BARS.len() - 1)]
|
||||
}
|
||||
|
||||
pub fn bar_x_range(index: usize, width: u32) -> (u32, u32) {
|
||||
let width = width.max(1) as u64;
|
||||
let len = BARS.len() as u64;
|
||||
let start = (index as u64 * width) / len;
|
||||
let end = ((index as u64 + 1) * width) / len;
|
||||
(start as u32, end.min(width) as u32)
|
||||
}
|
||||
|
||||
pub fn counter_colour(frame_index: u64) -> (u8, u8, u8) {
|
||||
let low = (frame_index & 0x00FF_FFFF) as u32;
|
||||
(
|
||||
(low & 0xFF) as u8,
|
||||
((low >> 8) & 0xFF) as u8,
|
||||
((low >> 16) & 0xFF) as u8,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn clear_colour(frame_index: u64) -> [f32; 3] {
|
||||
let (r, g, b) = BARS[(frame_index % BARS.len() as u64) as usize];
|
||||
[r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0]
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
mod cli {
|
||||
pub struct Options {
|
||||
pub frames: Option<u64>,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub borderless: bool,
|
||||
pub layer: bool,
|
||||
pub stress_pack_state: bool,
|
||||
pub resize_at: Option<u64>,
|
||||
pub frame_delay_ms: u64,
|
||||
}
|
||||
|
||||
impl Default for Options {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
frames: None,
|
||||
width: 1280,
|
||||
height: 720,
|
||||
borderless: false,
|
||||
layer: false,
|
||||
stress_pack_state: false,
|
||||
resize_at: None,
|
||||
frame_delay_ms: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Options {
|
||||
pub fn from_env() -> Self {
|
||||
let mut options = Options::default();
|
||||
let mut args = std::env::args().skip(1);
|
||||
while let Some(arg) = args.next() {
|
||||
match arg.as_str() {
|
||||
"--frames" => {
|
||||
if let Some(value) = args.next().and_then(|v| v.parse().ok()) {
|
||||
options.frames = Some(value);
|
||||
}
|
||||
}
|
||||
"--width" => {
|
||||
if let Some(value) = args.next().and_then(|v| v.parse().ok()) {
|
||||
options.width = value;
|
||||
}
|
||||
}
|
||||
"--height" => {
|
||||
if let Some(value) = args.next().and_then(|v| v.parse().ok()) {
|
||||
options.height = value;
|
||||
}
|
||||
}
|
||||
"--borderless" => options.borderless = true,
|
||||
"--windowed" => options.borderless = false,
|
||||
"--layer" => options.layer = true,
|
||||
"--stress-pack-state" => options.stress_pack_state = true,
|
||||
"--resize-at" => {
|
||||
if let Some(value) = args.next().and_then(|v| v.parse().ok()) {
|
||||
options.resize_at = Some(value);
|
||||
}
|
||||
}
|
||||
"--frame-delay-ms" => {
|
||||
if let Some(value) = args.next().and_then(|v| v.parse().ok()) {
|
||||
options.frame_delay_ms = value;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
options
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
mod windows_impl {
|
||||
use super::cli::Options;
|
||||
use super::pattern;
|
||||
use windows::Win32::Foundation::{HWND, LPARAM, LRESULT, WPARAM};
|
||||
use windows::Win32::Graphics::Gdi::{GetDC, HDC, ReleaseDC, WGL_SWAP_MAIN_PLANE};
|
||||
use windows::Win32::Graphics::OpenGL::{
|
||||
ChoosePixelFormat, GL_COLOR_BUFFER_BIT, GL_FRONT, GL_PACK_ALIGNMENT, GL_PACK_LSB_FIRST,
|
||||
GL_PACK_ROW_LENGTH, GL_PACK_SKIP_PIXELS, GL_PACK_SKIP_ROWS, GL_PACK_SWAP_BYTES,
|
||||
GL_READ_BUFFER, GL_SCISSOR_TEST, PFD_DOUBLEBUFFER, PFD_DRAW_TO_WINDOW, PFD_MAIN_PLANE,
|
||||
PFD_SUPPORT_OPENGL, PFD_TYPE_RGBA, PIXELFORMATDESCRIPTOR, SetPixelFormat, SwapBuffers,
|
||||
glClear, glClearColor, glDisable, glEnable, glGetIntegerv, glPixelStorei, glReadBuffer,
|
||||
glScissor, glViewport, wglCreateContext, wglDeleteContext, wglGetProcAddress,
|
||||
wglMakeCurrent, wglSwapLayerBuffers,
|
||||
};
|
||||
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::{
|
||||
CS_HREDRAW, CS_OWNDC, CS_VREDRAW, CW_USEDEFAULT, CreateWindowExW, DefWindowProcW,
|
||||
DispatchMessageW, GetClientRect, MSG, MoveWindow, PM_REMOVE, PeekMessageW, PostQuitMessage,
|
||||
RegisterClassExW, SW_SHOW, ShowWindow, TranslateMessage, WM_DESTROY, WM_QUIT, WNDCLASSEXW,
|
||||
WS_OVERLAPPEDWINDOW, WS_POPUP, WS_VISIBLE,
|
||||
};
|
||||
use windows_core::{PCSTR, Result, w};
|
||||
|
||||
const GL_PIXEL_PACK_BUFFER: u32 = 0x88EB;
|
||||
const GL_PIXEL_PACK_BUFFER_BINDING: u32 = 0x88ED;
|
||||
const GL_STREAM_READ: u32 = 0x88E1;
|
||||
|
||||
type GlGenBuffersFn = unsafe extern "system" fn(i32, *mut u32);
|
||||
type GlBindBufferFn = unsafe extern "system" fn(u32, u32);
|
||||
type GlBufferDataFn = unsafe extern "system" fn(u32, isize, *const core::ffi::c_void, u32);
|
||||
type GlDeleteBuffersFn = unsafe extern "system" fn(i32, *const u32);
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct BufferProcs {
|
||||
gen_buffers: GlGenBuffersFn,
|
||||
bind_buffer: GlBindBufferFn,
|
||||
buffer_data: GlBufferDataFn,
|
||||
delete_buffers: GlDeleteBuffersFn,
|
||||
}
|
||||
|
||||
struct PackStateStress {
|
||||
procs: Option<BufferProcs>,
|
||||
pbo: u32,
|
||||
}
|
||||
|
||||
impl PackStateStress {
|
||||
unsafe fn new() -> Self {
|
||||
let Some(procs) = (unsafe { BufferProcs::load() }) else {
|
||||
return Self {
|
||||
procs: None,
|
||||
pbo: 0,
|
||||
};
|
||||
};
|
||||
let mut pbo = 0u32;
|
||||
unsafe {
|
||||
(procs.gen_buffers)(1, &mut pbo);
|
||||
}
|
||||
if pbo != 0 {
|
||||
let storage = [0u8; 16];
|
||||
unsafe {
|
||||
(procs.bind_buffer)(GL_PIXEL_PACK_BUFFER, pbo);
|
||||
(procs.buffer_data)(
|
||||
GL_PIXEL_PACK_BUFFER,
|
||||
storage.len() as isize,
|
||||
storage.as_ptr().cast(),
|
||||
GL_STREAM_READ,
|
||||
);
|
||||
(procs.bind_buffer)(GL_PIXEL_PACK_BUFFER, 0);
|
||||
}
|
||||
}
|
||||
Self {
|
||||
procs: Some(procs),
|
||||
pbo,
|
||||
}
|
||||
}
|
||||
|
||||
fn pbo(&self) -> Option<u32> {
|
||||
(self.pbo != 0).then_some(self.pbo)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for PackStateStress {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
if let Some(procs) = self.procs {
|
||||
if self.pbo != 0 {
|
||||
(procs.bind_buffer)(GL_PIXEL_PACK_BUFFER, 0);
|
||||
(procs.delete_buffers)(1, &self.pbo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BufferProcs {
|
||||
unsafe fn load() -> Option<Self> {
|
||||
Some(Self {
|
||||
gen_buffers: unsafe { load_gl_proc(b"glGenBuffers\0")? },
|
||||
bind_buffer: unsafe { load_gl_proc(b"glBindBuffer\0")? },
|
||||
buffer_data: unsafe { load_gl_proc(b"glBufferData\0")? },
|
||||
delete_buffers: unsafe { load_gl_proc(b"glDeleteBuffers\0")? },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn load_gl_proc<T>(name: &'static [u8]) -> Option<T> {
|
||||
let proc = unsafe { wglGetProcAddress(PCSTR(name.as_ptr()))? };
|
||||
let address = proc as usize;
|
||||
if address <= 3 || address == usize::MAX {
|
||||
return None;
|
||||
}
|
||||
Some(unsafe { std::mem::transmute_copy::<_, T>(&proc) })
|
||||
}
|
||||
|
||||
extern "system" fn wnd_proc(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
|
||||
unsafe {
|
||||
match msg {
|
||||
WM_DESTROY => {
|
||||
PostQuitMessage(0);
|
||||
LRESULT(0)
|
||||
}
|
||||
_ => DefWindowProcW(hwnd, msg, wparam, lparam),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn create_window(width: u32, height: u32, borderless: bool) -> Result<HWND> {
|
||||
unsafe {
|
||||
let instance = GetModuleHandleW(None)?;
|
||||
let class_name = w!("FluxerOpenGLFixture");
|
||||
let wc = WNDCLASSEXW {
|
||||
cbSize: size_of::<WNDCLASSEXW>() as u32,
|
||||
style: CS_HREDRAW | CS_VREDRAW | CS_OWNDC,
|
||||
lpfnWndProc: Some(wnd_proc),
|
||||
hInstance: instance.into(),
|
||||
lpszClassName: class_name,
|
||||
..Default::default()
|
||||
};
|
||||
RegisterClassExW(&wc);
|
||||
let style = if borderless {
|
||||
WS_POPUP | WS_VISIBLE
|
||||
} else {
|
||||
WS_OVERLAPPEDWINDOW | WS_VISIBLE
|
||||
};
|
||||
let hwnd = CreateWindowExW(
|
||||
Default::default(),
|
||||
class_name,
|
||||
w!("Fluxer OpenGL SwapBuffers Fixture"),
|
||||
style,
|
||||
CW_USEDEFAULT,
|
||||
CW_USEDEFAULT,
|
||||
width as i32,
|
||||
height as i32,
|
||||
None,
|
||||
None,
|
||||
Some(instance.into()),
|
||||
None,
|
||||
)?;
|
||||
let _ = ShowWindow(hwnd, SW_SHOW);
|
||||
Ok(hwnd)
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn set_pixel_format(hdc: HDC) -> Result<()> {
|
||||
unsafe {
|
||||
let mut pfd = PIXELFORMATDESCRIPTOR {
|
||||
nSize: size_of::<PIXELFORMATDESCRIPTOR>() as u16,
|
||||
nVersion: 1,
|
||||
dwFlags: PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER,
|
||||
iPixelType: PFD_TYPE_RGBA,
|
||||
cColorBits: 32,
|
||||
cDepthBits: 24,
|
||||
cStencilBits: 8,
|
||||
iLayerType: PFD_MAIN_PLANE.0 as u8,
|
||||
..Default::default()
|
||||
};
|
||||
let format = ChoosePixelFormat(hdc, &pfd);
|
||||
if format == 0 {
|
||||
return Err(windows_core::Error::from_thread());
|
||||
}
|
||||
SetPixelFormat(hdc, format, &mut pfd)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn clear_rect(colour: (u8, u8, u8)) {
|
||||
unsafe {
|
||||
let (r, g, b) = colour;
|
||||
glClearColor(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn render_pattern(frame_index: u64, width: u32, height: u32) {
|
||||
unsafe {
|
||||
glViewport(0, 0, width as i32, height as i32);
|
||||
|
||||
glEnable(GL_SCISSOR_TEST);
|
||||
for index in 0..pattern::BARS.len() {
|
||||
let (start, end) = pattern::bar_x_range(index, width);
|
||||
if end <= start {
|
||||
continue;
|
||||
}
|
||||
glScissor(start as i32, 0, (end - start) as i32, height as i32);
|
||||
clear_rect(pattern::BARS[index]);
|
||||
}
|
||||
|
||||
let block = pattern::COUNTER_BLOCK.min(width).min(height);
|
||||
if block > 0 {
|
||||
let y = height - block;
|
||||
glScissor(0, y as i32, block as i32, block as i32);
|
||||
clear_rect(pattern::counter_colour(frame_index));
|
||||
}
|
||||
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn client_size(hwnd: HWND) -> (u32, u32) {
|
||||
unsafe {
|
||||
let mut rect = Default::default();
|
||||
let _ = GetClientRect(hwnd, &mut rect);
|
||||
(
|
||||
((rect.right - rect.left).max(1)) as u32,
|
||||
((rect.bottom - rect.top).max(1)) as u32,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn stress_pack_state(stress: &PackStateStress) {
|
||||
unsafe {
|
||||
glReadBuffer(GL_FRONT);
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
||||
glPixelStorei(GL_PACK_ROW_LENGTH, 0);
|
||||
glPixelStorei(GL_PACK_SKIP_PIXELS, 0);
|
||||
glPixelStorei(GL_PACK_SKIP_ROWS, 0);
|
||||
glPixelStorei(GL_PACK_SWAP_BYTES, 1);
|
||||
glPixelStorei(GL_PACK_LSB_FIRST, 1);
|
||||
if let (Some(procs), Some(pbo)) = (stress.procs, stress.pbo()) {
|
||||
(procs.bind_buffer)(GL_PIXEL_PACK_BUFFER, pbo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn assert_stress_pack_state(stress: &PackStateStress) {
|
||||
unsafe {
|
||||
let mut read_buffer = 0i32;
|
||||
let mut pack_alignment = 0i32;
|
||||
let mut pack_row_length = 0i32;
|
||||
let mut pack_skip_pixels = 0i32;
|
||||
let mut pack_skip_rows = 0i32;
|
||||
let mut pack_swap_bytes = 0i32;
|
||||
let mut pack_lsb_first = 0i32;
|
||||
let mut pack_buffer = 0i32;
|
||||
|
||||
glGetIntegerv(GL_READ_BUFFER, &mut read_buffer);
|
||||
glGetIntegerv(GL_PACK_ALIGNMENT, &mut pack_alignment);
|
||||
glGetIntegerv(GL_PACK_ROW_LENGTH, &mut pack_row_length);
|
||||
glGetIntegerv(GL_PACK_SKIP_PIXELS, &mut pack_skip_pixels);
|
||||
glGetIntegerv(GL_PACK_SKIP_ROWS, &mut pack_skip_rows);
|
||||
glGetIntegerv(GL_PACK_SWAP_BYTES, &mut pack_swap_bytes);
|
||||
glGetIntegerv(GL_PACK_LSB_FIRST, &mut pack_lsb_first);
|
||||
glGetIntegerv(GL_PIXEL_PACK_BUFFER_BINDING, &mut pack_buffer);
|
||||
|
||||
assert_eq!(
|
||||
read_buffer as u32, GL_FRONT,
|
||||
"GL_READ_BUFFER was not restored"
|
||||
);
|
||||
assert_eq!(pack_alignment, 1, "GL_PACK_ALIGNMENT was not restored");
|
||||
assert_eq!(pack_row_length, 0, "GL_PACK_ROW_LENGTH was not restored");
|
||||
assert_eq!(pack_skip_pixels, 0, "GL_PACK_SKIP_PIXELS was not restored");
|
||||
assert_eq!(pack_skip_rows, 0, "GL_PACK_SKIP_ROWS was not restored");
|
||||
assert_eq!(pack_swap_bytes, 1, "GL_PACK_SWAP_BYTES was not restored");
|
||||
assert_eq!(pack_lsb_first, 1, "GL_PACK_LSB_FIRST was not restored");
|
||||
if let Some(pbo) = stress.pbo() {
|
||||
assert_eq!(
|
||||
pack_buffer as u32, pbo,
|
||||
"GL_PIXEL_PACK_BUFFER_BINDING was not restored"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run() -> Result<()> {
|
||||
let options = Options::from_env();
|
||||
unsafe {
|
||||
let hwnd = create_window(options.width, options.height, options.borderless)?;
|
||||
|
||||
println!("HWND={}", hwnd.0 as isize);
|
||||
use std::io::Write;
|
||||
let _ = std::io::stdout().flush();
|
||||
|
||||
let hdc = GetDC(Some(hwnd));
|
||||
set_pixel_format(hdc)?;
|
||||
let context = wglCreateContext(hdc)?;
|
||||
wglMakeCurrent(hdc, context)?;
|
||||
let pack_state_stress = PackStateStress::new();
|
||||
|
||||
let mut frame_index: u64 = 0;
|
||||
let mut msg = MSG::default();
|
||||
let mut resized = false;
|
||||
'render: loop {
|
||||
while PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).as_bool() {
|
||||
if msg.message == WM_QUIT {
|
||||
break 'render;
|
||||
}
|
||||
let _ = TranslateMessage(&msg);
|
||||
DispatchMessageW(&msg);
|
||||
}
|
||||
|
||||
if let Some(resize_at) = options.resize_at {
|
||||
if !resized && frame_index >= resize_at {
|
||||
let width = options.width.saturating_add(160).max(1) as i32;
|
||||
let height = options.height.saturating_add(96).max(1) as i32;
|
||||
let _ = MoveWindow(hwnd, 0, 0, width, height, true);
|
||||
resized = true;
|
||||
}
|
||||
}
|
||||
|
||||
let (width, height) = client_size(hwnd);
|
||||
render_pattern(frame_index, width, height);
|
||||
if options.stress_pack_state {
|
||||
stress_pack_state(&pack_state_stress);
|
||||
}
|
||||
|
||||
if options.layer {
|
||||
wglSwapLayerBuffers(hdc, WGL_SWAP_MAIN_PLANE)?;
|
||||
} else {
|
||||
SwapBuffers(hdc)?;
|
||||
}
|
||||
|
||||
if options.stress_pack_state {
|
||||
assert_stress_pack_state(&pack_state_stress);
|
||||
}
|
||||
|
||||
frame_index += 1;
|
||||
if options.frame_delay_ms > 0 {
|
||||
std::thread::sleep(std::time::Duration::from_millis(options.frame_delay_ms));
|
||||
}
|
||||
if let Some(limit) = options.frames {
|
||||
if frame_index >= limit {
|
||||
break 'render;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let _ = wglMakeCurrent(
|
||||
HDC::default(),
|
||||
windows::Win32::Graphics::OpenGL::HGLRC::default(),
|
||||
);
|
||||
let _ = wglDeleteContext(context);
|
||||
ReleaseDC(Some(hwnd), hdc);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "ash"
|
||||
version = "0.38.0+1.3.281"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f"
|
||||
dependencies = [
|
||||
"libloading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "libloading"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "vulkan-present-fixture"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"ash",
|
||||
"windows",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
|
||||
dependencies = [
|
||||
"windows-collections",
|
||||
"windows-core",
|
||||
"windows-future",
|
||||
"windows-numerics",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-collections"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-future"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-link",
|
||||
"windows-threading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.59.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-numerics"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-threading"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
@@ -0,0 +1,26 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
[package]
|
||||
name = "vulkan-present-fixture"
|
||||
version = "0.0.0"
|
||||
edition = "2024"
|
||||
license = "AGPL-3.0-or-later"
|
||||
publish = false
|
||||
|
||||
# Empty workspace table keeps this fixture out of any parent Cargo workspace,
|
||||
# matching the sibling crates (hook/, vulkan-layer/) in this package.
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
|
||||
[[bin]]
|
||||
name = "vulkan-present-fixture"
|
||||
path = "src/main.rs"
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
ash = {version = "0.38.0", default-features = false, features = ["std", "loaded"]}
|
||||
windows = {version = "0.62.2", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_Graphics_Gdi",
|
||||
"Win32_System_LibraryLoader",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
]}
|
||||
windows-core = "0.62.2"
|
||||
@@ -0,0 +1,681 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn main() {
|
||||
eprintln!("vulkan-present-fixture is only functional on Windows");
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn main() {
|
||||
if let Err(error) = windows_impl::run() {
|
||||
eprintln!("vulkan-present-fixture failed: {error}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(not(windows), allow(dead_code))]
|
||||
mod pattern {
|
||||
pub const BARS: [(u8, u8, u8); 8] = [
|
||||
(255, 255, 255),
|
||||
(255, 255, 0),
|
||||
(0, 255, 255),
|
||||
(0, 255, 0),
|
||||
(255, 0, 255),
|
||||
(255, 0, 0),
|
||||
(0, 0, 255),
|
||||
(0, 0, 0),
|
||||
];
|
||||
|
||||
pub fn clear_colour(frame_index: u64) -> [f32; 4] {
|
||||
let (r, g, b) = BARS[(frame_index % BARS.len() as u64) as usize];
|
||||
[r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0]
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
mod cli {
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub enum FormatChoice {
|
||||
Bgra,
|
||||
Rgba,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PresentModeChoice {
|
||||
Fifo,
|
||||
Mailbox,
|
||||
Immediate,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PresentWaitChoice {
|
||||
Semaphore,
|
||||
None,
|
||||
}
|
||||
|
||||
pub struct Options {
|
||||
pub frames: Option<u64>,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub borderless: bool,
|
||||
pub format: FormatChoice,
|
||||
pub present_mode: PresentModeChoice,
|
||||
pub present_wait: PresentWaitChoice,
|
||||
pub resize_at: Option<u64>,
|
||||
pub resize_width: Option<u32>,
|
||||
pub resize_height: Option<u32>,
|
||||
}
|
||||
|
||||
impl Default for Options {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
frames: None,
|
||||
width: 1280,
|
||||
height: 720,
|
||||
borderless: false,
|
||||
format: FormatChoice::Bgra,
|
||||
present_mode: PresentModeChoice::Fifo,
|
||||
present_wait: PresentWaitChoice::Semaphore,
|
||||
resize_at: None,
|
||||
resize_width: None,
|
||||
resize_height: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Options {
|
||||
pub fn from_env() -> Self {
|
||||
let mut options = Options::default();
|
||||
let mut args = std::env::args().skip(1);
|
||||
while let Some(arg) = args.next() {
|
||||
match arg.as_str() {
|
||||
"--frames" => {
|
||||
if let Some(value) = args.next().and_then(|v| v.parse().ok()) {
|
||||
options.frames = Some(value);
|
||||
}
|
||||
}
|
||||
"--width" => {
|
||||
if let Some(value) = args.next().and_then(|v| v.parse().ok()) {
|
||||
options.width = value;
|
||||
}
|
||||
}
|
||||
"--height" => {
|
||||
if let Some(value) = args.next().and_then(|v| v.parse().ok()) {
|
||||
options.height = value;
|
||||
}
|
||||
}
|
||||
"--borderless" => options.borderless = true,
|
||||
"--windowed" => options.borderless = false,
|
||||
"--format" => {
|
||||
if let Some(value) = args.next() {
|
||||
options.format = match value.to_ascii_lowercase().as_str() {
|
||||
"rgba" => FormatChoice::Rgba,
|
||||
_ => FormatChoice::Bgra,
|
||||
};
|
||||
}
|
||||
}
|
||||
"--present-mode" => {
|
||||
if let Some(value) = args.next() {
|
||||
options.present_mode = match value.to_ascii_lowercase().as_str() {
|
||||
"mailbox" => PresentModeChoice::Mailbox,
|
||||
"immediate" => PresentModeChoice::Immediate,
|
||||
_ => PresentModeChoice::Fifo,
|
||||
};
|
||||
}
|
||||
}
|
||||
"--present-wait" => {
|
||||
if let Some(value) = args.next() {
|
||||
options.present_wait = match value.to_ascii_lowercase().as_str() {
|
||||
"none" => PresentWaitChoice::None,
|
||||
_ => PresentWaitChoice::Semaphore,
|
||||
};
|
||||
}
|
||||
}
|
||||
"--resize-at" => {
|
||||
if let Some(value) = args.next().and_then(|v| v.parse().ok()) {
|
||||
options.resize_at = Some(value);
|
||||
}
|
||||
}
|
||||
"--resize-width" => {
|
||||
if let Some(value) = args.next().and_then(|v| v.parse().ok()) {
|
||||
options.resize_width = Some(value);
|
||||
}
|
||||
}
|
||||
"--resize-height" => {
|
||||
if let Some(value) = args.next().and_then(|v| v.parse().ok()) {
|
||||
options.resize_height = Some(value);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
options
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
mod windows_impl {
|
||||
use super::cli::{FormatChoice, Options, PresentModeChoice, PresentWaitChoice};
|
||||
use super::pattern;
|
||||
use ash::{khr, vk};
|
||||
use std::error::Error;
|
||||
use windows::Win32::Foundation::{HWND, LPARAM, LRESULT, RECT, WPARAM};
|
||||
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::{
|
||||
CS_HREDRAW, CS_VREDRAW, CW_USEDEFAULT, CreateWindowExW, DefWindowProcW, DispatchMessageW,
|
||||
GetClientRect, MSG, MoveWindow, PM_REMOVE, PeekMessageW, PostQuitMessage, RegisterClassExW,
|
||||
SW_SHOW, ShowWindow, TranslateMessage, WM_DESTROY, WM_QUIT, WNDCLASSEXW,
|
||||
WS_OVERLAPPEDWINDOW, WS_POPUP, WS_VISIBLE,
|
||||
};
|
||||
use windows::core::w;
|
||||
|
||||
type DynError = Box<dyn Error>;
|
||||
|
||||
extern "system" fn wnd_proc(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
|
||||
unsafe {
|
||||
match msg {
|
||||
WM_DESTROY => {
|
||||
PostQuitMessage(0);
|
||||
LRESULT(0)
|
||||
}
|
||||
_ => DefWindowProcW(hwnd, msg, wparam, lparam),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn create_window(
|
||||
width: u32,
|
||||
height: u32,
|
||||
borderless: bool,
|
||||
) -> Result<(HWND, isize), DynError> {
|
||||
unsafe {
|
||||
let instance = GetModuleHandleW(None)?;
|
||||
let class_name = w!("FluxerVulkanFixture");
|
||||
let wc = WNDCLASSEXW {
|
||||
cbSize: size_of::<WNDCLASSEXW>() as u32,
|
||||
style: CS_HREDRAW | CS_VREDRAW,
|
||||
lpfnWndProc: Some(wnd_proc),
|
||||
hInstance: instance.into(),
|
||||
lpszClassName: class_name,
|
||||
..Default::default()
|
||||
};
|
||||
RegisterClassExW(&wc);
|
||||
let style = if borderless {
|
||||
WS_POPUP | WS_VISIBLE
|
||||
} else {
|
||||
WS_OVERLAPPEDWINDOW | WS_VISIBLE
|
||||
};
|
||||
let hwnd = CreateWindowExW(
|
||||
Default::default(),
|
||||
class_name,
|
||||
w!("Fluxer Vulkan Present Fixture"),
|
||||
style,
|
||||
CW_USEDEFAULT,
|
||||
CW_USEDEFAULT,
|
||||
width as i32,
|
||||
height as i32,
|
||||
None,
|
||||
None,
|
||||
Some(instance.into()),
|
||||
None,
|
||||
)?;
|
||||
let _ = ShowWindow(hwnd, SW_SHOW);
|
||||
Ok((hwnd, instance.0 as isize))
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn client_extent(hwnd: HWND) -> vk::Extent2D {
|
||||
unsafe {
|
||||
let mut rect = RECT::default();
|
||||
let _ = GetClientRect(hwnd, &mut rect);
|
||||
vk::Extent2D {
|
||||
width: (rect.right - rect.left).max(1) as u32,
|
||||
height: (rect.bottom - rect.top).max(1) as u32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn resize_window(hwnd: HWND, width: u32, height: u32) {
|
||||
unsafe {
|
||||
let _ = MoveWindow(hwnd, 0, 0, width as i32, height as i32, true);
|
||||
}
|
||||
}
|
||||
|
||||
fn pick_surface_format(
|
||||
formats: &[vk::SurfaceFormatKHR],
|
||||
choice: FormatChoice,
|
||||
) -> Result<vk::SurfaceFormatKHR, DynError> {
|
||||
let preferred: [vk::Format; 2] = match choice {
|
||||
FormatChoice::Bgra => [vk::Format::B8G8R8A8_UNORM, vk::Format::B8G8R8A8_SRGB],
|
||||
FormatChoice::Rgba => [vk::Format::R8G8B8A8_UNORM, vk::Format::R8G8B8A8_SRGB],
|
||||
};
|
||||
let any_8bit = [
|
||||
vk::Format::B8G8R8A8_UNORM,
|
||||
vk::Format::R8G8B8A8_UNORM,
|
||||
vk::Format::B8G8R8A8_SRGB,
|
||||
vk::Format::R8G8B8A8_SRGB,
|
||||
];
|
||||
formats
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|f| preferred.contains(&f.format))
|
||||
.or_else(|| {
|
||||
formats
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|f| any_8bit.contains(&f.format))
|
||||
})
|
||||
.or_else(|| formats.first().copied())
|
||||
.ok_or_else(|| "surface advertised no formats".into())
|
||||
}
|
||||
|
||||
fn pick_present_mode(
|
||||
available: &[vk::PresentModeKHR],
|
||||
choice: PresentModeChoice,
|
||||
) -> vk::PresentModeKHR {
|
||||
let wanted = match choice {
|
||||
PresentModeChoice::Fifo => vk::PresentModeKHR::FIFO,
|
||||
PresentModeChoice::Mailbox => vk::PresentModeKHR::MAILBOX,
|
||||
PresentModeChoice::Immediate => vk::PresentModeKHR::IMMEDIATE,
|
||||
};
|
||||
if available.contains(&wanted) {
|
||||
wanted
|
||||
} else {
|
||||
vk::PresentModeKHR::FIFO
|
||||
}
|
||||
}
|
||||
|
||||
struct Swapchain {
|
||||
handle: vk::SwapchainKHR,
|
||||
images: Vec<vk::Image>,
|
||||
command_buffers: Vec<vk::CommandBuffer>,
|
||||
extent: vk::Extent2D,
|
||||
format: vk::Format,
|
||||
}
|
||||
|
||||
impl Swapchain {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
unsafe fn create(
|
||||
device: &ash::Device,
|
||||
swapchain_device: &khr::swapchain::Device,
|
||||
surface_instance: &khr::surface::Instance,
|
||||
physical_device: vk::PhysicalDevice,
|
||||
surface: vk::SurfaceKHR,
|
||||
command_pool: vk::CommandPool,
|
||||
surface_format: vk::SurfaceFormatKHR,
|
||||
present_mode: vk::PresentModeKHR,
|
||||
requested_extent: vk::Extent2D,
|
||||
old: vk::SwapchainKHR,
|
||||
) -> Result<Self, DynError> {
|
||||
unsafe {
|
||||
let surface_caps = surface_instance
|
||||
.get_physical_device_surface_capabilities(physical_device, surface)?;
|
||||
|
||||
let extent = if surface_caps.current_extent.width != u32::MAX {
|
||||
surface_caps.current_extent
|
||||
} else {
|
||||
vk::Extent2D {
|
||||
width: requested_extent.width.clamp(
|
||||
surface_caps.min_image_extent.width,
|
||||
surface_caps.max_image_extent.width,
|
||||
),
|
||||
height: requested_extent.height.clamp(
|
||||
surface_caps.min_image_extent.height,
|
||||
surface_caps.max_image_extent.height,
|
||||
),
|
||||
}
|
||||
};
|
||||
|
||||
let image_count =
|
||||
surface_caps
|
||||
.min_image_count
|
||||
.max(2)
|
||||
.min(if surface_caps.max_image_count == 0 {
|
||||
u32::MAX
|
||||
} else {
|
||||
surface_caps.max_image_count
|
||||
});
|
||||
|
||||
let swapchain_info = vk::SwapchainCreateInfoKHR::default()
|
||||
.surface(surface)
|
||||
.min_image_count(image_count)
|
||||
.image_format(surface_format.format)
|
||||
.image_color_space(surface_format.color_space)
|
||||
.image_extent(extent)
|
||||
.image_array_layers(1)
|
||||
.image_usage(
|
||||
vk::ImageUsageFlags::COLOR_ATTACHMENT | vk::ImageUsageFlags::TRANSFER_DST,
|
||||
)
|
||||
.image_sharing_mode(vk::SharingMode::EXCLUSIVE)
|
||||
.pre_transform(surface_caps.current_transform)
|
||||
.composite_alpha(vk::CompositeAlphaFlagsKHR::OPAQUE)
|
||||
.present_mode(present_mode)
|
||||
.clipped(true)
|
||||
.old_swapchain(old);
|
||||
let handle = swapchain_device.create_swapchain(&swapchain_info, None)?;
|
||||
let images = swapchain_device.get_swapchain_images(handle)?;
|
||||
|
||||
let alloc_info = vk::CommandBufferAllocateInfo::default()
|
||||
.command_pool(command_pool)
|
||||
.level(vk::CommandBufferLevel::PRIMARY)
|
||||
.command_buffer_count(images.len() as u32);
|
||||
let command_buffers = device.allocate_command_buffers(&alloc_info)?;
|
||||
|
||||
Ok(Self {
|
||||
handle,
|
||||
images,
|
||||
command_buffers,
|
||||
extent,
|
||||
format: surface_format.format,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn destroy(
|
||||
self,
|
||||
device: &ash::Device,
|
||||
swapchain_device: &khr::swapchain::Device,
|
||||
command_pool: vk::CommandPool,
|
||||
) {
|
||||
unsafe {
|
||||
if !self.command_buffers.is_empty() {
|
||||
device.free_command_buffers(command_pool, &self.command_buffers);
|
||||
}
|
||||
swapchain_device.destroy_swapchain(self.handle, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run() -> Result<(), DynError> {
|
||||
let options = Options::from_env();
|
||||
unsafe {
|
||||
let (hwnd, hinstance) =
|
||||
create_window(options.width, options.height, options.borderless)?;
|
||||
|
||||
println!("HWND={}", hwnd.0 as isize);
|
||||
use std::io::Write;
|
||||
let _ = std::io::stdout().flush();
|
||||
|
||||
let entry = ash::Entry::load()?;
|
||||
|
||||
let app_name = c"fluxer-vulkan-present-fixture";
|
||||
let app_info = vk::ApplicationInfo::default()
|
||||
.application_name(app_name)
|
||||
.api_version(vk::API_VERSION_1_1);
|
||||
let instance_extensions = [
|
||||
khr::surface::NAME.as_ptr(),
|
||||
khr::win32_surface::NAME.as_ptr(),
|
||||
];
|
||||
let instance_info = vk::InstanceCreateInfo::default()
|
||||
.application_info(&app_info)
|
||||
.enabled_extension_names(&instance_extensions);
|
||||
let instance = entry.create_instance(&instance_info, None)?;
|
||||
|
||||
let surface_instance = khr::surface::Instance::new(&entry, &instance);
|
||||
let win32_surface = khr::win32_surface::Instance::new(&entry, &instance);
|
||||
|
||||
let surface_info = vk::Win32SurfaceCreateInfoKHR::default()
|
||||
.hinstance(hinstance)
|
||||
.hwnd(hwnd.0 as isize);
|
||||
let surface = win32_surface.create_win32_surface(&surface_info, None)?;
|
||||
|
||||
let physical_devices = instance.enumerate_physical_devices()?;
|
||||
let (physical_device, queue_family_index) = physical_devices
|
||||
.iter()
|
||||
.find_map(|&physical_device| {
|
||||
let families =
|
||||
instance.get_physical_device_queue_family_properties(physical_device);
|
||||
families.iter().enumerate().find_map(|(index, family)| {
|
||||
let index = index as u32;
|
||||
let graphics = family.queue_flags.contains(vk::QueueFlags::GRAPHICS);
|
||||
let present = surface_instance
|
||||
.get_physical_device_surface_support(physical_device, index, surface)
|
||||
.unwrap_or(false);
|
||||
(graphics && present).then_some((physical_device, index))
|
||||
})
|
||||
})
|
||||
.ok_or("no Vulkan device with a graphics+present queue family")?;
|
||||
|
||||
let queue_priorities = [1.0_f32];
|
||||
let queue_info = vk::DeviceQueueCreateInfo::default()
|
||||
.queue_family_index(queue_family_index)
|
||||
.queue_priorities(&queue_priorities);
|
||||
let device_extensions = [khr::swapchain::NAME.as_ptr()];
|
||||
let queue_infos = [queue_info];
|
||||
let device_info = vk::DeviceCreateInfo::default()
|
||||
.queue_create_infos(&queue_infos)
|
||||
.enabled_extension_names(&device_extensions);
|
||||
let device = instance.create_device(physical_device, &device_info, None)?;
|
||||
let queue = device.get_device_queue(queue_family_index, 0);
|
||||
let swapchain_device = khr::swapchain::Device::new(&instance, &device);
|
||||
|
||||
let formats =
|
||||
surface_instance.get_physical_device_surface_formats(physical_device, surface)?;
|
||||
let surface_format = pick_surface_format(&formats, options.format)?;
|
||||
let available_present_modes = surface_instance
|
||||
.get_physical_device_surface_present_modes(physical_device, surface)?;
|
||||
let present_mode = pick_present_mode(&available_present_modes, options.present_mode);
|
||||
eprintln!(
|
||||
"vulkan-present-fixture: vkFormat={} vkColorSpace={} vkPresentMode={}",
|
||||
surface_format.format.as_raw(),
|
||||
surface_format.color_space.as_raw(),
|
||||
present_mode.as_raw()
|
||||
);
|
||||
|
||||
let pool_info = vk::CommandPoolCreateInfo::default()
|
||||
.queue_family_index(queue_family_index)
|
||||
.flags(vk::CommandPoolCreateFlags::RESET_COMMAND_BUFFER);
|
||||
let command_pool = device.create_command_pool(&pool_info, None)?;
|
||||
|
||||
let mut swapchain = Swapchain::create(
|
||||
&device,
|
||||
&swapchain_device,
|
||||
&surface_instance,
|
||||
physical_device,
|
||||
surface,
|
||||
command_pool,
|
||||
surface_format,
|
||||
present_mode,
|
||||
client_extent(hwnd),
|
||||
vk::SwapchainKHR::null(),
|
||||
)?;
|
||||
|
||||
let image_available =
|
||||
device.create_semaphore(&vk::SemaphoreCreateInfo::default(), None)?;
|
||||
let render_finished =
|
||||
device.create_semaphore(&vk::SemaphoreCreateInfo::default(), None)?;
|
||||
let in_flight = device.create_fence(
|
||||
&vk::FenceCreateInfo::default().flags(vk::FenceCreateFlags::SIGNALED),
|
||||
None,
|
||||
)?;
|
||||
|
||||
let subresource_range = vk::ImageSubresourceRange {
|
||||
aspect_mask: vk::ImageAspectFlags::COLOR,
|
||||
base_mip_level: 0,
|
||||
level_count: 1,
|
||||
base_array_layer: 0,
|
||||
layer_count: 1,
|
||||
};
|
||||
|
||||
let recreate_swapchain = |swapchain: &mut Swapchain| -> Result<(), DynError> {
|
||||
device.device_wait_idle()?;
|
||||
let new = Swapchain::create(
|
||||
&device,
|
||||
&swapchain_device,
|
||||
&surface_instance,
|
||||
physical_device,
|
||||
surface,
|
||||
command_pool,
|
||||
surface_format,
|
||||
present_mode,
|
||||
client_extent(hwnd),
|
||||
swapchain.handle,
|
||||
)?;
|
||||
eprintln!(
|
||||
"vulkan-present-fixture: swapchain recreated extent={}x{} vkFormat={}",
|
||||
new.extent.width,
|
||||
new.extent.height,
|
||||
new.format.as_raw()
|
||||
);
|
||||
let old = std::mem::replace(swapchain, new);
|
||||
old.destroy(&device, &swapchain_device, command_pool);
|
||||
Ok(())
|
||||
};
|
||||
|
||||
let mut frame_index: u64 = 0;
|
||||
let mut resized = false;
|
||||
let mut msg = MSG::default();
|
||||
'render: loop {
|
||||
while PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).as_bool() {
|
||||
if msg.message == WM_QUIT {
|
||||
break 'render;
|
||||
}
|
||||
let _ = TranslateMessage(&msg);
|
||||
DispatchMessageW(&msg);
|
||||
}
|
||||
|
||||
if let Some(at) = options.resize_at {
|
||||
if !resized && frame_index >= at {
|
||||
if let (Some(width), Some(height)) =
|
||||
(options.resize_width, options.resize_height)
|
||||
{
|
||||
resize_window(hwnd, width, height);
|
||||
}
|
||||
recreate_swapchain(&mut swapchain)?;
|
||||
resized = true;
|
||||
}
|
||||
}
|
||||
|
||||
device.wait_for_fences(&[in_flight], true, u64::MAX)?;
|
||||
|
||||
let (image_index, suboptimal) = match swapchain_device.acquire_next_image(
|
||||
swapchain.handle,
|
||||
u64::MAX,
|
||||
image_available,
|
||||
vk::Fence::null(),
|
||||
) {
|
||||
Ok(value) => value,
|
||||
Err(vk::Result::ERROR_OUT_OF_DATE_KHR) => {
|
||||
recreate_swapchain(&mut swapchain)?;
|
||||
continue;
|
||||
}
|
||||
Err(error) => return Err(error.into()),
|
||||
};
|
||||
|
||||
device.reset_fences(&[in_flight])?;
|
||||
|
||||
let command_buffer = swapchain.command_buffers[image_index as usize];
|
||||
let image = swapchain.images[image_index as usize];
|
||||
device
|
||||
.reset_command_buffer(command_buffer, vk::CommandBufferResetFlags::empty())?;
|
||||
device.begin_command_buffer(
|
||||
command_buffer,
|
||||
&vk::CommandBufferBeginInfo::default()
|
||||
.flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT),
|
||||
)?;
|
||||
|
||||
let to_transfer = vk::ImageMemoryBarrier::default()
|
||||
.src_access_mask(vk::AccessFlags::empty())
|
||||
.dst_access_mask(vk::AccessFlags::TRANSFER_WRITE)
|
||||
.old_layout(vk::ImageLayout::UNDEFINED)
|
||||
.new_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL)
|
||||
.src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
|
||||
.dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
|
||||
.image(image)
|
||||
.subresource_range(subresource_range);
|
||||
device.cmd_pipeline_barrier(
|
||||
command_buffer,
|
||||
vk::PipelineStageFlags::TOP_OF_PIPE,
|
||||
vk::PipelineStageFlags::TRANSFER,
|
||||
vk::DependencyFlags::empty(),
|
||||
&[],
|
||||
&[],
|
||||
&[to_transfer],
|
||||
);
|
||||
|
||||
let clear = pattern::clear_colour(frame_index);
|
||||
let clear_value = vk::ClearColorValue { float32: clear };
|
||||
device.cmd_clear_color_image(
|
||||
command_buffer,
|
||||
image,
|
||||
vk::ImageLayout::TRANSFER_DST_OPTIMAL,
|
||||
&clear_value,
|
||||
&[subresource_range],
|
||||
);
|
||||
|
||||
let to_present = vk::ImageMemoryBarrier::default()
|
||||
.src_access_mask(vk::AccessFlags::TRANSFER_WRITE)
|
||||
.dst_access_mask(vk::AccessFlags::empty())
|
||||
.old_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL)
|
||||
.new_layout(vk::ImageLayout::PRESENT_SRC_KHR)
|
||||
.src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
|
||||
.dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
|
||||
.image(image)
|
||||
.subresource_range(subresource_range);
|
||||
device.cmd_pipeline_barrier(
|
||||
command_buffer,
|
||||
vk::PipelineStageFlags::TRANSFER,
|
||||
vk::PipelineStageFlags::BOTTOM_OF_PIPE,
|
||||
vk::DependencyFlags::empty(),
|
||||
&[],
|
||||
&[],
|
||||
&[to_present],
|
||||
);
|
||||
device.end_command_buffer(command_buffer)?;
|
||||
|
||||
let wait_semaphores = [image_available];
|
||||
let wait_stages = [vk::PipelineStageFlags::TRANSFER];
|
||||
let command_buffers = [command_buffer];
|
||||
let mut submit = vk::SubmitInfo::default()
|
||||
.wait_semaphores(&wait_semaphores)
|
||||
.wait_dst_stage_mask(&wait_stages)
|
||||
.command_buffers(&command_buffers);
|
||||
let signal_semaphores = [render_finished];
|
||||
if options.present_wait == PresentWaitChoice::Semaphore {
|
||||
submit = submit.signal_semaphores(&signal_semaphores);
|
||||
}
|
||||
device.queue_submit(queue, &[submit], in_flight)?;
|
||||
|
||||
let swapchains = [swapchain.handle];
|
||||
let image_indices = [image_index];
|
||||
if options.present_wait == PresentWaitChoice::None {
|
||||
device.wait_for_fences(&[in_flight], true, u64::MAX)?;
|
||||
}
|
||||
let mut present_info = vk::PresentInfoKHR::default()
|
||||
.swapchains(&swapchains)
|
||||
.image_indices(&image_indices);
|
||||
if options.present_wait == PresentWaitChoice::Semaphore {
|
||||
present_info = present_info.wait_semaphores(&signal_semaphores);
|
||||
}
|
||||
let present_result = swapchain_device.queue_present(queue, &present_info);
|
||||
let needs_recreate = match present_result {
|
||||
Ok(present_suboptimal) => present_suboptimal || suboptimal,
|
||||
Err(vk::Result::ERROR_OUT_OF_DATE_KHR) => true,
|
||||
Err(error) => return Err(error.into()),
|
||||
};
|
||||
if needs_recreate {
|
||||
recreate_swapchain(&mut swapchain)?;
|
||||
}
|
||||
|
||||
frame_index += 1;
|
||||
if let Some(limit) = options.frames {
|
||||
if frame_index >= limit {
|
||||
break 'render;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let _ = device.device_wait_idle();
|
||||
device.destroy_fence(in_flight, None);
|
||||
device.destroy_semaphore(render_finished, None);
|
||||
device.destroy_semaphore(image_available, None);
|
||||
swapchain.destroy(&device, &swapchain_device, command_pool);
|
||||
device.destroy_command_pool(command_pool, None);
|
||||
surface_instance.destroy_surface(surface, None);
|
||||
device.destroy_device(None);
|
||||
instance.destroy_instance(None);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user