From ed5f7e24a72217012e3548ce4c1270f2af8abe98 Mon Sep 17 00:00:00 2001 From: hodasemi Date: Tue, 17 Jan 2023 07:57:56 +0100 Subject: [PATCH 01/16] Create gui handler --- Cargo.toml | 2 + src/overlay/elements/mod.rs | 0 src/overlay/elements/pedals.rs | 0 src/overlay/elements/radar.rs | 0 src/overlay/mod.rs | 93 ++++++++++++++++++++++++++++++++-- src/overlay/rendering.rs | 6 +++ src/overlay/rfactor_data.rs | 8 +-- 7 files changed, 100 insertions(+), 9 deletions(-) create mode 100644 src/overlay/elements/mod.rs create mode 100644 src/overlay/elements/pedals.rs create mode 100644 src/overlay/elements/radar.rs diff --git a/Cargo.toml b/Cargo.toml index 9e877bf..0891f89 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,9 @@ crate-type = ["cdylib"] [dependencies] vulkan-rs = { git = "https://gavania.de/hodasemi/vulkan_lib.git" } +assetpath = { git = "https://gavania.de/hodasemi/vulkan_lib.git" } rfactor_sm_reader = { git = "https://gavania.de/hodasemi/rfactor_sm_reader.git" } +ui = { git = "https://gavania.de/hodasemi/ui.git" } anyhow = { version = "1.0.68", features = ["backtrace"] } cgmath = { version = "0.18.0", features = ["swizzle", "serde"] } diff --git a/src/overlay/elements/mod.rs b/src/overlay/elements/mod.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/overlay/elements/pedals.rs b/src/overlay/elements/pedals.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/overlay/elements/radar.rs b/src/overlay/elements/radar.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/overlay/mod.rs b/src/overlay/mod.rs index ab05a89..48460ba 100644 --- a/src/overlay/mod.rs +++ b/src/overlay/mod.rs @@ -2,7 +2,7 @@ use crate::write_log; use self::{ rendering::Rendering, - rfactor_data::{DataConfig, RFactorData}, + rfactor_data::{RFactorData, RadarConfig}, }; mod pipeline; @@ -10,20 +10,24 @@ mod rendering; mod rfactor_data; use anyhow::Result; +use assetpath::AssetPath; use std::sync::{Arc, Mutex}; +use ui::prelude::*; use vulkan_rs::prelude::*; use serde::{Deserialize, Serialize}; #[derive(Deserialize, Serialize)] pub struct OverlayConfig { - pub data_config: DataConfig, + pub radar_config: RadarConfig, + pub font_path: String, } impl OverlayConfig { pub const fn new() -> Self { Self { - data_config: DataConfig::new(), + radar_config: RadarConfig::new(), + font_path: String::new(), } } } @@ -35,6 +39,7 @@ pub struct Overlay { device: Option>, queue: Option>>, rendering: Option, + gui_handler: Option>, rfactor_data: Option, } @@ -48,6 +53,7 @@ impl Overlay { device: None, queue: None, rendering: None, + gui_handler: None, rfactor_data: None, } @@ -98,9 +104,29 @@ impl Overlay { write_log!("-> create rendering: old cleared"); - self.rendering = Some(Rendering::new(self.device(), self.queue(), swapchain)?); + let rendering = Rendering::new(self.device(), self.queue(), swapchain.clone())?; + + // only font is used + let mut create_info = GuiHandlerCreateInfo::default(); + create_info.font_path = AssetPath::from(self.config.font_path.clone()); + create_info.font_path.assume_prefix_free(); + + let ctx = Arc::new(ContextImpl::new( + self.device(), + self.queue(), + swapchain, + rendering.images().clone(), + )); + + self.gui_handler = Some(GuiHandler::new( + create_info, + &(ctx as Arc), + )?); + + self.rendering = Some(rendering); write_log!("-> create rendering: new created"); + write_log!("-> create rendering: end"); Ok(()) @@ -111,7 +137,7 @@ impl Overlay { if self.rfactor_data.is_none() { self.rfactor_data = RFactorData::new( - self.config.data_config, + self.config.radar_config, self.device(), self.rendering .as_mut() @@ -139,3 +165,60 @@ impl Overlay { self.rendering.as_mut().unwrap().render(swapchain, &objects) } } + +struct ContextImpl { + device: Arc, + queue: Arc>, + swapchain: Arc, + images: Vec>, +} + +impl ContextImpl { + fn new( + device: Arc, + queue: Arc>, + swapchain: Arc, + images: Vec>, + ) -> Self { + Self { + device, + queue, + swapchain, + images, + } + } +} + +impl ContextInterface for ContextImpl { + fn device(&self) -> &Arc { + &self.device + } + + fn queue(&self) -> &Arc> { + &self.queue + } + + fn format(&self) -> VkFormat { + self.swapchain.format() + } + + fn image_layout(&self) -> VkImageLayout { + VK_IMAGE_LAYOUT_PRESENT_SRC_KHR + } + + fn image_count(&self) -> usize { + self.images.len() + } + + fn images(&self) -> TargetMode>> { + TargetMode::Mono(self.images.clone()) + } + + fn width(&self) -> u32 { + self.swapchain.width() + } + + fn height(&self) -> u32 { + self.swapchain.height() + } +} diff --git a/src/overlay/rendering.rs b/src/overlay/rendering.rs index d36ba66..1637652 100644 --- a/src/overlay/rendering.rs +++ b/src/overlay/rendering.rs @@ -51,6 +51,7 @@ pub struct Rendering { pipeline: SingleColorPipeline, render_target: RenderTarget, command_buffer: Arc, + images: Vec>, queue: Arc>, } @@ -97,6 +98,7 @@ impl Rendering { pipeline: SingleColorPipeline::new(device.clone(), render_target.render_pass())?, render_target, command_buffer: CommandBuffer::new_primary().build(device.clone(), queue.clone())?, + images, queue, }) @@ -165,4 +167,8 @@ impl Rendering { Ok(()) } + + pub fn images(&self) -> &Vec> { + &self.images + } } diff --git a/src/overlay/rfactor_data.rs b/src/overlay/rfactor_data.rs index 919fd49..05835e7 100644 --- a/src/overlay/rfactor_data.rs +++ b/src/overlay/rfactor_data.rs @@ -20,7 +20,7 @@ pub trait RenderObject { } #[derive(Deserialize, Serialize, Clone, Copy, Debug)] -pub struct DataConfig { +pub struct RadarConfig { pub radar_scale: f32, pub radar_center_factor: f32, pub radar_transparency: f32, @@ -31,7 +31,7 @@ pub struct DataConfig { pub danger_color: Vector3, } -impl DataConfig { +impl RadarConfig { pub const fn new() -> Self { Self { radar_scale: 1.0, @@ -48,7 +48,7 @@ impl DataConfig { pub struct RFactorData { // config - config: DataConfig, + config: RadarConfig, // rf2 memory mapped data telemetry_reader: TelemetryReader, @@ -82,7 +82,7 @@ pub struct RFactorData { impl RFactorData { pub fn new( - config: DataConfig, + config: RadarConfig, device: Arc, descriptor_layout: &Arc, width: u32, From caa87fbc18d06411a3933fa95e63175887cf5d3e Mon Sep 17 00:00:00 2001 From: hodasemi Date: Tue, 17 Jan 2023 12:18:53 +0100 Subject: [PATCH 02/16] Refactor project for easier extension --- build.rs | 4 +- src/overlay/elements/mod.rs | 6 + src/overlay/elements/pedals.rs | 66 +++ src/overlay/{ => elements}/pipeline.rs | 28 +- src/overlay/elements/radar.rs | 495 ++++++++++++++++++ .../{ => elements}/shader/single_color.frag | 0 .../{ => elements}/shader/single_color.vert | 0 src/overlay/elements/ui_files/gui.xsd | 235 +++++++++ src/overlay/elements/ui_files/pedals.xml | 11 + src/overlay/mod.rs | 110 ++-- src/overlay/rendering.rs | 124 +---- src/overlay/rfactor_data.rs | 398 +------------- 12 files changed, 963 insertions(+), 514 deletions(-) rename src/overlay/{ => elements}/pipeline.rs (78%) rename src/overlay/{ => elements}/shader/single_color.frag (100%) rename src/overlay/{ => elements}/shader/single_color.vert (100%) create mode 100644 src/overlay/elements/ui_files/gui.xsd create mode 100644 src/overlay/elements/ui_files/pedals.xml diff --git a/build.rs b/build.rs index d0fb369..d5ffc56 100644 --- a/build.rs +++ b/build.rs @@ -10,8 +10,8 @@ const VK_HEADER: &[&str] = &[ const FN_PREFIX: &str = "PFN_"; const SHADER: &[&str] = &[ - "src/overlay/shader/single_color.vert", - "src/overlay/shader/single_color.frag", + "src/overlay/elements/shader/single_color.vert", + "src/overlay/elements/shader/single_color.frag", ]; fn query_vulkan_function_typedefs() { diff --git a/src/overlay/elements/mod.rs b/src/overlay/elements/mod.rs index e69de29..038032f 100644 --- a/src/overlay/elements/mod.rs +++ b/src/overlay/elements/mod.rs @@ -0,0 +1,6 @@ +mod pedals; +mod pipeline; +mod radar; + +pub use pedals::*; +pub use radar::*; diff --git a/src/overlay/elements/pedals.rs b/src/overlay/elements/pedals.rs index e69de29..12f970a 100644 --- a/src/overlay/elements/pedals.rs +++ b/src/overlay/elements/pedals.rs @@ -0,0 +1,66 @@ +use std::sync::Arc; + +use anyhow::Result; +use rfactor_sm_reader::{rF2VehicleTelemetry, VehicleScoringInfoV01}; +use ui::prelude::*; + +use crate::overlay::{rfactor_data::DataReceiver, UiOverlay}; + +pub struct Pedals { + gui: Arc, + + brake: Arc, + throttle: Arc, +} + +impl Pedals { + pub fn new(gui_handler: &Arc) -> Result { + const DESC: &str = include_str!("ui_files/pedals.xml"); + + let gui = GuiBuilder::from_str(gui_handler, DESC)?; + + let brake = gui.element("brake")?; + let throttle = gui.element("throttle")?; + + Ok(Self { + gui, + brake, + throttle, + }) + } +} + +impl Drop for Pedals { + fn drop(&mut self) { + self.gui.disable().unwrap(); + } +} + +impl UiOverlay for Pedals { + fn enable_ui(&mut self) -> Result<()> { + self.gui.enable() + } +} + +impl DataReceiver for Pedals { + fn scoring_update(&mut self, _vehicle_scoring: &[VehicleScoringInfoV01]) -> Result<()> { + Ok(()) + } + + fn telemetry_update( + &mut self, + player_id: Option, + telemetries: &[rF2VehicleTelemetry], + ) -> Result<()> { + if let Some(id) = player_id { + if let Some(telemetry) = telemetries.iter().find(|telemetry| telemetry.id == id) { + self.brake + .set_progress(telemetry.unfiltered_throttle as f32)?; + self.throttle + .set_progress(telemetry.unfiltered_throttle as f32)?; + } + } + + Ok(()) + } +} diff --git a/src/overlay/pipeline.rs b/src/overlay/elements/pipeline.rs similarity index 78% rename from src/overlay/pipeline.rs rename to src/overlay/elements/pipeline.rs index fca50bf..ee729a2 100644 --- a/src/overlay/pipeline.rs +++ b/src/overlay/elements/pipeline.rs @@ -3,7 +3,7 @@ use vulkan_rs::prelude::*; use std::{mem, sync::Arc}; -use super::rendering::PositionOnlyVertex; +use super::radar::PositionOnlyVertex; pub struct SingleColorPipeline { pipeline: Arc, @@ -11,7 +11,12 @@ pub struct SingleColorPipeline { } impl SingleColorPipeline { - pub fn new(device: Arc, renderpass: &Arc) -> Result { + pub fn new( + device: Arc, + renderpass: &Arc, + width: u32, + height: u32, + ) -> Result { let vertex_shader = ShaderModule::from_slice( device.clone(), include_bytes!("shader/single_color.vert.spv"), @@ -36,6 +41,23 @@ impl SingleColorPipeline { .add_descriptor_set_layout(&descriptor_layout) .build(device.clone())?; + let viewport = VkViewport { + x: 0.0, + y: 0.0, + width: width as f32, + height: height as f32, + minDepth: 0.0, + maxDepth: 1.0, + }; + + let scissor = VkRect2D { + offset: VkOffset2D { x: 0, y: 0 }, + extent: VkExtent2D { + width: width, + height: height, + }, + }; + let pipeline = Pipeline::new_graphics() .set_vertex_shader( vertex_shader.clone(), @@ -60,6 +82,8 @@ impl SingleColorPipeline { .default_color_blend(vec![VkPipelineColorBlendAttachmentState::default()]) .default_rasterization(VK_CULL_MODE_NONE, VK_FRONT_FACE_COUNTER_CLOCKWISE) .default_multisample(VK_SAMPLE_COUNT_1_BIT) + .add_viewport(viewport) + .add_scissor(scissor) .build(device, &pipeline_layout, &renderpass, 0)?; Ok(Self { diff --git a/src/overlay/elements/radar.rs b/src/overlay/elements/radar.rs index e69de29..e9216f4 100644 --- a/src/overlay/elements/radar.rs +++ b/src/overlay/elements/radar.rs @@ -0,0 +1,495 @@ +use anyhow::Result; +use cgmath::{ortho, vec2, vec3, vec4, Deg, InnerSpace, Matrix4, Rad, Vector2, Vector3, Vector4}; +use rfactor_sm_reader::*; +use serde::{Deserialize, Serialize}; +use vulkan_rs::prelude::*; + +use std::sync::{Arc, Mutex}; + +use super::pipeline::SingleColorPipeline; + +use crate::{ + overlay::{rendering::Rendering, rfactor_data::DataReceiver, UiOverlay}, + write_log, +}; + +#[derive(Clone)] +pub struct PositionOnlyVertex { + pub position: Vector4, +} + +impl PositionOnlyVertex { + /// + /// corners[0] - bottom left + /// corners[1] - top left + /// corners[2] - top right + /// corners[3] - bottom right + /// + pub fn from_2d_corners(ortho: Matrix4, corners: [Vector2; 4]) -> [Self; 6] { + [ + Self { + position: ortho * corners[0].extend(0.0).extend(1.0), + }, + Self { + position: ortho * corners[1].extend(0.0).extend(1.0), + }, + Self { + position: ortho * corners[2].extend(0.0).extend(1.0), + }, + Self { + position: ortho * corners[2].extend(0.0).extend(1.0), + }, + Self { + position: ortho * corners[3].extend(0.0).extend(1.0), + }, + Self { + position: ortho * corners[0].extend(0.0).extend(1.0), + }, + ] + } +} + +fn convert_vec(v: rF2Vec3) -> Vector3 { + vec3(v.x as f32, v.y as f32, v.z as f32) +} + +#[derive(Deserialize, Serialize, Clone, Copy, Debug)] +pub struct RadarConfig { + pub radar_scale: f32, + pub radar_center_factor: f32, + pub radar_transparency: f32, + pub height_scale: f32, + pub width_scale: f32, + pub radar_car_distance: f32, + pub safe_color: Vector3, + pub danger_color: Vector3, +} + +impl RadarConfig { + pub const fn new() -> Self { + Self { + radar_scale: 1.0, + radar_center_factor: 0.25, + radar_transparency: 0.5, + height_scale: 0.15, + width_scale: 0.4, + radar_car_distance: 20.0, + safe_color: vec3(0.0, 0.75, 0.0), + danger_color: vec3(0.75, 0.0, 0.0), + } + } +} + +pub struct Radar { + // config + config: RadarConfig, + + // radar objects + background: Option, + player_car: RadarObject, + cars: Vec, + + // buffer car objects, to prevent recreating them every update + car_handles: Vec, + + // game info + player_id: Option, + + // math objects + radar_center: Vector2, + ortho: Matrix4, + _window_width: u32, + _window_height: u32, + radar_extent: f32, + car_width: f32, + car_height: f32, + + device: Arc, + queue: Arc>, + + pipeline: SingleColorPipeline, + render_target: RenderTarget, + + enabled: bool, +} + +impl Radar { + pub fn new( + config: RadarConfig, + device: Arc, + queue: Arc>, + rendering: &Rendering, + ) -> Result { + write_log!(" =================== create RFactorData ==================="); + + let radar_extent = rendering.swapchain().width() as f32 * 0.075 * config.radar_scale; + let car_height = radar_extent * config.height_scale; + let car_width = car_height * config.width_scale; + let radar_center = vec2( + rendering.swapchain().width() as f32 / 2.0, + rendering.swapchain().height() as f32 / 2.0 + - rendering.swapchain().height() as f32 * config.radar_center_factor, + ); + + let flip_y = matrix4_from_diagonal(vec3(1.0, -1.0, 1.0)); + let ortho = flip_y + * ortho( + 0.0, + rendering.swapchain().width() as f32, + 0.0, + rendering.swapchain().height() as f32, + -1.0, + 1.0, + ); + + let render_target = RenderTarget::builder() + .add_sub_pass( + SubPass::builder( + rendering.swapchain().width(), + rendering.swapchain().height(), + ) + .set_prepared_targets(&rendering.images(), 0, [0.0, 0.0, 0.0, 1.0], false) + .build(&device, &queue)?, + ) + .build(&device)?; + + let pipeline = SingleColorPipeline::new( + device.clone(), + render_target.render_pass(), + rendering.swapchain().width(), + rendering.swapchain().height(), + )?; + + Ok(Self { + config, + + background: if config.radar_transparency == 0.0 { + None + } else { + Some(RadarObject::new( + device.clone(), + pipeline.descriptor_layout(), + PositionOnlyVertex::from_2d_corners( + ortho * Matrix4::from_translation(radar_center.extend(0.0)), + [ + vec2(-radar_extent, -radar_extent), + vec2(-radar_extent, radar_extent), + vec2(radar_extent, radar_extent), + vec2(radar_extent, -radar_extent), + ], + ), + [0.5, 0.5, 0.5, config.radar_transparency], + )?) + }, + player_car: RadarObject::new( + device.clone(), + pipeline.descriptor_layout(), + PositionOnlyVertex::from_2d_corners( + ortho * Matrix4::from_translation(radar_center.extend(0.0)), + [ + vec2(-car_width, -car_height), + vec2(-car_width, car_height), + vec2(car_width, car_height), + vec2(car_width, -car_height), + ], + ), + [0.0, 0.9, 0.0, 0.9], + )?, + cars: Vec::new(), + car_handles: Vec::new(), + + player_id: None, + + radar_center, + ortho, + _window_width: rendering.swapchain().width(), + _window_height: rendering.swapchain().height(), + radar_extent, + car_width, + car_height, + + device, + queue, + + render_target, + pipeline, + + enabled: false, + }) + } + + fn create_car_object(&self, offset: Vector2, color: [f32; 4]) -> Result { + write_log!(" =================== create car object ==================="); + + RadarObject::new( + self.device.clone(), + &self.pipeline.descriptor_layout(), + Self::create_car_vertices( + self.ortho + * Matrix4::from_translation(self.radar_center.extend(0.0)) + * Matrix4::from_translation(offset.extend(0.0)), + self.car_width, + self.car_height, + ), + color, + ) + } + + fn create_car_vertices( + mvp: Matrix4, + car_width: f32, + car_height: f32, + ) -> [PositionOnlyVertex; 6] { + PositionOnlyVertex::from_2d_corners( + mvp, + [ + vec2(-car_width, -car_height), + vec2(-car_width, car_height), + vec2(car_width, car_height), + vec2(car_width, -car_height), + ], + ) + } + + pub fn render(&self, image_index: u32) -> Result> { + let command_buffer = + CommandBuffer::new_primary().build(self.device.clone(), self.queue.clone())?; + + { + let mut recorder = command_buffer.begin(VkCommandBufferBeginInfo::new( + VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT + | VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT, + ))?; + + self.render_target + .begin(&recorder, VK_SUBPASS_CONTENTS_INLINE, image_index as usize); + + recorder.bind_pipeline(self.pipeline.pipeline())?; + + for object in self.objects() { + let buffer = &object.position_buffer; + + recorder.bind_descriptor_sets_minimal(&[&object.descriptor_set]); + recorder.bind_vertex_buffer(buffer); + recorder.draw_complete_single_instance(buffer.size() as u32); + } + + self.render_target.end(&recorder); + } + + Ok(command_buffer) + } + + fn objects(&self) -> Vec<&RadarObject> { + write_log!(" =================== get objects of radar ==================="); + + let mut objects = Vec::new(); + + // only draw radar when player is loaded into a map + if let Some(_player_id) = &self.player_id { + // only draw radar when any car is near enough + if !self.cars.is_empty() { + if let Some(background) = &self.background { + objects.push(background); + } + + for other_player_cars in &self.cars { + objects.push(other_player_cars); + } + + objects.push(&self.player_car); + } + } + + objects + } +} + +impl UiOverlay for Radar { + fn enable_ui(&mut self) -> Result<()> { + self.enabled = true; + + Ok(()) + } +} + +impl DataReceiver for Radar { + fn scoring_update(&mut self, _vehicle_scoring: &[VehicleScoringInfoV01]) -> Result<()> { + Ok(()) + } + + fn telemetry_update( + &mut self, + player_id: Option, + telemetries: &[rF2VehicleTelemetry], + ) -> Result<()> { + self.cars.clear(); + + if !self.enabled { + return Ok(()); + } + + if let Some(player_id) = player_id { + // make sure there are enough cars in buffer + if self.car_handles.len() < telemetries.len() { + let size_diff = telemetries.len() - self.car_handles.len(); + + for _ in 0..size_diff { + self.car_handles + .push(self.create_car_object(vec2(0.0, 0.0), [0.0, 0.0, 0.0, 0.0])?); + } + } + + let mut player_position = CarPosition::default(); + let mut other_positions = Vec::new(); + + for telemetry in telemetries { + let car = CarPosition::new( + convert_vec(telemetry.position), + [ + convert_vec(telemetry.orientation[0]), + convert_vec(telemetry.orientation[1]), + convert_vec(telemetry.orientation[2]), + ], + ); + + if telemetry.id == player_id { + player_position = car + } else { + other_positions.push(car); + } + } + + // update radar objects + let mut buffer_car_index = 0; + + for other_position in other_positions { + let diff = player_position.position - other_position.position; + let distance = diff.magnitude(); + + // check if car is close enough to the players car + if distance < self.config.radar_car_distance { + let offset = diff.xz() * (self.radar_extent / self.config.radar_car_distance); + + let buffered_car = self.car_handles[buffer_car_index].clone(); + buffer_car_index += 1; + buffered_car.update( + self.ortho, + offset, + player_position.rotation, + other_position.rotation, + self.radar_center, + self.car_width, + self.car_height, + [0.9, 0.9, 0.0, 0.9], + )?; + + self.cars.push(buffered_car); + } + } + } + + Ok(()) + } +} + +#[derive(Clone)] +struct RadarObject { + descriptor_set: Arc, + + // uniform buffer + color_buffer: Arc>, + + // vertex buffer + position_buffer: Arc>, +} + +impl RadarObject { + fn new( + device: Arc, + descriptor_layout: &Arc, + positions: [PositionOnlyVertex; 6], + color: [f32; 4], + ) -> Result { + let color_buffer = Buffer::builder() + .set_usage(VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT) + .set_memory_usage(MemoryUsage::CpuOnly) + .set_data(&color) + .build(device.clone())?; + + let position_buffer = Buffer::builder() + .set_usage(VK_BUFFER_USAGE_VERTEX_BUFFER_BIT) + .set_memory_usage(MemoryUsage::CpuOnly) + .set_data(&positions) + .build(device.clone())?; + + let descriptor_pool = DescriptorPool::builder() + .set_layout(descriptor_layout.clone()) + .build(device.clone())?; + + let descriptor_set = descriptor_pool.prepare_set().allocate()?; + descriptor_set.update(&[DescriptorWrite::uniform_buffers(0, &[&color_buffer])])?; + + Ok(Self { + descriptor_set, + color_buffer, + position_buffer, + }) + } + + pub fn update( + &self, + + ortho: Matrix4, + offset: Vector2, + player_rotation: impl Into>, + rotation: impl Into>, + radar_center: Vector2, + car_width: f32, + car_height: f32, + color: [f32; 4], + ) -> Result<()> { + self.position_buffer.fill(&Radar::create_car_vertices( + ortho + * Matrix4::from_translation(radar_center.extend(0.0)) + * Matrix4::from_angle_z(-player_rotation.into()) + * Matrix4::from_translation(offset.extend(0.0)) + * Matrix4::from_angle_z(rotation.into()), + car_width, + car_height, + ))?; + self.color_buffer.fill(&color) + } +} + +struct CarPosition { + pub position: Vector3, + pub rotation: Rad, +} + +impl CarPosition { + fn new(position: Vector3, orientation: [Vector3; 3]) -> Self { + Self { + position, + rotation: Rad(orientation[2].x.atan2(orientation[2].z)), + } + } +} + +impl Default for CarPosition { + fn default() -> Self { + Self { + position: vec3(0.0, 0.0, 0.0), + rotation: Rad(0.0), + } + } +} + +const fn matrix4_from_diagonal(diagonal: Vector3) -> Matrix4 { + Matrix4::from_cols( + vec4(diagonal.x, 0.0, 0.0, 0.0), + vec4(0.0, diagonal.y, 0.0, 0.0), + vec4(0.0, 0.0, diagonal.z, 0.0), + vec4(0.0, 0.0, 0.0, 1.0), + ) +} diff --git a/src/overlay/shader/single_color.frag b/src/overlay/elements/shader/single_color.frag similarity index 100% rename from src/overlay/shader/single_color.frag rename to src/overlay/elements/shader/single_color.frag diff --git a/src/overlay/shader/single_color.vert b/src/overlay/elements/shader/single_color.vert similarity index 100% rename from src/overlay/shader/single_color.vert rename to src/overlay/elements/shader/single_color.vert diff --git a/src/overlay/elements/ui_files/gui.xsd b/src/overlay/elements/ui_files/gui.xsd new file mode 100644 index 0000000..7e0042c --- /dev/null +++ b/src/overlay/elements/ui_files/gui.xsd @@ -0,0 +1,235 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/overlay/elements/ui_files/pedals.xml b/src/overlay/elements/ui_files/pedals.xml new file mode 100644 index 0000000..b3d093b --- /dev/null +++ b/src/overlay/elements/ui_files/pedals.xml @@ -0,0 +1,11 @@ + + + + + + + \ No newline at end of file diff --git a/src/overlay/mod.rs b/src/overlay/mod.rs index 48460ba..aa90311 100644 --- a/src/overlay/mod.rs +++ b/src/overlay/mod.rs @@ -2,21 +2,31 @@ use crate::write_log; use self::{ rendering::Rendering, - rfactor_data::{RFactorData, RadarConfig}, + rfactor_data::{DataReceiver, RFactorData}, }; -mod pipeline; +mod elements; mod rendering; mod rfactor_data; use anyhow::Result; use assetpath::AssetPath; -use std::sync::{Arc, Mutex}; +use std::{ + cell::RefCell, + rc::Rc, + sync::{Arc, Mutex}, +}; use ui::prelude::*; use vulkan_rs::prelude::*; +use elements::*; + use serde::{Deserialize, Serialize}; +pub trait UiOverlay: DataReceiver { + fn enable_ui(&mut self) -> Result<()>; +} + #[derive(Deserialize, Serialize)] pub struct OverlayConfig { pub radar_config: RadarConfig, @@ -41,6 +51,8 @@ pub struct Overlay { rendering: Option, gui_handler: Option>, + ui_elements: Vec>>, + rfactor_data: Option, } @@ -54,6 +66,7 @@ impl Overlay { queue: None, rendering: None, gui_handler: None, + ui_elements: Vec::new(), rfactor_data: None, } @@ -104,13 +117,16 @@ impl Overlay { write_log!("-> create rendering: old cleared"); - let rendering = Rendering::new(self.device(), self.queue(), swapchain.clone())?; + let mut rendering = Rendering::new(self.queue(), swapchain.clone())?; + + write_log!("-> create rendering: new created"); // only font is used let mut create_info = GuiHandlerCreateInfo::default(); create_info.font_path = AssetPath::from(self.config.font_path.clone()); create_info.font_path.assume_prefix_free(); + // provide trait required by GuiHandler let ctx = Arc::new(ContextImpl::new( self.device(), self.queue(), @@ -118,38 +134,75 @@ impl Overlay { rendering.images().clone(), )); - self.gui_handler = Some(GuiHandler::new( - create_info, - &(ctx as Arc), - )?); + // create GuiHandler + let gui_handler = GuiHandler::new(create_info, &(ctx as Arc))?; - self.rendering = Some(rendering); + // create ui elements - write_log!("-> create rendering: new created"); + // create radar + let radar = Rc::new(RefCell::new(Radar::new( + self.config.radar_config, + self.device(), + self.queue(), + &rendering, + )?)); + + // create pedals + let pedals = Rc::new(RefCell::new(Pedals::new(&gui_handler)?)); + + // add rendering callbacks + rendering.add_render_callback({ + let radar = radar.clone(); + + move |index| radar.borrow().render(index) + }); + + rendering.add_render_callback({ + let gui_handler = gui_handler.clone(); + let device = self.device(); + let queue = self.queue(); + + move |index| { + let command_buffer = + CommandBuffer::new_primary().build(device.clone(), queue.clone())?; + + { + let mut recorder = command_buffer.begin(VkCommandBufferBeginInfo::new( + VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT + | VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT, + ))?; + + gui_handler.process(&mut recorder, &TargetMode::Mono(index as usize))?; + } + + Ok(command_buffer) + } + }); + + // add ui elements to local list + self.ui_elements.push(radar); + self.ui_elements.push(pedals); write_log!("-> create rendering: end"); + self.rendering = Some(rendering); + self.gui_handler = Some(gui_handler); + Ok(()) } pub fn render(&mut self) -> Result<()> { - let swapchain = self.rendering.as_ref().unwrap().swapchain().clone(); - if self.rfactor_data.is_none() { - self.rfactor_data = RFactorData::new( - self.config.radar_config, - self.device(), - self.rendering - .as_mut() - .unwrap() - .single_color_pipeline() - .descriptor_layout(), - swapchain.width(), - swapchain.height(), - ) - .ok(); + self.rfactor_data = RFactorData::new().ok(); - write_log!("created RFactorData"); + if let Some(data) = &mut self.rfactor_data { + write_log!("created RFactorData"); + + for receiver in self.ui_elements.iter() { + receiver.borrow_mut().enable_ui()?; + data.add_receiver(receiver.clone()); + } + } } // check twice for rfactor data, because of borrowing rules @@ -157,12 +210,7 @@ impl Overlay { rfactor.update()?; } - let objects = match &self.rfactor_data { - Some(rfactor) => rfactor.objects(), - None => Vec::new(), - }; - - self.rendering.as_mut().unwrap().render(swapchain, &objects) + self.rendering.as_ref().unwrap().render() } } diff --git a/src/overlay/rendering.rs b/src/overlay/rendering.rs index 1637652..ba16122 100644 --- a/src/overlay/rendering.rs +++ b/src/overlay/rendering.rs @@ -1,5 +1,4 @@ use anyhow::Result; -use cgmath::{Matrix4, Vector2, Vector4}; use vulkan_rs::prelude::*; use std::{ @@ -7,61 +6,19 @@ use std::{ time::Duration, }; -use super::{pipeline::SingleColorPipeline, rfactor_data::RenderObject}; use crate::write_log; -#[derive(Clone)] -pub struct PositionOnlyVertex { - pub position: Vector4, -} - -impl PositionOnlyVertex { - /// - /// corners[0] - bottom left - /// corners[1] - top left - /// corners[2] - top right - /// corners[3] - bottom right - /// - pub fn from_2d_corners(ortho: Matrix4, corners: [Vector2; 4]) -> [Self; 6] { - [ - Self { - position: ortho * corners[0].extend(0.0).extend(1.0), - }, - Self { - position: ortho * corners[1].extend(0.0).extend(1.0), - }, - Self { - position: ortho * corners[2].extend(0.0).extend(1.0), - }, - Self { - position: ortho * corners[2].extend(0.0).extend(1.0), - }, - Self { - position: ortho * corners[3].extend(0.0).extend(1.0), - }, - Self { - position: ortho * corners[0].extend(0.0).extend(1.0), - }, - ] - } -} - pub struct Rendering { swapchain: Arc, - pipeline: SingleColorPipeline, - render_target: RenderTarget, - command_buffer: Arc, images: Vec>, queue: Arc>, + + render_callbacks: Vec Result>>>, } impl Rendering { - pub fn new( - device: Arc, - queue: Arc>, - swapchain: Arc, - ) -> Result { + pub fn new(queue: Arc>, swapchain: Arc) -> Result { crate::write_log!("-> Rendering ctor: begin"); let vk_images = swapchain.vk_images()?; write_log!(format!( @@ -77,14 +34,6 @@ impl Rendering { }; write_log!("-> Rendering ctor: wrapped images"); - let render_target = RenderTarget::builder() - .add_sub_pass( - SubPass::builder(swapchain.width(), swapchain.height()) - .set_prepared_targets(&images, 0, [0.0, 0.0, 0.0, 1.0], false) - .build(&device, &queue)?, - ) - .build(&device)?; - write_log!("-> Rendering ctor: created render_target"); write_log!(format!( @@ -95,12 +44,11 @@ impl Rendering { Ok(Self { swapchain, - pipeline: SingleColorPipeline::new(device.clone(), render_target.render_pass())?, - render_target, - command_buffer: CommandBuffer::new_primary().build(device.clone(), queue.clone())?, images, queue, + + render_callbacks: Vec::new(), }) } @@ -108,62 +56,24 @@ impl Rendering { &self.swapchain } - pub fn single_color_pipeline(&self) -> &SingleColorPipeline { - &self.pipeline + pub fn add_render_callback(&mut self, f: F) + where + F: Fn(u32) -> Result> + 'static, + { + self.render_callbacks.push(Box::new(f)); } - pub fn render( - &mut self, - swapchain: Arc, - objects: &[&dyn RenderObject], - ) -> Result<()> { + pub fn render(&self) -> Result<()> { let image_index = self.swapchain.current_index(); - let viewport = [VkViewport { - x: 0.0, - y: 0.0, - width: swapchain.width() as f32, - height: swapchain.height() as f32, - minDepth: 0.0, - maxDepth: 1.0, - }]; - - let scissor = [VkRect2D { - offset: VkOffset2D { x: 0, y: 0 }, - extent: VkExtent2D { - width: swapchain.width(), - height: swapchain.height(), - }, - }]; - - { - let mut recorder = self.command_buffer.begin(VkCommandBufferBeginInfo::new( - VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT - | VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT, - ))?; - - self.render_target - .begin(&recorder, VK_SUBPASS_CONTENTS_INLINE, image_index as usize); - - recorder.bind_pipeline(self.pipeline.pipeline())?; - recorder.set_scissor(&scissor); - recorder.set_viewport(&viewport); - - write_log!(format!("-> Rendering {} objects", objects.len())); - - for object in objects { - let buffer = object.buffer(); - - recorder.bind_descriptor_sets_minimal(&[object.descriptor()]); - recorder.bind_vertex_buffer(buffer); - recorder.draw_complete_single_instance(buffer.size() as u32); - } - - self.render_target.end(&recorder); - } + let command_buffers: Vec> = self + .render_callbacks + .iter() + .map(|c| c(image_index)) + .collect::>>>()?; let queue = self.queue.lock().unwrap(); - queue.minimal_submit(Duration::from_secs(10), &[self.command_buffer.clone()])?; + queue.minimal_submit(Duration::from_secs(10), &command_buffers)?; Ok(()) } diff --git a/src/overlay/rfactor_data.rs b/src/overlay/rfactor_data.rs index 05835e7..dbb974d 100644 --- a/src/overlay/rfactor_data.rs +++ b/src/overlay/rfactor_data.rs @@ -1,196 +1,52 @@ use anyhow::Result; -use cgmath::{ortho, vec2, vec3, vec4, Deg, InnerSpace, Matrix4, Rad, Vector2, Vector3}; use rfactor_sm_reader::*; -use vulkan_rs::prelude::*; -use serde::{Deserialize, Serialize}; +use std::{cell::RefCell, rc::Rc, time::Instant}; -use std::{sync::Arc, time::Instant}; - -use super::rendering::PositionOnlyVertex; use crate::write_log; -fn convert_vec(v: rF2Vec3) -> Vector3 { - vec3(v.x as f32, v.y as f32, v.z as f32) -} +use super::UiOverlay; -pub trait RenderObject { - fn descriptor(&self) -> &Arc; - fn buffer(&self) -> &Arc>; -} +pub trait DataReceiver { + fn scoring_update(&mut self, vehicle_scoring: &[VehicleScoringInfoV01]) -> Result<()>; -#[derive(Deserialize, Serialize, Clone, Copy, Debug)] -pub struct RadarConfig { - pub radar_scale: f32, - pub radar_center_factor: f32, - pub radar_transparency: f32, - pub height_scale: f32, - pub width_scale: f32, - pub radar_car_distance: f32, - pub safe_color: Vector3, - pub danger_color: Vector3, -} - -impl RadarConfig { - pub const fn new() -> Self { - Self { - radar_scale: 1.0, - radar_center_factor: 0.25, - radar_transparency: 0.5, - height_scale: 0.15, - width_scale: 0.4, - radar_car_distance: 20.0, - safe_color: vec3(0.0, 0.75, 0.0), - danger_color: vec3(0.75, 0.0, 0.0), - } - } + fn telemetry_update( + &mut self, + player_id: Option, + telemetries: &[rF2VehicleTelemetry], + ) -> Result<()>; } pub struct RFactorData { - // config - config: RadarConfig, - // rf2 memory mapped data telemetry_reader: TelemetryReader, scoring_reader: ScoringReader, - // radar objects - background: Option, - player_car: RadarObject, - cars: Vec, - - // buffer car objects, to prevent recreating them every update - car_handles: Vec, - - // game info + start_time: Instant, player_id: Option, - // math objects - radar_center: Vector2, - ortho: Matrix4, - _window_width: u32, - _window_height: u32, - radar_extent: f32, - car_width: f32, - car_height: f32, - - start_time: Instant, - - device: Arc, - descriptor_layout: Arc, + receivers: Vec>>, } impl RFactorData { - pub fn new( - config: RadarConfig, - device: Arc, - descriptor_layout: &Arc, - width: u32, - height: u32, - ) -> Result { + pub fn new() -> Result { write_log!(" =================== create RFactorData ==================="); - let radar_extent = width as f32 * 0.075 * config.radar_scale; - let car_height = radar_extent * config.height_scale; - let car_width = car_height * config.width_scale; - let radar_center = vec2( - width as f32 / 2.0, - height as f32 / 2.0 - height as f32 * config.radar_center_factor, - ); - - let flip_y = matrix4_from_diagonal(vec3(1.0, -1.0, 1.0)); - let ortho = flip_y * ortho(0.0, width as f32, 0.0, height as f32, -1.0, 1.0); let start_time = Instant::now(); Ok(Self { - config, - telemetry_reader: TelemetryReader::new(start_time.elapsed().as_secs_f32())?, scoring_reader: ScoringReader::new(start_time.elapsed().as_secs_f32())?, - background: if config.radar_transparency == 0.0 { - None - } else { - Some(RadarObject::new( - device.clone(), - descriptor_layout, - PositionOnlyVertex::from_2d_corners( - ortho * Matrix4::from_translation(radar_center.extend(0.0)), - [ - vec2(-radar_extent, -radar_extent), - vec2(-radar_extent, radar_extent), - vec2(radar_extent, radar_extent), - vec2(radar_extent, -radar_extent), - ], - ), - [0.5, 0.5, 0.5, config.radar_transparency], - )?) - }, - player_car: RadarObject::new( - device.clone(), - descriptor_layout, - PositionOnlyVertex::from_2d_corners( - ortho * Matrix4::from_translation(radar_center.extend(0.0)), - [ - vec2(-car_width, -car_height), - vec2(-car_width, car_height), - vec2(car_width, car_height), - vec2(car_width, -car_height), - ], - ), - [0.0, 0.9, 0.0, 0.9], - )?, - cars: Vec::new(), - car_handles: Vec::new(), - + start_time, player_id: None, - radar_center, - ortho, - _window_width: width, - _window_height: height, - radar_extent, - car_width, - car_height, - - start_time, - - device, - descriptor_layout: descriptor_layout.clone(), + receivers: Vec::new(), }) } - fn create_car_object(&self, offset: Vector2, color: [f32; 4]) -> Result { - write_log!(" =================== create car object ==================="); - - RadarObject::new( - self.device.clone(), - &self.descriptor_layout, - Self::create_car_vertices( - self.ortho - * Matrix4::from_translation(self.radar_center.extend(0.0)) - * Matrix4::from_translation(offset.extend(0.0)), - self.car_width, - self.car_height, - ), - color, - ) - } - - fn create_car_vertices( - mvp: Matrix4, - car_width: f32, - car_height: f32, - ) -> [PositionOnlyVertex; 6] { - PositionOnlyVertex::from_2d_corners( - mvp, - [ - vec2(-car_width, -car_height), - vec2(-car_width, car_height), - vec2(car_width, car_height), - vec2(car_width, -car_height), - ], - ) + pub fn add_receiver(&mut self, receiver: Rc>) { + self.receivers.push(receiver); } fn now(&self) -> f32 { @@ -200,8 +56,6 @@ impl RFactorData { pub fn update(&mut self) -> Result<()> { write_log!(" =================== update RFactorData ==================="); - let mut should_render = false; - // get scoring info if let Some((scoring_info, vehicle_scorings)) = self.scoring_reader.vehicle_scoring(self.now()) @@ -224,223 +78,23 @@ impl RFactorData { } } - if let Some(id) = &self.player_id { - if let Some(vehicle_scoring) = - vehicle_scorings.iter().find(|scoring| scoring.mID == *id) - { - should_render = vehicle_scoring.mInPits != 0; - } + for receiver in self.receivers.iter() { + receiver.borrow_mut().scoring_update(&vehicle_scorings)?; } } - // if player id is set (a map is loaded), check telemetry data - if let Some(player_id) = &self.player_id { - write_log!("before telemetry update"); - if let Some(telemetries) = self.telemetry_reader.query_telemetry(self.now()) { - write_log!("new telemetry update"); + // check telemetry data + write_log!("before telemetry update"); + if let Some(telemetries) = self.telemetry_reader.query_telemetry(self.now()) { + write_log!("new telemetry update"); - self.cars.clear(); - - if should_render { - // make sure there are enough cars in buffer - if self.car_handles.len() < telemetries.len() { - let size_diff = telemetries.len() - self.car_handles.len(); - - for _ in 0..size_diff { - self.car_handles.push( - self.create_car_object(vec2(0.0, 0.0), [0.0, 0.0, 0.0, 0.0])?, - ); - } - } - - let mut player_position = CarPosition::default(); - let mut other_positions = Vec::new(); - - for telemetry in telemetries { - let car = CarPosition::new( - convert_vec(telemetry.position), - [ - convert_vec(telemetry.orientation[0]), - convert_vec(telemetry.orientation[1]), - convert_vec(telemetry.orientation[2]), - ], - ); - - if telemetry.id == *player_id { - player_position = car - } else { - other_positions.push(car); - } - } - - // update radar objects - let mut buffer_car_index = 0; - - for other_position in other_positions { - let diff = player_position.position - other_position.position; - let distance = diff.magnitude(); - - // check if car is close enough to the players car - if distance < self.config.radar_car_distance { - let offset = - diff.xz() * (self.radar_extent / self.config.radar_car_distance); - - let buffered_car = self.car_handles[buffer_car_index].clone(); - buffer_car_index += 1; - buffered_car.update( - self.ortho, - offset, - player_position.rotation, - other_position.rotation, - self.radar_center, - self.car_width, - self.car_height, - [0.9, 0.9, 0.0, 0.9], - )?; - - self.cars.push(buffered_car); - } - } - } + for receiver in self.receivers.iter() { + receiver + .borrow_mut() + .telemetry_update(self.player_id, &telemetries)?; } } Ok(()) } - - pub fn objects(&self) -> Vec<&dyn RenderObject> { - write_log!(" =================== get objects of RFactorData ==================="); - - let mut objects: Vec<&dyn RenderObject> = Vec::new(); - - // only draw radar when player is loaded into a map - if let Some(_player_id) = &self.player_id { - // only draw radar when any car is near enough - if !self.cars.is_empty() { - if let Some(background) = &self.background { - objects.push(background); - } - - for other_player_cars in &self.cars { - objects.push(other_player_cars); - } - - objects.push(&self.player_car); - } - } - - objects - } -} - -#[derive(Clone)] -struct RadarObject { - descriptor_set: Arc, - - // uniform buffer - color_buffer: Arc>, - - // vertex buffer - position_buffer: Arc>, -} - -impl RadarObject { - fn new( - device: Arc, - descriptor_layout: &Arc, - positions: [PositionOnlyVertex; 6], - color: [f32; 4], - ) -> Result { - let color_buffer = Buffer::builder() - .set_usage(VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT) - .set_memory_usage(MemoryUsage::CpuOnly) - .set_data(&color) - .build(device.clone())?; - - let position_buffer = Buffer::builder() - .set_usage(VK_BUFFER_USAGE_VERTEX_BUFFER_BIT) - .set_memory_usage(MemoryUsage::CpuOnly) - .set_data(&positions) - .build(device.clone())?; - - let descriptor_pool = DescriptorPool::builder() - .set_layout(descriptor_layout.clone()) - .build(device.clone())?; - - let descriptor_set = descriptor_pool.prepare_set().allocate()?; - descriptor_set.update(&[DescriptorWrite::uniform_buffers(0, &[&color_buffer])])?; - - Ok(Self { - descriptor_set, - color_buffer, - position_buffer, - }) - } - - pub fn update( - &self, - - ortho: Matrix4, - offset: Vector2, - player_rotation: impl Into>, - rotation: impl Into>, - radar_center: Vector2, - car_width: f32, - car_height: f32, - color: [f32; 4], - ) -> Result<()> { - self.position_buffer - .fill(&RFactorData::create_car_vertices( - ortho - * Matrix4::from_translation(radar_center.extend(0.0)) - * Matrix4::from_angle_z(-player_rotation.into()) - * Matrix4::from_translation(offset.extend(0.0)) - * Matrix4::from_angle_z(rotation.into()), - car_width, - car_height, - ))?; - self.color_buffer.fill(&color) - } -} - -impl RenderObject for RadarObject { - fn descriptor(&self) -> &Arc { - &self.descriptor_set - } - - fn buffer(&self) -> &Arc> { - &self.position_buffer - } -} - -struct CarPosition { - pub position: Vector3, - pub rotation: Rad, -} - -impl CarPosition { - fn new(position: Vector3, orientation: [Vector3; 3]) -> Self { - Self { - position, - rotation: Rad(orientation[2].x.atan2(orientation[2].z)), - } - } -} - -impl Default for CarPosition { - fn default() -> Self { - Self { - position: vec3(0.0, 0.0, 0.0), - rotation: Rad(0.0), - } - } -} - -const fn matrix4_from_diagonal(diagonal: Vector3) -> Matrix4 { - Matrix4::from_cols( - vec4(diagonal.x, 0.0, 0.0, 0.0), - vec4(0.0, diagonal.y, 0.0, 0.0), - vec4(0.0, 0.0, diagonal.z, 0.0), - vec4(0.0, 0.0, 0.0, 1.0), - ) } From cab4701289957fe0bf29bbf637d2d98a2f1e0780 Mon Sep 17 00:00:00 2001 From: hodasemi Date: Tue, 17 Jan 2023 15:44:11 +0100 Subject: [PATCH 03/16] Add watermark --- .vscode/tasks.json | 5 +- font.png | Bin 0 -> 62608 bytes src/overlay/elements/mod.rs | 2 + src/overlay/elements/pedals.rs | 34 +++++--- src/overlay/elements/radar.rs | 53 ++++-------- src/overlay/elements/ui_files/gui.xsd | 9 ++ src/overlay/elements/ui_files/pedals.xml | 15 ++-- src/overlay/elements/ui_files/watermark.xml | 8 ++ src/overlay/elements/watermark.rs | 45 ++++++++++ src/overlay/mod.rs | 90 +++++++++++++++----- src/overlay/rendering.rs | 5 ++ 11 files changed, 191 insertions(+), 75 deletions(-) create mode 100644 font.png create mode 100644 src/overlay/elements/ui_files/watermark.xml create mode 100644 src/overlay/elements/watermark.rs diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 9e98ff5..ea777d8 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -4,11 +4,14 @@ { "type": "cargo", "command": "build", + "args": [ + "--release" + ], "problemMatcher": [ "$rustc" ], "group": "build", - "label": "rust: cargo build --release" + "label": "rust: cargo build" }, { "type": "shell", diff --git a/font.png b/font.png new file mode 100644 index 0000000000000000000000000000000000000000..d10f072d855c273bd7e762ea5475857df4f2179f GIT binary patch literal 62608 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

RSaj|#i^nGocW5wGUFB| z(&%Fw2(b)OUWI{c0rSvX0hOQH=*lQL=C#uXAKw;UHT;3XgOEeKzn3!+HUaYAFTj!X zUi!6z`BuUKC<<>yiy2Jik8bf8iYu8<)bNbo;5OVfV+tX0%-fg@OW%F-KP$rIYdI7q zHu?YRYnS)M_kip8X+eO~)~D;Iw>L90GbJC?L%>lnNqGLy3G5?KywVm4M9FEG6y=f* zCO;S%8Tm#FU`-)(3zX5*UA?;e!=TEvhv8*UZE>+=QY%Zs7Xl3SM!Z)?3Lc6u*c`{S z8_|>zzCx-lNh=Eo8~+!i9DB*m_g4Sa=b(C6&kMa=2qta`V9xn)>jt!d zj9wv3taGaPUk==R3~5CxR0#OZ&@gE`nThU(-EU;|FO>XAg^&cEfdYVRjX_E`MNI6R zm_k~@Di6?iMwVz2geXDoSl7|e(M{aX%E?(<&Po|RRsB3(30n<#cFp@Rr|NxIjuqn| z|G6$&fAaTvDC0dcg)*0 zy?6uzN$}v$J_CjH_U;MvI+dBT)&N}v2)W~+BvIo;wt2zg373`WqWhTTuw%m51vf94 zv2kU!DTcIec%u)@;unrYnPnv>_+fsc<`f368SHuju#f#ToLy&gOZ;d|Y;9LvMWwo* z(0h+De1}Vb(a=Jt=L@dXUEKK0<%`u{8ZBbvGMYf%=%7AoN;Gs}VzR)*Ke*k%YB+R% zbXjE;dtJr5OhXOA;RWP1NlxCAtcJn!J1#8d46y%n%1$>^y~;9+H>oCxK=ecp-a9j5 zbokjH<3tDA=|lvyvAmL!uV%ZlipqjY894~mXyYZRwve_tJ-*p7bv{n*Dx)tetlz_7 z867fLaXNwLE7^SxuzNWaux!El4p;rjl_g$7oqk|B#WgiwQJz~f61MD8raW;1bas&t zOGuyARa4`>;`88vDKPOqv(k07^getKXD!mL1u_H?yx~%dJDgGz9BJJUO+M4#f8}d_ z{z~ZPXc3M<5e-mca(GI8)1eTg&B?KZPT6uidJ~l8GZ-DEse0;X->~Sw1=hN{b155pE^ZxXT4&2Wi7Br z^!V!s`_0MfmrlW(PbQl{PJC9)y;sMTf^s5`XopxC5Kc)aOBa{1US&aevx>pP) z5xU;NW4}6cjLC;)!uVx1%e)$|c z-kU2V&77vP=b1FJ@$ZBON6+JG3AMqd*5~1$vR-^VZ^qWWVuhOXLn!FPtdG58j!93u zYeT0MW0BzGj0s6>Umh$lFemw$W&6ASrIN($LfQ&NB=BLKSd_K=;mu(1Ya??su^QIK zl42zar=13VPX|HULdy2aA=3bb-~Y`5_}!I^&Fk%vB_y~0iu=m5xMY>otn|=jQvANv z<(qH1l_m<+#r}?mp>831J4IiL7+A2p>mOOrZEdy&%~>kpBS`(*ua#H5A61iNl~ZyN zE9l@N>9W)L2!u@O9>}{XBMphkHN+;J$Be5To{9#em6Mm!RI$VHZ(OqA&7y<;oj6^O z!DY$3SMjD+imVrq&ehrk%Sj1*#JzqYC1tZj-wuDbg}@e#rgHpR{L?f<=rnGOIE35I z%q~)C;x>dZdg)6n0@^kS&30(fORx!{7d_+m&ncuU-r1)6+d^plVHu;R1-%cp8{_0c z%%2fAtMw5uXx{oTPe0PnPVMDy@dlOb(6;fba&9?swEL(0(bbSqpC+*z;uwho$%W8OcWajWmzaFC`db} z-T>qU{m=+vQ?q@sYROgmzF|s3;#bJIUg=4@R0%&Qh+%`anZZ}b`pURzJLy5z13H1& zWf%^J3W*J`VobbqS8xE*;l; zk`ZD2PDf!tWb^Hs>Jm5=Z!$lv854OcdzI#nw+{YBAHA6jwix}_)f$uZtU(9uEy4Vi z=LxspWaM;)h3~Udr-X}$xJ}xc!vd<07SYL_+t)hzwhA9!kswu1_%${#iZ}G-<6HF@ z)7V*LYM@I&IQNj zW#+kzqj$d>y>M71@j85X9VIT0GAMmc-2$DhL}ke6#`Kd}t~-!pQS0{A`FkWd2VrgN z<7*+0ZtVWs^tuTNCb==;Nx#-&OUAywT6fK* zCuNyMM_z0oj?es;69Fc2v_`hB#NwFv!LB-SXUtNS+I;j0f!A77fGG(v@v~SvuFHt^ z;oa?bck$jsp5k!d`%(7`MKv9rw=$REAdDWn;RQkT*=G|% z;QMkilRjyY-}SzD9fYy8vctJl&dA9R+@(h)Gc06JvsS`TeR6@nZtMkJ1UWi!8ewCf zh0@PWl3)F&`SAVMrsA5=+d4Z9oBM^GQJ!zV>-YT08ou$vPfWibbb}77+`YqE!=YjX zfnEqyW->1=hyF^Us-mf?WL%(Iq2a}up&jHb=N$0CHvU@g)JaL0jT4D}Mr2K>gNO$9v)Ngq$Q(6djfmMA!**Un5&}%?X z@2@!jvlTbTx{ZD4D|umY4_HfR_hitZ@=n18T4O6o(^_WIu5T-n8J}c#1#tX57${Dt z@aS?7?kfEJaMVByhGydt-2y)&)sNL_22i$x1fHqvo)59bowlIuXerd?9aoFp3&Uy$ zv5eVimWa^OEcMJLYdEn8e?Vgm@~eSQKgzT8*?m*scN0z;UXLichHZ`e3B>XTMa|NkUDn&L#oUA!X>3WUJ?zz5o3Zr#4pa&%O+5v$2d8cjQrr@_FB!#A|XRkP(&xVnYi z3;8&Qb`OoAa|Lc*Z{<=Sdgwq?`~n#>GoIvOw}#)lz$PBP6ij|`-hBv%>p0jmkUbNg z$Hmu6Tp4_&79LYvW8Hz3P?j>?{>HiR3vPIFzo(G2ez$|QNRTvjOjBY~yfS4CPGStY z_0mCU^7Plcg{G}`c;A-K6#d{3WmVROVS&{bZsFv5Byc(sd0-^5 z@ry`s!hhx>dd%NP`>+E?Z|g_;n{8@|vFa7H8wWRCuPx70=JWo6UT9eq0;FlSZYlbt2=}@L zX*~HA#Pums7*DwV`_Vd^B*NW~t(Nq7*AL*cd;1S6zxRV*Yf2**sjo&D-LiGzFNxiA zsrLCgb{ZFAcypq5;>>WDgQ*`EPaU)iVnfGRPyb04*=o(St}TKdJD_?YHFY^fmfou_lX{;xJ>_NV4%-}RL}oC1Yt8Mft!GB5)u+hoqP>q zpdcqF#~{pysZR@|n7{mgaIzVqPkv%z(G)*AnmS5~iwzF7(8s*Gxs#XFqsJhgz%J#s z{!p#|I^qqzguR^;>02H&9c>a0B&nLrU`otAUjw)EAs2@;je_Iw(h{5HSaHU>jZ(76 z8<@CinwqOYYexTz$v)cw=V6QKxNcXWL^>(S88bleDqQCCo#QIVJVQ}3Xk~fQAA0-0 zK=XKLS5J?VM>lyBPEu$IE4~l@GX&q4M4Vidwx#ET_+Kn|5ZqII1;hfd7Xf&~?db1+ zHet$jdy?P^;;{9v7r4X+DsP}@JIq%Il5mzFQ1(S%1|h|X9`FJ?`x!oD^pYhLFhI=W zY&*&Umy+-az&PfQxHa1Q@+p+zoD#30-&o zf!9E%Sb0?aNkDD5>NuUN2t8u3^H7K9Tm_RH(VJWpsgrfOF$ za#S%q6rWsaufMq$TL}XwJ=idr*;Y{pwFkO`xy4+D-ei3XPW z+-Ci$^`Jh1u55fL>*z2oQgDU^r#h|rsqU)kp<>ZjaLO1EtkL9}Dz@c6L_``E+`!7 zWmpuu;mk*SK{jG3gKU-p&O!`j6k~8%Omc!ya3rZX>0MNvvxp)Q88`I(-aDDFxokaS zAE7SR(D?waf{6Tj*FV!&uLwX1>%V?YCj8*Mv$Iob%4LxrqnyFB zg3-&G`T_rfk}^4|-vdl=%i(WUG(j=|W+#25Z%&a-ZsNZZ!r?pY6YV^7fq`}LhnjN1&9im+i{w#=>88DHBQC{HU zKMrFU(msZpv<1^iz0PZD-!uSDIIykSDKhj`p}0U3Jx|B zaX+-3D^Y*{l65l;LfBmif`Ug=U?J++**&AaeGhg6*jI%JCQ7Q>APe$_PX8CNg@r|7 zP0iY&Q`g_`rH4VOV=3`aVm5BiwcUhNp8_`n4UHLHHSp7Xtx9a@WNgYRaH{f7D6@ix zfiWJSuf;2F|J;mk<=eR_&s9vAesUWE2>)y1H-keDuuu9wci@-pDX^bUIqa9Ub>2hz zA4-Lrl<&i9zo>DHiw4lERF8eOF$%CNA?JT~e%~coo@H*7!Y5k~jwl6pXV`_iS0N$RtD!NA+dJ{+L=Nh1dyA) z$UU=Je4s~8Lx7w?izL^X1OZ~wWBrFT5keX&ni^dXlQzyrYc^kaw;uWR3AuLTVb*Z& z>M!^NY;p4A&$2R}-JZP1y5cMw@ziY<@`#%&B46MBd#UR|sb(fCkT5hqQU7}GZThNi zt*eSt=a1wms~TY4RfdZFMAJ1c&z;u%sdcv8b>V4&>5+Hv6#DkAdij-^(_XJJ_X@Tc z$zKlLo?lKlwTGjyDooM+=U|yE=A0Yuc&7K+Ca(N7RKk~tdZ@> z8Yj6!IqO0_T_?XnPE)2&FvpLoD-_jv6WmB0Mrg)VPUNIl{WS72Q3BlDdjC)yFXo{w z>di0SOmYLehU@G>_*vNnV59`c zwT02ACp$gK-B5Kp+P~xI_*z!rCy6=#r*?nb0waqM1^TTdSV-I)9LtTzW{ZO<2C&>`qX_ z1AvQx2-=EFFlUH9M8#EO@si_a`21ENkuZ+qO`o=G1Bn_gcK)x&s*ZHq`IjnBxE5ys z%h#1#L$+c(aM;kLMNpko=L}dAhc3oeR$X?@zsf|BoSb6lj@ldgO`_K$PfDRUXzO<$ z)2qJ{s-UCg5oJlBqd`hOR^53v6L+#GL+reE3!)r z*&<~(mY9)sBodm)*s>;M%f2sT{?|Ou`~Uuad%wJW=;+8XW`6hF_qCkYd7igX8Nz>y zlIB{q2^h-;Y>yEbJK6y3vmeop+mFfAJ>Nb@x&{47%)}dSRDuA}c>bJguGyjCl2O+o zw>|Kfs>f)~6^CI}YQ>7ww77Vr968b~?2}PmMIO(NxUEii-8jWw6$m-H&wq6B<9JmC z0OCcVw?lrxI4Trc!l1>Q7|5Lb39Ti3N_vOc6e9Y!vWN; zbejuDWdY}_{8s6|B)XiK7;ok0_XbvQ947oR zN^jF;6=Y;i^tB{B-!6V~Sz=j+6zq;efDAfm(B8p8)HU`5x4u6#psmA}!=H{`+fdBZ zQydPZEX&b$mJ17M=CoDwN-N9A!F7rKeJPh_R*{EUSZ@AriRoN1Ae3{3hXuM1JbPcdd z?#`)^!DB$#xRdXo(#8}zCxq2f6+p?&InX)5S$rPwG7!%WW9umC8=F3-@6fCiTwSgEZJc7E$J)!U_h-k*99|EQlI~pC;Z5YZPF< zzet-s?b-xG{?}b9^VYz_9+^(%@I8xAfoz{4I|Qa2IV_4B$^tPrCWfVleJDf;I_D)# zz4PJvFl=neM3p-xAisKJc?-=+XH8v|mdo$tO z2Tq|N+^Uyku&Am4%AG~TC9u5)btVm%7IXzE&>Eo1|1noY;*M%;ylHTdt_sTuRB8a2 z>8*tA)o7sl7grqr)${UK`?dWbD6@gib>Q__QaEog7;{;>;KCO9&_}lqVmJ*g ziqz+M@5B&tUxs?7-Vd?;awv~BRZO~F-xpblEtEo3K=K!y_*V_0>x`aC?k)yM(wBGhnd#`#K@-Ws;Q(SY&%YwuxP@LfSv+gFyFE{015IFuok7NroGID!oJ{q<+g`kQ z!Lt4P+x#O0f@hq5M}VUK+4k_Cr<0q_b~l=?W4j;HO?CjXqPw)5XBv;3e*(NI0VD1% z=*A(~vaqpHw%HiyQ|z&2F9Y2C0~QlgQ$nYLM<&Ows`=WPYnVs>5ewUx+oAN7&yElS zzaR+-zH_28^cb_bxq0I=o%Ve$Q46VYqqMASUj`+H6{v>QuQ}?&CJQV`K^!sqMrVq_ zi%TjD!~nMk1CCq*4-8`?qsO=uJe0{$6p2w>44DwHi91qu@Z4{q&4eWU@5Cf#-N(j8 zNK#Jjq>TTr9KDPMxh*~j*r4@5e}qwx*@n_<@YYmUPpRhox|jwiz7 zMf$^hywcys_Xge{uV#YP3%wz?u;9Dz zp}-oXp%!nw296Yj+$u1sVDp$82;`&e1xRZH+d^hUPF{^U7it+AyHg3G1*1pkS{x1+ zB@_Q&=j|pal@G#m=sN&iP|BTy(?}+hr}oKYXPtx27-+@1%k8rB)yi3jGj-qY#Hc;* zOI+}syK8J52&|fe`guN2`L0Q1qs7%_ui_>&)=Tuxj?l`$)Y+k)S%j7GN>h|su7|A_T)IJ0oI5kC73?qoXGF_cv#+lK?7 z^b#M&r*z{o3&EIati5xG^$oNthYTMg#-MsSR0}Y@Suk`s%vXe zt87Xw`knje3usC=6+0rsXKHvXmvM#dg?Yq?aznmwD%DO*QbSE8GY)+kUkTAa3cKkztwAeuo zZat^0V;9iJi8&HlDwdk3K3<6^A$$vumr=7QU>9m`T$XJM%F@}APm4`)aegI5-e#ph0IRs_PXqnEJ|BDcWB5gGfodO)Exll^nr?Q_*VBT`VeTXyMguN$<081iOR-| zK2Ekf%sR;lj5}*9D$3;2fx2ek1V2&ifn4Wc^MbIbjwsyqjsYG%GpnRKk1&R~~6lP8k1?9@4Ocm9~KZ+Xp% z`irkWJ)@YWlWsTN=n0JVq>Ai(tIOb?!}s-PWCag-`%Sp*L@V6)Jginxam3R zWb)N}`{BBkJJ8Gx!Q=;BT7ef;lc}rubMj@aP7QTpd9$u9btN&^pe1kJTe0cROjGg4 zj}I&4&00T*Ugt-5*g`SX+Bz?tG_Jk%4NOP#yVjLnPL!rD(j14`2b~WypxeFs2u?c6 z2-s`g8yg!Yn&t72uN6R%k6Wo*7Dy(W00~T&4rlfs>bU!U(`N|G!b&2W)FGeXhQldR z+W8;ErL9?MP^e;(hp;OGJdt5P``*V}JMK93CK?(%o-OxR$cLq!tsvkpQW(^ zxG+yS$9sTQ6dTFyKyjjZCnaqT3x z`7W-S0)VT&1~s*46O1zMYf_cK>IYgw|ZZ>I%k(hDvRtv57 zphXVh8}*E0i^eilM_otLYF4QIxVaCwM+{|;SOX}-#_{8u-WqB8BIXq<+lsg%*SUe6 zRH=#gX5Zbix4Sh3?+UP3c~Vo*W8n#f;L18-!CIH~dhf#UY+re{3<8W8p-t{7AfJJo zbY52W4Rnva`;miZ2G4|iPq{hl)g{nia(OD=SyzSR+ST9x=$f0|m(eGqV0J;A)#K3# zmGa(#e+!3j^QVB|O*u^5{qB?eYv62-=?Ab-ue2+du43&s;f6DH-zlBapq5=oGa23O z@IA|-NxJ)44OGp^qe$X+QtX@dSAsNovP=b7?%w z^m6k9y_9^;=%#0daX(vXID)&A6pN%@e6}1XWaofYrFAA{ir^oKdN_X&rHn8mRdW(2 zvY5^1k?Rz2H{pZkYcGg;?^{fcD^bHs_NXn%ha(tE7{e0OJBevn-*CzC5# z7w2YX^c{t4dLFIpzQeSwWZ@Z$D=Y2$v&5})W6`cl$lzX4oWyl;_qhSP$$@UiOeO<( za2OSVOIcToO(`avsrN6m;QU1F42z|D$pneImYnZ9{Wbu_g~)yJ$!=ILv##ylD~PCS zR?F}@;^@ldIq_MYyqr$&5KZ|IJtw6jdbx38Qk8IKAmU$LsemQOBa!@P)68ligTvvL zHfZh@h12r`4lfKk1;=0JTx`OM!-WLG#2x0KrzXb6?A|UA)599?Uzs>P*~+<{8{qMP zv99gCyp8iNiLUFIx_{`!XM0aiBh!zME%A~^4En6Ekv?Y5joa(}-2}=-K2@DdVd~TG zIClNM^0<9p=Ucr%HkueC|OO7J*c%*cLpi+7VubmM*hPiua>l6cej3`=-jo z*lGTYU{W{oxOeSHhw`CJDUune9Vm~tBGQ$I-hT_AFCV%nL29QX;*fM$mVAClJZZGh^UEB2DUwibmM-D z?Z%On(A$H39B@a?l&}v_y>o%O>^9XQ$kiaAGAJ6O79wVrov;AiyHmdqSulri-8Xr6 zPfrzXVq&6)TF^vdeqcds5s_HZa#?meL*B zqnje(`FLt-YQ8{gZ6FkzE2FifQ!c?c5vwmYNu=eG7iaeO3FCzJYB2A+`R?}Jkd%axW94n7@Tso; zh73+ToZnHbBTd*{MQ_u`FOxDw9vH}UJMSeVO_Zz3cDRA?~KACQwNL4(D>qDEmTh0q!koz#y&`H){I{^cc+P0gb+@&`k zRcsbO7+6_b+>V?Sz+M=Cm=zR__!GmU2ss0+PrxImEGjsggIKRO`!_*&#| z2z=VPK3jq{UspTJw#G`qcHLZzojP!*{BhoBKXXuh--JI1Yi4${w$@kBC26|o%rV>r zkQEx7J6aN?xwg8RtNMax#q79mKd556SL-K3Up|eZCcL6T>LAFmnukevLVT?HuqS=p zk?|6#v55YK1zR46jV2u1TtH1uO45^;m;a&~)%VbZ@dkFPQ!_+Be#hK%gi&W3_sYdJ z9MTZrG(*Ii*m@D(sjNaz_&j^X^rsr{<-XR4ZQHSVL$O*Bnb1CSe**y2o*K?P^LpsK zE|~|WaqA&d3oE{of$?CuoeYpJ*8|~uxgPl))6&`$chUhF{@d6$fG=K=kSOLqJ&jF~ z>6?HN%$Lc@$-e95;XRm2rW?Yzqskf6F&(>DyBdvVnaYx*?O_Ye=tA^_wK*KSKtn{P z<7A72YmIDWOn3Im(L#7`V7(Q;)9g_>3bfKbN2`?6AG+EpLtrAGnsvEdP{$ z^28FVr%Z}NE`%yWUUKvhoFA)I2ZqsQOOUt*pHP^9PSSZf_F}&Kh)#<`^ zQo+Ya#*JpE_8GAi;alV)^8U#@*i>IjoZ8u3zVG0wEB@d@mOqRR-SL{^qfJwk?8cjZ zU+Z5K&d|Gz9A2F_8kr3nu0?o6c6_ydtFlRHNd7$X2?>k`OPkxP@FA7p*(K z!<(xg*^c?HsLOQ%iHH^uB3IaE`c>A}pa8r;X=-lP=7&8hn(> zWY6Lyj7a{lr}&BFQa3iy^M@KYGJCNJE6j$lWBG$BL~L-m=4TG=z4rBhMNI;xhROEw z={kbs2;-F@(-}qsCmUb9q>GCNy*7~dDC1g_Ah6F_#IC9p|VGI;C7s{ICvOR zuD%qo9d%*&VH}QgRMpD`ZVocuR=4$gw@IO48nr0@%I@qRGuY_FCr;Nj2Q=OW;xhGl z!E57i_P0k+Z@4M-a`E-{av`LrSfRFnchd*X{^19f5%l1-vT_`CVon(PIHb!TE%FPO zY|x2%5H_M+Ss`&Dy_OzOUd4fT9}m370J!~y6bo)9Msp7KvoRrHq+nnBExa*<{yX85 z=AddR1r2)cVK47+E z>d6k`0Xbx0^4vFu-fK<8Cw9UG5bYPZyzQctqzQd@W5!E4U?Fu60y5;fmm<|z)&Pz3 zOrA4%63qBTsR)>Q$xlwz&fuk(g#l!W<5UE?Tb1LtT``5b_Xz>r3a|2E;?#q!)G zKhIT&-}#G+i~k~A^kNzblb}1Pdq=lcXHHcF+`gJj$V)&Lx@l`^Yd_%tfUCWECDinl zmJnsNWa}SPiUc$pe1(1|n1T%a{b_w|sYn?|z~W|VU&KyLO$EC|x?cW+K=dwz!6r#b zId{A5dT?-XL`6kK*8A$YxEV`(`>^rEDR^pQF)^`RDA1PDj~N6jf;~ANa)+VV9~Amx zLKn;pdXz7VAmFMl4W0G=7O7Ij*HB);ewdz|N4PRi`HjC>HDk}od2_fof*(OWRZ>~m qx8nO)48XMi|L6bV3OMM|4jkNjar^A6_vh&!L;WU7rBKP@>Hh$bg|Mpt literal 0 HcmV?d00001 diff --git a/src/overlay/elements/mod.rs b/src/overlay/elements/mod.rs index 038032f..91e69c3 100644 --- a/src/overlay/elements/mod.rs +++ b/src/overlay/elements/mod.rs @@ -1,6 +1,8 @@ mod pedals; mod pipeline; mod radar; +mod watermark; pub use pedals::*; pub use radar::*; +pub use watermark::*; diff --git a/src/overlay/elements/pedals.rs b/src/overlay/elements/pedals.rs index 12f970a..1e326ac 100644 --- a/src/overlay/elements/pedals.rs +++ b/src/overlay/elements/pedals.rs @@ -11,6 +11,8 @@ pub struct Pedals { brake: Arc, throttle: Arc, + fuel: Arc