diff --git a/iris-hostgl/src/accum.rs b/iris-hostgl/src/accum.rs new file mode 100644 index 00000000..e5b0367a --- /dev/null +++ b/iris-hostgl/src/accum.rs @@ -0,0 +1,398 @@ +//! The accumulation buffer, which the host cannot give a drawable. +//! +//! Every drawable is a framebuffer object (draw.rs), and OpenGL attaches an +//! accumulation buffer only to the window system's framebuffer, never to an +//! object: glAccum on the host does nothing at all. IRIX programs use it for +//! antialiasing by jittered passes, motion blur and depth of field, and IRIS +//! GL's acbuf is the same operation, so it is kept here instead -- per +//! drawable, a pair of floating-point textures the size of the drawable. +//! +//! Each operation is one pass of a small shader over the whole buffer, +//! +//! ```text +//! new = old * a + colour * b + c +//! ``` +//! +//! where `colour` is the read buffer copied into a texture first, and a, b +//! and c are what the operation makes them (`Accum::op`). The pass writes the +//! other texture of the pair, which then becomes the buffer: reading and +//! writing one texture at once is undefined, and blending into a 32-bit float +//! target, which would avoid the pair, is not something every GPU can do. +//! GL_RETURN draws `old * value` into the drawable instead, where the target's +//! own fixed-point storage clamps it to 0..1. +//! +//! As the specification has it, every operation is limited to the scissor +//! box, GL_RETURN honours the colour mask (and nothing else of the fragment +//! stage), and the values are kept unclamped between operations. The program's +//! state is saved around each pass and put back, so nothing it can see moves. + +use std::collections::HashMap; +use std::ffi::{c_void, CString}; + +use crate::gl::*; + +#[link(name = "OpenGL", kind = "framework")] +extern "C" { + fn glCreateShader(kind: u32) -> u32; + fn glShaderSource(s: u32, count: i32, strings: *const *const i8, lengths: *const i32); + fn glCompileShader(s: u32); + fn glGetShaderiv(s: u32, pname: u32, out: *mut i32); + fn glCreateProgram() -> u32; + fn glAttachShader(p: u32, s: u32); + fn glLinkProgram(p: u32); + fn glGetProgramiv(p: u32, pname: u32, out: *mut i32); + fn glUseProgram(p: u32); + fn glGetUniformLocation(p: u32, name: *const i8) -> i32; + fn glUniform1i(loc: i32, v: i32); + fn glUniform4f(loc: i32, a: f32, b: f32, c: f32, d: f32); + fn glGenTextures(n: i32, ids: *mut u32); + fn glDeleteTextures(n: i32, ids: *const u32); + fn glTexImage2D(target: u32, level: i32, internal: i32, w: i32, h: i32, border: i32, fmt: u32, ty: u32, data: *const c_void); + fn glTexParameteri(target: u32, pname: u32, v: i32); + fn glCopyTexSubImage2D(target: u32, level: i32, xoff: i32, yoff: i32, x: i32, y: i32, w: i32, h: i32); + fn glPushAttrib(mask: u32); + fn glPopAttrib(); + fn glMatrixMode(mode: u32); + fn glPushMatrix(); + fn glPopMatrix(); + fn glLoadIdentity(); + fn glColorMask(r: u8, g: u8, b: u8, a: u8); + fn glPolygonMode(face: u32, mode: u32); + fn glTexCoord2f(s: f32, t: f32); + fn glVertex2f(x: f32, y: f32); +} + +pub const GL_ACCUM: u32 = 0x0100; +pub const GL_LOAD: u32 = 0x0101; +pub const GL_RETURN: u32 = 0x0102; +pub const GL_MULT: u32 = 0x0103; +pub const GL_ADD: u32 = 0x0104; +pub const GL_ACCUM_BUFFER_BIT: u32 = 0x0200; +pub const GL_ACCUM_CLEAR_VALUE: u32 = 0x0B80; +pub const GL_ACCUM_RED_BITS: u32 = 0x0D58; +pub const GL_ACCUM_ALPHA_BITS: u32 = 0x0D5B; +/// What the accumulation buffer reports per component: its textures hold +/// 32-bit floats, and 16 is what SGI's machines with one reported. +pub const ACCUM_BITS: i64 = 16; + +const GL_TEXTURE_2D: u32 = 0x0DE1; +const GL_RGBA32F_ARB: i32 = 0x8814; +const GL_FLOAT: u32 = 0x1406; +const GL_TEXTURE_MIN_FILTER: u32 = 0x2801; +const GL_TEXTURE_MAG_FILTER: u32 = 0x2800; +const GL_TEXTURE_WRAP_S: u32 = 0x2802; +const GL_TEXTURE_WRAP_T: u32 = 0x2803; +const GL_CLAMP_TO_EDGE: i32 = 0x812F; +const GL_PROJECTION: u32 = 0x1701; +const GL_MODELVIEW: u32 = 0x1700; +const GL_TEXTURE: u32 = 0x1702; +const GL_ALL_ATTRIB_BITS: u32 = 0x000F_FFFF; +const GL_FRONT_AND_BACK: u32 = 0x0408; +const GL_FILL: u32 = 0x1B02; +const GL_QUADS: u32 = 0x0007; +const GL_CURRENT_PROGRAM: u32 = 0x8B8D; +const GL_READ_FRAMEBUFFER_BINDING: u32 = 0x8CAA; +const GL_VERTEX_SHADER: u32 = 0x8B31; +const GL_FRAGMENT_SHADER: u32 = 0x8B30; +const GL_COMPILE_STATUS: u32 = 0x8B81; +const GL_LINK_STATUS: u32 = 0x8B82; + +/// Everything a pass turns off, the fragment stage's tests and the vertex +/// stage's extras; GL_SCISSOR_TEST stays as the program set it. +const OFF: [u32; 22] = [ + 0x0B71, // GL_DEPTH_TEST + 0x0BE2, // GL_BLEND + 0x0BC0, // GL_ALPHA_TEST + 0x0B90, // GL_STENCIL_TEST + 0x0BF2, // GL_COLOR_LOGIC_OP + 0x0B44, // GL_CULL_FACE + 0x0B50, // GL_LIGHTING + 0x0B60, // GL_FOG + 0x0DE0, // GL_TEXTURE_1D + 0x0DE1, // GL_TEXTURE_2D + 0x806F, // GL_TEXTURE_3D + 0x3000, 0x3001, 0x3002, 0x3003, 0x3004, 0x3005, // GL_CLIP_PLANE0..5 + 0x0B42, // GL_POLYGON_STIPPLE + 0x8037, // GL_POLYGON_OFFSET_FILL + 0x0C60, 0x0C61, // GL_TEXTURE_GEN_S, _T + 0x8513, // GL_TEXTURE_CUBE_MAP +]; + +const VERTEX: &str = "varying vec2 uv; +void main() { gl_Position = gl_Vertex; uv = gl_MultiTexCoord0.xy; } +"; + +const FRAGMENT: &str = "uniform sampler2D acc; +uniform sampler2D src; +uniform vec4 a; +uniform vec4 b; +uniform vec4 c; +varying vec2 uv; +void main() { gl_FragColor = texture2D(acc, uv) * a + texture2D(src, uv) * b + c; } +"; + +/// One drawable's accumulation buffer. +struct Buffer { + w: i32, + h: i32, + /// The pair: `tex[cur]` is the buffer, the other is the next pass's target. + tex: [u32; 2], + fbo: [u32; 2], + cur: usize, + /// The read buffer's pixels, copied for GL_ACCUM and GL_LOAD to sample. + colour: u32, +} + +/// A context's accumulation buffers, by drawable, and the clear value +/// (glClearAccum), which is context state. +#[derive(Default)] +pub struct Accum { + buffers: HashMap, + clear: [f32; 4], + program: u32, + /// The shader would not build: glAccum does nothing, as it did before. + broken: bool, +} + +impl Accum { + pub fn clear_value(&self) -> [f32; 4] { + self.clear + } + + /// glClearAccum: clamped to -1..1, as the specification clamps it. + pub fn set_clear_value(&mut self, v: [f32; 4]) { + self.clear = v.map(|c| c.clamp(-1.0, 1.0)); + } + + /// Forget the buffers of drawables that are gone. + pub fn retain(&mut self, alive: impl Fn(u32) -> bool) { + let gone: Vec = self.buffers.keys().copied().filter(|id| !alive(*id)).collect(); + for id in gone { + self.free(id); + } + } + + fn free(&mut self, id: u32) { + if let Some(b) = self.buffers.remove(&id) { + // SAFETY: objects of the current context's share group. + unsafe { + glDeleteFramebuffersEXT(2, b.fbo.as_ptr()); + glDeleteTextures(2, b.tex.as_ptr()); + glDeleteTextures(1, &b.colour); + } + } + } + + /// glClear's GL_ACCUM_BUFFER_BIT, on drawable `draw` (`w` x `h`). + pub fn clear(&mut self, draw: u32, w: i32, h: i32) { + let c = self.clear; + self.pass(draw, w, h, Pass::Buffer { a: 0.0, b: 0.0, c, colour: false }); + } + + /// glAccum(op, value) on drawable `draw`. False for an operation that is + /// not one, which the caller turns into GL_INVALID_ENUM. + pub fn op(&mut self, draw: u32, w: i32, h: i32, op: u32, value: f32) -> bool { + let pass = match op { + GL_ACCUM => Pass::Buffer { a: 1.0, b: value, c: [0.0; 4], colour: true }, + GL_LOAD => Pass::Buffer { a: 0.0, b: value, c: [0.0; 4], colour: true }, + GL_ADD => Pass::Buffer { a: 1.0, b: 0.0, c: [value; 4], colour: false }, + GL_MULT => Pass::Buffer { a: value, b: 0.0, c: [0.0; 4], colour: false }, + GL_RETURN => Pass::Return { value }, + _ => return false, + }; + self.pass(draw, w, h, pass); + true + } + + fn pass(&mut self, draw: u32, w: i32, h: i32, pass: Pass) { + if w <= 0 || h <= 0 || self.broken { + return; + } + // SAFETY: GL calls on the current context, whose state is saved first + // and put back after; the objects are its share group's. + unsafe { + if self.program == 0 { + self.program = build(); + if self.program == 0 { + self.broken = true; + return; + } + } + let saved = Saved::save(); + let fresh = self.ensure(draw, w, h); + for cap in OFF { + glDisable(cap); + } + glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); + glViewport(0, 0, w, h); + glUseProgram(self.program); + let p = self.program; + let loc = |n: &str| { + let n = CString::new(n).unwrap(); + glGetUniformLocation(p, n.as_ptr()) + }; + glUniform1i(loc("acc"), 0); + glUniform1i(loc("src"), 1); + let b = self.buffers.get_mut(&draw).unwrap(); + glActiveTexture(GL_TEXTURE0_ARB); + glBindTexture(GL_TEXTURE_2D, b.tex[b.cur]); + match pass { + Pass::Buffer { a, b: k, c, colour } => { + // A buffer just made holds nothing yet: what it is read + // as is zero, whatever the pass makes of it. + let a = if fresh { 0.0 } else { a }; + if colour { + glActiveTexture(GL_TEXTURE0_ARB + 1); + glBindTexture(GL_TEXTURE_2D, b.colour); + glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, w, h); + glActiveTexture(GL_TEXTURE0_ARB); + } + glUniform4f(loc("a"), a, a, a, a); + glUniform4f(loc("b"), k, k, k, k); + glUniform4f(loc("c"), c[0], c[1], c[2], c[3]); + let next = 1 - b.cur; + glBindFramebufferEXT(GL_DRAW_FRAMEBUFFER, b.fbo[next]); + glColorMask(1, 1, 1, 1); + quad(); + b.cur = next; + } + Pass::Return { value } => { + let v = if fresh { 0.0 } else { value }; + glUniform4f(loc("a"), v, v, v, v); + glUniform4f(loc("b"), 0.0, 0.0, 0.0, 0.0); + glUniform4f(loc("c"), 0.0, 0.0, 0.0, 0.0); + // Into the drawable as it is bound, through the program's + // own draw buffer and colour mask. + glBindFramebufferEXT(GL_DRAW_FRAMEBUFFER, saved.draw_fbo); + quad(); + } + } + saved.restore(); + } + } + + /// Make or resize `draw`'s buffer. True when it was just made (or made + /// again for a new size): its contents are then undefined, which reads + /// here as zero. + unsafe fn ensure(&mut self, draw: u32, w: i32, h: i32) -> bool { + if let Some(b) = self.buffers.get(&draw) { + if b.w == w && b.h == h { + return false; + } + } + self.free(draw); + let mut b = Buffer { w, h, tex: [0; 2], fbo: [0; 2], cur: 0, colour: 0 }; + let mut was_draw = 0i32; + glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &mut was_draw); + glGenTextures(2, b.tex.as_mut_ptr()); + glGenFramebuffersEXT(2, b.fbo.as_mut_ptr()); + for i in 0..2 { + texture(b.tex[i], GL_RGBA32F_ARB, w, h, GL_FLOAT); + glBindFramebufferEXT(GL_DRAW_FRAMEBUFFER, b.fbo[i]); + glFramebufferTexture2DEXT(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, b.tex[i], 0); + } + glGenTextures(1, &mut b.colour); + texture(b.colour, GL_RGBA8 as i32, w, h, GL_UNSIGNED_BYTE); + glBindFramebufferEXT(GL_DRAW_FRAMEBUFFER, was_draw as u32); + self.buffers.insert(draw, b); + true + } +} + +enum Pass { + /// Into the accumulation buffer: old * a + colour * b + c. + Buffer { a: f32, b: f32, c: [f32; 4], colour: bool }, + /// Out to the drawable: old * value. + Return { value: f32 }, +} + +/// A texture of `w` x `h` in `internal`, sampled texel for texel. The +/// binding on unit 0 is the caller's to restore. +unsafe fn texture(t: u32, internal: i32, w: i32, h: i32, ty: u32) { + glBindTexture(GL_TEXTURE_2D, t); + glTexImage2D(GL_TEXTURE_2D, 0, internal, w, h, 0, GL_RGBA, ty, std::ptr::null()); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST as i32); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST as i32); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); +} + +/// The whole viewport, texel centres on pixel centres. +unsafe fn quad() { + glBegin(GL_QUADS); + glTexCoord2f(0.0, 0.0); + glVertex2f(-1.0, -1.0); + glTexCoord2f(1.0, 0.0); + glVertex2f(1.0, -1.0); + glTexCoord2f(1.0, 1.0); + glVertex2f(1.0, 1.0); + glTexCoord2f(0.0, 1.0); + glVertex2f(-1.0, 1.0); + glEnd(); +} + +/// The pass's shader program, or 0 if the host would not build it. +unsafe fn build() -> u32 { + let stage = |kind: u32, src: &str| -> u32 { + let s = glCreateShader(kind); + let text = CString::new(src).unwrap(); + let p = text.as_ptr(); + glShaderSource(s, 1, &p, std::ptr::null()); + glCompileShader(s); + let mut ok = 0; + glGetShaderiv(s, GL_COMPILE_STATUS, &mut ok); + if ok == 0 { 0 } else { s } + }; + let (v, f) = (stage(GL_VERTEX_SHADER, VERTEX), stage(GL_FRAGMENT_SHADER, FRAGMENT)); + if v == 0 || f == 0 { + return 0; + } + let p = glCreateProgram(); + glAttachShader(p, v); + glAttachShader(p, f); + glLinkProgram(p); + let mut ok = 0; + glGetProgramiv(p, GL_LINK_STATUS, &mut ok); + if ok == 0 { 0 } else { p } +} + +/// What a pass changes, as it was. +struct Saved { + program: i32, + active: i32, + draw_fbo: u32, + read_fbo: u32, +} + +impl Saved { + unsafe fn save() -> Saved { + let (mut program, mut active, mut draw, mut read) = (0, 0, 0, 0); + glGetIntegerv(GL_CURRENT_PROGRAM, &mut program); + glGetIntegerv(GL_ACTIVE_TEXTURE_ARB, &mut active); + glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &mut draw); + glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &mut read); + // Every attribute group, the bindings of every texture unit among + // them; the matrices separately, unit 0's texture matrix included. + glPushAttrib(GL_ALL_ATTRIB_BITS); + glActiveTexture(GL_TEXTURE0_ARB); + for m in [GL_PROJECTION, GL_MODELVIEW, GL_TEXTURE] { + glMatrixMode(m); + glPushMatrix(); + glLoadIdentity(); + } + Saved { program, active, draw_fbo: draw as u32, read_fbo: read as u32 } + } + + unsafe fn restore(self) { + glActiveTexture(GL_TEXTURE0_ARB); + for m in [GL_TEXTURE, GL_MODELVIEW, GL_PROJECTION] { + glMatrixMode(m); + glPopMatrix(); + } + glPopAttrib(); + glUseProgram(self.program as u32); + glActiveTexture(self.active as u32); + glBindFramebufferEXT(GL_DRAW_FRAMEBUFFER, self.draw_fbo); + glBindFramebufferEXT(GL_READ_FRAMEBUFFER, self.read_fbo); + } +} diff --git a/iris-hostgl/src/calls.rs b/iris-hostgl/src/calls.rs index bb98c3d4..f6421b1a 100644 --- a/iris-hostgl/src/calls.rs +++ b/iris-hostgl/src/calls.rs @@ -834,12 +834,7 @@ pub fn run(x: &mut Exec, op: usize, c: &[u8]) -> Option { 0 => { // glAccum let a0 = x.i32(c, 4) as u32; let a1 = x.f32(c, 8); - x.resolve_read(); - let f = x.func(0)?; - let g: extern "C" fn(u32, f32) = unsafe { std::mem::transmute(f) }; - g(a0, a1); - x.image_done(); - Some(0) + x.accum(a0, a1); Some(0) } 1 => { // glAlphaFunc let a0 = x.i32(c, 4) as u32; @@ -983,22 +978,14 @@ pub fn run(x: &mut Exec, op: usize, c: &[u8]) -> Option { } 17 => { // glClear let a0 = x.i32(c, 4) as u32; - let f = x.func(17)?; - let g: extern "C" fn(u32) = unsafe { std::mem::transmute(f) }; - g(a0); - x.image_done(); - Some(0) + x.clear(a0); Some(0) } 18 => { // glClearAccum let a0 = x.f32(c, 4); let a1 = x.f32(c, 8); let a2 = x.f32(c, 12); let a3 = x.f32(c, 16); - let f = x.func(18)?; - let g: extern "C" fn(f32, f32, f32, f32) = unsafe { std::mem::transmute(f) }; - g(a0, a1, a2, a3); - x.image_done(); - Some(0) + x.clear_accum(a0, a1, a2, a3); Some(0) } 19 => { // glClearColor let a0 = x.f32(c, 4); diff --git a/iris-hostgl/src/exec.rs b/iris-hostgl/src/exec.rs index 33f9f9ab..b3a47e59 100644 --- a/iris-hostgl/src/exec.rs +++ b/iris-hostgl/src/exec.rs @@ -27,6 +27,7 @@ use iris_hostcall::{Fault, GuestMemory, PAGE}; use crate::backend::Backend; use crate::calls; use crate::draw::{self, Draw}; +use crate::accum; use crate::emul; use crate::gl::*; @@ -95,6 +96,8 @@ pub struct ClientSide { pub interlace: bool, /// The SGI features the host has not got, done in the fragment stage. pub emul: emul::Emul, + /// The accumulation buffers framebuffer objects cannot have (accum.rs). + pub accum: accum::Accum, } #[derive(Default, Clone, Copy)] @@ -119,6 +122,7 @@ pub struct Exec<'a> { pub backend: &'a dyn Backend, pub client: &'a mut ClientSide, pub draws: &'a HashMap, + pub current_draw: u32, pub current_read: u32, /// Pixels on their way through, one buffer per image argument of the /// command (glSeparableFilter2D has two, glGetSeparableFilter three), so @@ -199,26 +203,18 @@ pub fn reverse_components(buf: &mut [u8], row: usize, n: usize, s: usize) { } /// The types that hold a whole pixel in one packed word, and the size of that -/// word, numbered as the guest numbers them (see `IRIX_5_6_5`). Their +/// word (the numbers are the standard ones: see `gl_type`). Their /// components are fields of an integer rather than separate values, so /// `reverse_components` does not apply to them -- see `gl_type`. fn packed_type(ty: u32) -> Option { match ty { - 0x8032 | IRIX_2_3_3_REV => Some(1), // UNSIGNED_BYTE_3_3_2, _2_3_3_REV - 0x8033 | 0x8034 | IRIX_5_6_5 | 0x8364 | 0x8365 | 0x8366 => Some(2), + 0x8032 | 0x8362 => Some(1), // UNSIGNED_BYTE_3_3_2, _2_3_3_REV + 0x8033 | 0x8034 | 0x8363 | 0x8364 | 0x8365 | 0x8366 => Some(2), 0x8035 | 0x8036 | 0x8367 | 0x8368 => Some(4), _ => None, } } -/// EXT_packed_pixels' 5_6_5 and 2_3_3_REV types as IRIX 6.5's -/// numbers them: the other way round from OpenGL 1.2 and every later -/// registry, where 0x8362 is UNSIGNED_BYTE_2_3_3_REV and 0x8363 -/// UNSIGNED_SHORT_5_6_5. A guest program passes these; the host is given the -/// standard number (`gl_type`). The rest of the packed types agree. -const IRIX_5_6_5: u32 = 0x8362; -const IRIX_2_3_3_REV: u32 = 0x8363; - /// An element of an array argument, as it comes from the guest. pub trait Elem: Copy + Default { const SIZE: usize; @@ -304,6 +300,7 @@ pub const MAX_TEXTURE_UNITS: i64 = 8; pub trait GetValue: Copy { fn to_i64(self) -> i64; fn from_i64(v: i64) -> Self; + fn from_f32(v: f32) -> Self; } macro_rules! get_value { @@ -315,6 +312,9 @@ macro_rules! get_value { fn from_i64(v: i64) -> Self { v as Self } + fn from_f32(v: f32) -> Self { + v as Self + } } )* }; } @@ -330,6 +330,7 @@ impl<'a> Exec<'a> { backend: &'a dyn Backend, client: &'a mut ClientSide, draws: &'a HashMap, + current_draw: u32, current_read: u32, ) -> Exec<'a> { Exec { @@ -341,6 +342,7 @@ impl<'a> Exec<'a> { backend, client, draws, + current_draw, current_read, scratch: Vec::new(), staged: Vec::new(), @@ -750,14 +752,14 @@ impl<'a> Exec<'a> { /// The pixel type to give the host, which depends on the format it goes /// with: ABGR packed into a word is RGBA packed the other way round, so - /// the type carries the reversal that the format no longer can. IRIX's - /// numbers for 5_6_5 and 2_3_3_REV become the standard ones. + /// the type carries the reversal that the format no longer can. + /// + /// The packed types' numbers are passed as they come. IRIX 6.5.22's + /// numbers them as OpenGL 1.2 does (0x8362 UNSIGNED_BYTE_2_3_3_REV, + /// 0x8363 UNSIGNED_SHORT_5_6_5); earlier 6.5 releases' headers had those two + /// the other way round, and a program built with one of them gets the + /// standard meaning, as 6.5.22 gives it. pub fn gl_type(&self, format: u32, ty: u32) -> u32 { - let ty = match ty { - IRIX_5_6_5 => 0x8363, - IRIX_2_3_3_REV => 0x8362, - t => t, - }; if format != GL_ABGR_EXT { return ty; } @@ -1127,6 +1129,37 @@ impl<'a> Exec<'a> { r } + /// glAccum, on the drawable drawn into (accum.rs). + pub fn accum(&mut self, op: u32, value: f32) { + self.resolve_read(); + let draws = self.draws; + self.client.accum.retain(|id| draws.contains_key(&id)); + let Some(d) = self.draws.get(&self.current_draw) else { return }; + // An operation that is not one does nothing (the specification's + // GL_INVALID_ENUM is not raised: the host has no accumulation + // buffer to raise it about). + self.client.accum.op(self.current_draw, d.w, d.h, op, value); + } + + /// glClearAccum: the value is ours to keep, as the buffer is. + pub fn clear_accum(&mut self, r: f32, g: f32, b: f32, a: f32) { + self.client.accum.set_clear_value([r, g, b, a]); + } + + /// glClear: the accumulation buffer's bit is ours, the rest the host's. + pub fn clear(&mut self, mask: u32) { + if mask & accum::GL_ACCUM_BUFFER_BIT != 0 { + if let Some(d) = self.draws.get(&self.current_draw) { + self.client.accum.clear(self.current_draw, d.w, d.h); + } + } + let rest = mask & !accum::GL_ACCUM_BUFFER_BIT; + if rest != 0 { + // SAFETY: the current context's GL. + unsafe { glClear(rest) }; + } + } + /// Before anything reads the framebuffer: a multisampled drawable's /// samples have to be resolved into the buffer reads come from. pub fn resolve_read(&mut self) { @@ -1204,6 +1237,20 @@ impl<'a> Exec<'a> { /// 8 is far above what IRIX-era software asks for (Quake III wants 2), and /// a ceiling is easier to raise later than a wrong answer is to find. pub fn get_limit(&mut self, pname: u32, out: &mut [T]) { + // The accumulation buffer is this library's (accum.rs), not the + // host's, which has none to report. + if (accum::GL_ACCUM_RED_BITS..=accum::GL_ACCUM_ALPHA_BITS).contains(&pname) { + if let Some(v) = out.first_mut() { + *v = T::from_i64(accum::ACCUM_BITS); + } + return; + } + if pname == accum::GL_ACCUM_CLEAR_VALUE { + for (v, c) in out.iter_mut().zip(self.client.accum.clear_value()) { + *v = T::from_f32(c); + } + return; + } if pname != GL_MAX_TEXTURE_UNITS_ARB { return; } diff --git a/iris-hostgl/src/gl.rs b/iris-hostgl/src/gl.rs index e5abb0f2..5f9432fe 100644 --- a/iris-hostgl/src/gl.rs +++ b/iris-hostgl/src/gl.rs @@ -13,6 +13,7 @@ use std::ffi::c_void; extern "C" { pub fn glFlush(); pub fn glFinish(); + pub fn glClear(mask: u32); pub fn glGetString(name: u32) -> *const u8; pub fn glGetError() -> u32; pub fn glGetIntegerv(pname: u32, out: *mut i32); diff --git a/iris-hostgl/src/lib.rs b/iris-hostgl/src/lib.rs index 74efdde7..eea5afc2 100644 --- a/iris-hostgl/src/lib.rs +++ b/iris-hostgl/src/lib.rs @@ -35,6 +35,7 @@ //! library says host GL is not available. #[cfg(target_os = "macos")] +mod accum; mod backend; #[cfg(target_os = "macos")] mod calls; diff --git a/iris-hostgl/src/service.rs b/iris-hostgl/src/service.rs index 95cf82ad..708d5f0f 100644 --- a/iris-hostgl/src/service.rs +++ b/iris-hostgl/src/service.rs @@ -283,6 +283,7 @@ impl GlService { ) -> Reply { let Some(c) = self.clients.get(&client) else { return Reply::Err(EINVAL) }; let current_read = c.read; + let current_draw = c.draw; // No context current (or it went away between retries): GL calls // without one do nothing. let Some(id) = c.current else { return Reply::Ok(0, 0) }; @@ -295,7 +296,7 @@ impl GlService { let mut n = 0u64; let mut fault: Option = None; { - let mut x = Exec::new(mem, &mut writes, funcs, reported, backend.as_ref(), &mut ctx.client, draws, current_read); + let mut x = Exec::new(mem, &mut writes, funcs, reported, backend.as_ref(), &mut ctx.client, draws, current_draw, current_read); while at + 4 <= len { let head = u32::from_be_bytes(bytes[at..at + 4].try_into().unwrap()); let (op, words) = ((head >> 16) as usize, (head & 0xffff) as usize); diff --git a/iris-hostgl/src/tests.rs b/iris-hostgl/src/tests.rs index 527cb993..d4ca33a2 100644 --- a/iris-hostgl/src/tests.rs +++ b/iris-hostgl/src/tests.rs @@ -465,13 +465,13 @@ fn pixels_by_reference_across_pages_in_guest_byte_order() { assert_eq!(pixel(&out, 64, 3, 8), [0, 0, 0, 255], "just outside"); } -/// IRIX numbers UNSIGNED_SHORT_5_6_5 0x8362 and UNSIGNED_BYTE_2_3_3_REV -/// 0x8363, the other way round from the standard. Each must be sized and -/// drawn as the guest meant it, and read back the same way. +/// UNSIGNED_SHORT_5_6_5 (0x8363) and UNSIGNED_BYTE_2_3_3_REV (0x8362), as +/// IRIX 6.5.22's and OpenGL 1.2 number them. Each must be sized +/// and drawn as the guest meant it, and read back the same way. #[test] -fn irix_packed_5_6_5_and_2_3_3_rev_keep_their_meaning() { - const IRIX_5_6_5: i32 = 0x8362; - const IRIX_2_3_3_REV: i32 = 0x8363; +fn packed_5_6_5_and_2_3_3_rev_keep_their_meaning() { + const IRIX_5_6_5: i32 = 0x8363; + const IRIX_2_3_3_REV: i32 = 0x8362; const GL_RGB: i32 = 0x1907; let mut g = Guest::new(64); g.context(0x0040_0004, 32, 32); @@ -527,7 +527,7 @@ fn addresses_above_4gb_arrive_whole() { let (mut writes, mut funcs, mut reported) = (Vec::new(), Vec::new(), HashSet::new()); let mut client = crate::exec::ClientSide::default(); let draws = std::collections::HashMap::new(); - let mut x = crate::exec::Exec::new(&mut mem, &mut writes, &mut funcs, &mut reported, &backend, &mut client, &draws, 0); + let mut x = crate::exec::Exec::new(&mut mem, &mut writes, &mut funcs, &mut reported, &backend, &mut client, &draws, 0, 0); assert_eq!(x.addr(&cmd, 16), out); assert_eq!(x.arr::(&cmd, 4).expect("decoded"), vec![1.5, -2.0, 3.25, 8.0]); x.put::(x.addr(&cmd, 16), &[0xdead_beef, 7]).expect("queued"); @@ -554,12 +554,12 @@ fn by_reference_array_decodes_across_a_page_boundary() { let mut client = crate::exec::ClientSide::default(); let draws = std::collections::HashMap::new(); { - let mut x = crate::exec::Exec::new(&mut mem, &mut writes, &mut funcs, &mut reported, &backend, &mut client, &draws, 0); + let mut x = crate::exec::Exec::new(&mut mem, &mut writes, &mut funcs, &mut reported, &backend, &mut client, &draws, 0, 0); assert!(x.arr::(&cmd, 4).is_none()); assert_eq!(x.fault, Some(Fault { page: (at & !(PAGE - 1)) + PAGE, write: false })); } mem.touch(Fault { page: (at & !(PAGE - 1)) + PAGE, write: false }); - let mut x = crate::exec::Exec::new(&mut mem, &mut writes, &mut funcs, &mut reported, &backend, &mut client, &draws, 0); + let mut x = crate::exec::Exec::new(&mut mem, &mut writes, &mut funcs, &mut reported, &backend, &mut client, &draws, 0, 0); let v = x.arr::(&cmd, 4).expect("decoded"); assert_eq!(x.fault, None); assert_eq!(v.len(), n); @@ -594,7 +594,7 @@ fn array_arguments_hold_what_the_call_reads() { let (mut writes, mut funcs, mut reported) = (Vec::new(), Vec::new(), HashSet::new()); let mut client = crate::exec::ClientSide::default(); let draws = std::collections::HashMap::new(); - let mut x = crate::exec::Exec::new(&mut mem, &mut writes, &mut funcs, &mut reported, &backend, &mut client, &draws, 0); + let mut x = crate::exec::Exec::new(&mut mem, &mut writes, &mut funcs, &mut reported, &backend, &mut client, &draws, 0, 0); let slack = crate::exec::ARRAY_SLACK; let short = x.arr_padded::(&cmd, 4, 16).expect("decoded"); assert_eq!(short.len(), 16 + slack); @@ -1355,3 +1355,89 @@ fn glcheck_reference() { } println!("glcheck: all frames checksum {all:#010x} (glcheck {frames} {size})"); } + +/// The accumulation buffer, which the host's framebuffer objects cannot +/// have (accum.rs): each operation, its values kept past 0..1 between +/// operations, the scissor box, the bits a program is told, and the +/// program's own state left as it was. +#[test] +fn accumulation_buffer_operations() { + const GL_ACCUM: i32 = 0x0100; + const GL_LOAD: i32 = 0x0101; + const GL_RETURN: i32 = 0x0102; + const GL_MULT: i32 = 0x0103; + const GL_ADD: i32 = 0x0104; + const GL_ACCUM_BUFFER_BIT: i32 = 0x0200; + const GL_ACCUM_RED_BITS: i32 = 0x0D58; + const GL_SCISSOR_TEST: i32 = 0x0C11; + let mut g = Guest::new(64); + g.context(0x0040_0005, 32, 32); + let px = g.mem.alloc(32 * 32 * 4, 0); + let bits = g.mem.alloc(4, 0); + let read = |e: &mut Enc| { + e.cmd("glReadPixels", &[V::I(0), V::I(0), V::I(32), V::I(32), V::I(GL_RGBA), V::I(GL_UNSIGNED_BYTE), V::A(px as u64)]); + }; + let near = |p: [u8; 4], want: [u8; 3], what: &str| { + for i in 0..3 { + assert!((p[i] as i32 - want[i] as i32).abs() <= 2, "{what}: {p:?}, wanted {want:?}"); + } + }; + + // Half of red loaded, half of blue added: purple comes back. + let mut e = Enc::default(); + ortho(&mut e, 32, 32); + e.cmd("glClearColor", &[V::F(1.0), V::F(0.0), V::F(0.0), V::F(1.0)]) + .cmd("glClear", &[V::I(GL_COLOR_BUFFER_BIT)]) + .cmd("glAccum", &[V::I(GL_LOAD), V::F(0.5)]) + .cmd("glClearColor", &[V::F(0.0), V::F(0.0), V::F(1.0), V::F(1.0)]) + .cmd("glClear", &[V::I(GL_COLOR_BUFFER_BIT)]) + .cmd("glAccum", &[V::I(GL_ACCUM), V::F(0.5)]) + .cmd("glClearColor", &[V::F(0.0), V::F(0.0), V::F(0.0), V::F(1.0)]) + .cmd("glClear", &[V::I(GL_COLOR_BUFFER_BIT)]) + .cmd("glAccum", &[V::I(GL_RETURN), V::F(1.0)]); + read(&mut e); + assert_eq!(g.batch(&e.take()), Reply::Ok(0, 0)); + near(pixel(g.mem.get(px, 32 * 32 * 4), 32, 16, 16), [128, 0, 128], "load + accum, returned"); + + // Past 1 and back: multiplied by 4 (2.0, unclamped in the buffer), then + // returned at a quarter. + let mut e = Enc::default(); + e.cmd("glAccum", &[V::I(GL_MULT), V::F(4.0)]) + .cmd("glClear", &[V::I(GL_COLOR_BUFFER_BIT)]) + .cmd("glAccum", &[V::I(GL_RETURN), V::F(0.25)]); + read(&mut e); + assert_eq!(g.batch(&e.take()), Reply::Ok(0, 0)); + near(pixel(g.mem.get(px, 32 * 32 * 4), 32, 16, 16), [128, 0, 128], "kept past 1.0"); + + // glClearAccum and glClear's bit, then an add; and the scissor box. + let mut e = Enc::default(); + e.cmd("glClearAccum", &[V::F(0.25), V::F(0.25), V::F(0.25), V::F(1.0)]) + .cmd("glClear", &[V::I(GL_ACCUM_BUFFER_BIT | GL_COLOR_BUFFER_BIT)]) + .cmd("glAccum", &[V::I(GL_ADD), V::F(0.25)]) + .cmd("glScissor", &[V::I(8), V::I(8), V::I(8), V::I(8)]) + .cmd("glEnable", &[V::I(GL_SCISSOR_TEST)]) + .cmd("glAccum", &[V::I(GL_RETURN), V::F(1.0)]) + .cmd("glDisable", &[V::I(GL_SCISSOR_TEST)]); + read(&mut e); + assert_eq!(g.batch(&e.take()), Reply::Ok(0, 0)); + let out = g.mem.get(px, 32 * 32 * 4).to_vec(); + near(pixel(&out, 32, 10, 10), [128, 128, 128], "cleared to 0.25, 0.25 added, inside the scissor box"); + near(pixel(&out, 32, 20, 20), [0, 0, 0], "outside the scissor box"); + + // A program asking how deep the buffer is is told; and what it set is + // as it set it: the colour it drew with last is still current. + let mut e = Enc::default(); + e.cmd("glGetIntegerv", &[V::I(GL_ACCUM_RED_BITS), V::A(bits as u64)]) + .cmd("glColor3ub", &[V::I(0), V::I(255), V::I(0)]) + .cmd("glAccum", &[V::I(GL_RETURN), V::F(1.0)]) + .cmd("glBegin", &[V::I(GL_QUADS)]) + .cmd("glVertex2f", &[V::F(0.0), V::F(0.0)]) + .cmd("glVertex2f", &[V::F(4.0), V::F(0.0)]) + .cmd("glVertex2f", &[V::F(4.0), V::F(4.0)]) + .cmd("glVertex2f", &[V::F(0.0), V::F(4.0)]) + .cmd("glEnd", &[]); + read(&mut e); + assert_eq!(g.batch(&e.take()), Reply::Ok(0, 0)); + assert_eq!(u32::from_be_bytes(g.mem.get(bits, 4).try_into().unwrap()), 16, "GL_ACCUM_RED_BITS"); + near(pixel(g.mem.get(px, 32 * 32 * 4), 32, 2, 2), [0, 255, 0], "the program's colour after a pass"); +} diff --git a/iris-hostgl/tools/glshim.py b/iris-hostgl/tools/glshim.py index 90c58eb3..291800a5 100644 --- a/iris-hostgl/tools/glshim.py +++ b/iris-hostgl/tools/glshim.py @@ -565,6 +565,14 @@ def special_call(name, params, local): return f"x.client_state({a[0]}, {'true' if name.startswith('glEnable') else 'false'}); Some(0)" if name == "glGetError": return "Some(x.get_error() as u64)" + # The accumulation buffer is the executor's (accum.rs): framebuffer + # objects have none. + if name == "glAccum": + return f"x.accum({a[0]}, {a[1]}); Some(0)" + if name == "glClearAccum": + return f"x.clear_accum({a[0]}, {a[1]}, {a[2]}, {a[3]}); Some(0)" + if name == "glClear": + return f"x.clear({a[0]}); Some(0)" # SGIS_multitexture is ARB_multitexture under older names, and the Mac has # only the ARB/core ones: the executor maps the unit and calls those. if name == "glSelectTextureSGIS":