mirror of
https://github.com/doukutsu-rs/doukutsu-rs
synced 2025-07-29 16:40:54 +00:00
Compare commits
3 commits
0db8ab0308
...
426f3ce4d0
Author | SHA1 | Date | |
---|---|---|---|
|
426f3ce4d0 | ||
|
86747e0230 | ||
|
fb87b3d5ab |
|
@ -51,7 +51,7 @@ exe = []
|
||||||
android = []
|
android = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
glutin = { path = "./3rdparty/glutin/glutin", optional = true }
|
#glutin = { path = "./3rdparty/glutin/glutin", optional = true }
|
||||||
#winit = { path = "./3rdparty/winit", optional = true, default_features = false, features = ["x11"] }
|
#winit = { path = "./3rdparty/winit", optional = true, default_features = false, features = ["x11"] }
|
||||||
#sdl2 = { path = "./3rdparty/rust-sdl2", optional = true, features = ["unsafe_textures", "bundled", "static-link"] }
|
#sdl2 = { path = "./3rdparty/rust-sdl2", optional = true, features = ["unsafe_textures", "bundled", "static-link"] }
|
||||||
#sdl2-sys = { path = "./3rdparty/rust-sdl2/sdl2-sys", optional = true, features = ["bundled", "static-link"] }
|
#sdl2-sys = { path = "./3rdparty/rust-sdl2/sdl2-sys", optional = true, features = ["bundled", "static-link"] }
|
||||||
|
@ -66,15 +66,15 @@ discord-rich-presence = { version = "0.2", optional = true }
|
||||||
downcast = "0.11"
|
downcast = "0.11"
|
||||||
encoding_rs = "0.8.33"
|
encoding_rs = "0.8.33"
|
||||||
fern = "0.6.2"
|
fern = "0.6.2"
|
||||||
#glutin = { git = "https://github.com/doukutsu-rs/glutin.git", rev = "2dd95f042e6e090d36f577cbea125560dd99bd27", optional = true, default_features = false, features = ["x11"] }
|
glutin = { version = "0.32.0", optional = true }
|
||||||
imgui = { git = "https://github.com/imgui-rs/imgui-rs.git", rev = "67f7f11363e62f09aa0e1288a17800e505860486" }
|
imgui = { git = "https://github.com/imgui-rs/imgui-rs.git", rev = "67f7f11363e62f09aa0e1288a17800e505860486" }
|
||||||
image = { version = "0.24", default-features = false, features = ["png", "bmp"] }
|
image = { version = "0.24", default-features = false, features = ["png", "bmp"] }
|
||||||
itertools = "0.10"
|
itertools = "0.13.0"
|
||||||
include-flate = "0.3.0"
|
include-flate = "0.3.0"
|
||||||
lazy_static = "1.4"
|
lazy_static = "1.4"
|
||||||
lewton = { version = "0.10", optional = true }
|
lewton = { version = "0.10", optional = true }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
num-derive = "0.3"
|
num-derive = "0.4"
|
||||||
num-traits = "0.2"
|
num-traits = "0.2"
|
||||||
open = "3.2"
|
open = "3.2"
|
||||||
paste = "1.0"
|
paste = "1.0"
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
|
use std::cell::RefCell;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use imgui::DrawData;
|
use imgui::DrawData;
|
||||||
|
@ -36,48 +37,65 @@ pub trait Backend {
|
||||||
pub trait BackendEventLoop {
|
pub trait BackendEventLoop {
|
||||||
fn run(&mut self, game: &mut Game, ctx: &mut Context);
|
fn run(&mut self, game: &mut Game, ctx: &mut Context);
|
||||||
|
|
||||||
fn new_renderer(&self, ctx: *mut Context) -> GameResult<Box<dyn BackendRenderer>>;
|
fn new_renderer(&self) -> GameResult<Box<dyn BackendRenderer>>;
|
||||||
|
|
||||||
fn as_any(&self) -> &dyn Any;
|
fn as_any(&self) -> &dyn Any;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait BackendRenderer {
|
pub trait BackendRenderer {
|
||||||
|
/// Human-readable name for the renderer. May return different values based on current platform or settings.
|
||||||
fn renderer_name(&self) -> String;
|
fn renderer_name(&self) -> String;
|
||||||
|
|
||||||
|
/// Clear the current render target with the specified color.
|
||||||
fn clear(&mut self, color: Color);
|
fn clear(&mut self, color: Color);
|
||||||
|
|
||||||
|
/// Present the current frame to the screen.
|
||||||
fn present(&mut self) -> GameResult;
|
fn present(&mut self) -> GameResult;
|
||||||
|
|
||||||
|
/// Sets the preferred frame swap mode.
|
||||||
fn set_swap_mode(&mut self, _mode: SwapMode) -> GameResult {
|
fn set_swap_mode(&mut self, _mode: SwapMode) -> GameResult {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prepare the renderer for drawing.
|
||||||
fn prepare_draw(&mut self, _width: f32, _height: f32) -> GameResult {
|
fn prepare_draw(&mut self, _width: f32, _height: f32) -> GameResult {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a new mutable texture with the specified dimensions.
|
||||||
fn create_texture_mutable(&mut self, width: u16, height: u16) -> GameResult<Box<dyn BackendTexture>>;
|
fn create_texture_mutable(&mut self, width: u16, height: u16) -> GameResult<Box<dyn BackendTexture>>;
|
||||||
|
|
||||||
|
/// Create a new texture with the specified dimensions and data.
|
||||||
fn create_texture(&mut self, width: u16, height: u16, data: &[u8]) -> GameResult<Box<dyn BackendTexture>>;
|
fn create_texture(&mut self, width: u16, height: u16, data: &[u8]) -> GameResult<Box<dyn BackendTexture>>;
|
||||||
|
|
||||||
|
/// Set the current blend mode.
|
||||||
fn set_blend_mode(&mut self, blend: BlendMode) -> GameResult;
|
fn set_blend_mode(&mut self, blend: BlendMode) -> GameResult;
|
||||||
|
|
||||||
|
/// Set the current render target.
|
||||||
fn set_render_target(&mut self, texture: Option<&Box<dyn BackendTexture>>) -> GameResult;
|
fn set_render_target(&mut self, texture: Option<&Box<dyn BackendTexture>>) -> GameResult;
|
||||||
|
|
||||||
|
/// Draw a filled rectangle with the specified color.
|
||||||
fn draw_rect(&mut self, rect: Rect, color: Color) -> GameResult;
|
fn draw_rect(&mut self, rect: Rect, color: Color) -> GameResult;
|
||||||
|
|
||||||
|
/// Draw an outlined rectangle with the specified line width and color.
|
||||||
fn draw_outline_rect(&mut self, rect: Rect, line_width: usize, color: Color) -> GameResult;
|
fn draw_outline_rect(&mut self, rect: Rect, line_width: usize, color: Color) -> GameResult;
|
||||||
|
|
||||||
|
/// Set the current clipping rectangle.
|
||||||
fn set_clip_rect(&mut self, rect: Option<Rect>) -> GameResult;
|
fn set_clip_rect(&mut self, rect: Option<Rect>) -> GameResult;
|
||||||
|
|
||||||
fn imgui(&self) -> GameResult<&mut imgui::Context>;
|
/// Get a reference to the imgui context.
|
||||||
|
fn imgui(&self) -> GameResult<Rc<RefCell<imgui::Context>>>;
|
||||||
|
|
||||||
|
/// Get an imgui texture id for the specified texture.
|
||||||
fn imgui_texture_id(&self, texture: &Box<dyn BackendTexture>) -> GameResult<imgui::TextureId>;
|
fn imgui_texture_id(&self, texture: &Box<dyn BackendTexture>) -> GameResult<imgui::TextureId>;
|
||||||
|
|
||||||
|
/// Prepare the imgui context for rendering.
|
||||||
fn prepare_imgui(&mut self, ui: &imgui::Ui) -> GameResult;
|
fn prepare_imgui(&mut self, ui: &imgui::Ui) -> GameResult;
|
||||||
|
|
||||||
|
/// Render the imgui draw data.
|
||||||
fn render_imgui(&mut self, draw_data: &DrawData) -> GameResult;
|
fn render_imgui(&mut self, draw_data: &DrawData) -> GameResult;
|
||||||
|
|
||||||
|
/// Draw a list of triangles, in mode similar to GL_TRIANGLES.
|
||||||
fn draw_triangle_list(
|
fn draw_triangle_list(
|
||||||
&mut self,
|
&mut self,
|
||||||
vertices: &[VertexData],
|
vertices: &[VertexData],
|
||||||
|
@ -89,18 +107,23 @@ pub trait BackendRenderer {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait BackendTexture {
|
pub trait BackendTexture {
|
||||||
|
/// Get the dimensions of the texture.
|
||||||
fn dimensions(&self) -> (u16, u16);
|
fn dimensions(&self) -> (u16, u16);
|
||||||
|
|
||||||
|
/// Adds a new drawing command to the texture batch.
|
||||||
fn add(&mut self, command: SpriteBatchCommand);
|
fn add(&mut self, command: SpriteBatchCommand);
|
||||||
|
|
||||||
|
/// Clear the texture batch.
|
||||||
fn clear(&mut self);
|
fn clear(&mut self);
|
||||||
|
|
||||||
|
/// Draw the texture batch to the screen.
|
||||||
fn draw(&mut self) -> GameResult;
|
fn draw(&mut self) -> GameResult;
|
||||||
|
|
||||||
fn as_any(&self) -> &dyn Any;
|
fn as_any(&self) -> &dyn Any;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait BackendGamepad {
|
pub trait BackendGamepad {
|
||||||
|
/// Run a gamepad rumble effect using the specified parameters.
|
||||||
fn set_rumble(&mut self, low_freq: u16, high_freq: u16, duration_ms: u32) -> GameResult;
|
fn set_rumble(&mut self, low_freq: u16, high_freq: u16, duration_ms: u32) -> GameResult;
|
||||||
|
|
||||||
fn instance_id(&self) -> u32;
|
fn instance_id(&self) -> u32;
|
||||||
|
|
|
@ -406,10 +406,8 @@ impl BackendEventLoop for HorizonEventLoop {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_renderer(&self, ctx: *mut Context) -> GameResult<Box<dyn BackendRenderer>> {
|
fn new_renderer(&self) -> GameResult<Box<dyn BackendRenderer>> {
|
||||||
let mut imgui = imgui::Context::create();
|
let mut imgui = imgui::Context::create();
|
||||||
let ctx = unsafe { &mut *ctx };
|
|
||||||
imgui.io_mut().display_size = [ctx.screen_size.0, ctx.screen_size.1];
|
|
||||||
imgui.fonts().build_alpha8_texture();
|
imgui.fonts().build_alpha8_texture();
|
||||||
|
|
||||||
let device = DeviceMaker::new().create();
|
let device = DeviceMaker::new().create();
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::mem;
|
use std::mem;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
use imgui::{DrawData, TextureId, Ui};
|
use imgui::{DrawData, TextureId, Ui};
|
||||||
|
|
||||||
|
@ -51,12 +52,9 @@ impl BackendEventLoop for NullEventLoop {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_renderer(&self, _ctx: *mut Context) -> GameResult<Box<dyn BackendRenderer>> {
|
fn new_renderer(&self) -> GameResult<Box<dyn BackendRenderer>> {
|
||||||
let mut imgui = imgui::Context::create();
|
let mut imgui = imgui::Context::create();
|
||||||
imgui.io_mut().display_size = [640.0, 480.0];
|
NullRenderer::new(imgui)
|
||||||
imgui.fonts().build_alpha8_texture();
|
|
||||||
|
|
||||||
Ok(Box::new(NullRenderer(RefCell::new(imgui))))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn as_any(&self) -> &dyn Any {
|
fn as_any(&self) -> &dyn Any {
|
||||||
|
@ -84,12 +82,12 @@ impl BackendTexture for NullTexture {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct NullRenderer(RefCell<imgui::Context>);
|
pub struct NullRenderer(Rc<RefCell<imgui::Context>>);
|
||||||
|
|
||||||
impl NullRenderer {
|
impl NullRenderer {
|
||||||
pub fn new(mut imgui: imgui::Context) -> Self {
|
pub fn new(mut imgui: imgui::Context) -> GameResult<Box<dyn BackendRenderer>> {
|
||||||
let _ = imgui.fonts().build_alpha8_texture();
|
let _ = imgui.fonts().build_alpha8_texture();
|
||||||
NullRenderer(RefCell::new(imgui))
|
Ok(Box::new(NullRenderer(Rc::new(RefCell::new(imgui)))))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,8 +130,8 @@ impl BackendRenderer for NullRenderer {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn imgui(&self) -> GameResult<&mut imgui::Context> {
|
fn imgui(&self) -> GameResult<Rc<RefCell<imgui::Context>>> {
|
||||||
unsafe { Ok(&mut *self.0.as_ptr()) }
|
Ok(self.0.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn imgui_texture_id(&self, _texture: &Box<dyn BackendTexture>) -> GameResult<TextureId> {
|
fn imgui_texture_id(&self, _texture: &Box<dyn BackendTexture>) -> GameResult<TextureId> {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use core::mem;
|
use core::mem;
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::ffi::c_void;
|
use std::ffi::c_void;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
|
@ -43,6 +44,8 @@ use crate::game::shared_game_state::WindowMode;
|
||||||
use crate::game::Game;
|
use crate::game::Game;
|
||||||
use crate::game::GAME_SUSPENDED;
|
use crate::game::GAME_SUSPENDED;
|
||||||
|
|
||||||
|
use super::graphics;
|
||||||
|
|
||||||
pub struct SDL2Backend {
|
pub struct SDL2Backend {
|
||||||
context: Sdl,
|
context: Sdl,
|
||||||
size_hint: (u16, u16),
|
size_hint: (u16, u16),
|
||||||
|
@ -146,7 +149,6 @@ impl WindowOrCanvas {
|
||||||
struct SDL2EventLoop {
|
struct SDL2EventLoop {
|
||||||
event_pump: EventPump,
|
event_pump: EventPump,
|
||||||
refs: Rc<RefCell<SDL2Context>>,
|
refs: Rc<RefCell<SDL2Context>>,
|
||||||
opengl_available: RefCell<bool>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SDL2Context {
|
struct SDL2Context {
|
||||||
|
@ -156,6 +158,7 @@ struct SDL2Context {
|
||||||
blend_mode: sdl2::render::BlendMode,
|
blend_mode: sdl2::render::BlendMode,
|
||||||
fullscreen_type: sdl2::video::FullscreenType,
|
fullscreen_type: sdl2::video::FullscreenType,
|
||||||
game_controller: GameControllerSubsystem,
|
game_controller: GameControllerSubsystem,
|
||||||
|
preferred_renderer: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SDL2EventLoop {
|
impl SDL2EventLoop {
|
||||||
|
@ -192,8 +195,6 @@ impl SDL2EventLoop {
|
||||||
window.set_icon(icon);
|
window.set_icon(icon);
|
||||||
}
|
}
|
||||||
|
|
||||||
let opengl_available = if let Ok(v) = std::env::var("CAVESTORY_NO_OPENGL") { v != "1" } else { true };
|
|
||||||
|
|
||||||
let event_loop = SDL2EventLoop {
|
let event_loop = SDL2EventLoop {
|
||||||
event_pump,
|
event_pump,
|
||||||
refs: Rc::new(RefCell::new(SDL2Context {
|
refs: Rc::new(RefCell::new(SDL2Context {
|
||||||
|
@ -203,8 +204,8 @@ impl SDL2EventLoop {
|
||||||
blend_mode: sdl2::render::BlendMode::Blend,
|
blend_mode: sdl2::render::BlendMode::Blend,
|
||||||
fullscreen_type: sdl2::video::FullscreenType::Off,
|
fullscreen_type: sdl2::video::FullscreenType::Off,
|
||||||
game_controller,
|
game_controller,
|
||||||
|
preferred_renderer: ctx.preferred_renderer.clone(),
|
||||||
})),
|
})),
|
||||||
opengl_available: RefCell::new(opengl_available),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Box::new(event_loop))
|
Ok(Box::new(event_loop))
|
||||||
|
@ -254,25 +255,101 @@ impl SDL2EventLoop {
|
||||||
|
|
||||||
let _ = enabled;
|
let _ = enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn create_imgui() -> GameResult<imgui::Context> {
|
||||||
|
let mut imgui = init_imgui()?;
|
||||||
|
|
||||||
|
let mut key_map = &mut imgui.io_mut().key_map;
|
||||||
|
key_map[ImGuiKey_Backspace as usize] = Scancode::Backspace as u32;
|
||||||
|
key_map[ImGuiKey_Delete as usize] = Scancode::Delete as u32;
|
||||||
|
key_map[ImGuiKey_Enter as usize] = Scancode::Return as u32;
|
||||||
|
|
||||||
|
Ok(imgui)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "render-opengl")]
|
||||||
|
fn try_create_opengl_renderer(&self) -> GameResult<Box<dyn BackendRenderer>> {
|
||||||
|
{
|
||||||
|
let mut refs = self.refs.borrow_mut();
|
||||||
|
match refs.window.window().gl_create_context() {
|
||||||
|
Ok(gl_ctx) => {
|
||||||
|
refs.window.window().gl_make_current(&gl_ctx).map_err(|e| GameError::RenderError(e.to_string()))?;
|
||||||
|
refs.gl_context = Some(gl_ctx);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
return Err(GameError::RenderError(format!("Failed to initialize OpenGL context: {}", err)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SDL2GLPlatform(Rc<RefCell<SDL2Context>>);
|
||||||
|
|
||||||
|
impl render_opengl::GLPlatformFunctions for SDL2GLPlatform {
|
||||||
|
fn get_proc_address(&self, name: &str) -> *const c_void {
|
||||||
|
let refs = self.0.borrow();
|
||||||
|
refs.video.gl_get_proc_address(name) as *const _
|
||||||
|
}
|
||||||
|
|
||||||
|
fn swap_buffers(&self) {
|
||||||
|
let mut refs = self.0.borrow();
|
||||||
|
refs.window.window().gl_swap_window();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_swap_mode(&self, mode: SwapMode) {
|
||||||
|
match mode {
|
||||||
|
SwapMode::Immediate => unsafe {
|
||||||
|
sdl2_sys::SDL_GL_SetSwapInterval(0);
|
||||||
|
},
|
||||||
|
SwapMode::VSync => unsafe {
|
||||||
|
sdl2_sys::SDL_GL_SetSwapInterval(1);
|
||||||
|
},
|
||||||
|
SwapMode::Adaptive => unsafe {
|
||||||
|
if sdl2_sys::SDL_GL_SetSwapInterval(-1) == -1 {
|
||||||
|
log::warn!("Failed to enable variable refresh rate, falling back to non-V-Sync.");
|
||||||
|
sdl2_sys::SDL_GL_SetSwapInterval(0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let platform = Box::new(SDL2GLPlatform(self.refs.clone()));
|
||||||
|
let gl_context: GLContext = GLContext { gles2_mode: false, platform };
|
||||||
|
|
||||||
|
let imgui = Self::create_imgui()?;
|
||||||
|
OpenGLRenderer::new(gl_context, imgui)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_create_sdl2_renderer(&self) -> GameResult<Box<dyn BackendRenderer>> {
|
||||||
|
let imgui = Self::create_imgui()?;
|
||||||
|
|
||||||
|
fn try_create_sdl2_renderer(&self) -> GameResult<Box<dyn BackendRenderer>> {
|
||||||
|
let mut refs = self.refs.borrow_mut();
|
||||||
|
let window = std::mem::take(&mut refs.window);
|
||||||
|
refs.window = window.make_canvas()?;
|
||||||
|
|
||||||
|
let imgui = Self::create_imgui()?;
|
||||||
|
SDL2Renderer::new(self.refs.clone(), imgui)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BackendEventLoop for SDL2EventLoop {
|
impl BackendEventLoop for SDL2EventLoop {
|
||||||
fn run(&mut self, game: &mut Game, ctx: &mut Context) {
|
fn run(&mut self, game: &mut Game, ctx: &mut Context) {
|
||||||
let imgui = unsafe { (&*(ctx.renderer.as_ref().unwrap() as *const Box<dyn BackendRenderer>)).imgui().unwrap() };
|
let imgui = graphics::imgui_context(ctx).unwrap();
|
||||||
let mut imgui_sdl2 = ImguiSdl2::new(imgui, self.refs.deref().borrow().window.window());
|
let mut imgui_sdl2 = ImguiSdl2::new(&mut imgui.borrow_mut(), self.refs.deref().borrow().window.window());
|
||||||
|
|
||||||
{
|
{
|
||||||
let state = game.state.get_mut();
|
let state = game.state.get_mut();
|
||||||
let (width, height) = self.refs.deref().borrow().window.window().size();
|
let (width, height) = self.refs.deref().borrow().window.window().size();
|
||||||
ctx.screen_size = (width.max(1) as f32, height.max(1) as f32);
|
ctx.screen_size = (width.max(1) as f32, height.max(1) as f32);
|
||||||
|
|
||||||
imgui.io_mut().display_size = [ctx.screen_size.0, ctx.screen_size.1];
|
imgui.borrow_mut().io_mut().display_size = [ctx.screen_size.0, ctx.screen_size.1];
|
||||||
let _ = state.handle_resize(ctx);
|
let _ = state.handle_resize(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
for event in self.event_pump.poll_iter() {
|
for event in self.event_pump.poll_iter() {
|
||||||
imgui_sdl2.handle_event(imgui, &event);
|
imgui_sdl2.handle_event(&mut imgui.borrow_mut(), &event);
|
||||||
|
|
||||||
match event {
|
match event {
|
||||||
Event::Quit { .. } => {
|
Event::Quit { .. } => {
|
||||||
|
@ -304,11 +381,7 @@ impl BackendEventLoop for SDL2EventLoop {
|
||||||
let state = game.state.get_mut();
|
let state = game.state.get_mut();
|
||||||
ctx.screen_size = (width.max(1) as f32, height.max(1) as f32);
|
ctx.screen_size = (width.max(1) as f32, height.max(1) as f32);
|
||||||
|
|
||||||
if let Some(renderer) = &ctx.renderer {
|
imgui.borrow_mut().io_mut().display_size = [ctx.screen_size.0, ctx.screen_size.1];
|
||||||
if let Ok(imgui) = renderer.imgui() {
|
|
||||||
imgui.io_mut().display_size = [ctx.screen_size.0, ctx.screen_size.1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
state.handle_resize(ctx).unwrap();
|
state.handle_resize(ctx).unwrap();
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
|
@ -431,7 +504,7 @@ impl BackendEventLoop for SDL2EventLoop {
|
||||||
game.update(ctx).unwrap();
|
game.update(ctx).unwrap();
|
||||||
|
|
||||||
imgui_sdl2.prepare_frame(
|
imgui_sdl2.prepare_frame(
|
||||||
imgui.io_mut(),
|
imgui.borrow_mut().io_mut(),
|
||||||
self.refs.deref().borrow().window.window(),
|
self.refs.deref().borrow().window.window(),
|
||||||
&self.event_pump.mouse_state(),
|
&self.event_pump.mouse_state(),
|
||||||
);
|
);
|
||||||
|
@ -440,74 +513,35 @@ impl BackendEventLoop for SDL2EventLoop {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_renderer(&self, ctx: *mut Context) -> GameResult<Box<dyn BackendRenderer>> {
|
fn new_renderer(&self) -> GameResult<Box<dyn BackendRenderer>> {
|
||||||
#[cfg(feature = "render-opengl")]
|
let mut renderers = {
|
||||||
{
|
let mut renderers: Vec<(&'static str, fn(&Self) -> GameResult<Box<dyn BackendRenderer>>)> = Vec::new();
|
||||||
let mut refs = self.refs.borrow_mut();
|
|
||||||
match refs.window.window().gl_create_context() {
|
|
||||||
Ok(gl_ctx) => {
|
|
||||||
refs.window.window().gl_make_current(&gl_ctx).map_err(|e| GameError::RenderError(e.to_string()))?;
|
|
||||||
refs.gl_context = Some(gl_ctx);
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
*self.opengl_available.borrow_mut() = false;
|
|
||||||
log::error!("Failed to initialize OpenGL context, falling back to SDL2 renderer: {}", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut imgui = init_imgui()?;
|
|
||||||
|
|
||||||
#[cfg(feature = "render-opengl")]
|
#[cfg(feature = "render-opengl")]
|
||||||
if *self.opengl_available.borrow() {
|
renderers.push((OpenGLRenderer::RENDERER_ID, Self::try_create_opengl_renderer));
|
||||||
let mut key_map = &mut imgui.io_mut().key_map;
|
|
||||||
key_map[ImGuiKey_Backspace as usize] = Scancode::Backspace as u32;
|
|
||||||
key_map[ImGuiKey_Delete as usize] = Scancode::Delete as u32;
|
|
||||||
key_map[ImGuiKey_Enter as usize] = Scancode::Return as u32;
|
|
||||||
|
|
||||||
struct SDL2GLPlatform(Rc<RefCell<SDL2Context>>);
|
renderers.push((SDL2Renderer::RENDERER_ID, Self::try_create_sdl2_renderer));
|
||||||
|
renderers
|
||||||
|
};
|
||||||
|
|
||||||
impl render_opengl::GLPlatformFunctions for SDL2GLPlatform {
|
if let Some(preferred_renderer) = &self.refs.borrow().preferred_renderer {
|
||||||
fn get_proc_address(&self, name: &str) -> *const c_void {
|
// remove the preferred renderer from the list
|
||||||
let refs = self.0.borrow();
|
let index = renderers.iter().position(|(id, _)| id == preferred_renderer);
|
||||||
refs.video.gl_get_proc_address(name) as *const _
|
let pref_renderer = index.map(|index| renderers.remove(index));
|
||||||
}
|
|
||||||
|
|
||||||
fn swap_buffers(&self) {
|
if let Some(pref_renderer) = pref_renderer {
|
||||||
let mut refs = self.0.borrow();
|
renderers.insert(0, pref_renderer);
|
||||||
refs.window.window().gl_swap_window();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_swap_mode(&self, mode: SwapMode) {
|
|
||||||
match mode {
|
|
||||||
SwapMode::Immediate => unsafe {
|
|
||||||
sdl2_sys::SDL_GL_SetSwapInterval(0);
|
|
||||||
},
|
|
||||||
SwapMode::VSync => unsafe {
|
|
||||||
sdl2_sys::SDL_GL_SetSwapInterval(1);
|
|
||||||
},
|
|
||||||
SwapMode::Adaptive => unsafe {
|
|
||||||
if sdl2_sys::SDL_GL_SetSwapInterval(-1) == -1 {
|
|
||||||
log::warn!("Failed to enable variable refresh rate, falling back to non-V-Sync.");
|
|
||||||
sdl2_sys::SDL_GL_SetSwapInterval(0);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let platform = Box::new(SDL2GLPlatform(self.refs.clone()));
|
for (id, renderer_fn) in renderers {
|
||||||
let gl_context: GLContext = GLContext { gles2_mode: false, platform, ctx };
|
match renderer_fn(self) {
|
||||||
|
Ok(renderer) => return Ok(renderer),
|
||||||
return Ok(Box::new(OpenGLRenderer::new(gl_context, imgui)));
|
Err(e) => log::warn!("Failed to create renderer {}: {}", id, e),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
Err(GameError::RenderError("Failed to create any renderer".to_owned()))
|
||||||
let mut refs = self.refs.borrow_mut();
|
|
||||||
let window = std::mem::take(&mut refs.window);
|
|
||||||
refs.window = window.make_canvas()?;
|
|
||||||
return Ok(Box::new(SDL2Renderer::new(self.refs.clone(), imgui)?));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn as_any(&self) -> &dyn Any {
|
fn as_any(&self) -> &dyn Any {
|
||||||
|
@ -563,8 +597,10 @@ struct SDL2Renderer {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SDL2Renderer {
|
impl SDL2Renderer {
|
||||||
|
pub const RENDERER_ID: &'static str = "sdl2";
|
||||||
|
|
||||||
#[allow(clippy::new_ret_no_self)]
|
#[allow(clippy::new_ret_no_self)]
|
||||||
pub fn new(refs: Rc<RefCell<SDL2Context>>, mut imgui: imgui::Context) -> GameResult<SDL2Renderer> {
|
pub fn new(refs: Rc<RefCell<SDL2Context>>, mut imgui: imgui::Context) -> GameResult<Box<dyn BackendRenderer>> {
|
||||||
imgui.set_renderer_name("SDL2Renderer".to_owned());
|
imgui.set_renderer_name("SDL2Renderer".to_owned());
|
||||||
let imgui_font_tex = {
|
let imgui_font_tex = {
|
||||||
let refs = refs.clone();
|
let refs = refs.clone();
|
||||||
|
@ -605,7 +641,7 @@ impl SDL2Renderer {
|
||||||
};
|
};
|
||||||
imgui.fonts().tex_id = TextureId::new(imgui_font_tex.texture.as_ref().unwrap().raw() as usize);
|
imgui.fonts().tex_id = TextureId::new(imgui_font_tex.texture.as_ref().unwrap().raw() as usize);
|
||||||
|
|
||||||
Ok((SDL2Renderer { refs, imgui: Rc::new(RefCell::new(imgui)), imgui_font_tex }))
|
Ok(Box::new(SDL2Renderer { refs, imgui: Rc::new(RefCell::new(imgui)), imgui_font_tex }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -843,8 +879,8 @@ impl BackendRenderer for SDL2Renderer {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn imgui(&self) -> GameResult<&mut imgui::Context> {
|
fn imgui(&self) -> GameResult<Rc<RefCell<imgui::Context>>> {
|
||||||
unsafe { Ok(&mut *self.imgui.as_ptr()) }
|
Ok(self.imgui.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn imgui_texture_id(&self, texture: &Box<dyn BackendTexture>) -> GameResult<TextureId> {
|
fn imgui_texture_id(&self, texture: &Box<dyn BackendTexture>) -> GameResult<TextureId> {
|
||||||
|
|
|
@ -293,48 +293,10 @@ impl BackendEventLoop for WinitEventLoop {
|
||||||
event_loop.run_app(self);
|
event_loop.run_app(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_renderer(&self, ctx: *mut Context) -> GameResult<Box<dyn BackendRenderer>> {
|
fn new_renderer(&self) -> GameResult<Box<dyn BackendRenderer>> {
|
||||||
let mut imgui = init_imgui()?;
|
let mut imgui = init_imgui()?;
|
||||||
imgui.io_mut().display_size = [640.0, 480.0];
|
imgui.io_mut().display_size = [640.0, 480.0];
|
||||||
|
|
||||||
// let refs = self.refs.clone();
|
|
||||||
// let user_data = Rc::into_raw(refs) as *mut c_void;
|
|
||||||
|
|
||||||
// unsafe fn get_proc_address(user_data: &mut *mut c_void, name: &str) -> *const c_void {
|
|
||||||
// let refs = Rc::from_raw(*user_data as *mut UnsafeCell<Option<WindowedContext<PossiblyCurrent>>>);
|
|
||||||
|
|
||||||
// let result = {
|
|
||||||
// let refs = &mut *refs.get();
|
|
||||||
|
|
||||||
// if let Some(refs) = refs {
|
|
||||||
// refs.get_proc_address(name)
|
|
||||||
// } else {
|
|
||||||
// std::ptr::null()
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
// *user_data = Rc::into_raw(refs) as *mut c_void;
|
|
||||||
|
|
||||||
// result
|
|
||||||
// }
|
|
||||||
|
|
||||||
// unsafe fn swap_buffers(user_data: &mut *mut c_void) {
|
|
||||||
// let refs = Rc::from_raw(*user_data as *mut UnsafeCell<Option<WindowedContext<PossiblyCurrent>>>);
|
|
||||||
|
|
||||||
// {
|
|
||||||
// let refs = &mut *refs.get();
|
|
||||||
|
|
||||||
// if let Some(refs) = refs {
|
|
||||||
// refs.swap_buffers();
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// *user_data = Rc::into_raw(refs) as *mut c_void;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// let gl_context = GLContext { gles2_mode: true, is_sdl: false, get_proc_address, swap_buffers, user_data, ctx };
|
|
||||||
|
|
||||||
// Ok(Box::new(OpenGLRenderer::new(gl_context, imgui)))
|
|
||||||
Ok(Box::new(super::backend_null::NullRenderer::new(imgui)))
|
Ok(Box::new(super::backend_null::NullRenderer::new(imgui)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ pub struct Context {
|
||||||
pub shutdown_requested: bool,
|
pub shutdown_requested: bool,
|
||||||
pub size_hint: (u16, u16),
|
pub size_hint: (u16, u16),
|
||||||
pub window_title: String,
|
pub window_title: String,
|
||||||
|
pub preferred_renderer: Option<String>,
|
||||||
pub(crate) filesystem: Filesystem,
|
pub(crate) filesystem: Filesystem,
|
||||||
pub(crate) renderer: Option<Box<dyn BackendRenderer>>,
|
pub(crate) renderer: Option<Box<dyn BackendRenderer>>,
|
||||||
pub(crate) gamepad_context: GamepadContext,
|
pub(crate) gamepad_context: GamepadContext,
|
||||||
|
@ -29,6 +30,7 @@ impl Context {
|
||||||
shutdown_requested: false,
|
shutdown_requested: false,
|
||||||
size_hint: (640, 480),
|
size_hint: (640, 480),
|
||||||
window_title: "Game".to_string(),
|
window_title: "Game".to_string(),
|
||||||
|
preferred_renderer: None,
|
||||||
filesystem: Filesystem::new(),
|
filesystem: Filesystem::new(),
|
||||||
renderer: None,
|
renderer: None,
|
||||||
gamepad_context: GamepadContext::new(),
|
gamepad_context: GamepadContext::new(),
|
||||||
|
@ -43,7 +45,7 @@ impl Context {
|
||||||
pub fn run(&mut self, game: &mut Game) -> GameResult {
|
pub fn run(&mut self, game: &mut Game) -> GameResult {
|
||||||
let backend = init_backend(self.headless, self.size_hint)?;
|
let backend = init_backend(self.headless, self.size_hint)?;
|
||||||
let mut event_loop = backend.create_event_loop(self)?;
|
let mut event_loop = backend.create_event_loop(self)?;
|
||||||
self.renderer = Some(event_loop.new_renderer(self as *mut Context)?);
|
self.renderer = Some(event_loop.new_renderer()?);
|
||||||
|
|
||||||
event_loop.run(game, self);
|
event_loop.run(game, self);
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
use crate::common::{Color, Rect};
|
use crate::common::{Color, Rect};
|
||||||
use crate::framework::backend::{BackendShader, BackendTexture, VertexData};
|
use crate::framework::backend::{BackendShader, BackendTexture, VertexData};
|
||||||
use crate::framework::context::Context;
|
use crate::framework::context::Context;
|
||||||
|
@ -130,7 +133,7 @@ pub fn set_clip_rect(ctx: &mut Context, rect: Option<Rect>) -> GameResult {
|
||||||
Err(GameError::RenderError("Rendering backend hasn't been initialized yet.".to_string()))
|
Err(GameError::RenderError("Rendering backend hasn't been initialized yet.".to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn imgui_context(ctx: &Context) -> GameResult<&mut imgui::Context> {
|
pub fn imgui_context(ctx: &Context) -> GameResult<Rc<RefCell<imgui::Context>>> {
|
||||||
if let Some(renderer) = ctx.renderer.as_ref() {
|
if let Some(renderer) = ctx.renderer.as_ref() {
|
||||||
return renderer.imgui();
|
return renderer.imgui();
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,7 +35,6 @@ pub trait GLPlatformFunctions {
|
||||||
pub struct GLContext {
|
pub struct GLContext {
|
||||||
pub gles2_mode: bool,
|
pub gles2_mode: bool,
|
||||||
pub platform: Box<dyn GLPlatformFunctions>,
|
pub platform: Box<dyn GLPlatformFunctions>,
|
||||||
pub ctx: *mut Context,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct OpenGLTexture {
|
pub struct OpenGLTexture {
|
||||||
|
@ -225,6 +224,10 @@ impl BackendTexture for OpenGLTexture {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw(&mut self) -> GameResult {
|
fn draw(&mut self) -> GameResult {
|
||||||
|
if self.vertices.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
let gl = self.gl.as_ref();
|
let gl = self.gl.as_ref();
|
||||||
if self.texture_id == 0 {
|
if self.texture_id == 0 {
|
||||||
|
@ -276,10 +279,10 @@ impl Drop for OpenGLTexture {
|
||||||
gl.gl.DeleteTextures(1, texture_id as *const _);
|
gl.gl.DeleteTextures(1, texture_id as *const _);
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.framebuffer_id != 0 {
|
// if self.framebuffer_id != 0 {
|
||||||
let framebuffer_id = &self.framebuffer_id;
|
// let framebuffer_id = &self.framebuffer_id;
|
||||||
gl.gl.DeleteFramebuffers(1, framebuffer_id as *const _);
|
// gl.gl.DeleteFramebuffers(1, framebuffer_id as *const _);
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -578,61 +581,113 @@ pub struct Gl {
|
||||||
pub context_active: RefCell<bool>,
|
pub context_active: RefCell<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_gl(gl_context: &mut GLContext) -> Gl {
|
impl Gl {
|
||||||
unsafe {
|
pub fn ensure_compatibility(&self) -> GameResult {
|
||||||
let gl = gl::Gles2::load_with(|ptr| gl_context.platform.get_proc_address(ptr));
|
fn ensure_function_loaded(ptr: &gl::FnPtr) -> GameResult {
|
||||||
|
if !ptr.is_loaded() {
|
||||||
|
return Err(RenderError("Created context does not provide required OpenGL functionality!".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
let version = {
|
Ok(())
|
||||||
let p = gl.GetString(gl::VERSION);
|
}
|
||||||
|
|
||||||
|
ensure_function_loaded(&self.gl.ActiveTexture)?;
|
||||||
|
ensure_function_loaded(&self.gl.AttachShader)?;
|
||||||
|
ensure_function_loaded(&self.gl.BindBuffer)?;
|
||||||
|
ensure_function_loaded(&self.gl.BindFramebuffer)?;
|
||||||
|
// ensure_function_loaded(&self.gl.BindSampler)?; // optional
|
||||||
|
ensure_function_loaded(&self.gl.BindTexture)?;
|
||||||
|
ensure_function_loaded(&self.gl.BlendEquation)?;
|
||||||
|
ensure_function_loaded(&self.gl.BlendFunc)?;
|
||||||
|
ensure_function_loaded(&self.gl.BlendFuncSeparate)?;
|
||||||
|
ensure_function_loaded(&self.gl.BufferData)?;
|
||||||
|
ensure_function_loaded(&self.gl.Clear)?;
|
||||||
|
ensure_function_loaded(&self.gl.ClearColor)?;
|
||||||
|
ensure_function_loaded(&self.gl.CompileShader)?;
|
||||||
|
ensure_function_loaded(&self.gl.CreateProgram)?;
|
||||||
|
ensure_function_loaded(&self.gl.CreateShader)?;
|
||||||
|
ensure_function_loaded(&self.gl.DeleteFramebuffers)?;
|
||||||
|
ensure_function_loaded(&self.gl.DeleteProgram)?;
|
||||||
|
ensure_function_loaded(&self.gl.DeleteShader)?;
|
||||||
|
ensure_function_loaded(&self.gl.DeleteTextures)?;
|
||||||
|
ensure_function_loaded(&self.gl.Disable)?;
|
||||||
|
ensure_function_loaded(&self.gl.DrawArrays)?;
|
||||||
|
ensure_function_loaded(&self.gl.DrawBuffers)?;
|
||||||
|
ensure_function_loaded(&self.gl.DrawElements)?;
|
||||||
|
ensure_function_loaded(&self.gl.Enable)?;
|
||||||
|
ensure_function_loaded(&self.gl.EnableVertexAttribArray)?;
|
||||||
|
ensure_function_loaded(&self.gl.FramebufferTexture2D)?;
|
||||||
|
ensure_function_loaded(&self.gl.GenBuffers)?;
|
||||||
|
ensure_function_loaded(&self.gl.GenFramebuffers)?;
|
||||||
|
ensure_function_loaded(&self.gl.GenTextures)?;
|
||||||
|
ensure_function_loaded(&self.gl.GetAttribLocation)?;
|
||||||
|
ensure_function_loaded(&self.gl.GetIntegerv)?;
|
||||||
|
ensure_function_loaded(&self.gl.GetShaderInfoLog)?;
|
||||||
|
ensure_function_loaded(&self.gl.GetShaderiv)?;
|
||||||
|
ensure_function_loaded(&self.gl.GetUniformLocation)?;
|
||||||
|
ensure_function_loaded(&self.gl.LinkProgram)?;
|
||||||
|
ensure_function_loaded(&self.gl.Scissor)?;
|
||||||
|
ensure_function_loaded(&self.gl.ShaderSource)?;
|
||||||
|
ensure_function_loaded(&self.gl.TexImage2D)?;
|
||||||
|
ensure_function_loaded(&self.gl.TexParameteri)?;
|
||||||
|
ensure_function_loaded(&self.gl.Uniform1f)?;
|
||||||
|
ensure_function_loaded(&self.gl.Uniform1i)?;
|
||||||
|
ensure_function_loaded(&self.gl.Uniform2f)?;
|
||||||
|
ensure_function_loaded(&self.gl.UniformMatrix4fv)?;
|
||||||
|
ensure_function_loaded(&self.gl.UseProgram)?;
|
||||||
|
ensure_function_loaded(&self.gl.VertexAttribPointer)?;
|
||||||
|
ensure_function_loaded(&self.gl.Viewport)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_version(&self) -> String {
|
||||||
|
let p = unsafe { self.gl.GetString(gl::VERSION) };
|
||||||
if p.is_null() {
|
if p.is_null() {
|
||||||
"unknown".to_owned()
|
"unknown".to_owned()
|
||||||
} else {
|
} else {
|
||||||
let data = CStr::from_ptr(p as *const _).to_bytes().to_vec();
|
let data = unsafe { CStr::from_ptr(p as *const _).to_bytes().to_vec() };
|
||||||
String::from_utf8(data).unwrap()
|
String::from_utf8_lossy(&data).to_string()
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
log::info!("OpenGL version {}", version);
|
|
||||||
|
|
||||||
Gl { gl, context_active: RefCell::new(true) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn load_gl(gl_context: &GLContext) -> Rc<Gl> {
|
||||||
|
Rc::new(Gl {
|
||||||
|
gl: unsafe { gl::Gles2::load_with(|ptr| gl_context.platform.get_proc_address(ptr)) },
|
||||||
|
context_active: RefCell::new(true),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub struct OpenGLRenderer {
|
pub struct OpenGLRenderer {
|
||||||
refs: GLContext,
|
refs: GLContext,
|
||||||
gl: Option<Rc<Gl>>,
|
gl: Rc<Gl>,
|
||||||
imgui: UnsafeCell<imgui::Context>,
|
imgui: Rc<RefCell<imgui::Context>>,
|
||||||
render_data: RenderData,
|
render_data: RenderData,
|
||||||
def_matrix: [[f32; 4]; 4],
|
def_matrix: [[f32; 4]; 4],
|
||||||
curr_matrix: [[f32; 4]; 4],
|
curr_matrix: [[f32; 4]; 4],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OpenGLRenderer {
|
impl OpenGLRenderer {
|
||||||
pub fn new(refs: GLContext, imgui: imgui::Context) -> OpenGLRenderer {
|
pub const RENDERER_ID: &'static str = "opengl2";
|
||||||
OpenGLRenderer {
|
|
||||||
|
pub fn new(refs: GLContext, mut imgui: imgui::Context) -> GameResult<Box<dyn BackendRenderer>> {
|
||||||
|
let gl = load_gl(&refs);
|
||||||
|
gl.ensure_compatibility()?;
|
||||||
|
|
||||||
|
log::info!("OpenGL version: {}", gl.get_version());
|
||||||
|
|
||||||
|
let mut render_data = RenderData::new();
|
||||||
|
render_data.init(refs.gles2_mode, &mut imgui, &gl);
|
||||||
|
|
||||||
|
Ok(Box::new(OpenGLRenderer {
|
||||||
refs,
|
refs,
|
||||||
gl: None,
|
gl,
|
||||||
imgui: UnsafeCell::new(imgui),
|
imgui: Rc::new(RefCell::new(imgui)),
|
||||||
render_data: RenderData::new(),
|
render_data,
|
||||||
def_matrix: [[0.0; 4]; 4],
|
def_matrix: [[0.0; 4]; 4],
|
||||||
curr_matrix: [[0.0; 4]; 4],
|
curr_matrix: [[0.0; 4]; 4],
|
||||||
}
|
}))
|
||||||
}
|
|
||||||
|
|
||||||
fn get_context(&mut self) -> Option<(&mut GLContext, Rc<Gl>)> {
|
|
||||||
let imgui = unsafe { &mut *self.imgui.get() };
|
|
||||||
|
|
||||||
let gles2 = self.refs.gles2_mode;
|
|
||||||
if let None = self.gl {
|
|
||||||
self.gl = Some(Rc::new(load_gl(&mut self.refs)));
|
|
||||||
}
|
|
||||||
let gl = self.gl.clone().unwrap();
|
|
||||||
|
|
||||||
if !self.render_data.initialized {
|
|
||||||
self.render_data.init(gles2, imgui, &gl);
|
|
||||||
}
|
|
||||||
|
|
||||||
Some((&mut self.refs, gl))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -646,13 +701,12 @@ impl BackendRenderer for OpenGLRenderer {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn clear(&mut self, color: Color) {
|
fn clear(&mut self, color: Color) {
|
||||||
if let Some((_, gl)) = self.get_context() {
|
|
||||||
unsafe {
|
unsafe {
|
||||||
|
let gl = &self.gl;
|
||||||
gl.gl.ClearColor(color.r, color.g, color.b, color.a);
|
gl.gl.ClearColor(color.r, color.g, color.b, color.a);
|
||||||
gl.gl.Clear(gl::COLOR_BUFFER_BIT);
|
gl.gl.Clear(gl::COLOR_BUFFER_BIT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fn present(&mut self) -> GameResult {
|
fn present(&mut self) -> GameResult {
|
||||||
{
|
{
|
||||||
|
@ -663,13 +717,12 @@ impl BackendRenderer for OpenGLRenderer {
|
||||||
}
|
}
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
if let Some((_, gl)) = self.get_context() {
|
let gl = &self.gl;
|
||||||
gl.gl.BindFramebuffer(gl::FRAMEBUFFER, self.render_data.render_fbo as _);
|
gl.gl.BindFramebuffer(gl::FRAMEBUFFER, self.render_data.render_fbo as _);
|
||||||
gl.gl.ClearColor(0.0, 0.0, 0.0, 1.0);
|
gl.gl.ClearColor(0.0, 0.0, 0.0, 1.0);
|
||||||
gl.gl.Clear(gl::COLOR_BUFFER_BIT | gl::DEPTH_BUFFER_BIT);
|
gl.gl.Clear(gl::COLOR_BUFFER_BIT | gl::DEPTH_BUFFER_BIT);
|
||||||
|
|
||||||
let matrix =
|
let matrix = [[2.0f32, 0.0, 0.0, 0.0], [0.0, -2.0, 0.0, 0.0], [0.0, 0.0, -1.0, 0.0], [-1.0, 1.0, 0.0, 1.0]];
|
||||||
[[2.0f32, 0.0, 0.0, 0.0], [0.0, -2.0, 0.0, 0.0], [0.0, 0.0, -1.0, 0.0], [-1.0, 1.0, 0.0, 1.0]];
|
|
||||||
|
|
||||||
self.render_data.tex_shader.bind_attrib_pointer(&gl, self.render_data.vbo);
|
self.render_data.tex_shader.bind_attrib_pointer(&gl, self.render_data.vbo);
|
||||||
gl.gl.UniformMatrix4fv(self.render_data.tex_shader.proj_mtx, 1, gl::FALSE, matrix.as_ptr() as _);
|
gl.gl.UniformMatrix4fv(self.render_data.tex_shader.proj_mtx, 1, gl::FALSE, matrix.as_ptr() as _);
|
||||||
|
@ -684,33 +737,22 @@ impl BackendRenderer for OpenGLRenderer {
|
||||||
VertexData { position: (1.0, 1.0), uv: (1.0, 0.0), color },
|
VertexData { position: (1.0, 1.0), uv: (1.0, 0.0), color },
|
||||||
];
|
];
|
||||||
|
|
||||||
self.draw_arrays_tex_id(
|
self.draw_arrays_tex_id(gl::TRIANGLES, &vertices, self.render_data.surf_texture, BackendShader::Texture)?;
|
||||||
gl::TRIANGLES,
|
|
||||||
&vertices,
|
|
||||||
self.render_data.surf_texture,
|
|
||||||
BackendShader::Texture,
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some((context, _)) = self.get_context() {
|
self.refs.platform.swap_buffers();
|
||||||
context.platform.swap_buffers();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_swap_mode(&mut self, mode: SwapMode) -> GameResult {
|
fn set_swap_mode(&mut self, mode: SwapMode) -> GameResult {
|
||||||
if let Some((ctx, _)) = self.get_context() {
|
self.refs.platform.set_swap_mode(mode);
|
||||||
ctx.platform.set_swap_mode(mode);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
|
||||||
Err(RenderError("No OpenGL context available!".to_string()))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare_draw(&mut self, width: f32, height: f32) -> GameResult {
|
fn prepare_draw(&mut self, width: f32, height: f32) -> GameResult {
|
||||||
if let Some((_, gl)) = self.get_context() {
|
|
||||||
unsafe {
|
unsafe {
|
||||||
|
let gl = &self.gl;
|
||||||
let (width_u, height_u) = (width as u32, height as u32);
|
let (width_u, height_u) = (width as u32, height as u32);
|
||||||
if self.render_data.last_size != (width_u, height_u) {
|
if self.render_data.last_size != (width_u, height_u) {
|
||||||
self.render_data.last_size = (width_u, height_u);
|
self.render_data.last_size = (width_u, height_u);
|
||||||
|
@ -753,12 +795,7 @@ impl BackendRenderer for OpenGLRenderer {
|
||||||
gl.gl.BindBuffer(gl::ARRAY_BUFFER, 0);
|
gl.gl.BindBuffer(gl::ARRAY_BUFFER, 0);
|
||||||
gl.gl.BindBuffer(gl::ELEMENT_ARRAY_BUFFER, 0);
|
gl.gl.BindBuffer(gl::ELEMENT_ARRAY_BUFFER, 0);
|
||||||
gl.gl.UseProgram(self.render_data.fill_shader.program_id);
|
gl.gl.UseProgram(self.render_data.fill_shader.program_id);
|
||||||
gl.gl.UniformMatrix4fv(
|
gl.gl.UniformMatrix4fv(self.render_data.fill_shader.proj_mtx, 1, gl::FALSE, self.curr_matrix.as_ptr() as _);
|
||||||
self.render_data.fill_shader.proj_mtx,
|
|
||||||
1,
|
|
||||||
gl::FALSE,
|
|
||||||
self.curr_matrix.as_ptr() as _,
|
|
||||||
);
|
|
||||||
gl.gl.UseProgram(self.render_data.fill_water_shader.program_id);
|
gl.gl.UseProgram(self.render_data.fill_water_shader.program_id);
|
||||||
gl.gl.Uniform1i(self.render_data.fill_water_shader.texture, 0);
|
gl.gl.Uniform1i(self.render_data.fill_water_shader.texture, 0);
|
||||||
gl.gl.UniformMatrix4fv(
|
gl.gl.UniformMatrix4fv(
|
||||||
|
@ -769,23 +806,15 @@ impl BackendRenderer for OpenGLRenderer {
|
||||||
);
|
);
|
||||||
gl.gl.UseProgram(self.render_data.tex_shader.program_id);
|
gl.gl.UseProgram(self.render_data.tex_shader.program_id);
|
||||||
gl.gl.Uniform1i(self.render_data.tex_shader.texture, 0);
|
gl.gl.Uniform1i(self.render_data.tex_shader.texture, 0);
|
||||||
gl.gl.UniformMatrix4fv(
|
gl.gl.UniformMatrix4fv(self.render_data.tex_shader.proj_mtx, 1, gl::FALSE, self.curr_matrix.as_ptr() as _);
|
||||||
self.render_data.tex_shader.proj_mtx,
|
|
||||||
1,
|
|
||||||
gl::FALSE,
|
|
||||||
self.curr_matrix.as_ptr() as _,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
|
||||||
Err(RenderError("No OpenGL context available!".to_string()))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_texture_mutable(&mut self, width: u16, height: u16) -> GameResult<Box<dyn BackendTexture>> {
|
fn create_texture_mutable(&mut self, width: u16, height: u16) -> GameResult<Box<dyn BackendTexture>> {
|
||||||
if let Some((_, gl)) = self.get_context() {
|
|
||||||
unsafe {
|
unsafe {
|
||||||
|
let gl = &self.gl;
|
||||||
let current_texture_id = return_param(|x| gl.gl.GetIntegerv(gl::TEXTURE_BINDING_2D, x)) as u32;
|
let current_texture_id = return_param(|x| gl.gl.GetIntegerv(gl::TEXTURE_BINDING_2D, x)) as u32;
|
||||||
let texture_id = return_param(|x| gl.gl.GenTextures(1, x));
|
let texture_id = return_param(|x| gl.gl.GenTextures(1, x));
|
||||||
|
|
||||||
|
@ -832,14 +861,11 @@ impl BackendRenderer for OpenGLRenderer {
|
||||||
gl: gl.clone(),
|
gl: gl.clone(),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
Err(RenderError("No OpenGL context available!".to_string()))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_texture(&mut self, width: u16, height: u16, data: &[u8]) -> GameResult<Box<dyn BackendTexture>> {
|
fn create_texture(&mut self, width: u16, height: u16, data: &[u8]) -> GameResult<Box<dyn BackendTexture>> {
|
||||||
if let Some((_, gl)) = self.get_context() {
|
|
||||||
unsafe {
|
unsafe {
|
||||||
|
let gl = &self.gl;
|
||||||
let current_texture_id = return_param(|x| gl.gl.GetIntegerv(gl::TEXTURE_BINDING_2D, x)) as u32;
|
let current_texture_id = return_param(|x| gl.gl.GetIntegerv(gl::TEXTURE_BINDING_2D, x)) as u32;
|
||||||
let texture_id = return_param(|x| gl.gl.GenTextures(1, x));
|
let texture_id = return_param(|x| gl.gl.GenTextures(1, x));
|
||||||
gl.gl.BindTexture(gl::TEXTURE_2D, texture_id);
|
gl.gl.BindTexture(gl::TEXTURE_2D, texture_id);
|
||||||
|
@ -871,13 +897,10 @@ impl BackendRenderer for OpenGLRenderer {
|
||||||
gl: gl.clone(),
|
gl: gl.clone(),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
Err(RenderError("No OpenGL context available!".to_string()))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_blend_mode(&mut self, blend: BlendMode) -> GameResult {
|
fn set_blend_mode(&mut self, blend: BlendMode) -> GameResult {
|
||||||
if let Some((_, gl)) = self.get_context() {
|
let gl = &self.gl;
|
||||||
match blend {
|
match blend {
|
||||||
BlendMode::Add => unsafe {
|
BlendMode::Add => unsafe {
|
||||||
gl.gl.Enable(gl::BLEND);
|
gl.gl.Enable(gl::BLEND);
|
||||||
|
@ -900,14 +923,11 @@ impl BackendRenderer for OpenGLRenderer {
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
|
||||||
Err(RenderError("No OpenGL context available!".to_string()))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_render_target(&mut self, texture: Option<&Box<dyn BackendTexture>>) -> GameResult {
|
fn set_render_target(&mut self, texture: Option<&Box<dyn BackendTexture>>) -> GameResult {
|
||||||
if let Some((_, gl)) = self.get_context() {
|
|
||||||
unsafe {
|
unsafe {
|
||||||
|
let gl = &self.gl;
|
||||||
if let Some(texture) = texture {
|
if let Some(texture) = texture {
|
||||||
let gl_texture = texture
|
let gl_texture = texture
|
||||||
.as_any()
|
.as_any()
|
||||||
|
@ -977,14 +997,11 @@ impl BackendRenderer for OpenGLRenderer {
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
|
||||||
Err(RenderError("No OpenGL context available!".to_string()))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_rect(&mut self, rect: Rect<isize>, color: Color) -> GameResult {
|
fn draw_rect(&mut self, rect: Rect<isize>, color: Color) -> GameResult {
|
||||||
unsafe {
|
unsafe {
|
||||||
if let Some((_, gl)) = self.get_context() {
|
let gl = &self.gl;
|
||||||
let color = color.to_rgba();
|
let color = color.to_rgba();
|
||||||
let mut uv = self.render_data.font_tex_size;
|
let mut uv = self.render_data.font_tex_size;
|
||||||
uv.0 = 0.0 / uv.0;
|
uv.0 = 0.0 / uv.0;
|
||||||
|
@ -1014,12 +1031,9 @@ impl BackendRenderer for OpenGLRenderer {
|
||||||
|
|
||||||
gl.gl.BindTexture(gl::TEXTURE_2D, 0);
|
gl.gl.BindTexture(gl::TEXTURE_2D, 0);
|
||||||
gl.gl.BindBuffer(gl::ARRAY_BUFFER, 0);
|
gl.gl.BindBuffer(gl::ARRAY_BUFFER, 0);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
|
||||||
Err(RenderError("No OpenGL context available!".to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_outline_rect(&mut self, _rect: Rect<isize>, _line_width: usize, _color: Color) -> GameResult {
|
fn draw_outline_rect(&mut self, _rect: Rect<isize>, _line_width: usize, _color: Color) -> GameResult {
|
||||||
|
@ -1027,8 +1041,8 @@ impl BackendRenderer for OpenGLRenderer {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_clip_rect(&mut self, rect: Option<Rect>) -> GameResult {
|
fn set_clip_rect(&mut self, rect: Option<Rect>) -> GameResult {
|
||||||
if let Some((_, gl)) = self.get_context() {
|
|
||||||
unsafe {
|
unsafe {
|
||||||
|
let gl = &self.gl;
|
||||||
if let Some(rect) = &rect {
|
if let Some(rect) = &rect {
|
||||||
gl.gl.Enable(gl::SCISSOR_TEST);
|
gl.gl.Enable(gl::SCISSOR_TEST);
|
||||||
gl.gl.Scissor(
|
gl.gl.Scissor(
|
||||||
|
@ -1043,13 +1057,10 @@ impl BackendRenderer for OpenGLRenderer {
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
|
||||||
Err(RenderError("No OpenGL context available!".to_string()))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn imgui(&self) -> GameResult<&mut imgui::Context> {
|
fn imgui(&self) -> GameResult<Rc<RefCell<imgui::Context>>> {
|
||||||
unsafe { Ok(&mut *self.imgui.get()) }
|
Ok(self.imgui.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn imgui_texture_id(&self, texture: &Box<dyn BackendTexture>) -> GameResult<TextureId> {
|
fn imgui_texture_id(&self, texture: &Box<dyn BackendTexture>) -> GameResult<TextureId> {
|
||||||
|
@ -1067,8 +1078,9 @@ impl BackendRenderer for OpenGLRenderer {
|
||||||
|
|
||||||
fn render_imgui(&mut self, draw_data: &DrawData) -> GameResult {
|
fn render_imgui(&mut self, draw_data: &DrawData) -> GameResult {
|
||||||
// https://github.com/michaelfairley/rust-imgui-opengl-renderer
|
// https://github.com/michaelfairley/rust-imgui-opengl-renderer
|
||||||
if let Some((_, gl)) = self.get_context() {
|
|
||||||
unsafe {
|
unsafe {
|
||||||
|
let gl = &self.gl;
|
||||||
gl.gl.ActiveTexture(gl::TEXTURE0);
|
gl.gl.ActiveTexture(gl::TEXTURE0);
|
||||||
gl.gl.Enable(gl::BLEND);
|
gl.gl.Enable(gl::BLEND);
|
||||||
gl.gl.BlendEquation(gl::FUNC_ADD);
|
gl.gl.BlendEquation(gl::FUNC_ADD);
|
||||||
|
@ -1078,8 +1090,8 @@ impl BackendRenderer for OpenGLRenderer {
|
||||||
gl.gl.Enable(gl::SCISSOR_TEST);
|
gl.gl.Enable(gl::SCISSOR_TEST);
|
||||||
|
|
||||||
let imgui = self.imgui()?;
|
let imgui = self.imgui()?;
|
||||||
let [width, height] = imgui.io().display_size;
|
let [width, height] = imgui.borrow().io().display_size;
|
||||||
let [scale_w, scale_h] = imgui.io().display_framebuffer_scale;
|
let [scale_w, scale_h] = imgui.borrow().io().display_framebuffer_scale;
|
||||||
|
|
||||||
let fb_width = width * scale_w;
|
let fb_width = width * scale_w;
|
||||||
let fb_height = height * scale_h;
|
let fb_height = height * scale_h;
|
||||||
|
@ -1185,7 +1197,6 @@ impl BackendRenderer for OpenGLRenderer {
|
||||||
|
|
||||||
gl.gl.Disable(gl::SCISSOR_TEST);
|
gl.gl.Disable(gl::SCISSOR_TEST);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -1237,7 +1248,7 @@ impl OpenGLRenderer {
|
||||||
mut texture: u32,
|
mut texture: u32,
|
||||||
shader: BackendShader,
|
shader: BackendShader,
|
||||||
) -> GameResult<()> {
|
) -> GameResult<()> {
|
||||||
if let Some((_, gl)) = self.get_context() {
|
let gl = &self.gl;
|
||||||
match shader {
|
match shader {
|
||||||
BackendShader::Fill => {
|
BackendShader::Fill => {
|
||||||
self.render_data.fill_shader.bind_attrib_pointer(&gl, self.render_data.vbo)?;
|
self.render_data.fill_shader.bind_attrib_pointer(&gl, self.render_data.vbo)?;
|
||||||
|
@ -1268,16 +1279,11 @@ impl OpenGLRenderer {
|
||||||
gl.gl.BindBuffer(gl::ARRAY_BUFFER, 0);
|
gl.gl.BindBuffer(gl::ARRAY_BUFFER, 0);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
|
||||||
Err(RenderError("No OpenGL context available!".to_string()))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for OpenGLRenderer {
|
impl Drop for OpenGLRenderer {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
if let Some(gl) = &self.gl {
|
self.gl.context_active.replace(false);
|
||||||
gl.context_active.replace(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
use imgui::{FontConfig, FontSource};
|
|
||||||
use imgui::sys::*;
|
use imgui::sys::*;
|
||||||
|
use imgui::{FontConfig, FontSource};
|
||||||
|
|
||||||
use crate::framework::context::Context;
|
use crate::framework::context::Context;
|
||||||
use crate::framework::error::GameResult;
|
use crate::framework::error::GameResult;
|
||||||
|
@ -108,19 +108,20 @@ impl UI {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn draw(&mut self, state: &mut SharedGameState, ctx: &mut Context, scene: &mut Box<dyn Scene>) -> GameResult {
|
pub fn draw(&mut self, state: &mut SharedGameState, ctx: &mut Context, scene: &mut Box<dyn Scene>) -> GameResult {
|
||||||
let ctx2 = unsafe { &mut *(ctx as *const Context as *mut Context) };
|
|
||||||
let imgui = imgui_context(ctx)?;
|
let imgui = imgui_context(ctx)?;
|
||||||
|
let mut imgui = imgui.borrow_mut();
|
||||||
|
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
imgui.io_mut().update_delta_time(now - self.last_frame);
|
imgui.io_mut().update_delta_time(now - self.last_frame);
|
||||||
self.last_frame = now;
|
self.last_frame = now;
|
||||||
|
|
||||||
let mut ui = imgui.new_frame();
|
let mut ui = imgui.new_frame();
|
||||||
|
|
||||||
scene.imgui_draw(&mut self.components, state, ctx2, &mut ui)?;
|
scene.imgui_draw(&mut self.components, state, ctx, &mut ui)?;
|
||||||
|
|
||||||
prepare_imgui(ctx2, &ui);
|
prepare_imgui(ctx, &ui);
|
||||||
let draw_data = imgui.render();
|
let draw_data = imgui.render();
|
||||||
render_imgui(ctx2, draw_data)?;
|
render_imgui(ctx, draw_data)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue