move uidev to workspace root

This commit is contained in:
galister
2025-06-21 17:39:28 +09:00
parent 814700cbff
commit 9759dff8b9
119 changed files with 14 additions and 6 deletions

12
uidev/src/assets.rs Normal file
View File

@@ -0,0 +1,12 @@
#[derive(rust_embed::Embed)]
#[folder = "assets/"]
pub struct Asset;
impl wgui::assets::AssetProvider for Asset {
fn load_from_path(&mut self, path: &str) -> anyhow::Result<Vec<u8>> {
match Asset::get(path) {
Some(data) => Ok(data.data.to_vec()),
None => anyhow::bail!("embedded file {} not found", path),
}
}
}

340
uidev/src/main.rs Normal file
View File

@@ -0,0 +1,340 @@
use glam::{Vec2, vec2};
use std::sync::Arc;
use testbed::{Testbed, testbed_any::TestbedAny};
use timestep::Timestep;
use tracing_subscriber::EnvFilter;
use tracing_subscriber::filter::LevelFilter;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
use vulkan::init_window;
use vulkano::{
Validated, VulkanError,
command_buffer::CommandBufferUsage,
format::Format,
image::{ImageUsage, view::ImageView},
swapchain::{
Surface, SurfaceInfo, Swapchain, SwapchainCreateInfo, SwapchainPresentInfo, acquire_next_image,
},
sync::GpuFuture,
};
use wgui::{
event::{MouseButton, MouseDownEvent, MouseMotionEvent, MouseUpEvent, MouseWheelEvent},
gfx::WGfx,
renderer_vk::{self},
};
use winit::{
event::{ElementState, Event, MouseScrollDelta, WindowEvent},
event_loop::ControlFlow,
keyboard::{KeyCode, PhysicalKey},
};
use crate::testbed::{testbed_dashboard::TestbedDashboard, testbed_generic::TestbedGeneric};
mod assets;
mod profiler;
mod testbed;
mod timestep;
mod vulkan;
fn init_logging() {
tracing_subscriber::registry()
.with(
tracing_subscriber::fmt::layer()
.pretty()
.with_writer(std::io::stderr),
)
.with(
/* read RUST_LOG env var */
EnvFilter::builder()
.with_default_directive(LevelFilter::DEBUG.into())
.from_env_lossy(),
)
.init();
}
fn load_testbed() -> anyhow::Result<Box<dyn Testbed>> {
let name = std::env::var("TESTBED").unwrap_or_default();
Ok(match name.as_str() {
"dashboard" => Box::new(TestbedDashboard::new()?),
"" => Box::new(TestbedGeneric::new()?),
_ => Box::new(TestbedAny::new(&name)?),
})
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
init_logging();
let (gfx, event_loop, window, surface) = init_window()?;
let inner_size = window.inner_size();
let mut swapchain_size = [inner_size.width, inner_size.height];
let mut swapchain_create_info =
swapchain_create_info(&gfx, gfx.surface_format, surface.clone(), swapchain_size);
let (mut swapchain, mut images) = {
let (swapchain, images) = Swapchain::new(
gfx.device.clone(),
surface.clone(),
swapchain_create_info.clone(),
)?;
let image_views = images
.into_iter()
.map(|image| ImageView::new_default(image).unwrap())
.collect::<Vec<_>>();
(swapchain, image_views)
};
let mut recreate = false;
let mut last_draw = std::time::Instant::now();
let mut scale = window.scale_factor() as f32;
let mut testbed = load_testbed()?;
let mut mouse = Vec2::ZERO;
let mut render_context =
renderer_vk::context::Context::new(gfx.clone(), gfx.surface_format, scale)?;
render_context.update_viewport(swapchain_size, scale)?;
println!("new swapchain_size: {swapchain_size:?}");
let mut profiler = profiler::Profiler::new(100);
let mut frame_index: u64 = 0;
let mut timestep = Timestep::new();
timestep.set_tps(60.0);
#[allow(deprecated)]
event_loop.run(move |event, elwt| {
elwt.set_control_flow(ControlFlow::Poll);
match event {
Event::WindowEvent {
event: WindowEvent::MouseWheel { delta, .. },
..
} => match delta {
MouseScrollDelta::LineDelta(x, y) => testbed
.layout()
.push_event(&wgui::event::Event::MouseWheel(MouseWheelEvent {
shift: Vec2::new(x, y),
pos: mouse / scale,
device: 0,
}))
.unwrap(),
MouseScrollDelta::PixelDelta(pos) => testbed
.layout()
.push_event(&wgui::event::Event::MouseWheel(MouseWheelEvent {
shift: Vec2::new(pos.x as f32 / 5.0, pos.y as f32 / 5.0),
pos: mouse / scale,
device: 0,
}))
.unwrap(),
},
Event::WindowEvent {
event: WindowEvent::MouseInput { state, button, .. },
..
} => {
if matches!(button, winit::event::MouseButton::Left) {
if matches!(state, winit::event::ElementState::Pressed) {
testbed
.layout()
.push_event(&wgui::event::Event::MouseDown(MouseDownEvent {
pos: mouse / scale,
button: MouseButton::Left,
device: 0,
}))
.unwrap();
} else {
testbed
.layout()
.push_event(&wgui::event::Event::MouseUp(MouseUpEvent {
pos: mouse / scale,
button: MouseButton::Left,
device: 0,
}))
.unwrap();
}
}
}
Event::WindowEvent {
event: WindowEvent::CursorMoved { position, .. },
..
} => {
mouse = vec2(position.x as _, position.y as _);
testbed
.layout()
.push_event(&wgui::event::Event::MouseMotion(MouseMotionEvent {
pos: mouse / scale,
device: 0,
}))
.unwrap();
}
Event::WindowEvent {
event: WindowEvent::KeyboardInput { event, .. },
..
} => {
if event.state == ElementState::Pressed {
if event.physical_key == PhysicalKey::Code(KeyCode::Equal) {
scale *= 1.25;
render_context
.update_viewport(swapchain_size, scale)
.unwrap();
}
if event.physical_key == PhysicalKey::Code(KeyCode::Minus) {
scale *= 0.75;
render_context
.update_viewport(swapchain_size, scale)
.unwrap();
}
}
}
Event::WindowEvent {
event: WindowEvent::CloseRequested,
..
} => {
elwt.exit();
}
Event::WindowEvent {
event: WindowEvent::Resized(_),
..
} => {
recreate = true;
}
Event::WindowEvent {
event: WindowEvent::RedrawRequested,
..
} => {
if recreate {
let inner_size = window.inner_size();
swapchain_size = [inner_size.width, inner_size.height];
swapchain_create_info.image_extent = swapchain_size;
(swapchain, images) = {
let (swapchain, images) = swapchain.recreate(swapchain_create_info.clone()).unwrap();
let image_views = images
.into_iter()
.map(|image| ImageView::new_default(image).unwrap())
.collect::<Vec<_>>();
(swapchain, image_views)
};
render_context
.update_viewport(swapchain_size, scale)
.unwrap();
println!("new swapchain_size: {swapchain_size:?}");
recreate = false;
window.request_redraw();
}
while timestep.on_tick() {
testbed.layout().tick().unwrap();
}
testbed
.update(
(swapchain_size[0] as f32 / scale) as _,
(swapchain_size[1] as f32 / scale) as _,
timestep.alpha,
)
.unwrap();
if !render_context.dirty && !testbed.layout().check_toggle_needs_redraw() {
// no need to redraw
std::thread::sleep(std::time::Duration::from_millis(5)); // dirty fix to prevent cpu burning precious cycles doing a busy loop
return;
}
log::trace!("drawing frame {}", frame_index);
frame_index += 1;
profiler.start();
{
let (image_index, _, acquire_future) =
match acquire_next_image(swapchain.clone(), None).map_err(Validated::unwrap) {
Ok(r) => r,
Err(VulkanError::OutOfDate) => {
recreate = true;
return;
}
Err(e) => panic!("failed to acquire next image: {e}"),
};
let tgt = images[image_index as usize].clone();
last_draw = std::time::Instant::now();
let mut cmd_buf = gfx
.create_gfx_command_buffer(CommandBufferUsage::OneTimeSubmit)
.unwrap();
cmd_buf.begin_rendering(tgt).unwrap();
let primitives = wgui::drawing::draw(testbed.layout()).unwrap();
render_context
.draw(&gfx, &mut cmd_buf, &primitives)
.unwrap();
cmd_buf.end_rendering().unwrap();
let cmd_buf = cmd_buf.build().unwrap();
acquire_future
.then_execute(gfx.queue_gfx.clone(), cmd_buf)
.unwrap()
.then_swapchain_present(
gfx.queue_gfx.clone(),
SwapchainPresentInfo::swapchain_image_index(swapchain.clone(), image_index),
)
.then_signal_fence_and_flush()
.unwrap()
.wait(None)
.unwrap();
}
profiler.end();
}
Event::AboutToWait => {
if last_draw.elapsed().as_millis() > 16 {
window.request_redraw();
}
}
_ => (),
}
})?;
Ok(())
}
fn swapchain_create_info(
graphics: &WGfx,
format: Format,
surface: Arc<Surface>,
extent: [u32; 2],
) -> SwapchainCreateInfo {
let surface_capabilities = graphics
.device
.physical_device()
.surface_capabilities(&surface, SurfaceInfo::default())
.unwrap(); // want panic
SwapchainCreateInfo {
min_image_count: surface_capabilities.min_image_count.max(2),
image_format: format,
image_extent: extent,
image_usage: ImageUsage::COLOR_ATTACHMENT,
composite_alpha: surface_capabilities
.supported_composite_alpha
.into_iter()
.next()
.unwrap(), // want panic
..Default::default()
}
}

49
uidev/src/profiler.rs Normal file
View File

@@ -0,0 +1,49 @@
use std::{sync::LazyLock, time::Instant};
static TIME_START: LazyLock<Instant> = LazyLock::new(Instant::now);
pub fn get_micros() -> u64 {
TIME_START.elapsed().as_micros() as u64
}
pub struct Profiler {
interval_us: u64,
last_measure_us: u64,
frametime_sum_us: u64,
measure_frames: u64,
time_start_us: u64,
}
impl Profiler {
pub fn new(interval_ms: u64) -> Self {
Self {
frametime_sum_us: 0,
interval_us: interval_ms * 1000,
last_measure_us: 0,
measure_frames: 0,
time_start_us: 0,
}
}
pub fn start(&mut self) {
self.time_start_us = get_micros();
}
pub fn end(&mut self) {
let cur_micros = get_micros();
let frametime = cur_micros - self.time_start_us;
self.measure_frames += 1;
self.frametime_sum_us += frametime;
if self.last_measure_us + self.interval_us < cur_micros {
log::debug!(
"avg frametime: {:.3}ms",
(self.frametime_sum_us / self.measure_frames) as f32 / 1000.0
);
self.last_measure_us = cur_micros;
self.frametime_sum_us = 0;
self.measure_frames = 0;
}
}
}

10
uidev/src/testbed/mod.rs Normal file
View File

@@ -0,0 +1,10 @@
use wgui::layout::Layout;
pub mod testbed_any;
pub mod testbed_dashboard;
pub mod testbed_generic;
pub trait Testbed {
fn update(&mut self, width: f32, height: f32, timestep_alpha: f32) -> anyhow::Result<()>;
fn layout(&mut self) -> &mut Layout;
}

View File

@@ -0,0 +1,28 @@
use crate::{assets, testbed::Testbed};
use glam::Vec2;
use wgui::layout::Layout;
pub struct TestbedAny {
pub layout: Layout,
}
impl TestbedAny {
pub fn new(name: &str) -> anyhow::Result<Self> {
let path = format!("gui/{name}.xml");
let (layout, _state) = wgui::parser::new_layout_from_assets(Box::new(assets::Asset {}), &path)?;
Ok(Self { layout })
}
}
impl Testbed for TestbedAny {
fn update(&mut self, width: f32, height: f32, timestep_alpha: f32) -> anyhow::Result<()> {
self
.layout
.update(Vec2::new(width, height), timestep_alpha)?;
Ok(())
}
fn layout(&mut self) -> &mut Layout {
&mut self.layout
}
}

View File

@@ -0,0 +1,29 @@
use crate::{assets, testbed::Testbed};
use glam::Vec2;
use wgui::layout::Layout;
pub struct TestbedDashboard {
pub layout: Layout,
}
impl TestbedDashboard {
pub fn new() -> anyhow::Result<Self> {
const XML_PATH: &str = "gui/dashboard.xml";
let (layout, _state) =
wgui::parser::new_layout_from_assets(Box::new(assets::Asset {}), XML_PATH)?;
Ok(Self { layout })
}
}
impl Testbed for TestbedDashboard {
fn update(&mut self, width: f32, height: f32, timestep_alpha: f32) -> anyhow::Result<()> {
self
.layout
.update(Vec2::new(width, height), timestep_alpha)?;
Ok(())
}
fn layout(&mut self) -> &mut Layout {
&mut self.layout
}
}

View File

@@ -0,0 +1,99 @@
use std::{cell::RefCell, rc::Rc};
use glam::{Mat4, Vec2};
use wgui::{
drawing::{self},
event::EventListener,
layout::{Layout, WidgetID},
renderer_vk::text::TextStyle,
};
use crate::{assets, testbed::Testbed};
pub struct TestbedGeneric {
pub layout: Layout,
rot: f32,
widget_id: Rc<RefCell<Option<WidgetID>>>,
}
impl TestbedGeneric {
pub fn new() -> anyhow::Result<Self> {
const XML_PATH: &str = "gui/testbed.xml";
let (mut layout, res) =
wgui::parser::new_layout_from_assets(Box::new(assets::Asset {}), XML_PATH)?;
use wgui::components::button;
let my_div_parent = res.require_by_id("my_div_parent")?;
// create some buttons for testing
for i in 0..4 {
let n = i as f32 / 4.0;
button::construct(
&mut layout,
my_div_parent,
button::Params {
text: "I'm a button!",
color: drawing::Color::new(1.0 - n, n * n, n, 1.0),
..Default::default()
},
)?;
}
let button = button::construct(
&mut layout,
my_div_parent,
button::Params {
text: "Click me!!",
color: drawing::Color::new(0.2, 0.2, 0.2, 1.0),
size: Vec2::new(256.0, 64.0),
text_style: TextStyle {
size: Some(30.0),
..Default::default()
},
},
)?;
let widget_id = Rc::new(RefCell::new(None));
let wid = widget_id.clone();
layout.add_event_listener(
button.body,
EventListener::MouseRelease(Box::new(move |data, _| {
button.set_text(data, "Congratulations!");
*wid.borrow_mut() = Some(data.widget_id);
})),
);
Ok(Self {
layout,
rot: 0.0,
widget_id,
})
}
}
impl Testbed for TestbedGeneric {
fn update(&mut self, width: f32, height: f32, timestep_alpha: f32) -> anyhow::Result<()> {
if let Some(widget_id) = *self.widget_id.borrow() {
self.rot += 0.01;
let a = self.layout.widget_map.get(widget_id).unwrap();
let mut widget = a.lock().unwrap();
widget.data.transform = Mat4::IDENTITY
* Mat4::from_rotation_y(-self.rot)
* Mat4::from_rotation_x(self.rot * 0.25)
* Mat4::from_rotation_z(-self.rot * 0.1);
self.layout.needs_redraw = true;
}
self
.layout
.update(Vec2::new(width, height), timestep_alpha)?;
Ok(())
}
fn layout(&mut self) -> &mut Layout {
&mut self.layout
}
}

70
uidev/src/timestep.rs Normal file
View File

@@ -0,0 +1,70 @@
use std::{sync::LazyLock, time::Instant};
static TIME_START: LazyLock<Instant> = LazyLock::new(Instant::now);
pub fn get_micros() -> u64 {
TIME_START.elapsed().as_micros() as u64
}
#[derive(Default)]
pub struct Timestep {
current_time_us: u64,
accumulator: f32,
time_micros: u64,
ticks: u32,
speed: f32,
pub alpha: f32,
delta: f32,
loopnum: u8,
}
impl Timestep {
pub fn new() -> Timestep {
let mut timestep = Timestep {
speed: 1.0,
..Default::default()
};
timestep.reset();
timestep
}
fn calculate_alpha(&mut self) {
self.alpha = (self.accumulator / self.delta).clamp(0.0, 1.0);
}
pub fn set_tps(&mut self, tps: f32) {
self.delta = 1000.0 / tps;
}
pub fn reset(&mut self) {
self.current_time_us = get_micros();
self.accumulator = 0.0;
}
pub fn on_tick(&mut self) -> bool {
let newtime = get_micros();
let frametime = newtime - self.current_time_us;
self.time_micros += frametime;
self.current_time_us = newtime;
self.accumulator += frametime as f32 * self.speed / 1000.0;
self.calculate_alpha();
if self.accumulator >= self.delta {
self.accumulator -= self.delta;
self.loopnum += 1;
self.ticks += 1;
if self.loopnum > 5 {
// cannot keep up!
self.loopnum = 0;
self.accumulator = 0.0;
return false;
}
true
} else {
self.loopnum = 0;
false
}
}
}

211
uidev/src/vulkan.rs Normal file
View File

@@ -0,0 +1,211 @@
use std::sync::{Arc, OnceLock};
use vulkano::{
device::{
Device, DeviceCreateInfo, DeviceExtensions, DeviceFeatures, Queue, QueueCreateInfo, QueueFlags,
physical::{PhysicalDevice, PhysicalDeviceType},
},
instance::{Instance, InstanceCreateInfo},
swapchain::SurfaceInfo,
};
use wgui::gfx::WGfx;
static VULKAN_LIBRARY: OnceLock<Arc<vulkano::VulkanLibrary>> = OnceLock::new();
fn get_vulkan_library() -> &'static Arc<vulkano::VulkanLibrary> {
VULKAN_LIBRARY.get_or_init(|| vulkano::VulkanLibrary::new().unwrap()) // want panic
}
#[allow(clippy::type_complexity)]
pub fn init_window() -> anyhow::Result<(
Arc<WGfx>,
winit::event_loop::EventLoop<()>,
Arc<winit::window::Window>,
Arc<vulkano::swapchain::Surface>,
)> {
use vulkano::{instance::InstanceCreateFlags, swapchain::Surface};
use winit::{event_loop::EventLoop, window::Window};
let event_loop = EventLoop::new().unwrap(); // want panic
let mut vk_instance_extensions = Surface::required_extensions(&event_loop).unwrap();
vk_instance_extensions.khr_get_physical_device_properties2 = true;
log::debug!("Instance exts for runtime: {:?}", &vk_instance_extensions);
let instance = Instance::new(
get_vulkan_library().clone(),
InstanceCreateInfo {
flags: InstanceCreateFlags::ENUMERATE_PORTABILITY,
enabled_extensions: vk_instance_extensions,
..Default::default()
},
)?;
#[allow(deprecated)]
let window = Arc::new(
event_loop
.create_window(Window::default_attributes())
.unwrap(), // want panic
);
let surface = Surface::from_window(instance.clone(), window.clone())?;
let mut device_extensions = DeviceExtensions::empty();
device_extensions.khr_swapchain = true;
log::debug!("Device exts for app: {:?}", &device_extensions);
let (physical_device, mut my_extensions, queue_families) = instance
.enumerate_physical_devices()?
.filter_map(|p| {
if p.supported_extensions().contains(&device_extensions) {
Some((p, device_extensions))
} else {
log::debug!(
"Not using {} because it does not implement the following device extensions:",
p.properties().device_name,
);
for (ext, missing) in p.supported_extensions().difference(&device_extensions) {
if missing {
log::debug!(" {ext}");
}
}
None
}
})
.filter_map(|(p, my_extensions)| {
try_all_queue_families(p.as_ref()).map(|families| (p, my_extensions, families))
})
.min_by_key(|(p, _, _)| prio_from_device_type(p))
.expect("no suitable physical device found");
log::info!(
"Using vkPhysicalDevice: {}",
physical_device.properties().device_name,
);
if physical_device.supported_extensions().img_filter_cubic {
my_extensions.img_filter_cubic = true;
log::info!("img_filter_cubic!");
}
let surface_format = physical_device
.surface_formats(&surface, SurfaceInfo::default())
.unwrap()[0] // want panic
.0;
log::info!("Using surface format: {surface_format:?}");
let (device, queues) = Device::new(
physical_device,
DeviceCreateInfo {
enabled_extensions: my_extensions,
enabled_features: DeviceFeatures {
dynamic_rendering: true,
..DeviceFeatures::empty()
},
queue_create_infos: queue_families
.iter()
.map(|fam| QueueCreateInfo {
queue_family_index: fam.queue_family_index,
queues: fam.priorities.clone(),
..Default::default()
})
.collect::<Vec<_>>(),
..Default::default()
},
)?;
let (queue_gfx, queue_xfer, _) = unwrap_queues(queues.collect());
let me = WGfx::new_from_raw(instance, device, queue_gfx, queue_xfer, surface_format);
Ok((me, event_loop, window, surface))
}
#[derive(Debug)]
struct QueueFamilyLayout {
queue_family_index: u32,
priorities: Vec<f32>,
}
fn prio_from_device_type(physical_device: &PhysicalDevice) -> u32 {
match physical_device.properties().device_type {
PhysicalDeviceType::DiscreteGpu => 0,
PhysicalDeviceType::IntegratedGpu => 1,
PhysicalDeviceType::VirtualGpu => 2,
PhysicalDeviceType::Cpu => 3,
_ => 4,
}
}
fn unwrap_queues(queues: Vec<Arc<Queue>>) -> (Arc<Queue>, Arc<Queue>, Option<Arc<Queue>>) {
match queues[..] {
[ref g, ref t, ref c] => (g.clone(), t.clone(), Some(c.clone())),
[ref gt, ref c] => (gt.clone(), gt.clone(), Some(c.clone())),
[ref gt] => (gt.clone(), gt.clone(), None),
_ => unreachable!(),
}
}
fn try_all_queue_families(physical_device: &PhysicalDevice) -> Option<Vec<QueueFamilyLayout>> {
queue_families_priorities(
physical_device,
vec![
// main-thread graphics + uploads
QueueFlags::GRAPHICS | QueueFlags::TRANSFER,
// capture-thread uploads
QueueFlags::TRANSFER,
],
)
.or_else(|| {
queue_families_priorities(
physical_device,
vec![
// main thread graphics
QueueFlags::GRAPHICS,
// main thread uploads
QueueFlags::TRANSFER,
// capture thread uploads
QueueFlags::TRANSFER,
],
)
})
.or_else(|| {
queue_families_priorities(
physical_device,
// main thread-only. software capture not supported.
vec![QueueFlags::GRAPHICS | QueueFlags::TRANSFER],
)
})
}
fn queue_families_priorities(
physical_device: &PhysicalDevice,
mut requested_queues: Vec<QueueFlags>,
) -> Option<Vec<QueueFamilyLayout>> {
let mut result = Vec::with_capacity(3);
for (idx, props) in physical_device.queue_family_properties().iter().enumerate() {
let mut remaining = props.queue_count;
let mut want = 0usize;
requested_queues.retain(|requested| {
if props.queue_flags.intersects(*requested) && remaining > 0 {
remaining -= 1;
want += 1;
false
} else {
true
}
});
if want > 0 {
result.push(QueueFamilyLayout {
queue_family_index: idx as u32,
priorities: std::iter::repeat_n(1.0, want).collect(),
});
}
}
if requested_queues.is_empty() {
log::debug!("Selected GPU queue families: {result:?}");
Some(result)
} else {
None
}
}