use parking_lot::Mutex;
use std::error::Error;
use std::fmt::{Display, Formatter};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::thread;
use tracing::{event, Level};
use vulkano::buffer::{BufferContents, Subbuffer};
use vulkano::command_buffer::{AutoCommandBufferBuilder, PrimaryAutoCommandBuffer};
use vulkano::pipeline::graphics::color_blend::{AttachmentBlend, ColorBlendAttachmentState, ColorBlendState};
use vulkano::pipeline::graphics::input_assembly::InputAssemblyState;
use vulkano::pipeline::graphics::multisample::MultisampleState;
use vulkano::pipeline::graphics::rasterization::{CullMode, RasterizationState};
use vulkano::pipeline::graphics::vertex_input::{Vertex, VertexDefinition};
use vulkano::pipeline::graphics::GraphicsPipelineCreateInfo;
use vulkano::pipeline::layout::PipelineDescriptorSetLayoutCreateInfo;
use vulkano::pipeline::{Pipeline, PipelineBindPoint, PipelineLayout, PipelineShaderStageCreateInfo};
use vulkano::render_pass::Subpass;
use vulkano::Validated;
use winit::event::ElementState;
use winit::keyboard::{KeyCode, PhysicalKey};
use crate::console::log::ConsoleLogRecord;
use crate::console::Console;
use crate::event::{Event, EventHandler};
use crate::gui::input::{InputDelegateEvent, InputDelegateLock};
use crate::gui::window::winit::WindowHandle;
use crate::render::font::compositor::FontCompositor;
use crate::render::font::layout::{LayoutAlignment, LayoutHorizontalAlignment, LayoutVerticalAlignment, TextLayout, TextLayoutCreateInfo};
use crate::render::font::sheet::{FontSheet, FontSheetRenderer, UnicodeFontSheetMap};
use crate::render::font::size::{FontSize, FontSizer};
use crate::render::font::Font;
use crate::render::pipeline::{EngineGraphicsPipeline, VKGraphicsPipelineSource, ViewportType};
use crate::render::render_pass::EngineRenderHandler;
use crate::render::uniform::Uniform;
use crate::render::{FlatVertex, RenderTarget, Renderer, RendererStaleEvent, VulkanoError};
use crate::resource::{Resource, ResourceLoadError};
use crate::NonExhaustive;
use crate::render::descriptor_set::EngineDescriptorSet;
#[derive(Debug, Clone)]
pub enum ConsoleGuiAlignment {
Top,
Bottom,
}
#[derive(Debug, Clone)]
pub struct ConsoleGuiLayout {
pub size: f32,
pub alignment: ConsoleGuiAlignment,
pub prompt_alignment: ConsoleGuiAlignment,
pub font_size: FontSize,
pub _ne: NonExhaustive,
}
impl Default for ConsoleGuiLayout {
fn default() -> Self {
Self {
size: 0.4,
alignment: ConsoleGuiAlignment::Top,
prompt_alignment: ConsoleGuiAlignment::Bottom,
font_size: FontSize::default(),
_ne: NonExhaustive(()),
}
}
}
impl ConsoleGuiLayout {
pub fn bounds(&self) -> (glm::TVec2<f32>, glm::TVec2<f32>) {
match self.alignment {
ConsoleGuiAlignment::Top => (
glm::vec2(0.0, 0.0),
glm::vec2(1.0, self.size),
),
ConsoleGuiAlignment::Bottom => (
glm::vec2(0.0, 1.0 - self.size),
glm::vec2(1.0, 1.0)
),
}
}
pub fn screen_bounds(&self) -> (glm::TVec2<f32>, glm::TVec2<f32>) {
let (min, max) = self.bounds();
(glm::vec2(
min.x * 2.0 - 1.0,
min.y * 2.0 - 1.0,
),
glm::vec2(
max.x * 2.0 - 1.0,
max.y * 2.0 - 1.0,
))
}
}
#[derive(BufferContents, Debug, Clone)]
#[repr(C)]
pub struct BackgroundSolid {
pub color: glm::TVec3<f32>,
pub alpha: f32,
}
impl Default for BackgroundSolid {
fn default() -> Self {
Self {
color: glm::vec3(0.1, 0.1, 0.1),
alpha: 0.8,
}
}
}
#[derive(Debug, Clone)]
pub enum ConsoleGuiBackground {
Solid(BackgroundSolid),
}
impl Default for ConsoleGuiBackground {
fn default() -> Self {
ConsoleGuiBackground::Solid(BackgroundSolid::default())
}
}
pub struct ConsoleGui {
console: Arc<Console>,
visible: AtomicBool,
layout: ConsoleGuiLayout,
background: ConsoleGuiBackground,
#[allow(unused)]
font: Arc<Resource<dyn Font>>,
font_sheet: Arc<Resource<FontSheet>>,
text_log: Mutex<TextLayout>,
text_prompt: Mutex<TextLayout>,
}
impl ConsoleGui {
#[inline]
pub fn builder(console: Arc<Console>) -> ConsoleGuiBuilder { ConsoleGuiBuilder::new(console) }
fn create_text_layouts(
target: &Arc<dyn RenderTarget>,
layout: &ConsoleGuiLayout,
font_sizer: &Arc<dyn FontSizer>,
) -> (TextLayout, TextLayout) {
let screen_size = target.extent().cast::<f32>();
let (bounds_min, bounds_max) = layout.bounds();
let scaled_font_size = layout.font_size.scale(target.scale_factor() as f32);
let prompt_height = font_sizer.v_advance(scaled_font_size);
let prompt_padding = font_sizer.line_gap(scaled_font_size) * 2.0;
let log_height = ((bounds_max.y - bounds_min.y) * screen_size.y) - (prompt_height + prompt_padding);
let start_offset = glm::vec2(
bounds_min.x * screen_size.x,
bounds_min.y * screen_size.y,
);
let log_offset = match &layout.prompt_alignment {
ConsoleGuiAlignment::Top => glm::vec2(
start_offset.x,
start_offset.y + prompt_height + prompt_padding,
),
ConsoleGuiAlignment::Bottom => start_offset.clone(),
};
let log_size = glm::vec2(screen_size.x, log_height);
let prompt_offset = match &layout.prompt_alignment {
ConsoleGuiAlignment::Top => start_offset.clone(),
ConsoleGuiAlignment::Bottom => glm::vec2(
start_offset.x,
start_offset.y + log_height + prompt_padding,
)
};
let prompt_size = glm::vec2(screen_size.x, prompt_height);
let text_log = TextLayout::for_render_target(
target,
font_sizer.clone(),
TextLayoutCreateInfo {
alignment: LayoutAlignment {
horizontal: LayoutHorizontalAlignment::Left,
vertical: match &layout.prompt_alignment {
ConsoleGuiAlignment::Top => LayoutVerticalAlignment::Top,
ConsoleGuiAlignment::Bottom => LayoutVerticalAlignment::Bottom,
},
},
size: layout.font_size,
canvas_offset: log_offset,
canvas_size: Some(log_size),
word_wrap: true,
..Default::default()
}
);
let text_prompt = TextLayout::for_render_target(
target,
font_sizer.clone(),
TextLayoutCreateInfo {
size: layout.font_size,
canvas_offset: prompt_offset,
canvas_size: Some(prompt_size),
..Default::default()
}
);
(text_log, text_prompt)
}
fn new(
console: Arc<Console>,
window_handle: &WindowHandle,
layout: ConsoleGuiLayout,
background: ConsoleGuiBackground,
font: Arc<Resource<dyn Font>>,
font_sheet: Arc<Resource<FontSheet>>,
) -> Arc<Self> {
let delegate = window_handle.input_state().create_delegate();
let (text_log, text_prompt) = Self::create_text_layouts(
window_handle.renderer().target(),
&layout,
font_sheet.read().sizer(),
);
let orig_gui = Arc::new(Self {
console,
visible: AtomicBool::new(false),
layout,
background,
font,
font_sheet,
text_log: Mutex::new(text_log),
text_prompt: Mutex::new(text_prompt),
});
window_handle.renderer().event_bus().register(Arc::downgrade(&orig_gui)).unwrap();
let gui = orig_gui.clone();
thread::spawn(move || {
let mut input_lock: Option<InputDelegateLock> = None;
delegate.start(|delegate, event| {
let consume = match &event {
InputDelegateEvent::Key(event) => {
if event.state == ElementState::Pressed {
if event.physical_key == PhysicalKey::Code(KeyCode::Backquote) {
let visible = !gui.visible.fetch_update(
Ordering::SeqCst,
Ordering::SeqCst,
|x| Some(!x),
).unwrap();
event!(Level::DEBUG, visible, "ConsoleGui toggled");
if visible {
input_lock = Some(delegate.lock(100));
} else {
input_lock = None;
}
true
} else if event.physical_key == PhysicalKey::Code(KeyCode::Enter) {
if gui.visible.load(Ordering::Relaxed) {
let cmd = {
let mut input = gui.text_prompt.lock();
let cmd = input.to_string();
input.clear();
cmd
};
let _ = gui.console.handle_command(cmd);
true
} else {
false
}
} else {
false
}
} else {
false
}
},
_ => false,
};
if !consume && gui.visible.load(Ordering::Relaxed) {
gui.text_prompt.lock().handle_input_event(&event);
}
});
});
orig_gui
}
pub fn bootstrap_renderer(self: &Arc<Self>)
-> impl FnOnce(&Arc<Renderer>, &Subpass) -> ConsoleRenderer {
let self_clone = self.clone();
|renderer, subpass| {
ConsoleRenderer::new(renderer, subpass, self_clone)
}
}
}
impl EventHandler<RendererStaleEvent> for ConsoleGui {
fn handle_event(&self, event: &mut Event<RendererStaleEvent>) {
let target = event.target();
let (mut new_text_log, mut new_text_prompt) = Self::create_text_layouts(
target,
&self.layout,
self.font_sheet.read().sizer(),
);
let mut text_log = self.text_log.lock();
let mut text_prompt = self.text_prompt.lock();
new_text_log.text_layout(&text_log);
new_text_prompt.text_layout(&text_prompt);
*text_log = new_text_log;
*text_prompt = new_text_prompt;
}
}
mod background_vert {
vulkano_shaders::shader! {
ty: "vertex",
src: r"
#version 460
layout(location = 0) in vec2 position;
void main() {
gl_Position = vec4(position, 0.0, 1.0);
}
",
}
}
mod background_solid_frag {
vulkano_shaders::shader! {
ty: "fragment",
src: r"
#version 460
layout(location = 0) out vec4 f_color;
layout(set = 0, binding = 0) uniform Background_Solid_Color_Data {
vec3 color;
float alpha;
} color;
void main() {
f_color = vec4(color.color, color.alpha);
}
",
}
}
struct ConsoleBackgroundSolidRenderer {
graphics: Arc<EngineGraphicsPipeline>,
buffer: Subbuffer<[FlatVertex]>,
descriptor_set: EngineDescriptorSet,
}
impl ConsoleBackgroundSolidRenderer {
fn new(renderer: &Arc<Renderer>, subpass: &Subpass, gui: &Arc<ConsoleGui>, bg: BackgroundSolid) -> Self {
let (min, max) = gui.layout.screen_bounds();
let buffer = FlatVertex::buffer(
renderer.device().memory_allocator().clone(),
min,
max,
);
let background_vert = background_vert::load(renderer.device().vk_device().clone())
.expect("Failed to create vertex shader module")
.entry_point("main").unwrap();
let background_frag = background_solid_frag::load(renderer.device().vk_device().clone())
.expect("Failed to create fragment shader module")
.entry_point("main").unwrap();
let vertex_input_state = Some(FlatVertex::per_vertex()
.definition(&background_vert.info().input_interface)
.unwrap());
let stages = [
PipelineShaderStageCreateInfo::new(background_vert),
PipelineShaderStageCreateInfo::new(background_frag),
];
let layout = PipelineLayout::new(
renderer.device().vk_device().clone(),
PipelineDescriptorSetLayoutCreateInfo::from_stages(&stages)
.into_pipeline_layout_create_info(renderer.device().vk_device().clone())
.unwrap(),
).unwrap();
let descriptor_layout = layout.set_layouts().get(0).unwrap().clone();
let create_info = GraphicsPipelineCreateInfo {
stages: stages.into_iter().collect(),
vertex_input_state,
subpass: Some(subpass.clone().into()),
color_blend_state: Some(ColorBlendState::with_attachment_states(
subpass.num_color_attachments(),
ColorBlendAttachmentState {
blend: Some(AttachmentBlend::alpha()),
..Default::default()
},
)),
input_assembly_state: Some(InputAssemblyState::default()),
rasterization_state: Some(RasterizationState {
cull_mode: CullMode::Back,
..Default::default()
}),
multisample_state: Some(MultisampleState::default()),
..GraphicsPipelineCreateInfo::layout(layout)
};
let graphics = EngineGraphicsPipeline::new(
renderer,
create_info,
ViewportType::TopLeft,
);
let color = Arc::new(Uniform::new(
renderer.device().clone(),
renderer.frame_manager(),
bg,
).unwrap());
let descriptor_set = EngineDescriptorSet::builder(renderer.clone())
.layout(descriptor_layout)
.descriptor_source(0, color)
.build();
Self {
graphics,
buffer,
descriptor_set,
}
}
fn build_commands(
&self,
builder: &mut AutoCommandBufferBuilder<PrimaryAutoCommandBuffer>,
) -> Result<(), Validated<VulkanoError>> {
let graphics = self.graphics.vk_graphics();
builder
.bind_pipeline_graphics(graphics.clone())?
.bind_descriptor_sets(
PipelineBindPoint::Graphics,
graphics.layout().clone(),
0,
self.descriptor_set.descriptor_set().map_err(VulkanoError::from_validated)?,
)?
.bind_vertex_buffers(0, self.buffer.clone())?
.draw(
self.buffer.len() as u32,
1,
0,
0,
)?;
Ok(())
}
}
enum ConsoleBackgroundRenderer {
Solid(ConsoleBackgroundSolidRenderer),
}
impl ConsoleBackgroundRenderer {
fn new(renderer: &Arc<Renderer>, subpass: &Subpass, gui: &Arc<ConsoleGui>) -> Self {
match &gui.background {
ConsoleGuiBackground::Solid(bg) => ConsoleBackgroundRenderer::Solid(
ConsoleBackgroundSolidRenderer::new(renderer, subpass, gui, bg.clone())
),
}
}
fn build_commands(
&self,
builder: &mut AutoCommandBufferBuilder<PrimaryAutoCommandBuffer>,
) -> Result<(), Validated<VulkanoError>> {
match self {
ConsoleBackgroundRenderer::Solid(renderer)
=> renderer.build_commands(builder),
}
}
}
pub struct ConsoleRenderer {
gui: Arc<ConsoleGui>,
background: ConsoleBackgroundRenderer,
font_compositor: FontCompositor,
}
impl ConsoleRenderer {
pub fn new(renderer: &Arc<Renderer>, subpass: &Subpass, gui: Arc<ConsoleGui>) -> Self {
let background = ConsoleBackgroundRenderer::new(renderer, subpass, &gui);
let font_compositor = FontCompositor::new(
FontSheetRenderer::new(
renderer,
subpass,
gui.font_sheet.clone(),
),
);
Self {
gui,
background,
font_compositor,
}
}
fn add_record_to_layout(record: &ConsoleLogRecord, layout: &mut TextLayout) {
let gray = glm::vec3(0.5, 0.5, 0.5);
let level_color = match record.level {
Level::TRACE => glm::vec3(0.8, 0.5, 0.75),
Level::DEBUG => glm::vec3(0.2, 0.7, 1.0),
Level::INFO => glm::vec3(0.4, 0.8, 0.4),
Level::WARN => glm::vec3(0.85, 0.6, 0.2),
Level::ERROR => glm::vec3(0.9, 0.2, 0.2),
};
layout
.color(gray).text(&record.date_time.format("%H:%M:%S ").to_string())
.color(level_color).text(&record.level.to_string())
.color(glm::vec3(1.0, 1.0, 1.0)).text(" ").text(&record.message);
}
}
impl EngineRenderHandler for ConsoleRenderer {
fn build_commands(
&self,
builder: &mut AutoCommandBufferBuilder<PrimaryAutoCommandBuffer>,
) -> Result<(), Validated<VulkanoError>> {
if !self.gui.visible.load(Ordering::Relaxed) {
return Ok(());
}
self.background.build_commands(builder)?;
let mut log_layout = self.gui.text_log.lock();
let prompt_layout = self.gui.text_prompt.lock();
log_layout.clear();
match &self.gui.layout.prompt_alignment {
ConsoleGuiAlignment::Top => {
for record in self.gui.console.log().iter().rev() {
Self::add_record_to_layout(&record, &mut log_layout);
if log_layout.height() < log_layout.info().canvas().size.y {
log_layout.newline();
} else {
break;
}
}
},
ConsoleGuiAlignment::Bottom => {
for record in self.gui.console.log().iter().rev() {
Self::add_record_to_layout(&record, &mut log_layout);
if log_layout.height() < log_layout.info().canvas().size.y {
log_layout.move_cursor_to(glm::vec2(0, 0));
log_layout.newline();
log_layout.move_cursor_to(glm::vec2(0, 0));
} else {
break;
}
}
},
}
let mut pass = self.font_compositor.begin_pass(builder);
pass.layout(&log_layout);
pass.layout(&prompt_layout);
pass.end_pass()?;
Ok(())
}
}
#[derive(Debug)]
pub enum ConsoleGuiBuildError {
MissingWindowParam,
MissingFontParam,
LayoutSizeOutOfBounds(f32),
ResourceLoadError(ResourceLoadError),
}
impl Display for ConsoleGuiBuildError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
ConsoleGuiBuildError::MissingWindowParam
=> f.write_str(".window() must be set"),
ConsoleGuiBuildError::MissingFontParam
=> f.write_str(".font() must be set"),
ConsoleGuiBuildError::LayoutSizeOutOfBounds(size)
=> write!(f, "Size ({size}) must be within (0.0..1.0]"),
ConsoleGuiBuildError::ResourceLoadError(err)
=> err.fmt(f),
}
}
}
impl Error for ConsoleGuiBuildError {}
impl ConsoleGuiBuildError {
fn check_layout(layout: ConsoleGuiLayout) -> Result<ConsoleGuiLayout, ConsoleGuiBuildError> {
if layout.size < 0.0 || layout.size > 1.0 {
Err(ConsoleGuiBuildError::LayoutSizeOutOfBounds(layout.size))
} else {
Ok(layout)
}
}
}
pub struct ConsoleGuiBuilder {
console: Arc<Console>,
window_handle: Option<WindowHandle>,
layout: Option<ConsoleGuiLayout>,
background: Option<ConsoleGuiBackground>,
font: Option<Arc<Resource<dyn Font>>>,
}
impl ConsoleGuiBuilder {
#[inline]
pub fn new(console: Arc<Console>) -> Self {
Self {
console,
window_handle: None,
layout: None,
background: None,
font: None,
}
}
pub fn window(&mut self, window_handle: &WindowHandle) -> &mut Self {
self.window_handle = Some(window_handle.clone());
self
}
pub fn layout(&mut self, layout: ConsoleGuiLayout) -> &mut Self {
self.layout = Some(layout);
self
}
pub fn background(&mut self, background: ConsoleGuiBackground) -> &mut Self {
self.background = Some(background);
self
}
pub fn font(&mut self, font: Arc<Resource<dyn Font>>) -> &mut Self {
self.font = Some(font);
self
}
pub fn build(&self) -> Result<Arc<ConsoleGui>, ConsoleGuiBuildError> {
let console = self.console.clone();
let window_handle = self.window_handle.as_ref()
.ok_or(ConsoleGuiBuildError::MissingWindowParam)?;
let layout = ConsoleGuiBuildError::check_layout(self.layout.clone().unwrap_or_default())?;
let background = self.background.clone().unwrap_or_default();
let font = self.font.clone()
.ok_or(ConsoleGuiBuildError::MissingFontParam)?;
let font_sheet = FontSheet::from_font(
&font,
window_handle.renderer().clone(),
64.0,
Arc::new(UnicodeFontSheetMap::basic_latin()),
).map_err(ConsoleGuiBuildError::ResourceLoadError)?;
let console_gui = ConsoleGui::new(
console,
window_handle,
layout,
background,
font,
font_sheet,
);
Ok(console_gui)
}
}