From 05b12b7941eac4f0bcdaddaffc689488dc2c84aa Mon Sep 17 00:00:00 2001 From: atomchild411 <143453386+atomchild411@users.noreply.github.com> Date: Fri, 2 Oct 2026 18:53:50 -0800 Subject: [PATCH 1/2] iris-hostgl: packed pixel types keep the standard numbers IRIX 6.5.22's numbers UNSIGNED_BYTE_2_3_3_REV 0x8362 and UNSIGNED_SHORT_5_6_5 0x8363, as OpenGL 1.2 does; earlier 6.5 headers (6.5.7's among them) had the two the other way round, and the host swapped them back. Programs and the guest libraries are built against 6.5.22 now, so the numbers go to the host as they come. Co-Authored-By: Claude Opus 5.5 --- iris-hostgl/src/exec.rs | 28 ++++++++++------------------ iris-hostgl/src/tests.rs | 12 ++++++------ 2 files changed, 16 insertions(+), 24 deletions(-) diff --git a/iris-hostgl/src/exec.rs b/iris-hostgl/src/exec.rs index 33f9f9ab..902bee90 100644 --- a/iris-hostgl/src/exec.rs +++ b/iris-hostgl/src/exec.rs @@ -199,26 +199,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; @@ -750,14 +742,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; } diff --git a/iris-hostgl/src/tests.rs b/iris-hostgl/src/tests.rs index 527cb993..955274b9 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); From 68595feebb28712329bc0a69dd326490873cdf20 Mon Sep 17 00:00:00 2001 From: atomchild411 <143453386+atomchild411@users.noreply.github.com> Date: Fri, 2 Oct 2026 19:08:49 -0800 Subject: [PATCH 2/2] iris-hostgl: the accumulation buffer Every drawable is a framebuffer object, and OpenGL attaches an accumulation buffer only to the window system's framebuffer: glAccum on the host did nothing. IRIX programs use it for jittered antialiasing, motion blur and depth of field, and IRIS GL's acbuf is the same operation. accum.rs keeps one per drawable instead: a pair of RGBA32F textures the drawable's size. Each operation is one pass of a small shader, new = old*a + colour*b + c, into the other texture of the pair (no float blending needed, which not every GPU has); GL_RETURN draws old*value into the drawable, clamped by its fixed-point storage. Every pass honours the scissor box, GL_RETURN the colour mask and draw buffer, and the program's state is saved around each pass and put back. glAccum, glClearAccum and glClear's GL_ACCUM_BUFFER_BIT go to it; GL_ACCUM_*_BITS answer 16 and GL_ACCUM_CLEAR_VALUE the value set. The executor now knows the drawable drawn into as well as the one read. Co-Authored-By: Claude Opus 5.5 --- iris-hostgl/src/accum.rs | 398 ++++++++++++++++++++++++++++++++++++ iris-hostgl/src/calls.rs | 19 +- iris-hostgl/src/exec.rs | 55 +++++ iris-hostgl/src/gl.rs | 1 + iris-hostgl/src/lib.rs | 1 + iris-hostgl/src/service.rs | 3 +- iris-hostgl/src/tests.rs | 94 ++++++++- iris-hostgl/tools/glshim.py | 8 + 8 files changed, 558 insertions(+), 21 deletions(-) create mode 100644 iris-hostgl/src/accum.rs 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 902bee90..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 @@ -296,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 { @@ -307,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 + } } )* }; } @@ -322,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 { @@ -333,6 +342,7 @@ impl<'a> Exec<'a> { backend, client, draws, + current_draw, current_read, scratch: Vec::new(), staged: Vec::new(), @@ -1119,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) { @@ -1196,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 955274b9..d4ca33a2 100644 --- a/iris-hostgl/src/tests.rs +++ b/iris-hostgl/src/tests.rs @@ -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":