Update glutin, docs, and adds a few other features (including multi window support) (#5)

Upgrades glutin, docs, and adds a few other features (including multi window support). But not the readme. That comes next.
This commit is contained in:
LoganDark 2021-01-20 19:12:52 -08:00 committed by GitHub
parent 484ff092ba
commit 02b3b62520
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 1073 additions and 436 deletions

View file

@ -1,6 +1,6 @@
[package]
name = "mini_gl_fb"
version = "0.7.0"
version = "0.8.0"
authors = ["shivshank"]
description = "Quick and easy window creation, input, and high speed bitmap rendering"
repository = "https://github.com/shivshank/mini_gl_fb"
@ -12,6 +12,6 @@ categories = ["rendering"]
readme = "README.md"
[dependencies]
glutin = "0.19.0"
glutin = "0.26.0"
gl = "0.10.0"
rustic_gl = "0.3.2"

View file

@ -4,12 +4,32 @@
[![Docs.rs](https://docs.rs/mini_gl_fb/badge.svg)](https://docs.rs/mini_gl_fb)
Mini GL Framebuffer provides an easy way to draw to a window from a pixel buffer. OpenGL
alternative to other easy framebuffer libraries.
alternative to other easy framebuffer libraries like `minifb` and `pixels`.
It's designed to be dead simple and easy to remember when you just want to get something on the
screen ASAP. It's also built to be super flexible and easy to grow out of in case your project
gets serious (MGlFb exposes all of its internals so you can iteratively remove it as a
dependency over time!).
gets serious. MGlFb exposes all of its internals so you can iteratively remove it as a
dependency over time!
You can also use `MiniGlFb::glutin_breakout` to do rad things like multi-window while keeping
the useful `Framebuffer` helper around. There's an example called `multi_window` which shows
this in action.
MGlFb should run on any platform you throw it at, thanks to `winit` and `glutin`'s
cross-platform compatibility. However, you do need proper GPU drivers which support OpenGL.
That means MGlFb won't work in certain virtual machines or on servers without a GPU.
Unfortunately, this isn't something that can be helped because MGlFb can't function without
OpenGL.
# Screenies
Here are some screenies of the `multi_window` example running on different platforms:
![Windows](screenies/multi_window_windows.png)
![Arch Linux (X11)](screenies/multi_window_x11.png)
![macOS](screenies/multi_window_macos.png)
It is a showcase of the advanced functionality that MGlFb can support, but it's not at all
representative of how little work is required to get started.
# Usage
@ -17,10 +37,10 @@ dependency over time!).
extern crate mini_gl_fb;
fn main() {
let mut fb = mini_gl_fb::gotta_go_fast("Hello world!", 800.0, 600.0);
let (mut event_loop, mut fb) = mini_gl_fb::gotta_go_fast("Hello world!", 800.0, 600.0);
let buffer = vec![[128u8, 0, 0, 255]; 800 * 600];
fb.update_buffer(&buffer);
fb.persist();
fb.persist(&mut event_loop);
}
```
@ -34,13 +54,15 @@ Get access to mouse position and key inputs with no hassle. The following is ext
Game of Life example:
```rust
use mini_gl_fb::glutin::event_loop::EventLoop;
use mini_gl_fb::Config;
let mut fb = mini_gl_fb::gotta_go_fast("Hello world!", 800.0, 600.0);
let (mut event_loop, mut fb) = mini_gl_fb::gotta_go_fast("Hello, World!", 800., 600.);
let buffer = vec![[128u8, 0, 0, 255]; 800 * 600];
// ...
fb.glutin_handle_basic_input(|fb, input| {
fb.glutin_handle_basic_input(&mut event_loop, |fb, input| {
let elapsed = previous.elapsed().unwrap();
let seconds = elapsed.as_secs() as f64 + elapsed.subsec_nanos() as f64 * 1e-9;
@ -93,17 +115,19 @@ You can also "breakout" and get access to the underlying glutin window while sti
setup:
```rust
let mut fb = mini_gl_fb::gotta_go_fast("Hello world!", 800.0, 600.0);
let (_, fb) = mini_gl_fb::gotta_go_fast("Hello world!", 800.0, 600.0);
let GlutinBreakout {
mut events_loop,
gl_window,
context,
mut fb,
} = fb.glutin_breakout();
fb.update_buffer(/*...*/);
```
The `multi_window` example works by running the winit event loop manually and handling events
for multiple `GlutinBreakout`s at once. You can do this too!
# Other features
- Black and white rendering, specifying one byte per pixel

View file

@ -1,14 +1,17 @@
#[macro_use]
extern crate mini_gl_fb;
use mini_gl_fb::{Config, BufferFormat};
use mini_gl_fb::BufferFormat;
use mini_gl_fb::glutin::event_loop::EventLoop;
use mini_gl_fb::glutin::dpi::LogicalSize;
fn main() {
let mut fb = mini_gl_fb::get_fancy(Config {
window_title: "Hello world!",
window_size: (800.0, 600.0),
buffer_size: (2, 2),
.. Default::default()
});
let mut event_loop = EventLoop::new();
let mut fb = mini_gl_fb::get_fancy(config! {
window_title: String::from("Hello world!"),
window_size: LogicalSize::new(800.0, 600.0),
buffer_size: Some(LogicalSize::new(2, 2))
}, &event_loop);
fb.change_buffer_format::<u8>(BufferFormat::R);
fb.use_grayscale_shader();
@ -16,5 +19,5 @@ fn main() {
let buffer = [128u8, 255, 50, 25];
fb.update_buffer(&buffer);
fb.persist();
fb.persist(&mut event_loop);
}

View file

@ -102,7 +102,7 @@ fn main() {
let width = 800.0;
let height = 600.0;
let mut fb = mini_gl_fb::gotta_go_fast("Hello shaders!", width, height);
let (mut event_loop, mut fb) = mini_gl_fb::gotta_go_fast("Hello shaders!", width, height);
let mut buffer = vec![[128u8, 0, 0, 255]; (width * height) as usize];
// let's write a red line into the buffer roughly along the diagonal (misses many pixels)
@ -119,5 +119,5 @@ fn main() {
fb.update_buffer(&buffer);
fb.persist_and_redraw(true);
fb.persist_and_redraw(&mut event_loop, true);
}

View file

@ -1,7 +1,10 @@
#[macro_use]
extern crate mini_gl_fb;
use mini_gl_fb::{Config, BufferFormat};
use mini_gl_fb::glutin::{MouseButton, VirtualKeyCode};
use mini_gl_fb::BufferFormat;
use mini_gl_fb::glutin::event::{VirtualKeyCode, MouseButton};
use mini_gl_fb::glutin::event_loop::EventLoop;
use mini_gl_fb::glutin::dpi::LogicalSize;
use std::time::SystemTime;
@ -9,12 +12,12 @@ const WIDTH: usize = 200;
const HEIGHT: usize = 200;
fn main() {
let mut fb = mini_gl_fb::get_fancy(Config {
window_title: "PSA: Conway wants you to appreciate group theory instead",
window_size: (800.0, 800.0),
buffer_size: (WIDTH as _, HEIGHT as _),
.. Default::default()
});
let mut event_loop = EventLoop::new();
let mut fb = mini_gl_fb::get_fancy(config! {
window_title: String::from("PSA: Conway wants you to appreciate group theory instead"),
window_size: LogicalSize::new(800.0, 800.0),
buffer_size: Some(LogicalSize::new(WIDTH as _, HEIGHT as _))
}, &event_loop);
fb.change_buffer_format::<u8>(BufferFormat::R);
fb.use_post_process_shader(POST_PROCESS);
@ -35,7 +38,7 @@ fn main() {
let mut previous = SystemTime::now();
let mut extra_delay: f64 = 0.0;
fb.glutin_handle_basic_input(|fb, input| {
fb.glutin_handle_basic_input(&mut event_loop, |fb, input| {
let elapsed = previous.elapsed().unwrap();
let seconds = elapsed.as_secs() as f64 + elapsed.subsec_nanos() as f64 * 1e-9;

View file

@ -1,68 +1,63 @@
extern crate mini_gl_fb;
use mini_gl_fb::{glutin, GlutinBreakout};
use glutin::GlContext;
extern crate glutin;
use std::cmp::{min, max};
use mini_gl_fb::GlutinBreakout;
use mini_gl_fb::glutin::event::WindowEvent::KeyboardInput;
use mini_gl_fb::glutin::event::{Event, VirtualKeyCode, ElementState, WindowEvent};
use mini_gl_fb::glutin::event_loop::ControlFlow;
fn main() {
let mut fb = mini_gl_fb::gotta_go_fast("Hello world!", 800.0, 600.0);
let (event_loop, mut fb) = mini_gl_fb::gotta_go_fast("Hello world!", 800.0, 600.0);
let mut buffer = vec![[128u8, 0, 0, 255]; 800 * 600];
fb.update_buffer(&buffer);
let GlutinBreakout {
mut events_loop,
gl_window,
context,
mut fb,
} = fb.glutin_breakout();
let mut running = true;
let mut mouse_x = 0;
let mut mouse_y = 0;
let mut mouse_down = false;
while running {
events_loop.poll_events(|event| {
use glutin::{Event, ElementState, VirtualKeyCode};
use glutin::WindowEvent::*;
event_loop.run(move |event, _, flow| {
match event {
Event::WindowEvent { event: CloseRequested, .. } => {
running = false;
Event::WindowEvent { event: WindowEvent::CloseRequested, .. } => {
*flow = ControlFlow::Exit;
}
Event::WindowEvent { event: KeyboardInput { input, .. }, .. } => {
if let Some(k) = input.virtual_keycode {
if k == VirtualKeyCode::Escape && input.state == ElementState::Released {
running = false;
if k == VirtualKeyCode::Escape && input.state == ElementState::Pressed {
*flow = ControlFlow::Exit;
}
}
}
Event::WindowEvent { event: Resized(logical_size), .. } => {
let dpi_factor = gl_window.get_hidpi_factor();
gl_window.resize(logical_size.to_physical(dpi_factor));
Event::WindowEvent { event: WindowEvent::Resized(size), .. } => {
context.resize(size);
context.window().request_redraw();
}
Event::WindowEvent { event: CursorMoved { position, .. }, .. } => {
let dpi_factor = gl_window.get_hidpi_factor();
let (x, y) = position.to_physical(dpi_factor).into();
mouse_x = min(max(x, 0), 800 - 1);
mouse_y = min(max(y, 0), 600 - 1);
Event::WindowEvent { event: WindowEvent::CursorMoved { position, .. }, .. } => {
let (x, y) = position.to_logical::<f64>(context.window().scale_factor()).into();
println!("({}, {})", x, y);
let mouse_x = min(max(x, 0), 800 - 1);
let mouse_y = min(max(fb.buffer_size.height - y, 0), 600 - 1);
if mouse_down {
buffer[(mouse_x + mouse_y * 800) as usize] = [64, 128, 255, 255];
fb.update_buffer(&buffer);
context.window().request_redraw();
}
}
Event::WindowEvent { event: MouseInput { state, .. }, .. } => {
Event::WindowEvent { event: WindowEvent::MouseInput { state, .. }, .. } => {
if state == ElementState::Pressed {
mouse_down = true;
} else {
mouse_down = false;
}
}
Event::RedrawRequested(_) => {
fb.redraw();
context.swap_buffers().unwrap();
}
_ => {}
}
});
fb.redraw();
gl_window.swap_buffers().unwrap();
}
}

273
examples/multi_window.rs Normal file
View file

@ -0,0 +1,273 @@
#[macro_use]
extern crate mini_gl_fb;
extern crate glutin;
use mini_gl_fb::glutin::event_loop::EventLoop;
use mini_gl_fb::glutin::event::{Event, WindowEvent, MouseButton, VirtualKeyCode, KeyboardInput, ElementState};
use mini_gl_fb::{get_fancy, GlutinBreakout};
use mini_gl_fb::glutin::dpi::{LogicalSize, LogicalPosition};
use mini_gl_fb::glutin::window::{Window, WindowId, CursorIcon};
use mini_gl_fb::glutin::event_loop::ControlFlow;
use mini_gl_fb::glutin::platform::run_return::EventLoopExtRunReturn;
/// Turn up this number to make the pixels bigger. 1 is one logical pixel
const SCALE_FACTOR: f64 = 2.;
/// A window being tracked by a `MultiWindow`. All tracked windows will be forwarded all events
/// received on the `MultiWindow`'s event loop.
trait TrackedWindow {
/// Handles one event from the event loop. Returns true if the window needs to be kept alive,
/// otherwise it will be closed. Window events should be checked to ensure that their ID is one
/// that the TrackedWindow is interested in.
fn handle_event(&mut self, event: &Event<()>) -> bool;
}
/// Manages multiple `TrackedWindow`s by forwarding events to them.
struct MultiWindow {
windows: Vec<Option<Box<dyn TrackedWindow>>>,
}
impl MultiWindow {
/// Creates a new `MultiWindow`.
pub fn new() -> Self {
MultiWindow {
windows: vec![],
}
}
/// Adds a new `TrackedWindow` to the `MultiWindow`.
pub fn add(&mut self, window: Box<dyn TrackedWindow>) {
self.windows.push(Some(window))
}
/// Runs the event loop until all `TrackedWindow`s are closed.
pub fn run(&mut self, event_loop: &mut EventLoop<()>) {
if !self.windows.is_empty() {
event_loop.run_return(|event, _, flow| {
*flow = ControlFlow::Wait;
for option in &mut self.windows {
if let Some(window) = option.as_mut() {
if !window.handle_event(&event) {
option.take();
}
}
}
self.windows.retain(Option::is_some);
if self.windows.is_empty() {
*flow = ControlFlow::Exit;
}
});
}
}
}
/// A basic window that allows you to draw in it. An example of how to implement a `TrackedWindow`.
struct DrawWindow {
pub breakout: GlutinBreakout,
pub buffer: Vec<[u8; 4]>,
pub buffer_size: LogicalSize<u32>,
pub bg: [u8; 4],
pub fg: [u8; 4],
mouse_state: ElementState,
line_start: Option<LogicalPosition<i32>>,
}
impl DrawWindow {
fn window(&self) -> &Window {
self.breakout.context.window()
}
pub fn matches_id(&self, id: WindowId) -> bool {
id == self.window().id()
}
/// Updates the window's buffer. Should only be done inside of RedrawRequested events; outside
/// of them, use `request_redraw` instead.
fn redraw(&mut self) {
self.breakout.fb.update_buffer(&self.buffer);
self.breakout.context.swap_buffers().unwrap();
}
/// Requests a redraw event for this window.
fn request_redraw(&self) {
self.window().request_redraw();
}
/// Resizes the window's buffer to a new size, attempting to preserve the current content as
/// much as possible. Fills new space with the background color, and deletes overflowing space.
fn resize(&mut self, new_size: LogicalSize<u32>) {
let mut new_buffer = vec![self.bg; new_size.width as usize * new_size.height as usize];
if self.buffer_size.width > 0 {
// use rchunks for inverted y
for (old_line, new_line) in self.buffer.chunks_exact(self.buffer_size.width as usize)
.zip(new_buffer.chunks_exact_mut(new_size.width as usize)) {
if old_line.len() <= new_line.len() {
new_line[0..old_line.len()].copy_from_slice(old_line)
} else {
new_line.copy_from_slice(&old_line[0..new_line.len()])
}
}
}
self.buffer = new_buffer;
self.buffer_size = new_size;
self.breakout.fb.resize_buffer(new_size.width, new_size.height);
}
fn plot(&mut self, position: LogicalPosition<i32>) {
if position.x < 0 || position.x >= self.buffer_size.width as i32 ||
position.y < 0 || position.y >= self.buffer_size.height as i32 {
return
}
let position = position.cast::<u32>();
let index = (position.x + position.y * self.buffer_size.width) as usize;
self.buffer[index] = self.fg;
}
// https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm
fn plot_line(&mut self, start: LogicalPosition<i32>, end: LogicalPosition<i32>) {
let (mut x0, mut y0): (i32, i32) = start.into();
let (x1, y1): (i32, i32) = end.into();
let dx = (x1 - x0).abs();
let sx = if x0 < x1 { 1 } else { -1 };
let dy = -(y1 - y0).abs();
let sy = if y0 < y1 { 1 } else { -1 };
let mut err = dx + dy;
while x0 != x1 || y0 != y1 {
self.plot(LogicalPosition::new(x0, y0));
let e2 = err * 2;
if e2 > dy {
err += dy;
x0 += sx;
}
if e2 <= dx {
err += dx;
y0 += sy;
}
}
self.plot(end);
}
/// Creates a new `DrawWindow` for the specified event loop, using the specified background and
/// foreground colors.
pub fn new(event_loop: &EventLoop<()>, bg: [u8; 4], fg: [u8; 4]) -> Self {
let mut new = Self {
breakout: get_fancy(config! {
resizable: true,
invert_y: false
}, &event_loop).glutin_breakout(),
buffer: vec![],
buffer_size: LogicalSize::new(0, 0),
bg,
fg,
mouse_state: ElementState::Released,
line_start: None,
};
new.resize(new.window().inner_size().to_logical(new.window().scale_factor() * SCALE_FACTOR));
new.window().set_cursor_icon(CursorIcon::Crosshair);
new
}
}
impl TrackedWindow for DrawWindow {
fn handle_event(&mut self, event: &Event<()>) -> bool {
match *event {
Event::WindowEvent {
window_id: id,
event: WindowEvent::CloseRequested,
..
} if self.matches_id(id) => {
return false;
}
Event::WindowEvent {
window_id: id,
event: WindowEvent::KeyboardInput {
input: KeyboardInput {
virtual_keycode: Some(VirtualKeyCode::Escape),
state: ElementState::Pressed,
..
},
..
},
..
} if self.matches_id(id) => {
if let Some(_) = self.window().fullscreen() {
self.window().set_fullscreen(None);
} else {
return false;
}
}
Event::RedrawRequested(id) if self.matches_id(id) => {
unsafe { self.breakout.make_current().unwrap(); }
self.redraw();
}
Event::WindowEvent {
window_id: id,
event: WindowEvent::Resized(size),
..
} if self.matches_id(id) => {
unsafe { self.breakout.make_current().unwrap(); }
self.breakout.fb.resize_viewport(size.width, size.height);
self.resize(size.to_logical(self.window().scale_factor() * SCALE_FACTOR));
self.request_redraw();
}
Event::WindowEvent {
window_id: id,
event: WindowEvent::MouseInput {
button: MouseButton::Left,
state,
..
},
..
} if self.matches_id(id) => {
self.mouse_state = state;
self.line_start = None;
}
Event::WindowEvent {
window_id: id,
event: WindowEvent::CursorMoved {
position,
..
},
..
} if self.matches_id(id) => {
if self.mouse_state == ElementState::Pressed {
let inner_size = self.window().inner_size();
let position = LogicalPosition::new(
((position.x / inner_size.width as f64) * self.buffer_size.width as f64).floor(),
((position.y / inner_size.height as f64) * self.buffer_size.height as f64).floor()
).cast::<i32>();
if let Some(line_start) = self.line_start {
self.plot_line(line_start, position);
} else {
self.plot(position);
}
self.line_start = Some(position);
self.request_redraw();
}
}
_ => {}
}
true
}
}
fn main() {
let mut event_loop = EventLoop::new();
let mut multi_window = MultiWindow::new();
multi_window.add(Box::new(DrawWindow::new(&event_loop, [25u8, 33, 40, 255], [54u8, 165, 209, 255])));
multi_window.add(Box::new(DrawWindow::new(&event_loop, [25u8, 40, 33, 255], [54u8, 209, 82, 255])));
multi_window.add(Box::new(DrawWindow::new(&event_loop, [40u8, 33, 25, 255], [209u8, 82, 54, 255])));
multi_window.run(&mut event_loop);
}

View file

@ -1,15 +1,18 @@
#[macro_use]
extern crate mini_gl_fb;
use mini_gl_fb::{Config, BufferFormat};
use mini_gl_fb::BufferFormat;
use mini_gl_fb::glutin::event_loop::EventLoop;
use mini_gl_fb::glutin::dpi::LogicalSize;
fn main() {
let mut fb = mini_gl_fb::get_fancy(Config {
window_title: "Hello world!",
window_size: (800.0, 600.0),
buffer_size: (2, 2),
resizable: true,
.. Default::default()
});
let mut event_loop = EventLoop::new();
let mut fb = mini_gl_fb::get_fancy(config! {
window_title: String::from("Hello world!"),
window_size: LogicalSize::new(800.0, 600.0),
buffer_size: Some(LogicalSize::new(2, 2)),
resizable: true
}, &event_loop);
fb.change_buffer_format::<u8>(BufferFormat::R);
@ -21,5 +24,5 @@ fn main() {
// This can also be configured at creation
// fb.set_resizable(true);
fb.persist();
fb.persist(&mut event_loop);
}

View file

@ -1,19 +1,21 @@
#[macro_use]
extern crate mini_gl_fb;
use mini_gl_fb::Config;
use mini_gl_fb::glutin::event_loop::EventLoop;
use mini_gl_fb::glutin::dpi::LogicalSize;
fn main() {
let mut fb = mini_gl_fb::get_fancy(Config {
window_title: "Hello world!",
window_size: (800.0, 600.0),
buffer_size: (2, 2),
.. Default::default()
});
let mut event_loop = EventLoop::new();
let mut fb = mini_gl_fb::get_fancy(config! {
window_title: String::from("Hello world!"),
window_size: LogicalSize::new(800.0, 600.0),
buffer_size: Some(LogicalSize::new(2, 2))
}, &event_loop);
let mut buffer = vec![[128u8, 0, 0, 255]; 4];
buffer[3] = [255, 255, 255, 255];
fb.update_buffer(&buffer);
fb.persist();
fb.persist(&mut event_loop);
}

View file

@ -1,8 +1,8 @@
extern crate mini_gl_fb;
fn main() {
let mut fb = mini_gl_fb::gotta_go_fast("Hello world!", 800.0, 600.0);
let (mut event_loop, mut fb) = mini_gl_fb::gotta_go_fast("Hello world!", 800.0, 600.0);
let buffer = vec![[128u8, 0, 0, 255]; 800 * 600];
fb.update_buffer(&buffer);
fb.persist();
fb.persist(&mut event_loop);
}

View file

@ -1,14 +1,17 @@
#[macro_use]
extern crate mini_gl_fb;
use mini_gl_fb::{Config, BufferFormat};
use mini_gl_fb::BufferFormat;
use mini_gl_fb::glutin::event_loop::EventLoop;
use mini_gl_fb::glutin::dpi::LogicalSize;
fn main() {
let mut fb = mini_gl_fb::get_fancy(Config {
window_title: "Hello world!",
window_size: (800.0, 600.0),
buffer_size: (2, 2),
.. Default::default()
});
let mut event_loop = EventLoop::new();
let mut fb = mini_gl_fb::get_fancy(config! {
window_title: String::from("Hello world!"),
window_size: LogicalSize::new(800.0, 600.0),
buffer_size: Some(LogicalSize::new(2, 2))
}, &event_loop);
fb.change_buffer_format::<u8>(BufferFormat::RG);
@ -16,5 +19,5 @@ fn main() {
let buffer = vec![[0u8, 50, 128, 255]; 4];
fb.update_buffer(&buffer);
fb.persist();
fb.persist(&mut event_loop);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

View file

@ -1,20 +1,224 @@
use glutin::{
GlWindow,
EventsLoop,
VirtualKeyCode,
MouseButton,
ModifiersState,
};
//! Contains the [`GlutinBreakout`] struct, which is a way to "break out" the Glutin context and
//! [`Framebuffer`] object and manipulate them directly.
use glutin::{WindowedContext, PossiblyCurrent, ContextError};
use core::Framebuffer;
use std::collections::HashMap;
use glutin::event::{MouseButton, VirtualKeyCode, ModifiersState};
/// `GlutinBreakout` is useful when you are growing out of the basic input methods and synchronous
/// nature of [`MiniGlFb`][crate::MiniGlFb], since it's more powerful than the the higher-level
/// abstrations. You can obtain it by calling
/// [`MiniGlFb::glutin_breakout()`][crate::MiniGlFb::glutin_breakout].
///
/// # Usage for multiple windows
/// The basic idea for managing multiple windows is to check each incoming event to determine which
/// window it's for. In order to draw to multiple windows individually, you have to switch the
/// context using [`make_current`][GlutinBreakout::make_current] before updating the window.
///
/// Here's a basic implementation (there's a lot of boilerplate because we're not using the
/// [`MiniGlFb`][crate::MiniGlFb] API - it's closer to using
/// [`winit`](https://docs.rs/winit/0.24.0/winit/index.html) directly):
///
/// ```
/// use mini_gl_fb::GlutinBreakout;
/// use mini_gl_fb::glutin::window::{Window, WindowId};
/// use mini_gl_fb::glutin::event::{Event, WindowEvent, KeyboardInput, VirtualKeyCode, ElementState};
/// use mini_gl_fb::glutin::event_loop::{EventLoop, ControlFlow};
/// use mini_gl_fb::config;
///
/// struct TrackedWindow {
/// pub breakout: GlutinBreakout,
/// pub background: [u8; 4]
/// }
///
/// impl TrackedWindow {
/// fn window(&self) -> &Window { self.breakout.context.window() }
/// fn matches_id(&self, id: WindowId) -> bool { id == self.breakout.context.window().id() }
///
/// pub fn handle_event(&mut self, event: &Event<()>) -> bool {
/// match event {
/// Event::WindowEvent { window_id: id, event, .. } if self.matches_id(*id) => {
/// match event {
/// WindowEvent::CloseRequested |
/// WindowEvent::KeyboardInput {
/// input: KeyboardInput {
/// virtual_keycode: Some(VirtualKeyCode::Escape),
/// state: ElementState::Pressed,
/// ..
/// },
/// ..
/// } => return false,
/// WindowEvent::Resized(size) => {
/// self.breakout.fb.resize_viewport(size.width, size.height);
/// let size = size.to_logical(self.window().scale_factor());
/// self.breakout.fb.resize_buffer(size.width, size.height);
/// }
/// _ => {
/// // do other stuff?
/// }
/// }
/// }
/// Event::RedrawRequested(id) if self.matches_id(*id) => {
/// // If you don't do this, OpenGL will get confused and only draw to one window.
/// unsafe { self.breakout.make_current().unwrap(); }
///
/// let size = self.window().inner_size().to_logical::<f64>(self.window().scale_factor());
///
/// // Unfortunately the performance of this is abysmal. Usually you should cache
/// // your buffer and only update it when needed or when the window is resized.
/// let pixels = size.width.floor() as usize * size.height.floor() as usize;
/// self.breakout.fb.update_buffer(&vec![self.background; pixels]);
/// self.breakout.context.swap_buffers();
/// }
/// _ => {}
/// }
///
/// true
/// }
/// }
///
/// fn main() {
/// let event_loop = EventLoop::new();
/// let mut windows: Vec<Option<TrackedWindow>> = vec![];
///
/// let config = config! {
/// resizable: true
/// };
///
/// windows.push(Some(TrackedWindow {
/// breakout: mini_gl_fb::get_fancy(config.clone(), &event_loop).glutin_breakout(),
/// background: [224u8, 66, 26, 255]
/// }));
///
/// windows.push(Some(TrackedWindow {
/// breakout: mini_gl_fb::get_fancy(config.clone(), &event_loop).glutin_breakout(),
/// background: [26u8, 155, 224, 255]
/// }));
///
/// // run event loop
/// event_loop.run(move |event, _, flow| {
/// *flow = ControlFlow::Wait;
///
/// for option in &mut windows {
/// if let Some(window) = option {
/// if !window.handle_event(&event) {
/// option.take();
/// }
/// }
/// }
///
/// windows.retain(Option::is_some);
///
/// if windows.is_empty() {
/// *flow = ControlFlow::Exit;
/// }
/// })
/// }
/// ```
///
/// It's hard to come up with a generalized, flexible implementation of this, especially if you need
/// to open more windows based on user input, or run tasks in other threads, etc. Basically, it's
/// open for you to play with, but it's not functionality that MGlFb wants to include first-class
/// just yet.
#[derive(Debug)]
pub struct GlutinBreakout {
pub events_loop: EventsLoop,
pub gl_window: GlWindow,
/// Contains the OpenGL context and its associated window. This is a
/// [`glutin`](https://docs.rs/glutin/0.26.0/glutin/) struct; go see their documentation on
/// [`WindowedContext`] for more information.
pub context: WindowedContext<PossiblyCurrent>,
/// Contains the [`Framebuffer`] for that context. Consult its documentation for information on
/// how to use it.
pub fb: Framebuffer,
}
impl GlutinBreakout {
/// Sets the current thread's OpenGL context to the one contained in this breakout.
///
/// Historically, MGlFb did not support multiple windows. It owned its own event loop and you
/// weren't allowed to use the library with your own. However, as of version 0.8, you are now
/// expected to bring your own event loop to all functions that involve one. This means that
/// multiple windows are very possible, and even supported, as long as you're willing to route
/// events yourself... and manage all the OpenGL contexts.
///
/// The problem with managing multiple OpenGL contexts from one thread is that the "current"
/// context is set per-thread. That means you basically have to switch through them really
/// quickly if you want to update multiple windows in "parallel". But how do you switch?
///
/// Glutin has you partially covered on this one - it has
/// [`make_current`][glutin::ContextWrapper<PossiblyCurrent, Window>::make_current]. However,
/// that method takes `self` and emits a new `WindowedContext`, and you can't really move `self`
/// into that function without unsafe code.
///
/// Here is an unsafe function containing code that makes the context current, in-place. That
/// way, you can switch contexts in one line of code, and focus on other stuff.
///
/// # Usage
///
/// ```
/// # use mini_gl_fb::glutin::event_loop::{EventLoop, ControlFlow};
/// # use mini_gl_fb::glutin::event::{Event, WindowEvent, KeyboardInput, VirtualKeyCode, ElementState};
/// # use mini_gl_fb::{config, get_fancy};
/// #
/// # let mut event_loop = EventLoop::new();
/// # let mut breakout = get_fancy(config! {
/// # window_title: String::from("GlutinBreakout::make_current()")
/// # }, &event_loop).glutin_breakout();
/// #
/// event_loop.run(move |event, _, flow| {
/// # *flow = ControlFlow::Wait;
/// #
/// match event {
/// // ...
/// # Event::WindowEvent { event, .. } => {
/// # match event {
/// # WindowEvent::CloseRequested |
/// # WindowEvent::KeyboardInput {
/// # input: KeyboardInput {
/// # virtual_keycode: Some(VirtualKeyCode::Escape),
/// # state: ElementState::Pressed,
/// # ..
/// # },
/// # ..
/// # } => *flow = ControlFlow::Exit,
/// # _ => ()
/// # }
/// # },
/// Event::RedrawRequested(..) => {
/// unsafe { breakout.make_current().unwrap(); }
/// // ...
/// # let window = breakout.context.window();
/// # let size = window.inner_size().to_logical::<f64>(window.scale_factor());
/// # let pixels = size.width.floor() as usize * size.height.floor() as usize;
/// # let your_buffer_here = vec![[0u8, 200, 240, 255]; pixels];
/// breakout.fb.update_buffer(&your_buffer_here);
/// breakout.context.swap_buffers();
/// }
/// // ...
/// # _ => {}
/// }
/// })
/// ```
pub unsafe fn make_current(&mut self) -> Result<(), ContextError> {
let context_ptr: *mut _ = &mut self.context;
let context = std::ptr::read(context_ptr);
let result = context.make_current();
if let Err((context, err)) = result {
std::ptr::write(context_ptr, context);
Err(err)
} else {
std::ptr::write(context_ptr, result.unwrap());
Ok(())
}
}
}
/// Used for [`MiniGlFb::glutin_handle_basic_input`][crate::MiniGlFb::glutin_handle_basic_input].
/// Contains the current state of the window in a polling-like fashion.
#[non_exhaustive]
#[derive(Default, Clone, PartialEq, Debug)]
pub struct BasicInput {
/// The mouse position in buffer coordinates.
///
@ -33,8 +237,14 @@ pub struct BasicInput {
///
/// If a key has not been pressed yet it will not be in the map.
pub keys: HashMap<VirtualKeyCode, (bool, bool)>,
/// The current modifier keys that are being pressed.
pub modifiers: ModifiersState,
/// This is set to `true` when the window is resized outside of your callback. If you do not
/// update the buffer in your callback, you should still draw it if this is `true`.
pub resized: bool,
/// If this is set to `true` by your callback, it will not be called as fast as possible, but
/// rather only when the input changes.
pub wait: bool,
}
impl BasicInput {

View file

@ -1,48 +1,123 @@
/// Configuration for "advanced" use cases, when `gotta_go_fast` isn't doing what you need.
use glutin::dpi::LogicalSize;
/// Configuration for "advanced" use cases, when [`gotta_go_fast`][crate::gotta_go_fast] isn't doing
/// what you need.
///
/// The following pattern is reccomended when creating a config:
/// The following pattern is recommended when creating a config:
///
/// ```
/// use mini_gl_fb::Config;
/// use mini_gl_fb::config;
/// use mini_gl_fb::glutin::dpi::LogicalSize;
///
/// let config: Config<&str> = Config {
/// let config = config! {
/// /* specify whichever fields you need to set, for example: */
/// window_size: (100.0, 100.0),
/// resizable: false,
/// .. Default::default()
/// window_size: LogicalSize::new(100.0, 100.0),
/// resizable: true,
/// };
/// ```
///
/// Since `Config` is `#[non_exhaustive]`, you cannot construct it directly, and can only obtain one
/// from a trait like [`Default`]. The [`config!`][config] macro makes it much less tedious to
/// construct custom configs. See its documentation for more information.
///
/// If there's a config option you want to see or think is missing, please open an issue!
pub struct Config<S: ToString> {
/// Sets the scale of the buffer. The buffer will automatically scale to the size of the
/// window. By default this will be the same size as the window_size.
pub buffer_size: (u32, u32),
#[non_exhaustive]
#[derive(Clone, PartialEq, Debug)]
pub struct Config {
/// Sets the pixel dimensions of the buffer. The buffer will automatically stretch to fill the
/// whole window. By default this will be the same as the window_size.
pub buffer_size: Option<LogicalSize<u32>>,
/// If this is true, the window created by mini_gl_fb will be set to resizable. This can be
/// changed later. Please note that the buffer itself will not be automatically resized, only
/// the viewport.
pub resizable: bool,
pub window_title: S,
pub window_size: (f64, f64),
/// The title of the window that will be created.
pub window_title: String,
/// The logical size of the window that gets created. On HiDPI screens the actual size may be
/// larger than this
pub window_size: LogicalSize<f64>,
/// By default, the origin of the buffer is the bottom-left. This is known as "inverted Y", as
/// most screen-space coordinate systems begin from the top-left. By explicitly setting this
/// option to `false`, you can switch to screen-space coordinates rather than OpenGL
/// coordinates. Otherwise, you will have to invert all mouse events received from winit/glutin.
pub invert_y: bool
}
impl<'a> Default for Config<&'a str> {
impl Default for Config {
fn default() -> Self {
Config {
buffer_size: (0, 0),
buffer_size: None,
resizable: false,
// :^)
window_title: "Super Mini GL Framebufferer 3!",
window_size: (600.0, 480.0),
window_title: String::from("Super Mini GL Framebufferer 3!"),
window_size: LogicalSize::new(600.0, 480.0),
invert_y: true
}
}
}
impl Default for Config<String> {
fn default() -> Self {
Config {
buffer_size: (0, 0),
resizable: false,
// :^)
window_title: "Super Mini GL Framebufferer 3!".to_string(),
window_size: (600.0, 480.0),
}
}
/// The `config!` macro is intended to make it easy for us to add new fields in the future while
/// staying backwards-compatible. This is done by making [`Config`] `#[non_exhaustive]` but still
/// providing [`Default`], so that users can obtain the defaults and modify it to their liking. The
/// `config!` macro automates this, and makes custom configs just as easy as constructing `Config`
/// directly would be.
///
/// You can use the macro like this:
///
/// ```
/// # use mini_gl_fb::config;
/// #
/// let config = config! {
/// resizable: true,
/// invert_y: false
/// };
///
/// assert_eq!(config.resizable, true);
/// assert_eq!(config.invert_y, false);
/// ```
///
/// As you can see, it's almost identical to a struct construction. You just use this macro in place
/// of `Config`. As such, it has a minimal impact on user code. That invocation roughly expands to:
///
/// ```
/// # use mini_gl_fb::Config;
/// #
/// let config = {
/// let mut config = Config::default();
/// config.resizable = true;
/// config.invert_y = false;
/// config
/// };
/// ```
///
/// This way, adding new fields will not affect existing code.
///
/// You can also create a copy of an existing config, with only a couple options changed:
///
/// ```
/// # use mini_gl_fb::config;
/// #
/// let original = config! {};
/// let copy = config! {
/// invert_y: false,
/// ..original
/// };
///
/// assert_eq!(original.invert_y, true);
/// assert_eq!(copy.invert_y, false);
/// ```
#[macro_export]
macro_rules! config {
{$($k:ident: $v:expr),+,..$from:expr$(,)?} => {{
let mut config: $crate::Config = ::std::clone::Clone::clone(&$from);
$(config.$k = $v;
)*config
}};
{$($k:ident: $v:expr),+$(,)?} => {{
let mut config: $crate::Config = ::std::default::Default::default();
$(config.$k = $v;
)*config
}};
{} => { <$crate::Config as ::std::default::Default>::default() }
}

View file

@ -2,49 +2,44 @@ use breakout::{GlutinBreakout, BasicInput};
use rustic_gl;
use glutin::{
EventsLoop,
WindowBuilder,
ContextBuilder,
GlWindow,
GlContext,
Event,
WindowEvent,
VirtualKeyCode,
ElementState,
};
use glutin::dpi::{LogicalSize, LogicalPosition};
use glutin::{ContextBuilder, WindowedContext, PossiblyCurrent};
use glutin::dpi::{LogicalSize, PhysicalPosition, PhysicalSize};
use gl;
use gl::types::*;
use std::mem::size_of_val;
use std::collections::HashMap;
use glutin::window::WindowBuilder;
use glutin::event_loop::{EventLoop, ControlFlow};
use glutin::platform::run_return::EventLoopExtRunReturn;
use glutin::event::{Event, WindowEvent, VirtualKeyCode, ElementState, KeyboardInput};
/// Create a context using glutin given a configuration.
pub fn init_glutin_context<S: ToString>(
pub fn init_glutin_context<S: ToString, ET: 'static>(
window_title: S,
window_width: f64,
window_height: f64,
resizable: bool,
) -> (EventsLoop, GlWindow) {
event_loop: &EventLoop<ET>
) -> WindowedContext<PossiblyCurrent> {
let window_size = LogicalSize::new(window_width, window_height);
let events_loop = EventsLoop::new();
let window = WindowBuilder::new()
.with_title(window_title.to_string())
.with_dimensions(window_size)
.with_inner_size(window_size)
.with_resizable(resizable);
let context = ContextBuilder::new();
let gl_window = GlWindow::new(window, context, &events_loop).unwrap();
let context: WindowedContext<PossiblyCurrent> = unsafe {
ContextBuilder::new()
.build_windowed(window, event_loop)
.unwrap()
.make_current()
.unwrap()
};
unsafe {
gl_window.make_current().unwrap();
gl::load_with(|symbol| gl_window.get_proc_address(symbol) as *const _);
}
gl::load_with(|symbol| context.get_proc_address(symbol) as *const _);
(events_loop, gl_window)
context
}
type VertexFormat = buffer_layout!([f32; 2], [f32; 2]);
@ -54,7 +49,8 @@ pub fn init_framebuffer(
buffer_width: u32,
buffer_height: u32,
viewport_width: u32,
viewport_height: u32
viewport_height: u32,
invert_y: bool
) -> Framebuffer {
// The config takes the size in u32 because that's all that actually makes sense but since
// OpenGL is from the Land of C where a Working Type System doesn't exist, we work with i32s
@ -98,15 +94,25 @@ pub fn init_framebuffer(
gl::BindBuffer(gl::ARRAY_BUFFER, vbo);
VertexFormat::declare(0);
let verts: [[f32; 2]; 12] = [
let verts: [[f32; 2]; 12] = if invert_y {
[
[-1., 1.], [0., 1.], // top left
[-1., -1.], [0., 0.], // bottom left
[1., -1.], [1., 0.], // bottom right
[1., -1.], [1., 0.], // bottom right
[1., 1.], [1., 1.], // top right
[-1., 1.], [0., 1.], // top left
];
use std::mem::size_of_val;
]
} else {
[
[-1., -1.], [0., 1.], // bottom left
[1., 1.], [1., 0.], // top right
[-1., 1.], [0., 0.], // top left
[1., 1.], [1., 0.], // top right
[-1., -1.], [0., 1.], // bottom left
[1., -1.], [1., 1.], // bottom right
]
};
gl::BufferData(gl::ARRAY_BUFFER,
size_of_val(&verts) as _,
verts.as_ptr() as *const _,
@ -120,10 +126,11 @@ pub fn init_framebuffer(
}
Framebuffer {
buffer_width,
buffer_height,
vp_width,
vp_height,
buffer_size: LogicalSize::new(buffer_width, buffer_height),
vp_size: PhysicalSize::new(vp_width, vp_height),
did_draw: false,
inverted_y: invert_y,
internal: FramebufferInternal {
program,
sampler_location,
vertex_shader: Some(vertex_shader),
@ -133,7 +140,7 @@ pub fn init_framebuffer(
vao,
vbo,
texture_format,
did_draw: false,
}
}
}
@ -146,214 +153,181 @@ pub fn init_framebuffer(
/// is no documentation and you find the method is non-trivial, it's a bug! Feel free to submit an
/// issue!
pub struct Internal {
pub events_loop: EventsLoop,
pub gl_window: GlWindow,
pub context: WindowedContext<PossiblyCurrent>,
pub fb: Framebuffer,
}
impl Internal {
pub fn update_buffer<T>(&mut self, image_data: &[T]) {
self.fb.update_buffer(image_data);
self.gl_window.swap_buffers().unwrap();
self.context.swap_buffers().unwrap();
}
pub fn set_resizable(&mut self, resizable: bool) {
self.gl_window.set_resizable(resizable);
self.context.window().set_resizable(resizable);
}
pub fn resize_viewport(&mut self, width: u32, height: u32) {
self.gl_window.resize((width, height).into());
self.context.resize((width, height).into());
self.fb.resize_viewport(width, height);
}
pub fn is_running(&mut self) -> bool {
let mut running = true;
let mut resized = None;
self.events_loop.poll_events(|event| {
match event {
Event::WindowEvent { event, .. } => match event {
WindowEvent::CloseRequested => running = false,
WindowEvent::KeyboardInput { input, .. } => {
if let Some(k) = input.virtual_keycode {
if k == VirtualKeyCode::Escape
&& input.state == ElementState::Released {
running = false;
}
}
}
WindowEvent::Resized(logical_size) => {
resized = Some(logical_size);
}
_ => {},
},
_ => {},
}
});
if let Some(size) = resized {
let dpi_factor = self.gl_window.get_hidpi_factor();
let (x, y) = size.to_physical(dpi_factor).into();
self.resize_viewport(x, y);
}
running
}
pub fn redraw(&mut self) {
self.fb.redraw();
self.gl_window.swap_buffers().unwrap();
self.context.swap_buffers().unwrap();
}
pub fn persist(&mut self) {
self.persist_and_redraw(false);
pub fn persist<ET: 'static>(&mut self, event_loop: &mut EventLoop<ET>) {
self.persist_and_redraw(event_loop, false);
}
pub fn persist_and_redraw(&mut self, redraw: bool) {
let mut running = true;
while running {
pub fn persist_and_redraw<ET: 'static>(&mut self, event_loop: &mut EventLoop<ET>, redraw: bool) {
event_loop.run_return(|event, _, flow| {
*flow = ControlFlow::Wait;
let mut new_size = None;
self.events_loop.poll_events(|event| {
match event {
Event::WindowEvent { event, .. } => match event {
WindowEvent::CloseRequested => running = false,
WindowEvent::CloseRequested => *flow = ControlFlow::Exit,
WindowEvent::KeyboardInput { input, .. } => {
if let Some(k) = input.virtual_keycode {
if k == VirtualKeyCode::Escape
&& input.state == ElementState::Released {
running = false;
&& input.state == ElementState::Pressed {
*flow = ControlFlow::Exit;
}
}
}
WindowEvent::Resized(logical_size) => {
new_size = Some(logical_size);
WindowEvent::Resized(physical_size) => {
new_size = Some(physical_size);
}
_ => {},
},
_ => {},
}
});
if let Some(size) = new_size {
let dpi_factor = self.gl_window.get_hidpi_factor();
let (x, y) = size.to_physical(dpi_factor).into();
self.resize_viewport(x, y);
// TODO: We should store window size in BasicInput, but as what type? OpenGL wants
// integer window sizes but we also have to deal with physical vs. logical size
self.resize_viewport(size.width, size.height);
self.redraw();
} else {
if redraw {
} else if redraw {
self.fb.redraw();
self.gl_window.swap_buffers().unwrap();
}
}
self.context.swap_buffers().unwrap();
}
});
}
pub fn glutin_handle_basic_input<F: FnMut(&mut Framebuffer, &BasicInput) -> bool>(
&mut self, mut handler: F
pub fn glutin_handle_basic_input<ET: 'static, F: FnMut(&mut Framebuffer, &BasicInput) -> bool>(
&mut self, event_loop: &mut EventLoop<ET>, mut handler: F
) {
let mut running = true;
let mut input = BasicInput {
// Not sure how to set mouse pos at start
mouse_pos: (0.0, 0.0),
mouse: HashMap::new(),
keys: HashMap::new(),
modifiers: Default::default(),
resized: false,
};
while running {
let mut previous_input: Option<BasicInput> = None;
let mut input = BasicInput::default();
event_loop.run_return(|event, _, flow| {
let mut new_size = None;
let mut new_mouse_pos: Option<LogicalPosition> = None;
self.events_loop.poll_events(|event| {
let mut new_mouse_pos: Option<PhysicalPosition<f64>> = None;
// Copy the current states into the previous state for input
for (_, val) in &mut input.keys {
val.0 = val.1;
}
for (_, val) in &mut input.mouse {
val.0 = val.1;
}
match event {
Event::WindowEvent { event, .. } => match event {
WindowEvent::CloseRequested => running = false,
WindowEvent::KeyboardInput { input: event_input, .. } => {
if let Some(vk) = event_input.virtual_keycode {
WindowEvent::CloseRequested => *flow = ControlFlow::Exit,
WindowEvent::KeyboardInput {
input: KeyboardInput {
virtual_keycode: Some(vk),
state,
..
},
..
} => {
let key = input.keys.entry(vk)
.or_insert((false, false));
key.1 = event_input.state == ElementState::Pressed;
key.1 = state == ElementState::Pressed;
}
input.modifiers = event_input.modifiers;
}
WindowEvent::CursorMoved { position, modifiers, ..} => {
WindowEvent::CursorMoved { position, .. } => {
new_mouse_pos = Some(position);
input.modifiers = modifiers;
}
WindowEvent::MouseInput { state, button, modifiers, .. } => {
WindowEvent::MouseInput { state, button, .. } => {
let button = input.mouse.entry(button)
.or_insert((false, false));
button.1 = state == ElementState::Pressed;
}
WindowEvent::ModifiersChanged(modifiers) => {
input.modifiers = modifiers;
}
WindowEvent::Resized(logical_size) => {
new_size = Some(logical_size);
}
_ => {},
_ => {}
},
_ => {},
_ => {}
}
});
if let Some(size) = new_size {
let dpi_factor = self.gl_window.get_hidpi_factor();
let (x, y) = size.to_physical(dpi_factor).into();
self.resize_viewport(x, y);
self.resize_viewport(size.width, size.height);
input.resized = false;
}
if let Some(pos) = new_mouse_pos {
let dpi_factor = self.gl_window.get_hidpi_factor();
let (x, y): (f64, f64) = pos.to_physical(dpi_factor).into();
let x_scale = self.fb.buffer_width as f64 / (self.fb.vp_width as f64);
let y_scale = self.fb.buffer_height as f64 / (self.fb.vp_height as f64);
let (x, y): (f64, f64) = pos.into();
let x_scale = self.fb.buffer_size.width as f64 / (self.fb.vp_size.width as f64);
let y_scale = self.fb.buffer_size.height as f64 / (self.fb.vp_size.height as f64);
let mouse_pos = (
x * x_scale,
// use the OpenGL texture coordinate system instead of window coordinates
self.fb.buffer_height as f64 - y * y_scale
if self.fb.inverted_y {
self.fb.buffer_size.height as f64 - y * y_scale
} else {
y * y_scale
}
);
input.mouse_pos = mouse_pos;
}
if running {
running = handler(&mut self.fb, &input);
if input.wait {
*flow = ControlFlow::Wait;
// handler only wants to be notified when the input changes
if previous_input.as_ref().map_or(true, |p| *p != input) {
if !handler(&mut self.fb, &input) {
*flow = ControlFlow::Exit;
}
}
} else {
// handler wants to be notified regardless
if !handler(&mut self.fb, &input) {
*flow = ControlFlow::Exit;
} else {
*flow = ControlFlow::Poll;
}
}
previous_input = Some(input.clone());
if self.fb.did_draw {
self.gl_window.swap_buffers().unwrap();
self.context.swap_buffers().unwrap();
self.fb.did_draw = false;
}
}
}
});
}
pub fn glutin_breakout(self) -> GlutinBreakout {
GlutinBreakout {
events_loop: self.events_loop,
gl_window: self.gl_window,
context: self.context,
fb: self.fb,
}
}
}
/// Provides the drawing functionality.
///
/// You can get direct access by using a breakout function, such as breakout_glutin.
///
/// # Disclaimer:
///
/// Accessing fields directly is not the intended usage. If a feature is missing please open an
/// issue. The fields are public, however, so that while you are waiting for a feature to be
/// exposed, if you need something in a pinch you can dig in easily and make it happen.
///
/// The internal fields may change.
///
/// TODO: Possibly create a FramebufferInternal struct?
pub struct Framebuffer {
pub buffer_width: i32,
pub buffer_height: i32,
pub vp_width: i32,
pub vp_height: i32,
/// Contains internal OpenGL things.
#[non_exhaustive]
#[derive(Debug)]
pub struct FramebufferInternal {
pub program: GLuint,
pub sampler_location: GLint,
pub vertex_shader: Option<GLuint>,
@ -363,17 +337,70 @@ pub struct Framebuffer {
pub vao: GLuint,
pub vbo: GLuint,
pub texture_format: (BufferFormat, GLenum),
}
/// The Framebuffer struct manages the framebuffer of a MGlFb window. Through this struct, you can
/// update the size and content of the buffer. Framebuffers are usually obtained through
/// [`MiniGlFb::glutin_breakout`][crate::MiniGlFb::glutin_breakout], but they're also returned by
/// [`init_framebuffer`].
///
/// # Basic usage
/// Firstly, one of the most important things to do when managing a Framebuffer manually is to make
/// sure that whenever the window is resized, the Framebuffer is the first to know. Usually, this is
/// handled for you by [`MiniGlFb`][crate::MiniGlFb], but that isn't the case when using the
/// [`GlutinBreakout`].
///
/// Whenever you receive a resize event for your window, make sure to call
/// [`Framebuffer::resize_viewport`] with the new physical dimensions of your window. You can also
/// figure out some logical dimensions and call [`Framebuffer::resize_buffer`] too.
///
/// Additionally, when managing multiple framebuffers at once, you should make sure to call
/// [`GlutinBreakout::make_current`] when appropriate, before calling any `Framebuffer` methods.
/// Forgetting to call `make_current` can cause OpenGL to get confused and draw to the wrong window,
/// which is probably not what you want.
#[non_exhaustive]
#[derive(Debug)]
pub struct Framebuffer {
/// The logical size of the buffer. When you update the buffer via
/// [`update_buffer`][Framebuffer::update_buffer], it is expected to contain
/// `buffer_size.width * buffer_size.height` pixels.
pub buffer_size: LogicalSize<i32>,
/// The physical size of the viewport. This should always be kept up to date with the size of
/// the window, and there is no reason to set it otherwise unless you're drawing multiple
/// buffers to one window or something funky like that.
pub vp_size: PhysicalSize<i32>,
/// This is set to `true` every time [`draw`][Framebuffer::draw] is called. (or, by extension,
/// [`update_buffer`][Framebuffer::update_buffer])
///
/// It's safe to set this to `false` afterwards, it's just a flag to let you know if code you're
/// calling into has updated the buffer or not.
pub did_draw: bool,
/// True if the origin should be the bottom left of the screen instead of the top left. For
/// historical reasons, this is the default. This should only be configured by changing the
/// [`Config`][crate::Config] passed to [`get_fancy`][crate::get_fancy].
pub inverted_y: bool,
/// Contains internal OpenGL things.
///
/// Accessing fields directly is not the intended usage. If a feature is missing please open an
/// issue. The fields are public, however, so that while you are waiting for a feature to be
/// exposed, if you need something in a pinch you can dig in easily and make it happen.
///
/// The internal fields may change.
pub internal: FramebufferInternal
}
impl Framebuffer {
pub fn update_buffer<T>(&mut self, image_data: &[T]) {
// Check the length of the passed slice so this is actually a safe method.
let (format, kind) = self.texture_format;
let (format, kind) = self.internal.texture_format;
let expected_size_in_bytes = size_of_gl_type_enum(kind)
* format.components()
* self.buffer_width as usize
* self.buffer_height as usize;
* self.buffer_size.width as usize
* self.buffer_size.height as usize;
let actual_size_in_bytes = size_of_val(image_data);
if actual_size_in_bytes != expected_size_in_bytes {
panic!(
@ -388,8 +415,8 @@ impl Framebuffer {
gl::TEXTURE_2D,
0,
gl::RGBA as _,
fb.buffer_width,
fb.buffer_height,
fb.buffer_size.width,
fb.buffer_size.height,
0,
format as GLenum,
kind,
@ -400,12 +427,12 @@ impl Framebuffer {
}
pub fn use_vertex_shader(&mut self, source: &str) {
rebuild_shader(&mut self.vertex_shader, gl::VERTEX_SHADER, source);
rebuild_shader(&mut self.internal.vertex_shader, gl::VERTEX_SHADER, source);
self.relink_program();
}
pub fn use_fragment_shader(&mut self, source: &str) {
rebuild_shader(&mut self.fragment_shader, gl::FRAGMENT_SHADER, source);
rebuild_shader(&mut self.internal.fragment_shader, gl::FRAGMENT_SHADER, source);
self.relink_program();
}
@ -415,7 +442,7 @@ impl Framebuffer {
}
pub fn use_geometry_shader(&mut self, source: &str) {
rebuild_shader(&mut self.geometry_shader, gl::GEOMETRY_SHADER, source);
rebuild_shader(&mut self.internal.geometry_shader, gl::GEOMETRY_SHADER, source);
self.relink_program();
}
@ -427,17 +454,15 @@ impl Framebuffer {
&mut self,
format: BufferFormat,
) {
self.texture_format = (format, T::to_gl_enum());
self.internal.texture_format = (format, T::to_gl_enum());
}
pub fn resize_buffer(&mut self, buffer_width: u32, buffer_height: u32) {
self.buffer_width = buffer_width as _;
self.buffer_height = buffer_height as _;
self.buffer_size = LogicalSize::new(buffer_width, buffer_height).cast();
}
pub fn resize_viewport(&mut self, width: u32, height: u32) {
self.vp_width = width as _;
self.vp_height = height as _;
self.vp_size = PhysicalSize::new(width, height).cast();
}
pub fn redraw(&mut self) {
@ -445,16 +470,16 @@ impl Framebuffer {
}
/// Draw the quad to the active context. Optionally issue other commands after binding
/// everything but before
/// everything but before drawing it.
///
/// You probably want `redraw` (equivalent to `.draw(|_| {})`).
/// You probably want [`redraw`][Framebuffer::redraw] (equivalent to `.draw(|_| {})`).
pub fn draw<F: FnOnce(&Framebuffer)>(&mut self, f: F) {
unsafe {
gl::Viewport(0, 0, self.vp_width, self.vp_height);
gl::UseProgram(self.program);
gl::BindVertexArray(self.vao);
gl::Viewport(0, 0, self.vp_size.width, self.vp_size.height);
gl::UseProgram(self.internal.program);
gl::BindVertexArray(self.internal.vao);
gl::ActiveTexture(0);
gl::BindTexture(gl::TEXTURE_2D, self.texture);
gl::BindTexture(gl::TEXTURE_2D, self.internal.texture);
f(self);
gl::DrawArrays(gl::TRIANGLES, 0, 6);
gl::BindTexture(gl::TEXTURE_2D, 0);
@ -466,17 +491,17 @@ impl Framebuffer {
pub fn relink_program(&mut self) {
unsafe {
gl::DeleteProgram(self.program);
self.program = build_program(&[
self.vertex_shader,
self.fragment_shader,
self.geometry_shader,
gl::DeleteProgram(self.internal.program);
self.internal.program = build_program(&[
self.internal.vertex_shader.clone(),
self.internal.fragment_shader.clone(),
self.internal.geometry_shader.clone(),
]);
}
}
}
#[derive(Copy, Clone, PartialEq, Eq)]
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
#[repr(u32)]
pub enum BufferFormat {
R = gl::RED,

View file

@ -2,26 +2,33 @@
//!
//! # Basic Usage
//!
//! Start with the function `gotta_go_fast`. This will create a basic window and give you a buffer
//! that you can draw to in one line. The main public API is available through the `MiniGlFb` type.
//! Start with the function [`gotta_go_fast`]. This will create an [`EventLoop`], basic window, and
//! a buffer that you can draw to, all in just one function call. The main public API is available
//! through the [`MiniGlFb`] type.
//!
//! ```rust
//! extern crate mini_gl_fb;
//!
//! fn main() {
//! let mut fb = mini_gl_fb::gotta_go_fast("Hello world!", 800.0, 600.0);
//! // Create the event loop and framebuffer
//! let (mut event_loop, mut fb) = mini_gl_fb::gotta_go_fast("Hello world!", 800.0, 600.0);
//!
//! // Fill the buffer with something
//! let buffer = vec![[128u8, 0, 0, 255]; 800 * 600];
//! fb.update_buffer(&buffer);
//! fb.persist();
//!
//! // Show the window until the user decides to quit (close button, or Esc)
//! fb.persist(&mut event_loop);
//! }
//! ```
//!
//! The default buffer format is 32bit RGBA, so every pixel is four bytes. Buffer[0] is the bottom
//! left pixel. The buffer should be tightly packed with no padding after each row.
//! The default buffer format is 32bit RGBA, so every pixel is four bytes. Buffer\[0\] is the bottom
//! left pixel (not the top). See [`Config::invert_y`] for information on this. The buffer should be
//! tightly packed with no padding after each row.
//!
//! # Interlude: Library philosophy
//!
//! All of the internals of this library are exposed. Any fields behind `mini_gl_fb.internal`
//! All of the internals of this library are exposed. Any fields behind [`MiniGlFb::internal`]
//! are not considered a part of the public API but are exposed in case the library is missing a
//! feature that you need "right now." This library is not here to box you in.
//!
@ -35,23 +42,32 @@
//!
//! # More advanced configuration
//!
//! Use the `get_fancy` function for more settings. See `Config` for what's available. This allows
//! you to, for instance, create a window with a buffer of a different size than the window.
//! Use the [`get_fancy`] function for more settings. See [`Config`] for what's available. This
//! allows you to, for instance, create a window with a buffer of a different size than the window.
//! This is useful for HiDPI support, since you can take advantage of the full resolution of the
//! screen.
//!
//! `get_fancy` (and all the functions in the library) require you to bring your own event loop.
//! This allows for multiple windows. See the `multi_window` example.
//!
//! ```rust
//! use mini_gl_fb::{get_fancy, Config};
//! use mini_gl_fb::{get_fancy, config};
//! use mini_gl_fb::glutin::event_loop::EventLoop;
//! use mini_gl_fb::glutin::dpi::LogicalSize;
//! # let window_title = "foo";
//! # let window_width = 800.0;
//! # let window_height = 600.0;
//!
//! let config = Config {
//! let event_loop = EventLoop::new();
//! let config = config! {
//! window_title: window_title.to_string(),
//! window_size: (window_width, window_height),
//! .. Default::default()
//! window_size: LogicalSize::new(window_width, window_height)
//! };
//! let fb = get_fancy(config);
//! let fb = get_fancy(config, &event_loop);
//! ```
//!
//! (See the [`Config`] documentation for an explanation of the `config!` macro.)
//!
//! If you think something else should be exposed as an option, open an issue!
//!
//! # Bring your own context (and event handling)!
@ -66,6 +82,26 @@
//! Currently uses the `gl` crate for OpenGL loading. OpenGL context creation may fail if your
//! setup does not support the newest OpenGL. This bug needs to be verified and is be fixable.
//! OpenGL ~3 is currently required, but OpenGL 2.1 support should be feasible if requested.
//!
//! # Feature matrix
//!
//! MGlFb does not implement every feature and is not compatible with everything, but there are
//! still reasons to choose it over other libraries.
//!
//! | Feature | `glutin`+`mini_gl_fb` | `winit`+`pixels` | `minifb` |
//! |-------------------------------|-----------------------|----------------------|-----------------------|
//! | `gotta_go_fast`-like function | Yes | No | No |
//! | Event-based API | Yes | Yes | No |
//! | Multi-window | Yes | Yes | Unsupported |
//! | Vsync | Yes (use glutin) | Confusing wgpu stuff | Only FPS locking |
//! | Buffer allocation | By user | Confusing wgpu stuff | By user |
//! | Resizable | Yes | Requires restart | Yes |
//! | Scalable | Yes | Integer | Buggy on Windows |
//! | Hardware-accelerated | Yes | Very | macOS-only |
//! | Basic input | Yes | No | Always |
//! | Multiple rendering backends | No (OpenGL) | Yes, by wgpu | No (one per platform) |
//! | Custom shaders | Yes | Pre-provided | No shaders |
//! | Requires OpenGL | 3.3+ | No | No |
#[macro_use]
pub extern crate rustic_gl;
@ -82,65 +118,63 @@ pub use config::Config;
pub use core::{Internal, BufferFormat, Framebuffer};
use core::ToGlType;
use glutin::event_loop::EventLoop;
use glutin::dpi::LogicalSize;
/// Creates a non resizable window and framebuffer with a given size in pixels.
/// Creates a non-resizable window and framebuffer with a given size in logical pixels. On HiDPI
/// screens, the physical size of the window may be larger or smaller than the provided values, but
/// the buffer will be scaled to match.
///
/// Please note that the window size is in logical device pixels, so on a high DPI monitor the
/// physical window size may be larger. In this case, the rendered buffer will be scaled it
/// automatically by OpenGL.
/// This function also creates an event loop for you. If you would like to create your own event
/// loop, you can use the `get_fancy` function directly.
pub fn gotta_go_fast<S: ToString>(
window_title: S,
window_width: f64,
window_height: f64
) -> MiniGlFb {
let config = Config {
) -> (EventLoop<()>, MiniGlFb) {
let event_loop = EventLoop::new();
let config = config! {
window_title: window_title.to_string(),
window_size: (window_width, window_height),
resizable: false,
.. Default::default()
window_size: LogicalSize::from((window_width, window_height)),
resizable: false
};
get_fancy(config)
let fancy = get_fancy(config, &event_loop);
(event_loop, fancy)
}
/// Create a window with a custom configuration.
///
/// If this configuration is not sufficient for you, check out the source for this function.
/// Creating the MiniGlFb instance is just a call to two functions!
/// Creating the `MiniGlFb` instance is just a call to two functions!
///
/// Many window settings can be changed after creation, so you most likely don't ever need to call
/// `get_fancy` with a custom config. However, if there is a bug in the OS/windowing system or
/// glutin or in this library, this function exists as a possible work around (or in case for some
/// reason everything must be absolutely correct at window creation)
pub fn get_fancy<S: ToString>(config: Config<S>) -> MiniGlFb {
let buffer_width = if config.buffer_size.0 == 0 { config.window_size.0.round() as _ }
else { config.buffer_size.0 };
let buffer_height = if config.buffer_size.1 == 0 { config.window_size.1.round() as _ }
else { config.buffer_size.1 };
pub fn get_fancy<ET: 'static>(config: Config, event_loop: &EventLoop<ET>) -> MiniGlFb {
let buffer_size = config.buffer_size.unwrap_or_else(|| config.window_size.cast());
let (events_loop, gl_window) = core::init_glutin_context(
let context = core::init_glutin_context(
config.window_title,
config.window_size.0,
config.window_size.1,
config.window_size.width,
config.window_size.height,
config.resizable,
event_loop
);
let dpi_factor = gl_window.get_hidpi_factor();
let (vp_width, vp_height) = gl_window.get_inner_size()
.unwrap()
.to_physical(dpi_factor)
.into();
let (vp_width, vp_height) = context.window().inner_size().into();
let fb = core::init_framebuffer(
buffer_width,
buffer_height,
buffer_size.width,
buffer_size.height,
vp_width,
vp_height,
config.invert_y
);
MiniGlFb {
internal: Internal {
events_loop,
gl_window,
context,
fb,
}
}
@ -174,21 +208,6 @@ impl MiniGlFb {
self.internal.update_buffer(image_data);
}
/// Checks if escape has been pressed or the window has been asked to close.
///
/// This function is a good choice for a while loop condition when you are making a simulation
/// that needs to progress over time but does not need to handle user input.
///
/// Calling this function clears the event queue and also handles resizes for you (if your
/// window is resizable). This does not resize the image buffer; the rendered buffer will
/// instead scale to fit the window.
///
/// Please note that if your window does change size, for buffer to appear scaled it must
/// be redrawn, typically either by calling `redraw` or `update_buffer`.
pub fn is_running(&mut self) -> bool {
self.internal.is_running()
}
pub fn redraw(&mut self) {
self.internal.redraw();
}
@ -205,7 +224,8 @@ impl MiniGlFb {
///
/// ```rust
/// # use mini_gl_fb::get_fancy;
/// # let mut fb = get_fancy::<&str>(Default::default());
/// # use mini_gl_fb::glutin::event_loop::EventLoop;
/// # let mut fb = get_fancy(Default::default(), &EventLoop::new());
/// fb.use_post_process_shader("
/// void main_image( out vec4 r_frag_color, in vec2 v_uv ) {
/// r_frag_color = texture(u_buffer, v_uv);
@ -251,7 +271,8 @@ impl MiniGlFb {
/// ```rust
/// use mini_gl_fb::BufferFormat;
/// # use mini_gl_fb::get_fancy;
/// # let mut fb = get_fancy::<&str>(Default::default());
/// # use mini_gl_fb::glutin::event_loop::EventLoop;
/// # let mut fb = get_fancy(Default::default(), &EventLoop::new());
///
/// fb.change_buffer_format::<u8>(BufferFormat::R);
/// fb.use_grayscale_shader();
@ -302,8 +323,8 @@ impl MiniGlFb {
///
/// Supports pressing escape to quit. Automatically scales the rendered buffer to the size of
/// the window if the window is resiable (but this does not resize the buffer).
pub fn persist(&mut self) {
self.internal.persist();
pub fn persist<ET: 'static>(&mut self, event_loop: &mut EventLoop<ET>) {
self.internal.persist(event_loop);
}
/// `persist` implementation.
@ -311,8 +332,8 @@ impl MiniGlFb {
/// When redraw is true, redraws as fast as possible. This function is primarily for debugging.
///
/// See `persist` method documentation for more info.
pub fn persist_and_redraw(&mut self, redraw: bool) {
self.internal.persist_and_redraw(redraw);
pub fn persist_and_redraw<ET: 'static>(&mut self, event_loop: &mut EventLoop<ET>, redraw: bool) {
self.internal.persist_and_redraw(event_loop, redraw);
}
/// Provides an easy interface for rudimentary input handling.
@ -329,16 +350,16 @@ impl MiniGlFb {
/// You can cause the handler to exit by returning false from it. This does not kill the
/// window, so as long as you still have it in scope, you can actually keep using it and,
/// for example, resume handling input but with a different handler callback.
pub fn glutin_handle_basic_input<F: FnMut(&mut Framebuffer, &BasicInput) -> bool>(
&mut self, handler: F
pub fn glutin_handle_basic_input<ET: 'static, F: FnMut(&mut Framebuffer, &BasicInput) -> bool>(
&mut self, event_loop: &mut EventLoop<ET>, handler: F
) {
self.internal.glutin_handle_basic_input(handler);
self.internal.glutin_handle_basic_input(event_loop, handler);
}
/// Need full access to Glutin's event handling? No problem!
///
/// Hands you the event loop and the window we created, so you can handle events however you
/// want, and the Framebuffer, so you can still draw easily!
/// Hands you the window we created, so you can handle events however you want, and the
/// Framebuffer, so you can still draw easily!
///
/// **IMPORTANT:** You should make sure to render something before swapping buffers or **the
/// window may flash violently**. You can call `fb.redraw()` directly before if you are unsure