initial commit

This commit is contained in:
2025-08-08 18:22:52 -04:00
parent eba8481bde
commit 56beeb80f3
31 changed files with 1223 additions and 1197 deletions

66
src/layout/mod.rs Normal file
View File

@@ -0,0 +1,66 @@
mod node;
pub use node::*;
use crate::primitive::{Color, Painter};
pub type UIColor = Color<u8>;
pub trait UINode: 'static {
fn draw(&self, painter: &mut Painter);
}
pub struct UI {
base: Box<dyn UINode>,
}
impl UI {
pub fn to_primitives(&self) -> Painter {
let mut painter = Painter::default();
self.base.draw(&mut painter);
painter
}
}
impl<N: UINode> From<N> for UI {
fn from(node: N) -> Self {
Self {
base: Box::new(node),
}
}
}
impl UINode for Box<dyn UINode> {
fn draw(&self, painter: &mut Painter) {
self.as_ref().draw(painter);
}
}
pub trait NodeArray<const LEN: usize> {
fn to_arr(self) -> [Box<dyn UINode>; LEN];
}
// I hate this language it's so bad why do I even use it
macro_rules! impl_node_arr {
($n:expr;$($T:tt)+) => {
impl<$($T: UINode,)*> NodeArray<$n> for ($($T,)*) {
fn to_arr(self) -> [Box<dyn UINode>; $n] {
#[allow(non_snake_case)]
let ($($T,)*) = self;
[$(Box::new($T),)*]
}
}
};
}
impl_node_arr!(1;A);
impl_node_arr!(2;A B);
impl_node_arr!(3;A B C);
impl_node_arr!(4;A B C D);
impl_node_arr!(5;A B C D E);
impl_node_arr!(6;A B C D E F);
impl_node_arr!(7;A B C D E F G);
impl_node_arr!(8;A B C D E F G H);
impl_node_arr!(9;A B C D E F G H I);
impl_node_arr!(10;A B C D E F G H I J);
impl_node_arr!(11;A B C D E F G H I J K);
impl_node_arr!(12;A B C D E F G H I J K L);

164
src/layout/node.rs Normal file
View File

@@ -0,0 +1,164 @@
use std::ops::Range;
use crate::{
primitive::{Axis, Painter, RoundedRectData, UIRegion},
NodeArray, UIColor, UINode,
};
#[derive(Clone, Copy)]
pub struct RoundedRect {
pub color: UIColor,
pub radius: f32,
pub thickness: f32,
pub inner_radius: f32,
}
impl RoundedRect {
pub fn color(mut self, color: UIColor) -> Self {
self.color = color;
self
}
}
impl UINode for RoundedRect {
fn draw(&self, painter: &mut Painter) {
painter.write(RoundedRectData {
color: self.color,
radius: self.radius,
thickness: self.thickness,
inner_radius: self.inner_radius,
});
}
}
pub struct Span {
pub elements: Vec<(Range<f32>, Box<dyn UINode>)>,
pub axis: Axis,
}
impl UINode for Span {
fn draw(&self, painter: &mut Painter) {
for (span, child) in &self.elements {
let mut sub_region = UIRegion::full();
let view = sub_region.axis_mut(self.axis);
*view.top_left.anchor = span.start;
*view.bot_right.anchor = span.end;
painter.draw_within(child, sub_region);
}
}
}
impl Span {
pub fn proportioned<const LEN: usize>(
axis: Axis,
ratios: [impl UINum; LEN],
elements: impl NodeArray<LEN>,
) -> Self {
let ratios = ratios.map(|r| r.to_f32());
let total: f32 = ratios.iter().sum();
let mut start = 0.0;
Self {
elements: elements
.to_arr()
.into_iter()
.zip(ratios)
.map(|(e, r)| {
let end = start + r / total;
let res = (start..end, e);
start = end;
res
})
.collect(),
axis,
}
}
}
pub struct Regioned<N: UINode> {
region: UIRegion,
inner: N,
}
impl<N: UINode> UINode for Regioned<N> {
fn draw(&self, painter: &mut Painter) {
painter.region.select(&self.region);
self.inner.draw(painter);
}
}
pub struct Padding {
left: f32,
right: f32,
top: f32,
bottom: f32,
}
impl Padding {
pub fn uniform(amt: f32) -> Self {
Self {
left: amt,
right: amt,
top: amt,
bottom: amt,
}
}
pub fn region(&self) -> UIRegion {
let mut region = UIRegion::full();
region.top_left.offset.x += self.left;
region.top_left.offset.y += self.top;
region.bot_right.offset.x -= self.right;
region.bot_right.offset.y -= self.bottom;
region
}
}
impl<T: UINum> From<T> for Padding {
fn from(amt: T) -> Self {
Self::uniform(amt.to_f32())
}
}
pub trait NodeUtil: Sized + UINode {
fn pad(self, padding: impl Into<Padding>) -> Regioned<Self>;
}
impl<T: UINode> NodeUtil for T {
fn pad(self, padding: impl Into<Padding>) -> Regioned<Self> {
Regioned {
region: padding.into().region(),
inner: self,
}
}
}
pub trait NodeArrayUtil<const LEN: usize> {
fn proportioned(self, axis: Axis, ratios: [impl UINum; LEN]) -> Span;
}
impl<T: NodeArray<LEN>, const LEN: usize> NodeArrayUtil<LEN> for T {
fn proportioned(self, axis: Axis, ratios: [impl UINum; LEN]) -> Span {
Span::proportioned(axis, ratios, self)
}
}
pub trait UINum {
fn to_f32(self) -> f32;
}
impl UINum for f32 {
fn to_f32(self) -> f32 {
self
}
}
impl UINum for u32 {
fn to_f32(self) -> f32 {
self as f32
}
}
impl UINum for i32 {
fn to_f32(self) -> f32 {
self as f32
}
}

10
src/lib.rs Normal file
View File

@@ -0,0 +1,10 @@
#![feature(macro_metavar_expr_concat)]
#![feature(const_ops)]
#![feature(const_trait_impl)]
#![feature(const_from)]
mod layout;
mod render;
pub use layout::*;
pub use render::*;

View File

@@ -1,36 +1,5 @@
use std::sync::Arc;
use app::App;
use render::Renderer;
use winit::{event::WindowEvent, event_loop::ActiveEventLoop, window::Window};
mod app;
mod render;
mod testing;
fn main() {
App::run();
}
pub struct Client {
window: Arc<Window>,
renderer: Renderer,
}
impl Client {
pub fn new(window: Arc<Window>) -> Self {
let renderer = Renderer::new(window.clone());
Self {
window,
renderer,
}
}
pub fn event(&mut self, event: WindowEvent, event_loop: &ActiveEventLoop) {
match event {
WindowEvent::CloseRequested => event_loop.exit(),
WindowEvent::RedrawRequested => self.renderer.draw(),
WindowEvent::Resized(size) => self.renderer.resize(&size),
_ => (),
}
}
testing::main();
}

34
src/render/data.rs Normal file
View File

@@ -0,0 +1,34 @@
use crate::primitive::UIRegion;
use wgpu::VertexAttribute;
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct PrimitiveInstance {
pub region: UIRegion,
pub ptr: u32,
}
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable, Default)]
pub struct WindowUniform {
pub width: f32,
pub height: f32,
}
impl PrimitiveInstance {
const ATTRIBS: [VertexAttribute; 5] = wgpu::vertex_attr_array![
0 => Float32x2,
1 => Float32x2,
2 => Float32x2,
3 => Float32x2,
4 => Uint32,
];
pub fn desc() -> wgpu::VertexBufferLayout<'static> {
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Self>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &Self::ATTRIBS,
}
}
}

View File

@@ -1,199 +1,184 @@
use std::sync::{
mpsc::{channel, Receiver},
Arc,
use crate::{
render::{data::PrimitiveInstance, util::ArrBuf},
UI,
};
use notify::{
event::{CreateKind, ModifyKind},
EventKind, RecommendedWatcher, Watcher,
use data::WindowUniform;
use wgpu::{
util::{BufferInitDescriptor, DeviceExt},
*,
};
use pollster::FutureExt;
use primitive::{RoundedRect, UIPos};
use shape::ShapePipeline;
use wgpu::util::StagingBelt;
use winit::{dpi::PhysicalSize, window::Window};
use winit::dpi::PhysicalSize;
mod data;
pub mod primitive;
mod shape;
mod util;
pub const CLEAR_COLOR: wgpu::Color = wgpu::Color::BLACK;
const SHAPE_SHADER: &str = include_str!("./shader.wgsl");
pub struct Renderer {
surface: wgpu::Surface<'static>,
device: wgpu::Device,
queue: wgpu::Queue,
config: wgpu::SurfaceConfiguration,
adapter: wgpu::Adapter,
encoder: wgpu::CommandEncoder,
staging_belt: StagingBelt,
shape_pipeline: ShapePipeline,
recv: Receiver<Vec<RoundedRect>>,
pub struct UIRenderNode {
bind_group_layout: BindGroupLayout,
bind_group: BindGroup,
pipeline: RenderPipeline,
window_buffer: Buffer,
instance: ArrBuf<PrimitiveInstance>,
data: ArrBuf<u32>,
}
impl Renderer {
pub fn new(window: Arc<Window>) -> Self {
let size = window.inner_size();
impl UIRenderNode {
pub fn draw<'a>(&'a self, pass: &mut RenderPass<'a>) {
pass.set_pipeline(&self.pipeline);
pass.set_bind_group(0, &self.bind_group, &[]);
if self.instance.len() != 0 {
pass.set_vertex_buffer(0, self.instance.buffer.slice(..));
pass.draw(0..4, 0..self.instance.len() as u32);
}
}
let (send, recv) = channel();
pub fn update(&mut self, device: &Device, queue: &Queue, ui: &UI) {
let primitives = ui.to_primitives();
self.instance.update(device, queue, &primitives.instances);
self.data.update(device, queue, &primitives.data);
self.bind_group = Self::bind_group(
device,
&self.bind_group_layout,
&self.window_buffer,
&self.data.buffer,
)
}
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("data/test.rhai");
let w2 = window.clone();
std::thread::spawn(move || {
let (tx, rx) = channel();
pub fn resize(&mut self, size: &PhysicalSize<u32>, queue: &Queue) {
let slice = &[WindowUniform {
width: size.width as f32,
height: size.height as f32,
}];
queue.write_buffer(&self.window_buffer, 0, bytemuck::cast_slice(slice));
}
let mut watcher = RecommendedWatcher::new(tx, Default::default()).unwrap();
watcher
.watch("data".as_ref(), notify::RecursiveMode::Recursive)
.unwrap();
for res in rx {
let Ok(ev) = res else {
continue;
};
if !ev.paths.contains(&path) {
continue;
}
if !matches!(
ev.kind,
EventKind::Create(CreateKind::File) | EventKind::Modify(ModifyKind::Data(_))
) {
continue;
}
println!("reloaded {ev:?}");
let mut engine = rhai::Engine::new();
engine.build_type::<RoundedRect>();
engine.register_type::<UIPos>();
engine.register_fn("anchor_offset", UIPos::anchor_offset);
engine.register_fn("rect", RoundedRect::default);
let Ok(code) = std::fs::read_to_string(&path) else {
continue;
};
let rects = engine
.eval::<rhai::Array>(&code)
.map_err(|e| println!("{e:?}"))
.unwrap_or_default()
.into_iter()
.filter_map(|d| match rhai::Dynamic::try_cast(d.clone()) {
Some(v) => Some(v),
None => {
println!("{d:?} is not a RoundedRect");
None
}
})
.collect::<Vec<RoundedRect>>();
send.send(rects).unwrap();
w2.request_redraw();
}
pub fn new(device: &Device, config: &SurfaceConfiguration) -> Self {
let shader = device.create_shader_module(ShaderModuleDescriptor {
label: Some("UI Shape Shader"),
source: ShaderSource::Wgsl(SHAPE_SHADER.into()),
});
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
backends: wgpu::Backends::PRIMARY,
..Default::default()
let window_uniform = WindowUniform::default();
let window_buffer = device.create_buffer_init(&BufferInitDescriptor {
label: Some("Camera Buffer"),
contents: bytemuck::cast_slice(&[window_uniform]),
usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
});
let surface = instance
.create_surface(window.clone())
.expect("Could not create window surface!");
let instance = ArrBuf::new(
device,
BufferUsages::VERTEX | BufferUsages::COPY_DST,
"instance",
);
let data = ArrBuf::new(
device,
BufferUsages::STORAGE | BufferUsages::COPY_DST,
"data",
);
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::default(),
compatible_surface: Some(&surface),
force_fallback_adapter: false,
})
.block_on()
.expect("Could not get adapter!");
let bind_group_layout = device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &[
BindGroupLayoutEntry {
binding: 0,
visibility: ShaderStages::VERTEX,
ty: BindingType::Buffer {
ty: BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
BindGroupLayoutEntry {
binding: 1,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Buffer {
ty: BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
label: Some("camera_bind_group_layout"),
});
let (device, queue) = adapter
.request_device(&wgpu::DeviceDescriptor {
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::default(),
..Default::default()
})
.block_on()
.expect("Could not get device!");
let bind_group = Self::bind_group(device, &bind_group_layout, &window_buffer, &data.buffer);
let surface_caps = surface.get_capabilities(&adapter);
let surface_format = surface_caps
.formats
.iter()
.copied()
.find(|f| f.is_srgb())
.unwrap_or(surface_caps.formats[0]);
let config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format: surface_format,
width: size.width,
height: size.height,
present_mode: surface_caps.present_modes[0],
alpha_mode: surface_caps.alpha_modes[0],
desired_maximum_frame_latency: 2,
view_formats: vec![],
};
surface.configure(&device, &config);
let staging_belt = StagingBelt::new(4096 * 4);
let encoder = Self::create_encoder(&device);
let shape_pipeline = ShapePipeline::new(&device, &config);
let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
label: Some("UI Shape Pipeline Layout"),
bind_group_layouts: &[&bind_group_layout],
push_constant_ranges: &[],
});
let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor {
label: Some("UI Shape Pipeline"),
layout: Some(&pipeline_layout),
vertex: VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[PrimitiveInstance::desc()],
compilation_options: Default::default(),
},
fragment: Some(FragmentState {
module: &shader,
entry_point: Some("fs_main"),
targets: &[Some(ColorTargetState {
format: config.format,
blend: Some(BlendState::ALPHA_BLENDING),
write_mask: ColorWrites::ALL,
})],
compilation_options: Default::default(),
}),
primitive: PrimitiveState {
topology: PrimitiveTopology::TriangleStrip,
strip_index_format: None,
front_face: FrontFace::Cw,
cull_mode: Some(Face::Back),
polygon_mode: PolygonMode::Fill,
unclipped_depth: false,
conservative: false,
},
depth_stencil: None,
multisample: MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
multiview: None,
cache: None,
});
Self {
surface,
device,
queue,
config,
adapter,
encoder,
staging_belt,
shape_pipeline,
recv,
bind_group_layout,
bind_group,
pipeline,
window_buffer,
instance,
data,
}
}
fn create_encoder(device: &wgpu::Device) -> wgpu::CommandEncoder {
device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("Render Encoder"),
pub fn bind_group(
device: &Device,
layout: &BindGroupLayout,
window_buffer: &Buffer,
data: &Buffer,
) -> BindGroup {
device.create_bind_group(&BindGroupDescriptor {
layout,
entries: &[
BindGroupEntry {
binding: 0,
resource: window_buffer.as_entire_binding(),
},
BindGroupEntry {
binding: 1,
resource: data.as_entire_binding(),
},
],
label: Some("ui_bind_group"),
})
}
pub fn draw(&mut self) {
if let Some(rects) = self.recv.try_iter().last() {
self.shape_pipeline
.update(&self.device, &self.queue, &rects);
}
let output = self.surface.get_current_texture().unwrap();
let view = output
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder = std::mem::replace(&mut self.encoder, Self::create_encoder(&self.device));
{
let render_pass = &mut encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(CLEAR_COLOR),
store: wgpu::StoreOp::Store,
},
})],
..Default::default()
});
self.shape_pipeline.draw(render_pass);
}
self.queue.submit(std::iter::once(encoder.finish()));
self.staging_belt.finish();
output.present();
self.staging_belt.recall();
}
pub fn resize(&mut self, size: &PhysicalSize<u32>) {
self.config.width = size.width;
self.config.height = size.height;
self.surface.configure(&self.device, &self.config);
self.shape_pipeline.resize(size, &self.queue);
}
}

View File

@@ -1,73 +0,0 @@
use glam::Vec2;
use rhai::{CustomType, TypeBuilder};
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable, CustomType)]
pub struct RoundedRect {
pub top_left: UIPos,
pub bottom_right: UIPos,
pub colors: [[f32; 4]; 4],
pub radius: f32,
pub inner_radius: f32,
pub thickness: f32,
}
impl Default for RoundedRect {
fn default() -> Self {
Self {
top_left: Default::default(),
bottom_right: Default::default(),
colors: [
[1.0, 0.0, 0.0, 1.0],
[0.0, 1.0, 0.0, 1.0],
[0.0, 0.0, 1.0, 1.0],
[1.0, 1.0, 1.0, 1.0],
],
radius: Default::default(),
inner_radius: Default::default(),
thickness: Default::default(),
}
}
}
#[derive(PartialEq, Clone)]
pub struct Text {
pub content: String,
pub align: Align,
pub pos: Vec2,
pub bounds: (f32, f32),
}
impl Text {
pub fn empty() -> Self {
Self {
content: String::new(),
align: Align::Left,
pos: Vec2::default(),
bounds: (0.0, 0.0),
}
}
}
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable, Default)]
pub struct UIPos {
pub anchor: Vec2,
pub offset: Vec2,
}
impl UIPos {
pub fn anchor_offset(anchor_x: f32, anchor_y: f32, offset_x: f32, offset_y: f32) -> Self {
Self {
anchor: Vec2::new(anchor_x, anchor_y),
offset: Vec2::new(offset_x, offset_y),
}
}
}
#[derive(PartialEq, Clone)]
pub enum Align {
Left,
Right,
Center,
}

View File

@@ -0,0 +1,47 @@
#![allow(clippy::multiple_bound_locations)]
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Zeroable)]
pub struct Color<T: ColorNum> {
r: T,
g: T,
b: T,
a: T,
}
impl<T: ColorNum> Color<T> {
pub const BLACK: Self = Self::rgb(T::MIN, T::MIN, T::MIN);
pub const WHITE: Self = Self::rgb(T::MAX, T::MAX, T::MAX);
pub const RED: Self = Self::rgb(T::MAX, T::MIN, T::MIN);
pub const ORANGE: Self = Self::rgb(T::MAX, T::MID, T::MIN);
pub const YELLOW: Self = Self::rgb(T::MAX, T::MAX, T::MIN);
pub const LIME: Self = Self::rgb(T::MID, T::MAX, T::MIN);
pub const GREEN: Self = Self::rgb(T::MIN, T::MAX, T::MIN);
pub const CYAN: Self = Self::rgb(T::MIN, T::MAX, T::MAX);
pub const BLUE: Self = Self::rgb(T::MIN, T::MIN, T::MAX);
pub const MAGENTA: Self = Self::rgb(T::MAX, T::MIN, T::MAX);
}
impl<T: ColorNum> Color<T> {
pub const fn new(r: T, g: T, b: T, a: T) -> Self {
Self { r, g, b, a }
}
pub const fn rgb(r: T, g: T, b: T) -> Self {
Self { r, g, b, a: T::MAX }
}
}
pub trait ColorNum {
const MIN: Self;
const MID: Self;
const MAX: Self;
}
impl ColorNum for u8 {
const MIN: Self = u8::MIN;
const MID: Self = u8::MAX / 2;
const MAX: Self = u8::MAX;
}
unsafe impl bytemuck::Pod for Color<u8> {}

View File

@@ -0,0 +1,14 @@
use crate::primitive::{Color, PrimitiveData};
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct RoundedRectData {
pub color: Color<u8>,
pub radius: f32,
pub thickness: f32,
pub inner_radius: f32,
}
impl PrimitiveData for RoundedRectData {
const DISCRIM: u32 = 0;
}

View File

@@ -0,0 +1,97 @@
use crate::primitive::{point::point, Point};
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable, Default)]
pub struct UIPos {
pub anchor: Point,
pub offset: Point,
}
impl UIPos {
pub const fn anchor_offset(anchor_x: f32, anchor_y: f32, offset_x: f32, offset_y: f32) -> Self {
Self {
anchor: point(anchor_x, anchor_y),
offset: point(offset_x, offset_y),
}
}
pub const fn top_left() -> Self {
Self::anchor_offset(0.0, 0.0, 0.0, 0.0)
}
pub const fn bottom_right() -> Self {
Self::anchor_offset(1.0, 1.0, 0.0, 0.0)
}
pub const fn within(&self, region: &UIRegion) -> UIPos {
let range = region.bot_right.anchor - region.top_left.anchor;
let region_offset = region
.top_left
.offset
.lerp(region.bot_right.offset, self.anchor);
UIPos {
anchor: region.top_left.anchor + self.anchor * range,
offset: self.offset + region_offset,
}
}
pub fn axis_mut(&mut self, axis: Axis) -> UIPosAxisView<'_> {
match axis {
Axis::X => UIPosAxisView {
anchor: &mut self.anchor.x,
offset: &mut self.offset.x,
},
Axis::Y => UIPosAxisView {
anchor: &mut self.anchor.y,
offset: &mut self.offset.y,
},
}
}
}
pub struct UIPosAxisView<'a> {
pub anchor: &'a mut f32,
pub offset: &'a mut f32,
}
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct UIRegion {
pub top_left: UIPos,
pub bot_right: UIPos,
}
impl UIRegion {
pub const fn full() -> Self {
Self {
top_left: UIPos::top_left(),
bot_right: UIPos::bottom_right(),
}
}
pub fn within(&self, parent: &Self) -> Self {
Self {
top_left: self.top_left.within(parent),
bot_right: self.bot_right.within(parent),
}
}
pub fn select(&mut self, inner: &Self) {
*self = inner.within(self);
}
pub fn axis_mut(&mut self, axis: Axis) -> UIRegionAxisView<'_> {
UIRegionAxisView {
top_left: self.top_left.axis_mut(axis),
bot_right: self.bot_right.axis_mut(axis),
}
}
}
pub struct UIRegionAxisView<'a> {
pub top_left: UIPosAxisView<'a>,
pub bot_right: UIPosAxisView<'a>,
}
#[derive(Copy, Clone)]
pub enum Axis {
X,
Y,
}

View File

@@ -0,0 +1,50 @@
mod color;
mod def;
mod format;
mod point;
pub use color::*;
pub use def::*;
pub use format::*;
pub use point::*;
use crate::{render::data::PrimitiveInstance, UINode};
use bytemuck::Pod;
pub struct Painter {
pub region: UIRegion,
pub instances: Vec<PrimitiveInstance>,
pub data: Vec<u32>,
}
impl Default for Painter {
fn default() -> Self {
Self {
region: UIRegion::full(),
instances: Default::default(),
data: Default::default(),
}
}
}
/// NOTE: Self must have at least u32 alignment
pub trait PrimitiveData: Pod {
const DISCRIM: u32;
}
impl Painter {
pub fn write<D: PrimitiveData>(&mut self, data: D) {
let ptr = self.data.len() as u32;
let region = self.region;
self.instances.push(PrimitiveInstance { region, ptr });
self.data.push(D::DISCRIM);
self.data
.extend_from_slice(bytemuck::cast_slice::<_, u32>(&[data]));
}
pub fn draw_within(&mut self, node: &impl UINode, region: UIRegion) {
let old = self.region;
self.region.select(&region);
node.draw(self);
self.region = old;
}
}

View File

@@ -0,0 +1,84 @@
use std::ops::*;
#[repr(C)]
#[derive(Clone, Copy, PartialEq, Default, bytemuck::Pod, bytemuck::Zeroable)]
pub struct Point {
pub x: f32,
pub y: f32,
}
pub const fn point(x: f32, y: f32) -> Point {
Point::new(x, y)
}
impl Point {
pub const fn new(x: f32, y: f32) -> Self {
Self { x, y }
}
pub const fn lerp(self, to: Self, amt: impl const Into<Self>) -> Self {
let amt = amt.into();
Self {
x: lerp(self.x, to.x, amt.x),
y: lerp(self.y, to.y, amt.y),
}
}
}
const fn lerp(x: f32, y: f32, amt: f32) -> f32 {
(1.0 - amt) * x + y * amt
}
impl const From<f32> for Point {
fn from(v: f32) -> Self {
Self { x: v, y: v }
}
}
macro_rules! impl_op_inner {
($op:ident $fn:ident $opa:ident $fna:ident) => {
impl const $op for Point {
type Output = Self;
fn $fn(self, rhs: Self) -> Self::Output {
Self {
x: self.x.$fn(rhs.x),
y: self.y.$fn(rhs.y),
}
}
}
impl $opa for Point {
fn $fna(&mut self, rhs: Self) {
self.x.$fna(rhs.x);
self.y.$fna(rhs.y);
}
}
impl const $op<f32> for Point {
type Output = Self;
fn $fn(self, rhs: f32) -> Self::Output {
Self {
x: self.x.$fn(rhs),
y: self.y.$fn(rhs),
}
}
}
impl $opa<f32> for Point {
fn $fna(&mut self, rhs: f32) {
self.x.$fna(rhs);
self.y.$fna(rhs);
}
}
};
}
macro_rules! impl_op {
($op:ident $fn:ident) => {
impl_op_inner!($op $fn ${concat($op,Assign)} ${concat($fn,_assign)});
};
}
impl_op!(Add add);
impl_op!(Sub sub);
impl_op!(Mul mul);
impl_op!(Div div);

110
src/render/shader.wgsl Normal file
View File

@@ -0,0 +1,110 @@
@group(0) @binding(0)
var<uniform> window: WindowUniform;
@group(0) @binding(1)
var<storage> data: array<u32>;
struct WindowUniform {
dim: vec2<f32>,
};
struct InstanceInput {
@location(0) top_left_anchor: vec2<f32>,
@location(1) top_left_offset: vec2<f32>,
@location(2) bottom_right_anchor: vec2<f32>,
@location(3) bottom_right_offset: vec2<f32>,
@location(4) pointer: u32,
}
struct RoundedRect {
color: u32,
radius: f32,
thickness: f32,
inner_radius: f32,
}
struct VertexOutput {
@location(0) pointer: u32,
@location(1) top_left: vec2<f32>,
@location(2) bot_right: vec2<f32>,
@builtin(position) clip_position: vec4<f32>,
};
struct Region {
pos: vec2<f32>,
top_left: vec2<f32>,
bot_right: vec2<f32>,
}
@vertex
fn vs_main(
@builtin(vertex_index) vi: u32,
in: InstanceInput,
) -> VertexOutput {
var out: VertexOutput;
let top_left = in.top_left_anchor * window.dim + in.top_left_offset;
let bot_right = in.bottom_right_anchor * window.dim + in.bottom_right_offset;
let size = bot_right - top_left;
var pos = top_left + vec2<f32>(
f32(vi % 2u),
f32(vi / 2u)
) * size;
pos = pos / window.dim * 2.0 - 1.0;
out.clip_position = vec4<f32>(pos.x, -pos.y, 0.0, 1.0);
out.pointer = in.pointer;
out.top_left = top_left;
out.bot_right = bot_right;
return out;
}
@fragment
fn fs_main(
in: VertexOutput
) -> @location(0) vec4<f32> {
let pos = in.clip_position.xy;
let ty = data[in.pointer];
let dp = in.pointer + 1u;
let region = Region(pos, in.top_left, in.bot_right);
switch ty {
case 0u: {
return draw_rounded_rect(region, RoundedRect(
data[dp + 0u],
bitcast<f32>(data[dp + 1u]),
bitcast<f32>(data[dp + 2u]),
bitcast<f32>(data[dp + 3u]),
));
}
default: {}
}
return vec4(1.0, 0.0, 1.0, 1.0);
}
fn draw_rounded_rect(region: Region, rect: RoundedRect) -> vec4<f32> {
var color = unpack4x8unorm(rect.color);
let edge = 0.5;
let size = region.bot_right - region.top_left;
let corner = size / 2.0;
let center = region.top_left + corner;
let dist = distance_from_rect(region.pos, center, corner, rect.radius);
color.a *= 1.0 - smoothstep(-min(edge, rect.radius), edge, dist);
if rect.thickness > 0.0 {
let dist2 = distance_from_rect(region.pos, center, corner - rect.thickness, rect.inner_radius);
color.a *= smoothstep(-min(edge, rect.inner_radius), edge, dist2);
}
return color;
}
fn distance_from_rect(pixel_pos: vec2<f32>, rect_center: vec2<f32>, rect_corner: vec2<f32>, radius: f32) -> f32 {
// vec from center to pixel
let p = pixel_pos - rect_center;
// vec from inner rect corner to pixel
let q = abs(p) - (rect_corner - radius);
return length(max(q, vec2<f32>(0.0, 0.0))) - radius;
}

View File

@@ -1,74 +0,0 @@
use wgpu::{RenderPass, VertexAttribute};
use crate::render::primitive::RoundedRect;
pub struct RoundedRectBuffer {
buffer: wgpu::Buffer,
len: usize,
}
#[repr(C)]
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable, Default)]
pub struct WindowUniform {
pub width: f32,
pub height: f32,
}
impl RoundedRectBuffer {
pub fn new(device: &wgpu::Device) -> Self {
Self {
buffer: Self::init_buf(device, 0),
len: 0,
}
}
pub fn update(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
rects: &[RoundedRect],
) {
if self.len != rects.len() {
self.len = rects.len();
self.buffer = Self::init_buf(device, std::mem::size_of_val(rects));
}
queue.write_buffer(&self.buffer, 0, bytemuck::cast_slice(rects));
}
fn init_buf(device: &wgpu::Device, size: usize) -> wgpu::Buffer {
device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Instance Buffer"),
size: size as u64,
mapped_at_creation: false,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
})
}
pub fn len(&self) -> usize {
self.len
}
pub fn set_in<'a>(&'a self, pass: &mut RenderPass<'a>) {
pass.set_vertex_buffer(0, self.buffer.slice(..));
}
}
impl RoundedRect {
const ATTRIBS: [VertexAttribute; 11] = wgpu::vertex_attr_array![
0 => Float32x2,
1 => Float32x2,
2 => Float32x2,
3 => Float32x2,
4 => Float32x4,
5 => Float32x4,
6 => Float32x4,
7 => Float32x4,
8 => Float32,
9 => Float32,
10 => Float32,
];
pub fn desc() -> wgpu::VertexBufferLayout<'static> {
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<RoundedRect>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &Self::ATTRIBS,
}
}
}

View File

@@ -1,106 +0,0 @@
use wgpu::{
util::{BufferInitDescriptor, DeviceExt},
BufferUsages,
};
use crate::render::primitive::RoundedRect;
use super::{
data::{RoundedRectBuffer, WindowUniform},
ShapeBuffers, ShapePipeline, SHAPE_SHADER,
};
impl ShapePipeline {
pub fn new(device: &wgpu::Device, config: &wgpu::SurfaceConfiguration) -> Self {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("UI Shape Shader"),
source: wgpu::ShaderSource::Wgsl(SHAPE_SHADER.into()),
});
let window_uniform = WindowUniform::default();
let window_buffer = device.create_buffer_init(&BufferInitDescriptor {
label: Some("Camera Buffer"),
contents: bytemuck::cast_slice(&[window_uniform]),
usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
});
let instance_buffer = RoundedRectBuffer::new(device);
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}],
label: Some("camera_bind_group_layout"),
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
layout: &bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: window_buffer.as_entire_binding(),
}],
label: Some("camera_bind_group"),
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("UI Shape Pipeline Layout"),
bind_group_layouts: &[&bind_group_layout],
push_constant_ranges: &[],
});
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("UI Shape Pipeline"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[RoundedRect::desc()],
compilation_options: Default::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format: config.format,
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: Default::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleStrip,
strip_index_format: None,
front_face: wgpu::FrontFace::Cw,
cull_mode: Some(wgpu::Face::Back),
polygon_mode: wgpu::PolygonMode::Fill,
unclipped_depth: false,
conservative: false,
},
depth_stencil: None,
multisample: wgpu::MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
multiview: None,
cache: None,
});
let buffers = ShapeBuffers {
window: window_buffer,
instance: instance_buffer,
};
Self {
bind_group,
pipeline,
buffers,
}
}
}

View File

@@ -1,44 +0,0 @@
use crate::render::primitive::RoundedRect;
use data::{RoundedRectBuffer, WindowUniform};
use wgpu::{BindGroup, Buffer, RenderPass, RenderPipeline};
use winit::dpi::PhysicalSize;
mod data;
mod layout;
pub const SHAPE_SHADER: &str = include_str!("./shader.wgsl");
pub struct ShapeBuffers {
pub window: Buffer,
pub instance: RoundedRectBuffer,
}
pub struct ShapePipeline {
pub bind_group: BindGroup,
pub pipeline: RenderPipeline,
pub buffers: ShapeBuffers,
}
impl ShapePipeline {
pub fn draw<'a>(&'a self, pass: &mut RenderPass<'a>) {
pass.set_pipeline(&self.pipeline);
pass.set_bind_group(0, &self.bind_group, &[]);
if self.buffers.instance.len() != 0 {
self.buffers.instance.set_in(pass);
pass.draw(0..4, 0..self.buffers.instance.len() as u32);
}
}
pub fn update(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, rects: &[RoundedRect]) {
self.buffers.instance.update(device, queue, rects);
}
pub fn resize(&mut self, size: &PhysicalSize<u32>, queue: &wgpu::Queue) {
let slice = &[WindowUniform {
width: size.width as f32,
height: size.height as f32,
}];
queue.write_buffer(&self.buffers.window, 0, bytemuck::cast_slice(slice));
}
}

View File

@@ -1,97 +0,0 @@
// vertex shader
struct VertexOutput {
@location(0) color: vec4<f32>,
@location(1) center: vec2<f32>,
@location(2) corner: vec2<f32>,
@location(3) radius: f32,
@location(4) inner_radius: f32,
@location(5) thickness: f32,
@builtin(position) clip_position: vec4<f32>,
};
struct WindowUniform {
dim: vec2<f32>,
};
struct InstanceInput {
@location(0) top_left_anchor: vec2<f32>,
@location(1) top_left_offset: vec2<f32>,
@location(2) bottom_right_anchor: vec2<f32>,
@location(3) bottom_right_offset: vec2<f32>,
@location(4) top_right_color: vec4<f32>,
@location(5) top_left_color: vec4<f32>,
@location(6) bottom_right_color: vec4<f32>,
@location(7) bottom_left_color: vec4<f32>,
@location(8) radius: f32,
@location(9) inner_radius: f32,
@location(10) thickness: f32,
}
@group(0) @binding(0)
var<uniform> window: WindowUniform;
@vertex
fn vs_main(
@builtin(vertex_index) vi: u32,
in: InstanceInput,
) -> VertexOutput {
var out: VertexOutput;
let top_left = in.top_left_anchor * window.dim + in.top_left_offset;
let bottom_right = in.bottom_right_anchor * window.dim + in.bottom_right_offset;
let size = bottom_right - top_left;
var pos = top_left + vec2<f32>(
f32(vi % 2u),
f32(vi / 2u)
) * size;
pos = pos / window.dim * 2.0 - 1.0;
out.clip_position = vec4<f32>(pos.x, -pos.y, 0.0, 1.0);
if vi == 0u {
out.color = in.top_left_color;
} else if vi == 1u {
out.color = in.top_right_color;
} else if vi == 2u {
out.color = in.bottom_left_color;
} else if vi == 3u {
out.color = in.bottom_right_color;
}
out.corner = size / 2.0;
out.center = top_left + out.corner;
out.radius = in.radius;
out.inner_radius = in.inner_radius;
out.thickness = in.thickness;
return out;
}
@fragment
fn fs_main(
in: VertexOutput
) -> @location(0) vec4<f32> {
var color = in.color;
let edge = 0.5;
let dist = distance_from_rect(in.clip_position.xy, in.center, in.corner, in.radius);
color.a *= 1.0 - smoothstep(-min(edge, in.radius), edge, dist);
if in.thickness > 0.0 {
let dist2 = distance_from_rect(in.clip_position.xy, in.center, in.corner - in.thickness, in.inner_radius);
color.a *= smoothstep(-min(edge, in.inner_radius), edge, dist2);
}
return color;
}
fn distance_from_rect(pixel_pos: vec2<f32>, rect_center: vec2<f32>, rect_corner: vec2<f32>, radius: f32) -> f32 {
// vec from center to pixel
let p = pixel_pos - rect_center;
// vec from inner rect corner to pixel
let q = abs(p) - (rect_corner - radius);
return length(max(q, vec2<f32>(0.0, 0.0))) - radius;
}

View File

@@ -1 +0,0 @@
pub mod pipeline;

View File

@@ -1,127 +0,0 @@
use glyphon::{
Attrs, Buffer, Color, Family, FontSystem, Metrics, Resolution, Shaping, SwashCache, TextArea,
TextAtlas, TextBounds, TextRenderer,
};
use wgpu::{MultisampleState, RenderPass};
use crate::{
client::ui::element::Align,
render::{primitive::TextPrimitive, surface::RenderSurface},
};
pub struct TextPipeline {
pub renderer: glyphon::TextRenderer,
pub font_system: glyphon::FontSystem,
pub atlas: glyphon::TextAtlas,
pub cache: glyphon::SwashCache,
pub text_buffers: Vec<glyphon::Buffer>,
pub old_text: Vec<TextPrimitive>,
}
impl TextPipeline {
pub fn new(surface: &RenderSurface) -> Self {
let RenderSurface {
device,
config,
queue,
..
} = surface;
let font_system = FontSystem::new();
let cache = SwashCache::new();
let mut atlas = TextAtlas::new(&device, &queue, config.format);
let renderer = TextRenderer::new(&mut atlas, &device, MultisampleState::default(), None);
Self {
font_system,
atlas,
cache,
renderer,
text_buffers: Vec::new(),
old_text: Vec::new(),
}
}
pub fn draw<'a>(&'a self, pass: &mut RenderPass<'a>) {
self.renderer.render(&self.atlas, pass).unwrap();
}
pub fn update(&mut self, surface: &RenderSurface, text: &[TextPrimitive]) {
let buffers = &mut self.text_buffers;
if buffers.len() < text.len() {
self.old_text.resize(text.len(), TextPrimitive::empty());
buffers.resize_with(text.len(), || {
Buffer::new(&mut self.font_system, Metrics::new(20.0, 25.0))
})
}
for ((buffer, text), old) in buffers.iter_mut().zip(text).zip(&mut self.old_text) {
if text != old {
*old = text.clone();
buffer.set_size(&mut self.font_system, f32::MAX, f32::MAX);
buffer.set_text(
&mut self.font_system,
&text.content,
Attrs::new().family(Family::SansSerif),
Shaping::Basic,
);
}
}
let color = Color::rgb(255, 255, 255);
let areas = buffers.iter().zip(text).map(|(buffer, text)| {
let width = measure(&buffer).0;
let mut left = text.pos.x
- match text.align {
Align::Left => 0.0,
Align::Center => width / 2.0,
Align::Right => width,
};
let x = text.pos.x;
let w = text.bounds.0;
let x_bounds = match text.align {
Align::Left => (x, x + w),
Align::Center => (x - w / 2.0, x + w / 2.0),
Align::Right => (x - w, x),
};
if left < x_bounds.0 {
left = x_bounds.0;
}
TextArea {
buffer: &buffer,
left,
top: text.pos.y,
scale: 1.0,
bounds: TextBounds {
left: x_bounds.0 as i32,
top: text.pos.y as i32,
right: x_bounds.1 as i32,
bottom: (text.pos.y + text.bounds.1) as i32,
},
default_color: color,
}
});
self.renderer
.prepare(
&surface.device,
&surface.queue,
&mut self.font_system,
&mut self.atlas,
Resolution {
width: surface.config.width,
height: surface.config.height,
},
areas,
&mut self.cache,
)
.unwrap();
}
}
fn measure(buffer: &glyphon::Buffer) -> (f32, f32) {
let (width, total_lines) = buffer
.layout_runs()
.fold((0.0, 0usize), |(width, total_lines), run| {
(run.line_w.max(width), total_lines + 1)
});
(width, total_lines as f32 * buffer.metrics().line_height)
}

View File

@@ -1,129 +0,0 @@
use wgpu::{
util::DeviceExt,
BindGroup, BindGroupLayout, Device, Queue,
};
use crate::render::surface::RenderSurface;
use super::{
pipeline::{TexturePipeline, TEXTURE_SHADER},
vertex::{TextureVertex, TEXTURE_VERTICES}, texture::GameTexture,
};
impl TexturePipeline {
pub fn new(surface: &RenderSurface) -> Self {
let RenderSurface {
device,
config,
queue,
..
} = surface;
let (bind_group_layout, diffuse_bind_group) = Self::init_textures(device, queue);
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("UI Texture Shader"),
source: wgpu::ShaderSource::Wgsl(TEXTURE_SHADER.into()),
});
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Texture Vertex Buffer"),
contents: bytemuck::cast_slice(TEXTURE_VERTICES),
usage: wgpu::BufferUsages::VERTEX,
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("UI Texture Pipeline Layout"),
bind_group_layouts: &[&bind_group_layout],
push_constant_ranges: &[],
});
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("UI Texture Pipeline"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: "vs_main",
buffers: &[TextureVertex::desc()],
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: "fs_main",
targets: &[Some(wgpu::ColorTargetState {
format: config.format,
blend: Some(wgpu::BlendState::REPLACE),
write_mask: wgpu::ColorWrites::ALL,
})],
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleStrip,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: Some(wgpu::Face::Back),
polygon_mode: wgpu::PolygonMode::Fill,
unclipped_depth: false,
conservative: false,
},
depth_stencil: None,
multisample: wgpu::MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
multiview: None,
});
Self {
pipeline,
vertex_buffer,
diffuse_bind_group,
}
}
fn init_textures(device: &Device, queue: &Queue) -> (BindGroupLayout, BindGroup) {
let diffuse_bytes = include_bytes!("./textures/happy-tree.png");
let diffuse_texture =
GameTexture::from_bytes(&device, &queue, diffuse_bytes, "happy-tree.png").unwrap();
let texture_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
multisampled: false,
view_dimension: wgpu::TextureViewDimension::D2,
sample_type: wgpu::TextureSampleType::Float { filterable: true },
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
// This should match the filterable field of the
// corresponding Texture entry above.
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
],
label: Some("texture_bind_group_layout"),
});
let diffuse_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
layout: &texture_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&diffuse_texture.view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&diffuse_texture.sampler),
},
],
label: Some("diffuse_bind_group"),
});
(texture_bind_group_layout, diffuse_bind_group)
}
}

View File

@@ -1,22 +0,0 @@
mod init;
mod vertex;
mod texture;
use wgpu::{RenderPass, RenderPipeline};
pub const TEXTURE_SHADER: &str = include_str!("./shader.wgsl");
pub struct TexturePipeline {
pub pipeline: RenderPipeline,
pub vertex_buffer: wgpu::Buffer,
pub diffuse_bind_group: wgpu::BindGroup,
}
impl TexturePipeline {
pub fn draw<'a>(&'a self, pass: &mut RenderPass<'a>) {
pass.set_pipeline(&self.pipeline);
pass.set_bind_group(0, &self.diffuse_bind_group, &[]);
pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
pass.draw(0..4, 0..1);
}
}

View File

@@ -1,34 +0,0 @@
// Vertex shader
struct VertexInput {
@location(0) position: vec2<f32>,
@location(1) tex_coords: vec2<f32>,
}
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) tex_coords: vec2<f32>,
}
@vertex
fn vs_main(
model: VertexInput,
) -> VertexOutput {
var out: VertexOutput;
out.tex_coords = model.tex_coords;
out.clip_position = vec4<f32>(model.position, 0.0, 1.0);
return out;
}
// Fragment shader
@group(0) @binding(0)
var t_diffuse: texture_2d<f32>;
@group(0)@binding(1)
var s_diffuse: sampler;
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
return textureSample(t_diffuse, s_diffuse, in.tex_coords);
}

View File

@@ -1,78 +0,0 @@
use image::{GenericImageView, ImageResult};
pub struct GameTexture {
pub texture: wgpu::Texture,
pub view: wgpu::TextureView,
pub sampler: wgpu::Sampler,
}
impl GameTexture {
pub fn from_bytes(
device: &wgpu::Device,
queue: &wgpu::Queue,
bytes: &[u8],
label: &str
) -> ImageResult<Self> {
let img = image::load_from_memory(bytes)?;
Self::from_image(device, queue, &img, Some(label))
}
pub fn from_image(
device: &wgpu::Device,
queue: &wgpu::Queue,
img: &image::DynamicImage,
label: Option<&str>
) -> ImageResult<Self> {
let rgba = img.to_rgba8();
let dimensions = img.dimensions();
let size = wgpu::Extent3d {
width: dimensions.0,
height: dimensions.1,
depth_or_array_layers: 1,
};
let texture = device.create_texture(
&wgpu::TextureDescriptor {
label,
size,
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8UnormSrgb,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
}
);
queue.write_texture(
wgpu::ImageCopyTexture {
aspect: wgpu::TextureAspect::All,
texture: &texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
},
&rgba,
wgpu::ImageDataLayout {
offset: 0,
bytes_per_row: Some(4 * dimensions.0),
rows_per_image: Some(dimensions.1),
},
size,
);
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
let sampler = device.create_sampler(
&wgpu::SamplerDescriptor {
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Nearest,
mipmap_filter: wgpu::FilterMode::Nearest,
..Default::default()
}
);
Ok(Self { texture, view, sampler })
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -1,35 +0,0 @@
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct TextureVertex {
position: [f32; 2],
tex_coords: [f32; 2],
}
impl TextureVertex {
const ATTRIBS: [wgpu::VertexAttribute; 2] =
wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2];
pub fn desc() -> wgpu::VertexBufferLayout<'static> {
use std::mem;
wgpu::VertexBufferLayout {
array_stride: mem::size_of::<TextureVertex>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &Self::ATTRIBS,
}
}
}
pub const TEXTURE_VERTICES: &[TextureVertex] = &[
TextureVertex { position: [0.0, 0.0], tex_coords: [0.0, 1.0], },
TextureVertex { position: [0.5, 0.0], tex_coords: [1.0, 1.0], },
TextureVertex { position: [0.0, 0.5], tex_coords: [0.0, 0.0], },
TextureVertex { position: [0.5, 0.5], tex_coords: [1.0, 0.0], },
];
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct ShapeVertex {
position: [f32; 2],
tex_coords: [f32; 2],
}

48
src/render/util/mod.rs Normal file
View File

@@ -0,0 +1,48 @@
use std::marker::PhantomData;
use bytemuck::Pod;
use wgpu::*;
pub struct ArrBuf<T: Pod> {
label: &'static str,
usage: BufferUsages,
pub buffer: Buffer,
len: usize,
_pd: PhantomData<T>,
}
impl<T: Pod> ArrBuf<T> {
pub fn new(device: &Device, usage: BufferUsages, label: &'static str) -> Self {
Self {
label,
usage,
buffer: Self::init_buf(device, 0, usage, label),
len: 0,
_pd: PhantomData,
}
}
pub fn update(&mut self, device: &Device, queue: &Queue, data: &[T]) {
if self.len != data.len() {
self.len = data.len();
self.buffer =
Self::init_buf(device, std::mem::size_of_val(data), self.usage, self.label);
}
queue.write_buffer(&self.buffer, 0, bytemuck::cast_slice(data));
}
fn init_buf(device: &Device, size: usize, usage: BufferUsages, label: &'static str) -> Buffer {
let mut size = size as u64;
if usage.contains(BufferUsages::STORAGE) {
size = size.max(1);
}
device.create_buffer(&BufferDescriptor {
label: Some(label),
size,
mapped_at_creation: false,
usage,
})
}
#[allow(clippy::len_without_is_empty)]
pub fn len(&self) -> usize {
self.len
}
}

View File

@@ -30,7 +30,7 @@ impl ApplicationHandler for App {
}
}
fn window_event(&mut self, event_loop: &ActiveEventLoop, id: WindowId, event: WindowEvent) {
fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
self.client.as_mut().unwrap().event(event, event_loop);
}
}

58
src/testing/mod.rs Normal file
View File

@@ -0,0 +1,58 @@
use std::sync::Arc;
use app::App;
use gui::{primitive::Axis, NodeArrayUtil, NodeUtil, RoundedRect, UIColor};
use render::Renderer;
use winit::{event::WindowEvent, event_loop::ActiveEventLoop, window::Window};
mod app;
mod render;
pub fn main() {
App::run();
}
pub struct Client {
renderer: Renderer,
}
impl Client {
pub fn new(window: Arc<Window>) -> Self {
let mut renderer = Renderer::new(window);
let rect = RoundedRect {
color: UIColor::WHITE,
radius: 10.0,
thickness: 0.0,
inner_radius: 0.0,
};
let ui = (
(
rect.color(UIColor::BLUE),
(
rect.color(UIColor::RED),
(rect.color(UIColor::ORANGE), rect.color(UIColor::LIME))
.proportioned(Axis::Y, [1, 1]),
rect.color(UIColor::YELLOW),
)
.proportioned(Axis::X, [2, 2, 1])
.pad(10),
)
.proportioned(Axis::X, [1, 3]),
rect.color(UIColor::GREEN),
)
.proportioned(Axis::Y, [3, 1])
.pad(10)
.into();
renderer.update(&ui);
Self { renderer }
}
pub fn event(&mut self, event: WindowEvent, event_loop: &ActiveEventLoop) {
match event {
WindowEvent::CloseRequested => event_loop.exit(),
WindowEvent::RedrawRequested => self.renderer.draw(),
WindowEvent::Resized(size) => self.renderer.resize(&size),
_ => (),
}
}
}

132
src/testing/render/mod.rs Normal file
View File

@@ -0,0 +1,132 @@
use gui::{UIRenderNode, UI};
use pollster::FutureExt;
use std::sync::Arc;
use wgpu::util::StagingBelt;
use winit::{dpi::PhysicalSize, window::Window};
pub const CLEAR_COLOR: wgpu::Color = wgpu::Color::BLACK;
pub struct Renderer {
surface: wgpu::Surface<'static>,
device: wgpu::Device,
queue: wgpu::Queue,
config: wgpu::SurfaceConfiguration,
encoder: wgpu::CommandEncoder,
staging_belt: StagingBelt,
ui_node: UIRenderNode,
}
impl Renderer {
pub fn update(&mut self, ui: &UI) {
self.ui_node.update(&self.device, &self.queue, ui);
}
pub fn draw(&mut self) {
let output = self.surface.get_current_texture().unwrap();
let view = output
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder = std::mem::replace(&mut self.encoder, Self::create_encoder(&self.device));
{
let render_pass = &mut encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(CLEAR_COLOR),
store: wgpu::StoreOp::Store,
},
depth_slice: None,
})],
..Default::default()
});
self.ui_node.draw(render_pass);
}
self.queue.submit(std::iter::once(encoder.finish()));
self.staging_belt.finish();
output.present();
self.staging_belt.recall();
}
pub fn resize(&mut self, size: &PhysicalSize<u32>) {
self.config.width = size.width;
self.config.height = size.height;
self.surface.configure(&self.device, &self.config);
self.ui_node.resize(size, &self.queue);
}
fn create_encoder(device: &wgpu::Device) -> wgpu::CommandEncoder {
device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("Render Encoder"),
})
}
pub fn new(window: Arc<Window>) -> Self {
let size = window.inner_size();
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
backends: wgpu::Backends::PRIMARY,
..Default::default()
});
let surface = instance
.create_surface(window.clone())
.expect("Could not create window surface!");
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::default(),
compatible_surface: Some(&surface),
force_fallback_adapter: false,
})
.block_on()
.expect("Could not get adapter!");
let (device, queue) = adapter
.request_device(&wgpu::DeviceDescriptor {
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::default(),
..Default::default()
})
.block_on()
.expect("Could not get device!");
let surface_caps = surface.get_capabilities(&adapter);
let surface_format = surface_caps
.formats
.iter()
.copied()
.find(|f| f.is_srgb())
.unwrap_or(surface_caps.formats[0]);
let config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format: surface_format,
width: size.width,
height: size.height,
present_mode: wgpu::PresentMode::AutoVsync,
alpha_mode: surface_caps.alpha_modes[0],
desired_maximum_frame_latency: 2,
view_formats: vec![],
};
surface.configure(&device, &config);
let staging_belt = StagingBelt::new(4096 * 4);
let encoder = Self::create_encoder(&device);
let shape_pipeline = UIRenderNode::new(&device, &config);
Self {
surface,
device,
queue,
config,
encoder,
staging_belt,
ui_node: shape_pipeline,
}
}
}