8000 Conf::headless: run with no window on macOS and Linux by 16dprice · Pull Request #668 · not-fl3/miniquad · GitHub
[go: up one dir, main page]

Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
217 changes: 217 additions & 0 deletions examples/headless.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
//! `Conf::headless`: the triangle example with no window.
//!
//! Draws one frame into the offscreen default framebuffer, reads a pixel
//! from the middle of the triangle and one from a corner, checks both, and
//! quits. Run it from a terminal: nothing should appear on screen or in the
//! Dock, and it should exit 0 within a second.
//!
//! It is also the check that the Linux path works with no display server at
//! all — `env -u DISPLAY -u WAYLAND_DISPLAY cargo run --example headless`
//! passes on a machine with only Mesa's software EGL installed.

use miniquad::*;

#[repr(C)]
struct Vertex {
pos: [f32; 2],
color: [f32; 4],
}

struct Stage {
pipeline: Pipeline,
bindings: Bindings,
ctx: Box<dyn RenderingBackend>,
frames: u32,
}

impl Stage {
pub fn new() -> Stage {
let mut ctx: Box<dyn RenderingBackend> = window::new_rendering_backend();

#[rustfmt::skip]
let vertices: [Vertex; 3] = [
Vertex { pos : [ -0.5, -0.5 ], color: [1., 0., 0., 1.] },
Vertex { pos : [ 0.5, -0.5 ], color: [0., 1., 0., 1.] },
Vertex { pos : [ 0.0, 0.5 ], color: [0., 0., 1., 1.] },
];
let vertex_buffer = ctx.new_buffer(
BufferType::VertexBuffer,
BufferUsage::Immutable,
BufferSource::slice(&vertices),
);

let indices: [u16; 3] = [0, 1, 2];
let index_buffer = ctx.new_buffer(
BufferType::IndexBuffer,
BufferUsage::Immutable,
BufferSource::slice(&indices),
);

let bindings = Bindings {
vertex_buffers: vec![vertex_buffer],
index_buffer,
images: vec![],
};

let shader = ctx
.new_shader(
match ctx.info().backend {
Backend::OpenGl => ShaderSource::Glsl {
vertex: shader::VERTEX,
fragment: shader::FRAGMENT,
},
Backend::Metal => ShaderSource::Msl {
program: shader::METAL,
},
< 8000 span class='blob-code-inner blob-code-marker ' data-code-marker="+"> },
shader::meta(),
)
.unwrap();

let pipeline = ctx.new_pipeline(
&[BufferLayout::default()],
&[
VertexAttribute::new("in_pos", VertexFormat::Float2),
VertexAttribute::new("in_color", VertexFormat::Float4),
],
shader,
PipelineParams::default(),
);

Stage {
pipeline,
bindings,
ctx,
frames: 0,
}
}
}

impl EventHandler for Stage {
fn update(&mut self) {}

fn draw(&mut self) {
self.ctx
.begin_default_pass(PassAction::clear_color(0., 0., 0., 1.));
self.ctx.apply_pipeline(&self.pipeline);
self.ctx.apply_bindings(&self.bindings);
self.ctx.draw(0, 3, 1);
self.ctx.end_render_pass();
self.ctx.commit_frame();

// Give it a couple of frames, as a real app would take, then look.
self.frames += 1;
if self.frames < 3 {
return;
}

let (w, h) = window::screen_size();
let (w, h) = (w as i32, h as i32);
let middle = read_pixel(w / 2, h / 2);
let corner = read_pixel(1, 1);
println!(
"{}x{} framebuffer: middle {:?}, corner {:?}",
w, h, middle, corner
);
assert_eq!(
corner,
[0, 0, 0, 255],
"the corner should be the clear color"
);
assert!(
middle[0] > 0 || middle[1] > 0 || middle[2] > 0,
"the middle of the triangle should have been drawn"
);
println!("ok");
window::order_quit();
}
}

/// One RGBA8 pixel of the current framebuffer.
fn read_pixel(x: i32, y: i32) -> [u8; 4] {
let mut px = [0u8; 4];
unsafe {
gl::glReadPixels(
x,
y,
1,
1,
gl::GL_RGBA,
gl::GL_UNSIGNED_BYTE,
px.as_mut_ptr() as *mut _,
);
}
px
}

fn main() {
let conf = conf::Conf {
window_title: "headless".to_string(),
window_width: 320,
window_height: 240,
headless: true,
..Default::default()
};
miniquad::start(conf, || Box::new(Stage::new()));
}

mod shader {
use miniquad::*;

pub const VERTEX: &str = r#"#version 100
attribute vec2 in_pos;
attribute vec4 in_color;

varying lowp vec4 color;

void main() {
gl_Position = vec4(in_pos, 0, 1);
color = in_color;
}"#;

pub const FRAGMENT: &str = r#"#version 100
varying lowp vec4 color;

void main() {
gl_FragColor = color;
}"#;

pub const METAL: &str = r#"
#include <metal_stdlib>

using namespace metal;

struct Vertex
{
float2 in_pos [[attribute(0)]];
float4 in_color [[attribute(1)]];
};

struct RasterizerData
{
float4 position [[position]];
float4 color [[user(locn0)]];
};

vertex RasterizerData vertexShader(Vertex v [[stage_in]])
{
RasterizerData out;

out.position = float4(v.in_pos.xy, 0.0, 1.0);
out.color = v.in_color;

return out;
}

fragment float4 fragmentShader(RasterizerData in [[stage_in]])
{
return in.color;
}"#;

pub fn meta() -> ShaderMeta {
ShaderMeta {
images: vec![],
uniforms: UniformBlockLayout { uniforms: vec![] },
}
}
}
19 changes: 19 additions & 0 deletions src/conf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,23 @@ pub struct Conf {
/// an external `.desktop` file
pub icon: Option<Icon>,

/// If `true`, create no window and run without a display. The default
/// framebuffer is an offscreen one of `window_width` x `window_height`
/// pixels at a dpi scale of 1; `update` and `draw` run at roughly 60 Hz;
/// no input or window events ever arrive; `window::screen_size()`, render
/// passes and framebuffer readback all work as usual. Meant for tests
/// and CI machines with no display attached.
///
/// Honored on macOS with [`AppleGfxApi::OpenGl`] (where it also creates
/// no `NSApplication`, so nothing appears in the Dock and no focus
/// moves), and on Linux, where it takes an EGL pbuffer context off
/// `EGL_DEFAULT_DISPLAY` and so needs neither an X11 nor a Wayland
/// server — [`Platform::linux_backend`] plays no part. Other backends
/// ignore it and open a window as usual. `sample_count` is ignored for a
/// headless run.
/// Defaults to `false`.
pub headless: bool,

/// Platform-specific hints (e.g., context creation, driver settings).
pub platform: Platform,
}
Expand Down Expand Up @@ -284,6 +301,7 @@ impl Default for Conf {
sample_count: 1,
window_resizable: true,
icon: Some(Icon::miniquad_logo()),
headless: false,
platform: Default::default(),
}
}
Expand All @@ -301,6 +319,7 @@ impl Default for Conf {
sample_count: 1,
window_resizable: false, //
icon: Some(Icon::miniquad_logo()),
headless: false,
platform: Default::default(),
}
}
Expand Down
7 changes: 7 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,13 @@ where
{
let mut f = Some(f);
let f = &mut f;
// Before `linux_backend` is consulted at all: a headless run wants no
// window system, and on a machine with neither X11 nor Wayland
// running, both of those backends fail at connect time.
if conf.headless {
native::linux_headless::run(&conf, f).expect("headless EGL backend failed");
return;
}
match conf.platform.linux_backend {
conf::LinuxBackend::X11Only => {
native::linux_x11::run(&conf, f).expect("X11 backend failed")
Expand Down
3 changes: 3 additions & 0 deletions src/native.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ pub mod linux_x11;
#[cfg(target_os = "linux")]
pub mod linux_wayland;

#[cfg(target_os = "linux")]
pub mod linux_headless;

#[cfg(target_os = "android")]
pub mod android;

Expand Down
25 changes: 25 additions & 0 deletions src/native/egl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ use std::fmt::Display;
pub const EGL_SUCCESS: u32 = 12288;

pub const EGL_WINDOW_BIT: u32 = 4;
pub const EGL_PBUFFER_BIT: u32 = 1;

pub const EGL_RENDERABLE_TYPE: u32 = 12352;
pub const EGL_OPENGL_ES2_BIT: u32 = 4;

pub const EGL_ALPHA_SIZE: u32 = 12321;
pub const EGL_BLUE_SIZE: u32 = 12322;
Expand All @@ -35,6 +39,27 @@ pub const EGL_SURFACE_TYPE: u32 = 12339;
pub const EGL_NONE: u32 = 12344;
pub const EGL_CONTEXT_CLIENT_VERSION: u32 = 12440;

pub const EGL_NO_DISPLAY: EGLDisplay = null_mut();
pub const EGL_NO_SURFACE: EGLSurface = null_mut();
/// `EGL_DEFAULT_DISPLAY` is the null native display handle: it asks the
/// driver for whatever display it can provide without a window system, which
/// is what a headless run wants.
pub const EGL_DEFAULT_DISPLAY: EGLNativeDisplayType = null_mut();

/// `EGL_MESA_platform_surfaceless`: a platform that is not a window system at
/// all. `EGL_DEFAULT_DISPLAY` asks the driver for its *default* display, which
/// on a Linux box built for a desktop means the X11 one — so it resolves, and
/// then `eglInitialize` fails when there is no X server behind it. This asks
/// for the display that never had a window system to begin with.
pub const EGL_PLATFORM_SURFACELESS_MESA: u32 = 0x31DD;

/// `eglGetPlatformDisplayEXT`, from `EGL_EXT_platform_base`. Resolved through
/// `eglGetProcAddress` rather than declared in [`LibEgl`]: it is an extension,
/// and `declare_module!` unwraps every symbol it names, so a driver without it
/// would fail to load libEGL at all rather than fall back.
pub type GetPlatformDisplayExt =
unsafe extern "C" fn(EGLint, *mut ::core::ffi::c_void, *const EGLint) -> EGLDisplay;

pub type NativeDisplayType = EGLNativeDisplayType;
pub type NativePixmapType = EGLNativePixmapType;
pub type NativeWindowType = EGLNativeWindowType;
Expand Down
2 changes: 2 additions & 0 deletions src/native/gl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ pub const GL_TEXTURE_3D: u32 = 0x806F;
pub const GL_CW: u32 = 0x0900;
pub const GL_LINEAR: u32 = 0x2601;
pub const GL_RENDERBUFFER: u32 = 0x8D41;
pub const GL_DEPTH24_STENCIL8: u32 = 0x88F0;
pub const GL_DEPTH_STENCIL_ATTACHMENT: u32 = 0x821A;
pub const GL_GEQUAL: u32 = 0x0206;
pub const GL_COLOR_BUFFER_BIT: u32 = 0x00004000;
pub const GL_RGBA32F: u32 = 0x8814;
Expand Down
Loading
Loading
0