Files
gex/web/mesa/gexgl-mesa.cpp
velzieandClaude Opus 5.5 a6140a3928 D3D: /dev/gex-d3d, a D3D11 program's frames drawn by Mesa's D3D front end
A D3D program's calls reach the page as a stream of records through the GL
device's ring (its own magic and version). With GEX_MESA_DIRK=1,
web/build-mesa-wasm.sh fetches dxvk at Mesa's pinned commit
(third_party/mesa/subprojects/dxvk) and links its D3D11/DXGI front end on a
gex_wgpu screen into the replay (web/mesa/gexd3d-mesa.cpp). A present files
the frame under the client's window like a GL client's: the compositor shows
it, newest first, and the frame report counts D3D presents and how far behind
a window is. d3d=1 on the page turns the device on; a window with no surface
shows nothing.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_c8c04c00da9a4525ab21b0b9f146c598
2026-10-27 14:13:49 -04:00

3217 lines
153 KiB
C++

/* Execute gex's API-level GL wire protocol against Mesa, in the browser.
*
* The guest's GL driver (guest/gl) sends GL calls down the wire; this decodes
* them, maps the guest's object names onto the host's, and runs them through
* Mesa's st/mesa frontend on one of two Gallium drivers:
*
* GEX_MESA_WGPU gex_wgpu, onto the browser's WebGPU
* otherwise softpipe, the software reference
*
* Built by the gex-wasm target in the Mesa fork (third_party/mesa,
* src/gallium/targets/gex-wasm/meson.build), which defines GEX_MESA_WASM.
*/
#if !defined(GEX_MESA_WASM)
#error "gexgl-mesa.cpp is built by Mesa's gex-wasm target, which defines GEX_MESA_WASM"
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glext.h>
#include "frontend/api.h"
#include "frontend/sw_winsys.h"
#include "mesa/main/menums.h"
#include "mesa/glapi/glapi/glapi.h"
#include "mesa/state_tracker/st_context.h"
#include "pipe/p_state.h"
#if defined(GEX_MESA_WGPU)
/* Defined in the driver (C). Reported from the present path below. */
extern "C" unsigned gex_wgpu_draws_declined;
extern "C" unsigned gex_wgpu_draws_issued;
extern "C" unsigned gex_wgpu_declined_constant;
extern "C" unsigned gex_wgpu_declined_texture;
extern "C" unsigned gex_wgpu_declined_vertex;
extern "C" void gex_wgpu_report_draw_targets(void);
extern "C" const char *gex_wgpu_declined_last_why;
#endif
#if defined(GEX_MESA_WGPU)
/* The same Mesa frontend, driven onto the browser's WebGPU instead of a
* software rasterizer. Only the pipe_screen differs: st/mesa, NIR and this
* decoder are identical, which is what makes softpipe a usable oracle for
* this backend rather than a separate implementation. */
#include <webgpu/webgpu.h>
#include "gex_wgpu/gex_wgpu_public.h"
extern "C" WGPUDevice emscripten_webgpu_get_device(void);
#else
#include "softpipe/sp_public.h"
#include "sw/null/null_sw_winsys.h"
#endif
#include <emscripten/emscripten.h>
#include <emscripten/html5.h>
#include <algorithm>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <limits>
#include <map>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include "../../guest/gl/gexgl-pixel.h"
namespace {
template <typename Function>
Function mesa_proc(const char* name) {
return reinterpret_cast<Function>(_mesa_glapi_get_proc_address(name));
}
GLint mesa_get_program_resource_location_index(GLuint program,
GLenum interface_type,
const GLchar* name) {
using Function = GLint (*)(GLuint, GLenum, const GLchar*);
static Function function =
mesa_proc<Function>("glGetProgramResourceLocationIndex");
return function(program, interface_type, name);
}
void mesa_patch_parameter_fv(GLenum pname, const GLfloat* values) {
using Function = void (*)(GLenum, const GLfloat*);
static Function function = mesa_proc<Function>("glPatchParameterfv");
function(pname, values);
}
void mesa_get_internal_format_iv(GLenum target, GLenum internal_format,
GLenum pname, GLsizei count, GLint* values) {
using Function = void (*)(GLenum, GLenum, GLenum, GLsizei, GLint*);
static Function function = mesa_proc<Function>("glGetInternalformativ");
function(target, internal_format, pname, count, values);
}
#define glGetProgramResourceLocationIndex mesa_get_program_resource_location_index
#define glPatchParameterfv mesa_patch_parameter_fv
#define glGetInternalformativ mesa_get_internal_format_iv
#define GEX_GL_CALL(function, ...) \
mesa_proc<decltype(&function)>(#function)(__VA_ARGS__)
enum Op : uint32_t {
CONTEXT_CREATE = 1, SURFACE_RESIZE = 2, FINISH = 3, PRESENT = 4,
SURFACE_ATTACH = 5, SURFACE_CURRENT = 6, SURFACE_DETACH = 7,
CONTEXT_DESTROY = 8,
CLEAR_COLOR = 16, CLEAR = 17, VIEWPORT = 18, ENABLE = 19, DISABLE = 20,
BLEND_FUNC = 21, DEPTH_FUNC = 22, CULL_FACE = 23, FRONT_FACE = 24,
SCISSOR = 25, PIXEL_STOREI = 26, CLEAR_DEPTH = 27, DEPTH_MASK = 28,
HINT = 29, COLOR_MASK = 30, BLEND_EQUATION = 31,
GEN_BUFFERS = 32, DELETE_BUFFERS = 33, BIND_BUFFER = 34,
BUFFER_DATA = 35, BUFFER_SUB_DATA = 36, BLEND_FUNC_SEPARATE = 37,
POLYGON_OFFSET = 38, LINE_WIDTH = 39, CLEAR_STENCIL = 40, DEPTH_RANGE = 41,
BIND_BUFFER_BASE = 42, GEN_SAMPLERS = 43, DELETE_SAMPLERS = 44,
BIND_SAMPLER = 45, SAMPLER_PARAMETER_I = 46, SAMPLER_PARAMETER_F = 47,
CREATE_SHADER = 48, SHADER_SOURCE = 49, COMPILE_SHADER = 50,
CREATE_PROGRAM = 51, ATTACH_SHADER = 52, BIND_ATTRIB_LOCATION = 53,
LINK_PROGRAM = 54, USE_PROGRAM = 55, DELETE_SHADER = 56,
DELETE_PROGRAM = 57, GET_UNIFORM_LOCATION = 58, GET_ATTRIB_LOCATION = 59,
GET_SHADER_IV = 60, GET_PROGRAM_IV = 61, GET_SHADER_INFO_LOG = 62,
GET_PROGRAM_INFO_LOG = 63,
UNIFORM_1I = 64, UNIFORM_1F = 65, UNIFORM_2F = 66, UNIFORM_3F = 67,
UNIFORM_4F = 68, UNIFORM_MATRIX4FV = 69, GET_ACTIVE_UNIFORM = 70,
GET_ACTIVE_ATTRIB = 71, UNIFORM_1IV = 72, UNIFORM_1FV = 73,
UNIFORM_2FV = 74, UNIFORM_3FV = 75, UNIFORM_4FV = 76,
UNIFORM_2IV = 77, UNIFORM_3IV = 78, UNIFORM_4IV = 79,
ENABLE_VERTEX_ATTRIB_ARRAY = 80, DISABLE_VERTEX_ATTRIB_ARRAY = 81,
VERTEX_ATTRIB_POINTER = 82, GEN_VERTEX_ARRAYS = 83,
DELETE_VERTEX_ARRAYS = 84, BIND_VERTEX_ARRAY = 85,
UNIFORM_MATRIX2FV = 86, UNIFORM_MATRIX3FV = 87,
DETACH_SHADER = 88, GET_ATTACHED_SHADERS = 89,
GET_UNIFORM_BLOCK_INDEX = 90, UNIFORM_BLOCK_BINDING = 91,
UNIFORM_2I = 92, UNIFORM_3I = 93, UNIFORM_4I = 94,
GEN_TEXTURES = 96, DELETE_TEXTURES = 97, BIND_TEXTURE = 98,
ACTIVE_TEXTURE = 99, TEX_PARAMETER_I = 100, TEX_IMAGE_2D = 101,
TEX_SUB_IMAGE_2D = 102, GEN_FRAMEBUFFERS = 103, DELETE_FRAMEBUFFERS = 104,
BIND_FRAMEBUFFER = 105, CHECK_FRAMEBUFFER_STATUS = 106,
FRAMEBUFFER_TEXTURE_2D = 107, GEN_RENDERBUFFERS = 108,
DELETE_RENDERBUFFERS = 109, BIND_RENDERBUFFER = 110, RENDERBUFFER_STORAGE = 111,
DRAW_ARRAYS = 112, DRAW_ELEMENTS = 113, FRAMEBUFFER_RENDERBUFFER = 114,
GENERATE_MIPMAP = 115, TEX_STORAGE_2D = 116, TEX_STORAGE_3D = 117,
TEX_IMAGE_3D = 118, FRAMEBUFFER_TEXTURE_LAYER = 119,
DRAW_BUFFERS = 120, VERTEX_ATTRIB_I_POINTER = 121,
VERTEX_ATTRIB_DIVISOR = 122, DRAW_ARRAYS_INSTANCED = 123,
DRAW_ELEMENTS_INSTANCED = 124, BLIT_FRAMEBUFFER = 125,
VERTEX_ATTRIB_4F = 126, VERTEX_ATTRIB_I4I = 127,
READ_PIXELS = 128, GET_STRING = 129, GET_INTEGERV = 130, GET_ERROR = 131,
GET_STRING_I = 132, BLEND_COLOR = 133, CLEAR_BUFFER_FV = 134,
CLEAR_BUFFER_IV = 135, CLEAR_BUFFER_UIV = 136, CLEAR_BUFFER_FI = 137,
STENCIL_FUNC = 138, STENCIL_FUNC_SEPARATE = 139, STENCIL_MASK = 140,
STENCIL_MASK_SEPARATE = 141, STENCIL_OP = 142,
STENCIL_OP_SEPARATE = 143, BLEND_EQUATION_SEPARATE = 144,
GET_UNIFORM_FV = 145, GET_UNIFORM_IV = 146, TEX_PARAMETER_F = 147,
TEX_PARAMETER_FV = 148, TEX_PARAMETER_IV = 149,
GEN_QUERIES = 150, DELETE_QUERIES = 151, IS_QUERY = 152,
GEN_TRANSFORM_FEEDBACKS = 153, DELETE_TRANSFORM_FEEDBACKS = 154,
BIND_TRANSFORM_FEEDBACK = 155, IS_TRANSFORM_FEEDBACK = 156,
PAUSE_TRANSFORM_FEEDBACK = 157, RESUME_TRANSFORM_FEEDBACK = 158,
GET_TRANSFORM_FEEDBACK_VARYING = 159, GET_SHADER_SOURCE = 160,
GET_ACTIVE_UNIFORM_NAME = 161, GET_ACTIVE_UNIFORM_BLOCK_NAME = 162,
GET_ACTIVE_UNIFORM_BLOCKIV = 163, GET_ACTIVE_UNIFORMSIV = 164,
GET_UNIFORM_INDICES = 165,
/* Fixed function; see crates/gex-gl/src/proto.rs. */
MATRIX_MODE = 166, LOAD_IDENTITY = 167, PUSH_MATRIX = 168,
POP_MATRIX = 169, TRANSLATEF = 170, ROTATEF = 171, FRUSTUM = 172,
BEGIN = 173, END = 174, VERTEX3F = 175, NORMAL3F = 176,
SHADE_MODEL = 177, LIGHTFV = 178, MATERIALFV = 179, GEN_LISTS = 180,
NEW_LIST = 181, END_LIST = 182, CALL_LIST = 183, DELETE_LISTS = 184,
/* Protocol 11: client arrays and fixed-function matrices. */
ENABLE_CLIENT_STATE = 185, DISABLE_CLIENT_STATE = 186,
VERTEX_POINTER = 187, COLOR_POINTER = 188, TEX_COORD_POINTER = 189,
CLIENT_ACTIVE_TEXTURE = 190, ORTHO = 191, SCALEF = 192,
LOAD_MATRIXF = 193, MULT_MATRIXF = 194, COLOR4F = 195, TEX_ENVI = 196,
ALPHA_FUNC = 197,
/* Protocol 12: the attribute stack. */
PUSH_ATTRIB = 198, POP_ATTRIB = 199,
/* A client-memory vertex array's bytes with its pointer command; see
* crates/gex-gl/src/proto.rs. */
CLIENT_ARRAY = 200,
};
struct Reader {
const uint8_t* p;
const uint8_t* end;
bool ok = true;
uint32_t u32() {
if (end - p < 4) { ok = false; return 0; }
uint32_t v; std::memcpy(&v, p, 4); p += 4; return v;
}
int32_t i32() { return static_cast<int32_t>(u32()); }
uint64_t u64() {
const uint64_t low = u32();
const uint64_t high = u32();
return low | (high << 32);
}
int64_t i64() { return static_cast<int64_t>(u64()); }
float f32() { uint32_t u = u32(); float f; std::memcpy(&f, &u, 4); return f; }
double f64() { uint64_t u = u64(); double f; std::memcpy(&f, &u, 8); return f; }
const uint8_t* blob(uint32_t& n) {
n = u32();
if (!ok || static_cast<size_t>(end - p) < n) { ok = false; n = 0; return nullptr; }
const uint32_t padded = n <= UINT32_MAX - 3u ? (n + 3u) & ~3u : 0;
if (padded < n || static_cast<size_t>(end - p) < padded) {
ok = false; n = 0; return nullptr;
}
const uint8_t* out = p; p += padded;
return out;
}
std::string string() {
uint32_t n; const uint8_t* s = blob(n);
return s ? std::string(reinterpret_cast<const char*>(s), n) : std::string();
}
};
std::vector<uint8_t> reply;
/* CLIENT_ARRAY's bytes, one copy per array slot. GL keeps the pointer, so the
* bytes stay until the slot's next CLIENT_ARRAY replaces them -- which the
* guest sends before every draw that reads a client array. */
std::unordered_map<uint32_t, std::vector<uint8_t>> client_arrays;
std::unordered_map<uint32_t, GLuint> buffers, textures, shaders, programs;
/* The head of each shader's source, kept only so a failed compile can show
* what it was handed. A GLSL error names a line and column and nothing else,
* and "0:1(1): syntax error, unexpected NEW_IDENTIFIER" is unreadable without
* the line it is pointing at. Capped, because some of these are kilobytes. */
std::unordered_map<GLuint, std::string> shader_source_heads;
std::unordered_set<uint32_t> delete_pending_programs;
uint32_t current_guest_program = 0;
std::unordered_map<uint32_t, GLuint> vertex_arrays, framebuffers, renderbuffers;
std::unordered_map<uint32_t, GLuint> samplers;
std::unordered_map<uint32_t, GLuint> queries, transform_feedbacks;
std::unordered_map<uint32_t, GLuint> program_pipelines;
/* Surface size, remembered until the context that draws to it exists. Above
* the backend blocks because a new drawable is built with it. */
int backend_width = 1;
int backend_height = 1;
/* What the *page* configured, before any client existed.
*
* `gex_gl_init` and `gex_gl_set_api` are asked once, by the embedder, and
* what they set is per client from then on -- so a client that turns up
* later has to inherit them rather than start from this file's defaults.
* Without that the second GL application got a 1x1 drawable and an ES 3.0
* context however the page had configured the device. */
int device_profile = static_cast<int>(API_OPENGLES2);
int device_major = 3;
int device_minor = 0;
int device_width = 1;
int device_height = 1;
#if !defined(GEX_MESA_WGPU)
struct sw_winsys* mesa_winsys = nullptr;
#endif
struct pipe_frontend_screen mesa_frontend_screen = {};
struct st_context* mesa_context = nullptr;
/* The default framebuffer.
*
* st/mesa reaches framebuffer 0 through a drawable the winsys owns, so a
* context made current with none has no framebuffer 0 at all: every clear,
* draw and read against it is GL_INVALID_FRAMEBUFFER_OPERATION. The guest
* renders to framebuffer 0 -- that is what its surface is -- so the backend
* has to supply one. Nothing here is a window; the attachments are plain
* pipe resources of the size the transport was initialized with, which is
* also what makes replay deterministic.
*/
struct gex_drawable {
struct pipe_frontend_drawable base;
struct st_visual visual;
struct pipe_resource* attachments[ST_ATTACHMENT_COUNT];
unsigned width;
unsigned height;
};
/* One surface a client draws into: a window, or a pbuffer, or -- handle zero
* -- the default one a client that never created a surface at all still has.
*
* There used to be a single drawable for the whole device, which is right up
* until a client has two windows or two clients each have one: the second
* window's frames went into the first's, and whichever surface attached last
* owned them both. The guest names its surfaces now (`op::SURFACE_ATTACH`
* carries the handle its EGL made up), so there is a drawable per handle and
* a frame belongs to the window it was drawn for.
*
* `produced` and `displayed` are per surface for the same reason: a client
* swapping window A must not be released by a frame of window B reaching the
* screen. */
struct gex_surface {
gex_drawable drawable{};
/* Unique across the device, not just within a client: two clients both
* call their first surface handle 1, and the page keeps one frame per
* surface. */
unsigned uid = 0;
unsigned kind = 0; /* surface::WAYLAND, surface::X, or none */
unsigned id = 0; /* wl_surface object id, or XID */
unsigned token = 0; /* the client's pid, as the compositor knows it */
uint32_t produced = 0;
uint32_t displayed = 0;
unsigned generation = 0;
/* Its frames come from the D3D device (gex_gl_external_frame). */
bool external = false;
};
/* This client's surfaces, and which is current. `sur` is always the entry
* `current_surface` names once a context exists; ordered, so the enumeration
* the page reads is stable. */
std::map<uint32_t, gex_surface> surfaces;
uint32_t current_surface = 0;
gex_surface* sur = nullptr;
unsigned next_surface_uid = 1;
gex_drawable* drawable_of(struct pipe_frontend_drawable* drawable) {
return reinterpret_cast<gex_drawable*>(drawable);
}
bool drawable_flush_front(struct st_context* st,
struct pipe_frontend_drawable* drawable,
enum st_attachment_type statt) {
(void)st;
(void)drawable;
(void)statt;
/* There is no window to copy to; the front buffer is the resource. */
return true;
}
bool drawable_flush_swapbuffers(struct st_context* st,
struct pipe_frontend_drawable* drawable) {
(void)st;
(void)drawable;
return true;
}
bool drawable_validate(struct st_context* st,
struct pipe_frontend_drawable* drawable,
const enum st_attachment_type* statts, unsigned count,
struct pipe_resource** out,
struct pipe_resource** resolve) {
(void)st;
gex_drawable* self = drawable_of(drawable);
struct pipe_screen* screen = mesa_frontend_screen.screen;
if (!screen) return false;
for (unsigned i = 0; i < count; ++i) {
const enum st_attachment_type statt = statts[i];
if (statt < 0 || statt >= ST_ATTACHMENT_COUNT) return false;
if (!self->attachments[statt]) {
enum pipe_format format = PIPE_FORMAT_NONE;
unsigned bind = 0;
switch (statt) {
case ST_ATTACHMENT_FRONT_LEFT:
case ST_ATTACHMENT_BACK_LEFT:
format = self->visual.color_format;
/* No PIPE_BIND_DISPLAY_TARGET: nothing here is scanned out,
* and the null software winsys has no display target to give.
*/
bind = PIPE_BIND_RENDER_TARGET | PIPE_BIND_SAMPLER_VIEW;
break;
case ST_ATTACHMENT_DEPTH_STENCIL:
format = self->visual.depth_stencil_format;
bind = PIPE_BIND_DEPTH_STENCIL;
break;
default:
break;
}
if (format == PIPE_FORMAT_NONE) {
out[i] = nullptr;
continue;
}
struct pipe_resource templ = {};
templ.target = PIPE_TEXTURE_2D;
templ.format = format;
templ.width0 = self->width;
templ.height0 = self->height;
templ.depth0 = 1;
templ.array_size = 1;
templ.last_level = 0;
templ.nr_samples = self->visual.samples;
templ.nr_storage_samples = self->visual.samples;
templ.bind = bind;
self->attachments[statt] = screen->resource_create(screen, &templ);
if (!self->attachments[statt]) return false;
}
/* The caller owns what it is handed, so hand it a reference. */
out[i] = nullptr;
pipe_resource_reference(&out[i], self->attachments[statt]);
}
if (resolve) *resolve = nullptr;
return true;
}
/* The visual every one of this client's drawables is built with.
*
* Chosen once, in create_backend_context, and wanted again every time a new
* surface turns up -- which now happens long after the context exists. */
struct st_visual backend_visual = {};
/* The drawable for a handle, made if this is the first time it is named.
*
* Handle zero is the surface a client that never created one still has: the
* CTS and the replay harnesses draw into framebuffer 0 without ever calling
* eglCreateWindowSurface, and they must keep working. */
gex_surface* surface_for(uint32_t handle) {
auto found = surfaces.find(handle);
if (found != surfaces.end()) return &found->second;
gex_surface& made = surfaces[handle];
made.uid = next_surface_uid++;
made.drawable.visual = backend_visual;
made.drawable.width = backend_width > 0 ? (unsigned)backend_width : 1;
made.drawable.height = backend_height > 0 ? (unsigned)backend_height : 1;
made.drawable.base.visual = &made.drawable.visual;
made.drawable.base.fscreen = &mesa_frontend_screen;
/* st/mesa keys its framebuffer cache on this identifier, so every
* drawable needs its own: two surfaces both claiming ID 1 are one
* framebuffer as far as st is concerned, which is the confusion this
* whole table exists to end. */
made.drawable.base.ID = handle + 1;
made.drawable.base.stamp = 1;
made.drawable.base.flush_front = drawable_flush_front;
made.drawable.base.validate = drawable_validate;
made.drawable.base.flush_swapbuffers = drawable_flush_swapbuffers;
return &made;
}
/* Told to whatever is decoding for us, when it keeps anything per surface.
* Defined per backend below; a no-op for the ones that do not. */
void note_surface_to_backend(unsigned uid);
/* This surface is gone: let go of any window canvas it was shown in.
* Defined per backend below. */
void forget_window_canvas(unsigned uid);
/* Draw into this handle from here on: `op::SURFACE_CURRENT`, and what an
* attach does as well. */
bool use_surface(uint32_t handle) {
gex_surface* next = surface_for(handle);
current_surface = handle;
if (sur == next) return true;
sur = next;
/* Every flush from here belongs to this surface, not only the one that
* presents it. */
note_surface_to_backend(sur->uid);
/* Before the context exists there is nothing to make current with; the
* context creation that follows uses whatever is current then. */
if (!mesa_context) return true;
return st_api_make_current(mesa_context, &sur->drawable.base,
&sur->drawable.base);
}
/* Told to the page so it can drop the frame it keeps for a surface. Set by
* the backend that has one; see `gex_gl_surface_gone`. */
uint32_t surface_gone_handle = 0;
unsigned surface_gone_generation = 0;
/* Everything a surface owns goes back: its colour and depth buffers, and the
* page's copy of its last frame.
*
* Without this a closed window's buffers stayed allocated for as long as the
* client lived, and -- worse -- the page went on drawing the frame it had
* kept for a window that no longer existed. */
void drop_surface(uint32_t handle, bool repoint = true) {
auto found = surfaces.find(handle);
if (found == surfaces.end()) return;
for (struct pipe_resource*& attachment : found->second.drawable.attachments)
pipe_resource_reference(&attachment, nullptr);
/* And out of st/mesa's table of live drawables, so the next make-current
* lets go of the framebuffer built on it. Left there, the entry outlived
* the memory, and a later drawable at the same address with the same ID
* (the same handle in the next client) was taken for the old one. */
st_api_destroy_drawable(&found->second.drawable.base);
const unsigned uid = found->second.uid;
surfaces.erase(found);
forget_window_canvas(uid);
surface_gone_handle = uid;
++surface_gone_generation;
if (current_surface != handle || !repoint) return;
/* Whatever is left, or the default surface if nothing is: a context must
* always have a framebuffer 0, and the client may well draw again. */
sur = nullptr;
use_surface(surfaces.empty() ? 0 : surfaces.begin()->first);
}
uint32_t produced = 0;
uint32_t displayed = 0;
/* Frames shown, by where they came from, across every client: the frame
* report says which, since a D3D program's process may run GL as well. */
uint32_t displayed_gl = 0, displayed_d3d = 0;
/* The frames a D3D window's last show left unshown: none, newest-first. */
uint32_t d3d_behind = 0;
GLuint default_vao = 0;
std::vector<GLenum> current_draw_buffers{GL_BACK};
GLfloat current_clear_color[4] = {0.f, 0.f, 0.f, 0.f};
GLfloat current_clear_depth = 1.f;
GLint current_clear_stencil = 0;
/* A negative count is legal to pass to the glGen and glDelete calls: GL answers
GL_INVALID_VALUE, and packed negative-count coverage (KHR-GL32
.buffer_objects.gen_buffers) checks exactly that. Read as signed and hand
the negative straight to GL rather than reading it as unsigned and sizing a
four-billion-element vector from it, which is an allocation failure that
takes the whole runtime with it. Returns true when the case is done. */
template <typename Fn>
static bool gexgl_negative_count(GLsizei n, Fn call)
{
if (n >= 0) return false;
call(n, static_cast<GLuint *>(nullptr));
return true;
}
gexgl_pixel_store pack_store = gexgl_default_pixel_store();
gexgl_pixel_store unpack_store = gexgl_default_pixel_store();
#include "gexgl-compat-host.h"
int get_frontend_param(struct pipe_frontend_screen* fscreen,
enum st_manager_param param) {
(void)fscreen;
(void)param;
return 0;
}
/* Which client API the recorded stream was written against.
*
* The wire does not carry this: the guest picks it with eglBindAPI, which is
* a guest-local call, and its default is GLES. A GLES guest's shader source
* ("precision mediump float;", attribute/varying, gl_FragColor) does not
* compile in a core profile, so the backend has to be told which language it
* is about to be handed. GLES is the default because it is the transport's
* own default; gex_gl_init_api selects otherwise for a desktop-GL trace.
*/
int backend_profile = static_cast<int>(API_OPENGLES2);
int backend_major = 3;
int backend_minor = 0;
/* What the guest says its frames belong to, and a count of how many times it
* has said something different. Read by the page after a submit. */
unsigned attached_kind = 0;
unsigned attached_id = 0;
unsigned attached_token = 0;
unsigned attached_generation = 0;
/* The last batch this device refused, for whoever asks: which opcode, why
* (1 short read, 2 trailing data, 3 unknown), how big its payload was, and
* whose it was. */
uint32_t last_reject_op = 0;
uint32_t last_reject_why = 0;
uint32_t last_reject_payload = 0;
uint32_t last_reject_client = 0;
/* Set when a command has already said *why* it refused, so the dispatcher's
* generic report does not overwrite it with "unhandled opcode" -- which is
* how a context that could not be created described itself for one confusing
* afternoon. */
bool last_reject_detailed = false;
/* Everything above that belongs to one *client* rather than to the device.
*
* GL object names are the guest's to allocate -- that is the whole point of
* this protocol -- so two clients hand out the same names, and a single set
* of these maps gives the second client the first one's objects. Two GL
* applications at once therefore drew each other's geometry with each other's
* textures, and closing one broke the other, since the names it deleted were
* names the survivor was still using.
*
* A context each is the fix, and it is what a display server does. The state
* is swapped rather than referred to through a pointer so that the thousands
* of lines that use these by name -- including a generated file -- stay as
* they are; swapping maps is a pointer exchange each, and it happens once per
* batch at most.
*/
struct client_state {
std::unordered_map<uint32_t, GLuint> buffers, textures, shaders, programs,
vertex_arrays, framebuffers, renderbuffers, samplers, queries,
transform_feedbacks, program_pipelines;
std::unordered_map<GLuint, std::string> shader_source_heads;
std::unordered_set<uint32_t> delete_pending_programs;
uint32_t current_guest_program = 0;
GLuint default_vao = 0;
std::vector<GLenum> current_draw_buffers{GL_BACK};
GLfloat current_clear_color[4] = {0.f, 0.f, 0.f, 0.f};
GLfloat current_clear_depth = 1.f;
GLint current_clear_stencil = 0;
gexgl_pixel_store pack_store = gexgl_default_pixel_store();
gexgl_pixel_store unpack_store = gexgl_default_pixel_store();
int backend_profile = static_cast<int>(API_OPENGLES2);
int backend_major = 3;
int backend_minor = 0;
int backend_width = 1;
int backend_height = 1;
unsigned attached_kind = 0, attached_id = 0, attached_token = 0;
struct st_context* mesa_context = nullptr;
/* A Gallium screen each: see `ensure_screen`. */
struct pipe_frontend_screen mesa_frontend_screen = {};
#if !defined(GEX_MESA_WGPU)
struct sw_winsys* mesa_winsys = nullptr;
#endif
struct st_visual backend_visual = {};
std::map<uint32_t, gex_surface> surfaces;
uint32_t current_surface = 0;
};
std::map<uint32_t, client_state> clients;
/* Which client's state is loaded into the globals. Zero is "the only one",
* which is what a caller that never says who it is gets -- the smoke and
* conformance harnesses call submit directly. */
uint32_t current_client = 0;
void swap_client_state(client_state& other) {
std::swap(buffers, other.buffers);
std::swap(textures, other.textures);
std::swap(shaders, other.shaders);
std::swap(programs, other.programs);
std::swap(vertex_arrays, other.vertex_arrays);
std::swap(framebuffers, other.framebuffers);
std::swap(renderbuffers, other.renderbuffers);
std::swap(samplers, other.samplers);
std::swap(queries, other.queries);
std::swap(transform_feedbacks, other.transform_feedbacks);
std::swap(program_pipelines, other.program_pipelines);
std::swap(shader_source_heads, other.shader_source_heads);
std::swap(delete_pending_programs, other.delete_pending_programs);
std::swap(current_guest_program, other.current_guest_program);
std::swap(default_vao, other.default_vao);
std::swap(current_draw_buffers, other.current_draw_buffers);
for (int i = 0; i < 4; ++i)
std::swap(current_clear_color[i], other.current_clear_color[i]);
std::swap(current_clear_depth, other.current_clear_depth);
std::swap(current_clear_stencil, other.current_clear_stencil);
std::swap(pack_store, other.pack_store);
std::swap(unpack_store, other.unpack_store);
std::swap(backend_profile, other.backend_profile);
std::swap(backend_major, other.backend_major);
std::swap(backend_minor, other.backend_minor);
std::swap(backend_width, other.backend_width);
std::swap(backend_height, other.backend_height);
std::swap(attached_kind, other.attached_kind);
std::swap(attached_id, other.attached_id);
std::swap(attached_token, other.attached_token);
std::swap(mesa_context, other.mesa_context);
std::swap(mesa_frontend_screen, other.mesa_frontend_screen);
#if !defined(GEX_MESA_WGPU)
std::swap(mesa_winsys, other.mesa_winsys);
#endif
std::swap(backend_visual, other.backend_visual);
std::swap(surfaces, other.surfaces);
std::swap(current_surface, other.current_surface);
}
/* GEX_GL_COUNTS=1 (`?mesaenv=GEX_GL_COUNTS=1`): a line per PRESENT decoded,
* and one per 256 batches, with the client's batches and presents so far and
* the surface's frames produced and displayed. For a guest that swaps many
* times and a window that shows one frame: which of "never arrived", "arrived
* and was not presented" and "presented and not displayed" it is. */
static bool gl_counts() {
static int on = -1;
if (on < 0) {
const char *value = getenv("GEX_GL_COUNTS");
on = value && *value && *value != '0';
}
return on != 0;
}
static std::map<uint32_t, std::pair<uint64_t, uint64_t>> gl_client_counts;
static void gl_count_line(const char *what) {
const auto &c = gl_client_counts[current_client];
fprintf(stderr,
"gexgl: counts %s -- client %u: %llu batches, %llu presents; "
"surface %u produced %u displayed %u\n",
what, current_client, (unsigned long long)c.first,
(unsigned long long)c.second, sur ? sur->uid : 0u,
sur ? sur->produced : 0u, sur ? sur->displayed : 0u);
}
/* GEX_GL_ERRORS=1 (`?mesaenv=GEX_GL_ERRORS=1`): glGetError after every
* command, and each opcode/error pair reported the first few times it
* happens, with the client. It takes the error the program would have read
* itself, so it is a diagnostic only. */
static void gl_error_check(uint32_t op) {
static int on = -1;
if (on < 0) {
const char *value = getenv("GEX_GL_ERRORS");
on = value && *value && *value != '0';
}
if (!on) return;
const GLenum e = glGetError();
if (e == GL_NO_ERROR) return;
static std::map<std::pair<uint32_t, uint32_t>, uint32_t> seen;
const uint32_t n = ++seen[{op, (uint32_t)e}];
if (n <= 3 || (n & (n - 1)) == 0)
fprintf(stderr, "gexgl: opcode %u -> GL error 0x%x (%u times, client %u)\n",
op, (unsigned)e, n, current_client);
}
/* Run what follows as this client. */
void use_client(uint32_t client) {
if (client == current_client) return;
const bool fresh = clients.find(client) == clients.end();
swap_client_state(clients[current_client]);
current_client = client;
swap_client_state(clients[client]);
if (fresh) {
/* What the page configured for the device, which this client has not
* had a chance to be told about: it never calls init or set_api --
* the embedder does, once. */
backend_profile = device_profile;
backend_major = device_major;
backend_minor = device_minor;
backend_width = device_width;
backend_height = device_height;
}
/* The GL context and the drawable are part of what was just swapped in,
* so this is where the host's GL is pointed at them. A client whose
* context does not exist yet gets one from its own CONTEXT_CREATE. */
sur = nullptr;
if (mesa_context) {
gex_surface* into = surface_for(current_surface);
sur = into;
st_api_make_current(mesa_context, &into->drawable.base,
&into->drawable.base);
/* And the renderer next door, which keeps a frame per surface: a
* client switch changes the drawable without going through
* `use_surface`, so without this the frames of the client that just
* took over were filed under the window of the one that left. */
note_surface_to_backend(into->uid);
}
}
#if defined(GEX_MESA_WGPU)
/* Report which Gallium entry points st/mesa is going to need and this driver
* does not provide.
*
* The corpus in gex_wgpu_smoke.c drives pipe_context directly, so it only ever
* exercises the hooks it happens to call. st/mesa calls many more, and a
* missing one is an indirect call through a null pointer -- "RuntimeError:
* null function" with no name attached. Listing them up front turns that into
* a work list.
*/
int audit_driver_hooks(struct pipe_screen* screen) {
struct pipe_context* ctx = screen->context_create(screen, nullptr, 0);
if (!ctx) {
fprintf(stderr, "gex-wgpu: context_create returned null\n");
return -1;
}
int missing = 0;
#define GEX_NEED(hook) \
do { \
if (!ctx->hook) { \
fprintf(stderr, "gex-wgpu: missing pipe_context::%s\n", #hook); \
++missing; \
} \
} while (0)
/* Resource access: st/mesa's uploaders map buffers during context
* creation, before any GL call is decoded. */
GEX_NEED(buffer_map);
GEX_NEED(buffer_unmap);
GEX_NEED(buffer_subdata);
GEX_NEED(transfer_flush_region);
GEX_NEED(texture_map);
GEX_NEED(texture_unmap);
GEX_NEED(texture_subdata);
/* Whole-framebuffer clear; clear_render_target alone is not enough. */
GEX_NEED(clear);
GEX_NEED(resource_copy_region);
GEX_NEED(blit);
GEX_NEED(flush_resource);
/* Queries: st/mesa creates them for occlusion and timer objects. */
GEX_NEED(create_query);
GEX_NEED(destroy_query);
GEX_NEED(begin_query);
GEX_NEED(end_query);
GEX_NEED(get_query_result);
GEX_NEED(set_active_query_state);
/* State the frontend sets unconditionally at context init. */
GEX_NEED(set_sample_mask);
GEX_NEED(set_min_samples);
GEX_NEED(set_clip_state);
GEX_NEED(set_polygon_stipple);
GEX_NEED(set_debug_callback);
GEX_NEED(memory_barrier);
GEX_NEED(texture_barrier);
GEX_NEED(render_condition);
/* Teardown paths call these without checking, so a null one turns a
* refused context version into a crash instead of a fallback. */
GEX_NEED(sampler_view_release);
GEX_NEED(sampler_view_destroy);
#undef GEX_NEED
ctx->destroy(ctx);
if (missing)
fprintf(stderr, "gex-wgpu: %d required pipe_context hooks missing\n",
missing);
return missing;
}
#endif
/* What the context came up as, and what stands between it and the next
* desktop OpenGL version.
*
* st/mesa derives the version from the extensions it can enable, which come
* from the driver's caps, so "raise the version" is never a thing to set --
* it is a list of extensions to earn. The rungs below are exactly what
* compute_version() in src/mesa/main/version.c tests, so this list is
* generated from the authority rather than guessed, and it names the *next*
* rung only: the one after it is not actionable yet.
*/
void report_version_gap() {
const GLubyte* version = glGetString(GL_VERSION);
const GLubyte* glsl = glGetString(GL_SHADING_LANGUAGE_VERSION);
fprintf(stderr, "gex-wgpu: context is %s / %s\n",
version ? reinterpret_cast<const char*>(version) : "?",
glsl ? reinterpret_cast<const char*>(glsl) : "?");
/* The ladder below is the desktop one, and Mesa numbers an ES context on
* its own scale -- an ES 2.0 context has Version 20, which the desktop
* rungs would read as GL 2.0 and report a meaningless gap to GL 2.1. */
const gl_api api = mesa_context->ctx->API;
if (api != API_OPENGL_CORE && api != API_OPENGL_COMPAT) {
fprintf(stderr,
"gex-wgpu: OpenGL ES context; the desktop ladder does not "
"apply to it\n");
return;
}
const struct gl_extensions& e = mesa_context->ctx->Extensions;
const unsigned have = mesa_context->ctx->Version;
/* Every requirement from GL 4.1 to 4.6 that is not met, not only the next
* rung's: what stands between this context and 4.6, in one list. The
* conditions are compute_version()'s (src/mesa/main/version.c). */
if (have >= 40) {
const struct gl_constants& c = mesa_context->ctx->Const;
struct need { unsigned version; const char* name; bool met; };
const need needs[] = {
{41, "MaxTextureSize >= 16384", c.MaxTextureSize >= 16384},
{41, "MaxRenderbufferSize >= 16384", c.MaxRenderbufferSize >= 16384},
{41, "MaxCubeTextureLevels >= 15", c.MaxCubeTextureLevels >= 15},
{41, "Max3DTextureLevels >= 12", c.Max3DTextureLevels >= 12},
{41, "MaxArrayTextureLayers >= 2048", c.MaxArrayTextureLayers >= 2048},
{41, "ARB_ES2_compatibility", (bool)e.ARB_ES2_compatibility},
{41, "ARB_shader_precision", (bool)e.ARB_shader_precision},
{41, "ARB_vertex_attrib_64bit", (bool)e.ARB_vertex_attrib_64bit},
{41, "ARB_viewport_array", (bool)e.ARB_viewport_array},
{42, "ARB_base_instance", (bool)e.ARB_base_instance},
{42, "ARB_conservative_depth", (bool)e.ARB_conservative_depth},
{42, "ARB_internalformat_query", (bool)e.ARB_internalformat_query},
{42, "ARB_shader_atomic_counters", (bool)e.ARB_shader_atomic_counters},
{42, "ARB_shader_image_load_store", (bool)e.ARB_shader_image_load_store},
{42, "ARB_shading_language_420pack", (bool)e.ARB_shading_language_420pack},
{42, "ARB_shading_language_packing", (bool)e.ARB_shading_language_packing},
{42, "ARB_texture_compression_bptc", (bool)e.ARB_texture_compression_bptc},
{42, "ARB_transform_feedback_instanced", (bool)e.ARB_transform_feedback_instanced},
{43, "vertex MaxUniformBlocks >= 14", c.Program[MESA_SHADER_VERTEX].MaxUniformBlocks >= 14},
{43, "ARB_ES3_compatibility", (bool)e.ARB_ES3_compatibility},
{43, "ARB_arrays_of_arrays", (bool)e.ARB_arrays_of_arrays},
{43, "ARB_compute_shader", (bool)e.ARB_compute_shader},
{43, "ARB_copy_image", (bool)e.ARB_copy_image},
{43, "ARB_explicit_uniform_location", (bool)e.ARB_explicit_uniform_location},
{43, "ARB_fragment_layer_viewport", (bool)e.ARB_fragment_layer_viewport},
{43, "ARB_framebuffer_no_attachments", (bool)e.ARB_framebuffer_no_attachments},
{43, "ARB_internalformat_query2", (bool)e.ARB_internalformat_query2},
{43, "ARB_robust_buffer_access_behavior", (bool)e.ARB_robust_buffer_access_behavior},
{43, "ARB_shader_image_size", (bool)e.ARB_shader_image_size},
{43, "ARB_shader_storage_buffer_object", (bool)e.ARB_shader_storage_buffer_object},
{43, "ARB_stencil_texturing", (bool)e.ARB_stencil_texturing},
{43, "ARB_texture_buffer_range", (bool)e.ARB_texture_buffer_range},
{43, "ARB_texture_query_levels", (bool)e.ARB_texture_query_levels},
{43, "ARB_texture_view", (bool)e.ARB_texture_view},
{44, "MaxVertexAttribStride >= 2048", c.MaxVertexAttribStride >= 2048},
{44, "ARB_buffer_storage", (bool)e.ARB_buffer_storage},
{44, "ARB_enhanced_layouts", (bool)e.ARB_enhanced_layouts},
{44, "ARB_query_buffer_object", (bool)e.ARB_query_buffer_object},
{44, "ARB_texture_mirror_clamp_to_edge", (bool)e.ARB_texture_mirror_clamp_to_edge},
{44, "ARB_texture_stencil8", (bool)e.ARB_texture_stencil8},
{44, "ARB_vertex_type_10f_11f_11f_rev", (bool)e.ARB_vertex_type_10f_11f_11f_rev},
{45, "ARB_ES3_1_compatibility", (bool)e.ARB_ES3_1_compatibility},
{45, "ARB_clip_control", (bool)e.ARB_clip_control},
{45, "ARB_conditional_render_inverted", (bool)e.ARB_conditional_render_inverted},
{45, "ARB_cull_distance", (bool)e.ARB_cull_distance},
{45, "ARB_derivative_control", (bool)e.ARB_derivative_control},
{45, "ARB_shader_texture_image_samples", (bool)e.ARB_shader_texture_image_samples},
{45, "NV_texture_barrier", (bool)e.NV_texture_barrier},
{46, "ARB_gl_spirv", (bool)e.ARB_gl_spirv},
{46, "ARB_spirv_extensions", (bool)e.ARB_spirv_extensions},
{46, "ARB_indirect_parameters", (bool)e.ARB_indirect_parameters},
{46, "ARB_polygon_offset_clamp", (bool)e.ARB_polygon_offset_clamp},
{46, "ARB_shader_atomic_counter_ops", (bool)e.ARB_shader_atomic_counter_ops},
{46, "ARB_shader_draw_parameters", (bool)e.ARB_shader_draw_parameters},
{46, "ARB_shader_group_vote", (bool)e.ARB_shader_group_vote},
{46, "ARB_texture_filter_anisotropic", (bool)e.ARB_texture_filter_anisotropic},
{46, "ARB_transform_feedback_overflow_query", (bool)e.ARB_transform_feedback_overflow_query},
};
for (const need& n : needs)
if (n.version > have && !n.met)
fprintf(stderr, "gex-wgpu: to 4.6: GL %u.%u wants %s\n",
n.version / 10, n.version % 10, n.name);
fprintf(stderr, "gex-wgpu: to 4.6: constants MaxTextureSize %u "
"MaxRenderbufferSize %u MaxCubeTextureLevels %u "
"Max3DTextureLevels %u MaxArrayTextureLayers %u "
"vertex MaxUniformBlocks %u MaxVertexAttribStride %u "
"glsl_feature_level %u\n",
c.MaxTextureSize, c.MaxRenderbufferSize, c.MaxCubeTextureLevels,
c.Max3DTextureLevels, c.MaxArrayTextureLayers,
c.Program[MESA_SHADER_VERTEX].MaxUniformBlocks,
c.MaxVertexAttribStride,
mesa_frontend_screen.screen->caps.glsl_feature_level);
}
/* The driver's advertised level, not ctx->Const.GLSLVersion: Mesa clamps
* that down to match the GL version it settled on, so reading it back
* would report the *consequence* of the version as if it were a cause. */
const unsigned glsl_version =
mesa_frontend_screen.screen->caps.glsl_feature_level;
struct rung {
unsigned version;
unsigned glsl;
const char* name;
/* GLboolean, not bool: gl_extensions stores these as unsigned char and
* narrowing them inside a braced initializer is ill-formed. */
GLboolean present;
};
const rung ladder[] = {
{14, 0, "ARB_shadow", e.ARB_shadow},
{20, 0, "ARB_vertex_shader", e.ARB_vertex_shader},
{20, 0, "ARB_fragment_shader", e.ARB_fragment_shader},
{20, 0, "ARB_texture_non_power_of_two", e.ARB_texture_non_power_of_two},
{20, 0, "EXT_blend_equation_separate", e.EXT_blend_equation_separate},
{20, 0, "EXT_stencil_two_side", e.EXT_stencil_two_side},
{21, 0, "EXT_texture_sRGB", e.EXT_texture_sRGB},
{30, 130, "ARB_color_buffer_float", e.ARB_color_buffer_float},
{30, 130, "ARB_depth_buffer_float", e.ARB_depth_buffer_float},
{30, 130, "ARB_half_float_vertex", e.ARB_half_float_vertex},
{30, 130, "ARB_map_buffer_range", e.ARB_map_buffer_range},
{30, 130, "ARB_shader_texture_lod", e.ARB_shader_texture_lod},
{30, 130, "ARB_texture_float", e.ARB_texture_float},
{30, 130, "ARB_texture_rg", e.ARB_texture_rg},
{30, 130, "ARB_texture_compression_rgtc", e.ARB_texture_compression_rgtc},
{30, 130, "EXT_draw_buffers2", e.EXT_draw_buffers2},
{30, 130, "ARB_framebuffer_object", e.ARB_framebuffer_object},
{30, 130, "EXT_framebuffer_sRGB", e.EXT_framebuffer_sRGB},
{30, 130, "EXT_packed_float", e.EXT_packed_float},
{30, 130, "EXT_texture_array", e.EXT_texture_array},
{30, 130, "EXT_texture_integer", e.EXT_texture_integer},
{30, 130, "EXT_texture_shared_exponent", e.EXT_texture_shared_exponent},
{30, 130, "EXT_transform_feedback", e.EXT_transform_feedback},
{30, 130, "NV_conditional_render", e.NV_conditional_render},
{31, 140, "ARB_draw_instanced", e.ARB_draw_instanced},
{31, 140, "ARB_uniform_buffer_object", e.ARB_uniform_buffer_object},
{31, 140, "EXT_texture_snorm", e.EXT_texture_snorm},
{31, 140, "NV_primitive_restart", e.NV_primitive_restart},
{31, 140, "NV_texture_rectangle", e.NV_texture_rectangle},
{32, 150, "ARB_depth_clamp", e.ARB_depth_clamp},
{32, 150, "ARB_draw_elements_base_vertex",
e.ARB_draw_elements_base_vertex},
{32, 150, "ARB_fragment_coord_conventions",
e.ARB_fragment_coord_conventions},
{32, 150, "EXT_provoking_vertex", e.EXT_provoking_vertex},
{32, 150, "ARB_seamless_cube_map", e.ARB_seamless_cube_map},
{32, 150, "ARB_sync", e.ARB_sync},
{32, 150, "ARB_texture_multisample", e.ARB_texture_multisample},
{32, 150, "EXT_vertex_array_bgra", e.EXT_vertex_array_bgra},
{33, 330, "ARB_blend_func_extended", e.ARB_blend_func_extended},
{33, 330, "ARB_explicit_attrib_location",
e.ARB_explicit_attrib_location},
{33, 330, "ARB_instanced_arrays", e.ARB_instanced_arrays},
{33, 330, "ARB_shader_bit_encoding", e.ARB_shader_bit_encoding},
{33, 330, "ARB_texture_rgb10_a2ui", e.ARB_texture_rgb10_a2ui},
{33, 330, "ARB_timer_query", e.ARB_timer_query},
{33, 330, "ARB_vertex_type_2_10_10_10_rev",
e.ARB_vertex_type_2_10_10_10_rev},
{33, 330, "EXT_texture_swizzle", e.EXT_texture_swizzle},
/* GL 4.0 and above. The ladder stopped at 3.3 once 3.3 was reached,
* which left the tool answering "at or above the highest rung listed"
* and naming nothing to do next. Only the extensions Mesa tracks as
* their own flag are listed: a few of each version's requirements are
* folded into other flags or always-true in this build, so a rung
* going quiet here means the tracked ones are done, not that the
* version is. */
{40, 400, "ARB_draw_buffers_blend", e.ARB_draw_buffers_blend},
{40, 400, "ARB_draw_indirect", e.ARB_draw_indirect},
{40, 400, "ARB_gpu_shader5", e.ARB_gpu_shader5},
{40, 400, "ARB_gpu_shader_fp64", e.ARB_gpu_shader_fp64},
{40, 400, "ARB_sample_shading", e.ARB_sample_shading},
{40, 400, "ARB_tessellation_shader", e.ARB_tessellation_shader},
{40, 400, "ARB_texture_buffer_object_rgb32",
e.ARB_texture_buffer_object_rgb32},
{40, 400, "ARB_texture_cube_map_array", e.ARB_texture_cube_map_array},
{40, 400, "ARB_texture_gather", e.ARB_texture_gather},
{40, 400, "ARB_texture_query_lod", e.ARB_texture_query_lod},
{40, 400, "ARB_transform_feedback2", e.ARB_transform_feedback2},
{40, 400, "ARB_transform_feedback3", e.ARB_transform_feedback3},
{41, 410, "ARB_ES2_compatibility", e.ARB_ES2_compatibility},
{41, 410, "ARB_shader_precision", e.ARB_shader_precision},
{41, 410, "ARB_vertex_attrib_64bit", e.ARB_vertex_attrib_64bit},
{41, 410, "ARB_viewport_array", e.ARB_viewport_array},
{42, 420, "ARB_base_instance", e.ARB_base_instance},
{42, 420, "ARB_conservative_depth", e.ARB_conservative_depth},
{42, 420, "ARB_internalformat_query", e.ARB_internalformat_query},
{42, 420, "ARB_shader_atomic_counters", e.ARB_shader_atomic_counters},
{42, 420, "ARB_shader_image_load_store",
e.ARB_shader_image_load_store},
{42, 420, "ARB_shading_language_420pack",
e.ARB_shading_language_420pack},
{42, 420, "ARB_shading_language_packing",
e.ARB_shading_language_packing},
{42, 420, "ARB_texture_compression_bptc",
e.ARB_texture_compression_bptc},
{42, 420, "ARB_transform_feedback_instanced",
e.ARB_transform_feedback_instanced},
};
/* The next rung above what we have. */
unsigned next = 0;
for (const rung& r : ladder) {
if (r.version > have && (next == 0 || r.version < next))
next = r.version;
}
if (!next) {
fprintf(stderr, "gex-wgpu: at or above the highest rung listed\n");
return;
}
unsigned missing = 0;
/* Two rungs are gated by constants rather than extensions, and leaving
* them out made this report "0 requirements" while the version stayed
* put. GL 3.0 wants four colour attachments and 4x multisampling --
* WebGPU has both, so neither is a dead end. */
if (next == 30) {
if (mesa_context->ctx->Const.MaxColorAttachments < 4) {
fprintf(stderr, "gex-wgpu: GL 3.0 wants 4 colour attachments, have %u\n",
mesa_context->ctx->Const.MaxColorAttachments);
++missing;
}
if (mesa_context->ctx->Const.MaxSamples < 4 &&
!mesa_context->ctx->Const.FakeSWMSAA) {
fprintf(stderr, "gex-wgpu: GL 3.0 wants 4x multisampling, have %u\n",
mesa_context->ctx->Const.MaxSamples);
++missing;
}
}
/* ARB_uniform_buffer_object is gated on constants too, and its rung says
* only the extension's name. st/mesa's can_ubo wants every enabled stage
* to have twelve uniform blocks -- one fewer than max_const_buffers,
* since slot zero holds GL's default block -- and to allow indirect
* constant addressing, plus a 16 KiB uniform block size. */
if (next == 31) {
/* Another constant rung, like GL 3.0's colour attachments: the version
* check wants sixteen texture image units in the vertex stage. */
const unsigned vertex_units =
mesa_context->ctx->Const.Program[MESA_SHADER_VERTEX]
.MaxTextureImageUnits;
if (vertex_units < 16) {
fprintf(stderr,
"gex-wgpu: GL 3.1 wants 16 vertex texture image units, "
"have %u\n", vertex_units);
++missing;
}
const struct pipe_screen* pscreen = mesa_frontend_screen.screen;
const mesa_shader_stage ubo_stages[] = {MESA_SHADER_VERTEX,
MESA_SHADER_FRAGMENT};
for (mesa_shader_stage sh : ubo_stages) {
fprintf(stderr,
"gex-wgpu: %s: MaxUniformBlocks %u (needs 12), "
"max_const_buffers %u, indirect_const_addr %d, "
"max_instructions %u\n",
sh == MESA_SHADER_VERTEX ? "vertex" : "fragment",
mesa_context->ctx->Const.Program[sh].MaxUniformBlocks,
pscreen->shader_caps[sh].max_const_buffers,
(int)pscreen->shader_caps[sh].indirect_const_addr,
pscreen->shader_caps[sh].max_instructions);
}
fprintf(stderr, "gex-wgpu: MaxUniformBlockSize %u (needs 16384)\n",
mesa_context->ctx->Const.MaxUniformBlockSize);
}
for (const rung& r : ladder) {
if (r.version != next || r.present)
continue;
fprintf(stderr, "gex-wgpu: GL %u.%u wants %s\n",
next / 10, next % 10, r.name);
++missing;
}
for (const rung& r : ladder) {
if (r.version == next && r.glsl && glsl_version < r.glsl) {
fprintf(stderr, "gex-wgpu: GL %u.%u wants GLSL %u, have %u\n",
next / 10, next % 10, r.glsl, glsl_version);
++missing;
break;
}
}
fprintf(stderr, "gex-wgpu: %u requirement(s) between GL %u.%u and %u.%u\n",
missing, have / 10, have % 10, next / 10, next % 10);
}
/* The depth/stencil format the driver under us will actually take.
*
* Which byte order of Z24S8 a driver has is a driver's business, so ask
* rather than assume; the order here is preference.
*/
static enum pipe_format depth_stencil_for(struct pipe_screen *screen)
{
static const enum pipe_format wanted[] = {
PIPE_FORMAT_Z24_UNORM_S8_UINT,
PIPE_FORMAT_S8_UINT_Z24_UNORM,
PIPE_FORMAT_Z32_FLOAT_S8X24_UINT,
};
if (screen) {
for (unsigned i = 0; i < sizeof wanted / sizeof *wanted; i++) {
if (screen->is_format_supported(screen, wanted[i], PIPE_TEXTURE_2D, 1, 1,
PIPE_BIND_DEPTH_STENCIL))
return wanted[i];
}
}
return PIPE_FORMAT_Z24_UNORM_S8_UINT;
}
/* This client's Gallium screen, made if it has none yet.
*
* One screen per client, not one per device: the screen is where a driver
* keeps its per-client caches.
*/
int ensure_screen() {
if (mesa_frontend_screen.screen) return 0;
#if defined(GEX_MESA_WGPU)
/* The page owns the device; the screen only retains its queue. */
WGPUDevice device = emscripten_webgpu_get_device();
if (!device) return -1;
mesa_frontend_screen.screen = gex_wgpu_screen_create(device);
#else
mesa_winsys = null_sw_create();
if (!mesa_winsys) return -1;
mesa_frontend_screen.screen = softpipe_create_screen(mesa_winsys);
#endif
mesa_frontend_screen.get_param = get_frontend_param;
if (!mesa_frontend_screen.screen) return -2;
#if defined(GEX_MESA_WGPU)
audit_driver_hooks(mesa_frontend_screen.screen);
#endif
return 0;
}
/* What the page asks for once, before any client exists: the size everything
* starts at, and a screen for whoever is current -- which also reports at
* once whether this backend works at all. The context itself waits for
* CONTEXT_CREATE, where the guest finally knows whether the application
* wants desktop GL or ES. */
int initialize_backend(int width, int height) {
backend_width = width;
backend_height = height;
/* And for clients that turn up later: see `device_width`. */
device_width = width;
device_height = height;
return ensure_screen();
}
/* Create the GL context the guest asked for.
*
* `api` is 0 for OpenGL ES and 1 for desktop OpenGL, as
* crates/gex-gl/src/proto.rs defines it. A zero major means "the best you
* can do", which is what an application that never named a version should
* get: st/mesa grants the highest version its extension set supports.
*/
int create_backend_context(unsigned api, unsigned major, unsigned minor,
unsigned profile) {
if (mesa_context)
return 0;
/* This client's own screen: the page only ever made one, for whoever
* was current at the time. */
if (ensure_screen() != 0 || !mesa_frontend_screen.screen)
return -1;
/* A visual the guest's default framebuffer can actually be: a colour
* buffer it draws to and a depth/stencil buffer, both at the size the
* transport was given. */
struct st_visual visual = {};
visual.buffer_mask = ST_ATTACHMENT_FRONT_LEFT_MASK |
ST_ATTACHMENT_BACK_LEFT_MASK |
ST_ATTACHMENT_DEPTH_STENCIL_MASK;
visual.color_format = PIPE_FORMAT_R8G8B8A8_UNORM;
visual.depth_stencil_format = depth_stencil_for(mesa_frontend_screen.screen);
visual.accum_format = PIPE_FORMAT_NONE;
visual.samples = 0;
/* st/mesa computes the version it can actually support while creating the
* context, and then only *compares* it with what was asked for: a request
* of 1.0 skips that comparison entirely. So ask for 1.0, read back the
* version it settled on, and enforce the application's request here. That
* is one context instead of a descending ladder of create-and-destroy, and
* the number it yields is the real ceiling rather than the best of a list
* somebody has to keep up to date.
*
* A core profile only exists from 3.2 up; below that the compatibility
* profile is the one that is defined.
*/
struct st_context_attribs attribs = {};
attribs.visual = visual;
attribs.major = 1;
attribs.minor = 0;
/* `profile` is 1 when the guest requires compatibility -- a GLX context
* created the legacy way. The difference that matters here is the default
* vertex array object, which core does not have and GL2-era code assumes:
* without it st drops every draw before the driver is called. */
attribs.profile = api != 1 ? API_OPENGLES2
: profile == 1 || major * 10 + minor < 32 ? API_OPENGL_COMPAT : API_OPENGL_CORE;
fprintf(stderr, "gex-wgpu: context requested api=%u %u.%u\n",
api, major, minor);
enum st_context_error error = ST_CONTEXT_SUCCESS;
mesa_context = st_api_create_context(&mesa_frontend_screen, &attribs,
&error, nullptr);
if (api == 1 && attribs.profile == API_OPENGL_CORE &&
(!mesa_context || mesa_context->ctx->Version < 32)) {
/* Either the core profile could not be created at all, or it came up
* below 3.2 where a core profile is not defined. The compatibility
* profile is what a driver of this capability actually offers, so try
* it before giving up -- an application asking for desktop GL should
* get the desktop context that exists, not none. */
if (mesa_context) {
st_destroy_context(mesa_context);
mesa_context = nullptr;
}
attribs.profile = API_OPENGL_COMPAT;
mesa_context = st_api_create_context(&mesa_frontend_screen, &attribs,
&error, nullptr);
}
if (!mesa_context) {
fprintf(stderr,
"gex-wgpu: no %s context: st error %d\n",
api == 1 ? "desktop GL" : "GLES", (int)error);
return -3 - static_cast<int>(error);
}
fprintf(stderr, "gex-wgpu: negotiated %s %u.%u\n",
api == 1 ? "desktop GL" : "GLES",
mesa_context->ctx->Version / 10, mesa_context->ctx->Version % 10);
/* The application named a version this context cannot reach. Say so and
* hand back the context that does exist.
*
* Destroying it instead was worse in a way that took a real application to
* see. The guest's context_create has no failure path -- eglCreateContext
* has already returned by the time this runs -- so the refusal could only
* surface as CONTEXT_CREATE failing, which the decoder reports as a
* protocol error, which latches the transport. SuperTuxKart asks for 4.4,
* and the entire session died on the request rather than on any use of it.
*
* What makes granting it defensible is that nothing is misrepresented:
* glGetString reports this context's real version, which the guest passes
* through verbatim, so an application that checks gets the truth and one
* that calls a 4.4 entry point anyway gets an ordinary GL error for that
* call. Refusing up front is only better when the refusal can be reported,
* and here it cannot. */
if (major && mesa_context->ctx->Version < major * 10u + minor) {
fprintf(stderr,
"gex-wgpu: application asked for %u.%u, this context is %u.%u "
"(granted; GL_VERSION reports the real one)\n",
major, minor, mesa_context->ctx->Version / 10,
mesa_context->ctx->Version % 10);
}
/* Every drawable of this client is built with this visual, including the
* ones that turn up later: a window created after the context is a
* surface of its own now. */
backend_visual = visual;
/* And the ones that turned up *before* it. The two commands arrive in
* either order -- a GLX program attaches its window inside
* glXMakeCurrent, just before the context is made -- so a surface can
* already exist with no visual at all, and a context made current
* against a drawable with no colour format simply fails. That is what
* -9 was: a window attached first, and glxgears with no context. */
for (auto& entry : surfaces) {
gex_drawable& drawable = entry.second.drawable;
if (drawable.visual.color_format != PIPE_FORMAT_NONE) continue;
drawable.visual = visual;
drawable.width = backend_width > 0 ? (unsigned)backend_width : 1;
drawable.height = backend_height > 0 ? (unsigned)backend_height : 1;
++drawable.base.stamp;
}
gex_surface* into = surface_for(current_surface);
sur = into;
if (!st_api_make_current(mesa_context, &into->drawable.base,
&into->drawable.base))
return -9;
note_surface_to_backend(into->uid);
#if defined(GEX_MESA_WGPU)
report_version_gap();
#endif
return 0;
}
#if defined(GEX_MESA_WGPU)
/* Putting the drawable on the canvas.
*
* st/mesa's drawable is a plain texture this file owns -- nothing is scanned
* out, which is what makes replay deterministic -- so presenting is a copy
* from that texture into the canvas surface's current texture. The surface is
* configured RGBA8Unorm to match the visual's colour format exactly: WebGPU
* will not copy between rgba8unorm and bgra8unorm, and a blit through a
* render pass to swap two channels would be a shader and a pipeline for
* something the configuration can simply avoid.
*
* Without this the whole path renders correctly into a texture nobody ever
* sees: every frame counted as presented over a canvas that stayed black. */
WGPUSurface mesa_surface = nullptr;
/* One-shot markers through the first present.
*
* The WebGPU device was destroyed 1.5 ms after the first frame was presented,
* with no validation error and nothing in JavaScript calling `destroy()`. The
* only way to tell which of these calls did it is to say where we are before
* each one, once. */
#define GEX_PRESENT_MARK(what) \
do { \
static bool said_##what; \
if (!said_##what) { \
said_##what = true; \
fprintf(stderr, "gex-wgpu: first present -- " #what "\n"); \
} \
} while (0)
/* Declared here rather than with the other externs at the top: WGPUTexture
* only exists once webgpu.h has been included. */
extern "C" WGPUTexture gex_wgpu_presented_texture;
/* Resize the default framebuffer to what the application asked for.
*
* This used to read its two arguments and throw them away, so the default
* framebuffer was always the canvas's size no matter what
* eglCreatePbufferSurface or eglCreateWindowSurface requested -- while
* eglQuerySurface, answered on the guest side, reported the requested size
* faithfully. Everything that draws to framebuffer 0 and reads it back at the
* size it asked for was therefore wrong: the CTS asks for 64x64, believed it
* had 64x64, drew across 320x240 and checked pixels in the corner.
* KHR-GL33.primitive_restart failed on exactly that, with primitive restart
* itself working perfectly.
*
* Three things have to move together. The cached attachments are the old
* size, so they are dropped and drawable_validate builds them again; st only
* asks for them when the drawable's stamp changes, so that is bumped; and the
* canvas surface is configured for the old size, so it is released and
* configure_present_surface redoes it. */
void resize_drawable(int width, int height) {
if (width <= 0 || height <= 0) return;
/* Also the size a *later* context creation will build its drawable at.
*
* The two commands can arrive in either order. A GLX program resizes
* through glXMakeCurrent -- eglCreateWindowSurface reads the window's
* geometry -- and the context is created by the same call, just after; so
* the resize landed first and context_create then rebuilt mesa_drawable
* from these, throwing it away. glmark2 asked for 800x600, got 300x300,
* and rendered its whole scene outside the framebuffer. */
backend_width = width;
backend_height = height;
if ((unsigned)width == sur->drawable.width &&
(unsigned)height == sur->drawable.height)
return;
sur->drawable.width = (unsigned)width;
sur->drawable.height = (unsigned)height;
for (struct pipe_resource*& attachment : sur->drawable.attachments)
pipe_resource_reference(&attachment, nullptr);
++sur->drawable.base.stamp;
/* Counted-draw bookkeeping only, but it names a texture that is gone. */
gex_wgpu_presented_texture = nullptr;
if (mesa_surface) {
wgpuSurfaceRelease(mesa_surface);
mesa_surface = nullptr;
}
}
/* The device's queue, fetched once and kept.
*
* This used to be fetched and released around every present. `webgpu.h` says
* the getter hands out a reference the caller owns, so releasing it is
* correct by the letter -- and with this implementation the device stopped
* existing a frame or two later, reported as
* `lost: destroyed: Device was destroyed`, which is a device that has quietly
* stopped doing anything: no failed calls, no validation errors, frame
* counters still climbing, and a black window. Holding the queue for the
* device's whole life also saves a fetch per frame, which is what a driver
* would do anyway. */
WGPUQueue present_queue(WGPUDevice device) {
static WGPUQueue queue = nullptr;
if (!queue) queue = wgpuDeviceGetQueue(device);
return queue;
}
/* The device frames are shown with: this client's screen's, or for a
* client with no GL of its own (one whose frames come from the D3D device,
* gexd3d-mesa.cpp) the page's, which every screen is made on. */
WGPUDevice display_device() {
return mesa_frontend_screen.screen
? gex_wgpu_screen_device(mesa_frontend_screen.screen)
: emscripten_webgpu_get_device();
}
void configure_present_surface() {
if (mesa_surface) return;
GEX_PRESENT_MARK(configure_begin);
WGPUDevice device = display_device();
if (!device) return;
WGPUInstance instance = wgpuCreateInstance(nullptr);
if (!instance) return;
WGPUEmscriptenSurfaceSourceCanvasHTMLSelector source{};
source.chain.sType = WGPUSType_EmscriptenSurfaceSourceCanvasHTMLSelector;
source.selector = WGPUStringView{"#gex-gl", WGPU_STRLEN};
WGPUSurfaceDescriptor descriptor{};
descriptor.nextInChain = &source.chain;
GEX_PRESENT_MARK(create_surface);
mesa_surface = wgpuInstanceCreateSurface(instance, &descriptor);
if (!mesa_surface) {
fprintf(stderr, "gex-wgpu: no canvas surface; nothing will be shown\n");
return;
}
fprintf(stderr, "gex-wgpu: first present -- canvas resize to %ux%u\n",
sur->drawable.width, sur->drawable.height);
emscripten_set_canvas_element_size("#gex-gl", (int)sur->drawable.width,
(int)sur->drawable.height);
WGPUSurfaceConfiguration cfg{};
cfg.device = device;
cfg.format = WGPUTextureFormat_RGBA8Unorm;
/* Read as well as written. The frame is presented into this canvas and
* then *taken* from it: the compositor with a scene uploads the canvas
* into a texture, and the one that puts each window in an element
* copies it into the window's own canvas. Both are ordinary image-source
* reads of a canvas, and a WebGPU canvas configured without CopySrc
* cannot be one -- which did not fail the read so much as take the device
* with it: `lost: destroyed` in the same breath as the first present, and
* a black window from then on, with draws still counting up. */
cfg.usage = WGPUTextureUsage_RenderAttachment | WGPUTextureUsage_CopyDst |
WGPUTextureUsage_CopySrc | WGPUTextureUsage_TextureBinding;
cfg.width = sur->drawable.width;
cfg.height = sur->drawable.height;
cfg.alphaMode = WGPUCompositeAlphaMode_Opaque;
cfg.presentMode = WGPUPresentMode_Fifo;
GEX_PRESENT_MARK(surface_configure);
wgpuSurfaceConfigure(mesa_surface, &cfg);
GEX_PRESENT_MARK(configure_done);
}
/* Nothing keeps a frame per surface on this backend: it presents into a
* canvas, the window's own or the shared one. */
void note_surface_to_backend(unsigned uid) { (void)uid; }
/* A canvas of the window's own, by surface uid.
*
* The shared `#gex-gl` canvas holds one frame, so the page used to copy it
* into each window's 2D canvas with `drawImage`. That read of a WebGPU
* canvas through 2D came back fully transparent on Chromium on Linux
* -- a red clear included -- so a card game counted every frame
* presented and showed none. A page that can give the window a canvas
* names it `#gex-gl-<uid>` (`gex_gl_surface_canvas`), and the frame is
* copied there texture to texture: on the GPU all the way, nothing read
* back, and one frame per window rather than whichever presented last.
*
* `reshow` is a frame owed to a canvas that arrived after the frame did:
* a client that draws only when something changes (a card game) would
* otherwise leave its new canvas empty until the next click. */
struct window_canvas {
WGPUSurface surface = nullptr;
unsigned width = 0, height = 0;
bool reshow = false;
};
std::map<unsigned, window_canvas> window_canvases;
void forget_window_canvas(unsigned uid) {
auto found = window_canvases.find(uid);
if (found == window_canvases.end()) return;
if (found->second.surface) wgpuSurfaceRelease(found->second.surface);
window_canvases.erase(found);
}
/* The surface to present the current window into, configured at its size:
* its own canvas when the page gave it one, the shared canvas otherwise. */
WGPUSurface present_target(WGPUDevice device) {
auto found = window_canvases.find(sur->uid);
if (found == window_canvases.end()) {
configure_present_surface();
return mesa_surface;
}
window_canvas& wc = found->second;
const unsigned w = sur->drawable.width, h = sur->drawable.height;
if (wc.surface && wc.width == w && wc.height == h) return wc.surface;
char selector[32];
snprintf(selector, sizeof selector, "#gex-gl-%u", sur->uid);
if (!wc.surface) {
static WGPUInstance instance = wgpuCreateInstance(nullptr);
if (!instance) return nullptr;
WGPUEmscriptenSurfaceSourceCanvasHTMLSelector source{};
source.chain.sType = WGPUSType_EmscriptenSurfaceSourceCanvasHTMLSelector;
source.selector = WGPUStringView{selector, WGPU_STRLEN};
WGPUSurfaceDescriptor descriptor{};
descriptor.nextInChain = &source.chain;
wc.surface = wgpuInstanceCreateSurface(instance, &descriptor);
if (!wc.surface) {
static bool said;
if (!said) {
said = true;
fprintf(stderr, "gex-wgpu: no surface for %s; that window "
"falls back to the shared canvas\n", selector);
}
window_canvases.erase(found);
configure_present_surface();
return mesa_surface;
}
}
emscripten_set_canvas_element_size(selector, (int)w, (int)h);
WGPUSurfaceConfiguration cfg{};
cfg.device = device;
cfg.format = WGPUTextureFormat_RGBA8Unorm;
/* Written only: nothing reads this canvas back. */
cfg.usage = WGPUTextureUsage_RenderAttachment | WGPUTextureUsage_CopyDst;
cfg.width = w;
cfg.height = h;
cfg.alphaMode = WGPUCompositeAlphaMode_Opaque;
cfg.presentMode = WGPUPresentMode_Fifo;
wgpuSurfaceConfigure(wc.surface, &cfg);
wc.width = w;
wc.height = h;
return wc.surface;
}
void present_backend() {
glFinish();
++produced;
if (sur) ++sur->produced;
}
/* True when this surface has something to show: a new frame, or its last one
* owed to a canvas that arrived after it. */
bool display_pending(const gex_surface& s) {
if (s.displayed < s.produced) return true;
if (!s.produced) return false;
auto found = window_canvases.find(s.uid);
return found != window_canvases.end() && found->second.reshow;
}
/* The current surface's newest frame has been shown. A GL client is paced
* by these counts, one frame each; a D3D window is not, and its frames are
* shown newest-first: the ones it presented in between are passed over, not
* shown later one per page frame as though each were new. */
void note_shown() {
++displayed;
if (sur->external) {
sur->displayed = sur->produced;
++displayed_d3d;
d3d_behind = sur->produced - sur->displayed;
} else {
++sur->displayed;
++displayed_gl;
}
}
bool display_backend() {
if (!sur || !display_pending(*sur)) return false;
/* A reshow repeats a frame already counted: it moves no counter. */
const bool fresh = sur->displayed < sur->produced;
{
auto found = window_canvases.find(sur->uid);
if (found != window_canvases.end()) found->second.reshow = false;
}
struct pipe_resource *source =
sur->drawable.attachments[ST_ATTACHMENT_BACK_LEFT];
WGPUTexture from = source ? gex_wgpu_resource_texture(source) : nullptr;
WGPUDevice device = display_device();
WGPUSurface target = device ? present_target(device) : nullptr;
if (!target || !from || !device) {
/* Counted anyway: the frame really was produced, and reporting
* otherwise would stall a caller that waits for the count to move.
* Said out loud once, because counting a frame nobody copied is
* indistinguishable from presenting one -- the caller sees a rising
* `presented` and a canvas that never changes. */
static bool said;
if (!said) {
said = true;
fprintf(stderr,
"gex-wgpu: display copies nothing (%s); the canvas will "
"not change while frames keep counting\n",
!target ? "no canvas surface"
: !from ? "no back-left drawable texture"
: "no device");
}
if (fresh) note_shown();
return true;
}
/* Tell the driver which texture reaches the canvas, so it can count the
* draws that land in it rather than in some other same-sized target. */
gex_wgpu_presented_texture = from;
WGPUSurfaceTexture surface_texture{};
GEX_PRESENT_MARK(get_current_texture);
wgpuSurfaceGetCurrentTexture(target, &surface_texture);
if (surface_texture.status !=
WGPUSurfaceGetCurrentTextureStatus_SuccessOptimal &&
surface_texture.status !=
WGPUSurfaceGetCurrentTextureStatus_SuccessSuboptimal) {
static bool said;
if (!said) {
said = true;
fprintf(stderr,
"gex-wgpu: surface texture unavailable (status %u); the "
"canvas will not change while frames keep counting\n",
(unsigned)surface_texture.status);
}
if (fresh) note_shown();
return true;
}
{
/* Every ~10 s of frames: how many draws actually reached the device
* against how many were declined. A frame presents either way, so this
* is what distinguishes "drawing nothing" from "not started". */
static unsigned frames;
/* GEX_WGPU_FRAME_REPORT=N (`?mesaenv=GEX_WGPU_FRAME_REPORT=1`): the
* report every N frames instead, for a program that presents a
* handful of frames and whose second one is the question (which
* texture its draws and blits went to, against the one shown). */
static unsigned every;
if (!every) {
const char *value = getenv("GEX_WGPU_FRAME_REPORT");
every = value && atoi(value) > 0 ? (unsigned)atoi(value) : 600;
}
/* Braced: the report below was once outside the `if` by mistake, so
* its ten formatted stderr lines (and the page's console plumbing
* for each) ran on every displayed frame. */
if ((frames++ % every) == 0) {
fprintf(stderr,
"gex-wgpu: frame %u -- issued %u, declined %u "
"(constant %u, texture %u, vertex %u; last: %s); "
"new frames shown: %u GL, %u D3D (%u behind)\n",
frames, gex_wgpu_draws_issued, gex_wgpu_draws_declined,
gex_wgpu_declined_constant, gex_wgpu_declined_texture,
gex_wgpu_declined_vertex,
gex_wgpu_declined_last_why ? gex_wgpu_declined_last_why
: "-",
displayed_gl, displayed_d3d, d3d_behind);
gex_wgpu_report_draw_targets();
}
}
GEX_PRESENT_MARK(copy_begin);
WGPUCommandEncoder encoder = wgpuDeviceCreateCommandEncoder(device, nullptr);
if (encoder) {
WGPUTexelCopyTextureInfo src = WGPU_TEXEL_COPY_TEXTURE_INFO_INIT;
src.texture = from;
WGPUTexelCopyTextureInfo dst = WGPU_TEXEL_COPY_TEXTURE_INFO_INIT;
dst.texture = surface_texture.texture;
/* The smaller of the two: a window canvas is sized to its drawable,
* the shared one may still be at its old size for a frame. */
WGPUExtent3D size{
std::min(sur->drawable.width,
wgpuTextureGetWidth(surface_texture.texture)),
std::min(sur->drawable.height,
wgpuTextureGetHeight(surface_texture.texture)),
1};
wgpuCommandEncoderCopyTextureToTexture(encoder, &src, &dst, &size);
WGPUCommandBuffer commands = wgpuCommandEncoderFinish(encoder, nullptr);
if (commands) {
GEX_PRESENT_MARK(submit);
wgpuQueueSubmit(present_queue(device), 1, &commands);
wgpuCommandBufferRelease(commands);
}
wgpuCommandEncoderRelease(encoder);
}
GEX_PRESENT_MARK(texture_release);
wgpuTextureRelease(surface_texture.texture);
GEX_PRESENT_MARK(done);
if (fresh) note_shown();
return true;
}
/* The page has given surface `uid` a canvas of its own, `#gex-gl-<uid>`, or
* (`on` zero) taken it away. Its last frame is shown there at the next
* display, without counting as a new one. */
void set_window_canvas(unsigned uid, bool on) {
if (!on) {
forget_window_canvas(uid);
return;
}
window_canvas& wc = window_canvases[uid];
/* A canvas named again may be a new element under the old id: build its
* surface afresh. */
if (wc.surface) {
wgpuSurfaceRelease(wc.surface);
wc.surface = nullptr;
}
wc.width = wc.height = 0;
wc.reshow = true;
}
#else
void note_surface_to_backend(unsigned uid) { (void)uid; }
void forget_window_canvas(unsigned uid) { (void)uid; }
void set_window_canvas(unsigned uid, bool on) { (void)uid; (void)on; }
bool display_pending(const gex_surface& s) { return s.displayed < s.produced; }
void present_backend() {
glFinish();
++produced;
if (sur) ++sur->produced;
}
bool display_backend() {
if (!sur || sur->displayed >= sur->produced) return false;
++sur->displayed;
++displayed;
return true;
}
/* Give the current drawable the size the guest's window has.
*
* This used to do nothing at all, with a comment about there being no
* drawable of ours -- true of the backend that draws into an embedder's
* surface, and not of this one, which owns its drawable. So a window's size
* never reached it: the drawable stayed whatever the transport was started
* with, a 300x300 window drew into a 300x150 framebuffer, and the top half of
* the gears was simply missing. The attachments are dropped so
* drawable_validate builds them again, and the stamp is bumped because that
* is the only thing st/mesa re-reads them for. */
void resize_drawable(int width, int height) {
if (width <= 0 || height <= 0) return;
backend_width = width;
backend_height = height;
if (!sur) return;
if ((unsigned)width == sur->drawable.width &&
(unsigned)height == sur->drawable.height)
return;
sur->drawable.width = (unsigned)width;
sur->drawable.height = (unsigned)height;
for (struct pipe_resource*& attachment : sur->drawable.attachments)
pipe_resource_reference(&attachment, nullptr);
++sur->drawable.base.stamp;
}
#endif
void put_u32(uint32_t v) {
const size_t at = reply.size(); reply.resize(at + 4); std::memcpy(reply.data() + at, &v, 4);
}
void put_u64(uint64_t v) {
put_u32(static_cast<uint32_t>(v));
put_u32(static_cast<uint32_t>(v >> 32));
}
void put_blob(const void* p, uint32_t n) {
const size_t padded = (static_cast<size_t>(n) + 3u) & ~size_t{3};
put_u32(n); const size_t at = reply.size(); reply.resize(at + padded, 0);
if (p && n) std::memcpy(reply.data() + at, p, n);
}
GLuint lookup(const std::unordered_map<uint32_t, GLuint>& map, uint32_t name) {
if (!name) return 0; auto it = map.find(name); return it == map.end() ? 0 : it->second;
}
/* Binding a name the application never generated is GL_INVALID_OPERATION in a
core profile, and mapping the unknown name to zero turned that into a
silent unbind -- KHR-GL32.buffer_objects.gen_buffers checks exactly this.
The guest allocates its own names, so the host cannot be handed the same
one; a name the host's GL has itself never generated provokes the same
error, which is the answer GL owes the application. */
constexpr GLuint GEXGL_UNGENERATED_NAME = 0x7ffffffeu;
GLuint lookup_bindable(const std::unordered_map<uint32_t, GLuint>& map,
uint32_t name) {
if (!name) return 0;
auto it = map.find(name);
return it == map.end() ? GEXGL_UNGENERATED_NAME : it->second;
}
/* Programs and shaders share one name space in GL, and the difference between
the two errors a program-typed query owes is which table the name is in:
GL_INVALID_OPERATION for a shader, GL_INVALID_VALUE for a name GL never
generated. Looking a shader up in the programs table alone produced zero,
which the host then reported as GL_INVALID_VALUE -- the wrong one, and the
only thing KHR-GL32.get_uniform_tests was still failing on. Searching both
hands the host the real object and lets it answer for itself. */
GLuint lookup_program_or_shader(uint32_t name) {
if (!name) return 0;
auto program = programs.find(name);
if (program != programs.end()) return program->second;
auto shader = shaders.find(name);
if (shader != shaders.end()) return shader->second;
return GEXGL_UNGENERATED_NAME;
}
uint32_t external_name(const std::unordered_map<uint32_t, GLuint>& map, GLuint name) {
if (!name) return 0;
for (const auto& [external, internal] : map) {
if (internal == name) return external;
}
return 0;
}
template <typename DeleteObjects>
void delete_guest_objects(std::unordered_map<uint32_t, GLuint>& map, DeleteObjects deleteObjects) {
// A corrupted/old transport could already contain two guest aliases for one
// host object. Deduplicate before deleting so recovery itself is safe.
std::unordered_set<GLuint> unique;
for (const auto& [guest, host] : map) {
(void)guest;
if (host) unique.insert(host);
}
std::vector<GLuint> names(unique.begin(), unique.end());
if (!names.empty()) deleteObjects(static_cast<GLsizei>(names.size()), names.data());
map.clear();
}
void reset_guest_namespace() {
glUseProgram(0);
current_guest_program = 0;
delete_pending_programs.clear();
for (const auto& [guest, host] : programs) {
(void)guest;
if (host) glDeleteProgram(host);
}
programs.clear();
for (const auto& [guest, host] : shaders) {
(void)guest;
if (host) glDeleteShader(host);
}
shaders.clear();
delete_guest_objects(buffers, glDeleteBuffers);
delete_guest_objects(queries, glDeleteQueries);
delete_guest_objects(transform_feedbacks, glDeleteTransformFeedbacks);
delete_guest_objects(program_pipelines, [](GLsizei count, const GLuint* names) {
mesa_proc<decltype(&glDeleteProgramPipelines)>("glDeleteProgramPipelines")(count, names);
});
delete_guest_objects(textures, glDeleteTextures);
delete_guest_objects(samplers, glDeleteSamplers);
delete_guest_objects(vertex_arrays, glDeleteVertexArrays);
delete_guest_objects(framebuffers, glDeleteFramebuffers);
delete_guest_objects(renderbuffers, glDeleteRenderbuffers);
current_draw_buffers.assign(1, GL_BACK);
}
size_t integer_query_components(GLenum pname) {
switch (pname) {
case GL_VIEWPORT:
case GL_SCISSOR_BOX:
case GL_COLOR_WRITEMASK:
case GL_COLOR_CLEAR_VALUE:
case GL_BLEND_COLOR:
return 4;
case GL_DEPTH_RANGE:
case GL_MAX_VIEWPORT_DIMS:
case GL_ALIASED_POINT_SIZE_RANGE:
case GL_ALIASED_LINE_WIDTH_RANGE:
case GL_POINT_SIZE_RANGE:
case GL_SMOOTH_LINE_WIDTH_RANGE:
case GL_VIEWPORT_BOUNDS_RANGE:
case GL_POLYGON_MODE:
return 2;
case GL_MAX_COMPUTE_WORK_GROUP_COUNT:
case GL_MAX_COMPUTE_WORK_GROUP_SIZE:
return 3;
case GL_COMPRESSED_TEXTURE_FORMATS: {
GLint count = 0;
glGetIntegerv(GL_NUM_COMPRESSED_TEXTURE_FORMATS, &count);
return static_cast<size_t>(std::clamp(count, 0, 4096));
}
case GL_PROGRAM_BINARY_FORMATS: {
GLint count = 0;
glGetIntegerv(GL_NUM_PROGRAM_BINARY_FORMATS, &count);
return static_cast<size_t>(std::clamp(count, 0, 4096));
}
case GL_SHADER_BINARY_FORMATS: {
GLint count = 0;
glGetIntegerv(GL_NUM_SHADER_BINARY_FORMATS, &count);
return static_cast<size_t>(std::clamp(count, 0, 4096));
}
default:
/* The fixed-function state, matrices among them, and every other
* multi-valued pname the shared table knows. */
return gexgl_pname_count(pname);
}
}
uint32_t gexgl_state_query_components(GLenum pname) {
return static_cast<uint32_t>(integer_query_components(pname));
}
uint32_t uniform_components(GLuint program, GLint location) {
GLint count = 0, max_name = 0;
glGetProgramiv(program, GL_ACTIVE_UNIFORMS, &count);
glGetProgramiv(program, GL_ACTIVE_UNIFORM_MAX_LENGTH, &max_name);
std::vector<char> name(std::max(max_name, 1));
GLenum type = 0;
for (GLint i = 0; i < count; ++i) {
GLsizei length = 0; GLint size = 0; GLenum candidate = 0;
glGetActiveUniform(program, i, name.size(), &length, &size, &candidate, name.data());
if (glGetUniformLocation(program, name.data()) == location) { type = candidate; break; }
}
switch (type) {
case GL_FLOAT_VEC2: case GL_INT_VEC2: case GL_UNSIGNED_INT_VEC2: case GL_BOOL_VEC2:
case GL_DOUBLE_VEC2: return 2;
case GL_FLOAT_VEC3: case GL_INT_VEC3: case GL_UNSIGNED_INT_VEC3: case GL_BOOL_VEC3:
case GL_DOUBLE_VEC3: return 3;
case GL_FLOAT_VEC4: case GL_INT_VEC4: case GL_UNSIGNED_INT_VEC4: case GL_BOOL_VEC4:
case GL_DOUBLE_VEC4: return 4;
case GL_FLOAT_MAT2: case GL_DOUBLE_MAT2: return 4;
case GL_FLOAT_MAT3: case GL_DOUBLE_MAT3: return 9;
case GL_FLOAT_MAT4: case GL_DOUBLE_MAT4: return 16;
case GL_FLOAT_MAT2x3: case GL_FLOAT_MAT3x2:
case GL_DOUBLE_MAT2x3: case GL_DOUBLE_MAT3x2: return 6;
case GL_FLOAT_MAT2x4: case GL_FLOAT_MAT4x2:
case GL_DOUBLE_MAT2x4: case GL_DOUBLE_MAT4x2: return 8;
case GL_FLOAT_MAT3x4: case GL_FLOAT_MAT4x3:
case GL_DOUBLE_MAT3x4: case GL_DOUBLE_MAT4x3: return 12;
default: return 1;
}
}
/* For the generated compatibility-profile cases (gexgl-compat-host.h). */
#define GEXGEN_NAME(kind, name) lookup(kind, name)
void* gexgen_proc(const char* name, const char* alias) {
void* f = reinterpret_cast<void*>(_mesa_glapi_get_proc_address(name));
if (!f && alias) f = reinterpret_cast<void*>(_mesa_glapi_get_proc_address(alias));
return f;
}
void gexgen_missing(const char* name) {
static std::unordered_set<std::string> said;
if (said.insert(name).second)
fprintf(stderr, "gexgl: Mesa has no %s; the call does nothing\n", name);
}
#include "gl-compat-generated-decoders.inc"
/* A desktop shader whose #version is above what this context compiles.
*
* st/mesa caps the GLSL version at the GL version the driver earns (4.00
* on gex_wgpu today), while applications ask for what their content was
* written for: Unity's HLSLcc emits "#version 410" for a geometry stage
* (a game's grass) and up to "#version 460" where the host offers it. Refused,
* the program fails to link and the draw goes ahead with whatever program
* was bound -- a field of grass becomes its input points. Most of what
* 4.10-4.60 add to the language is also available as extensions this
* context has, so the source is compiled as the highest version it accepts
* with those enabled; anything it really uses that is missing still fails
* to compile, by name. GEXGL_NO_VERSION_FIT=1 keeps the source as sent. */
std::string fit_glsl_version(std::string src) {
static int off = -1;
if (off < 0) off = getenv("GEXGL_NO_VERSION_FIT") ? 1 : 0;
if (off) return src;
size_t at = src.find_first_not_of(" \t\r\n");
if (at == std::string::npos || src.compare(at, 8, "#version") != 0) return src;
const size_t eol = src.find('\n', at);
const std::string line = src.substr(at, eol == std::string::npos ? std::string::npos : eol - at);
unsigned want = 0;
char profile[32] = "";
if (std::sscanf(line.c_str(), "#version %u %31s", &want, profile) < 1) return src;
if (std::strcmp(profile, "es") == 0 || want <= 400 || want > 460) return src;
const char* lang = reinterpret_cast<const char*>(glGetString(GL_SHADING_LANGUAGE_VERSION));
unsigned major = 0, minor = 0;
if (!lang || std::sscanf(lang, "%u.%u", &major, &minor) != 2) return src;
const unsigned have = major * 100 + minor;
if (have >= want || have < 400) return src;
std::string head = "#version " + std::to_string(have);
if (profile[0]) head += std::string(" ") + profile;
head += "\n";
static const char* const extensions[] = {
"GL_ARB_separate_shader_objects", "GL_ARB_shading_language_420pack",
"GL_ARB_explicit_attrib_location", "GL_ARB_explicit_uniform_location",
"GL_ARB_gpu_shader5", "GL_ARB_texture_query_levels",
"GL_ARB_shader_image_load_store", "GL_ARB_arrays_of_arrays",
"GL_ARB_enhanced_layouts", "GL_ARB_shader_storage_buffer_object",
"GL_ARB_shader_bit_encoding", "GL_ARB_shading_language_packing",
"GL_ARB_texture_cube_map_array", "GL_ARB_texture_gather",
"GL_ARB_derivative_control", "GL_ARB_shader_texture_image_samples",
"GL_ARB_cull_distance", "GL_ARB_conservative_depth",
"GL_ARB_fragment_layer_viewport", "GL_ARB_viewport_array",
};
for (const char* e : extensions) head += std::string("#extension ") + e + " : enable\n";
/* The original line stays as a comment, so line numbers shift by the
* extension count and an error still shows what was asked for. */
head += "// " + line + "\n";
static unsigned said;
if (said++ < 4)
fprintf(stderr, "gexgl: \"%s\" compiled as GLSL %u with extensions\n", line.c_str(), have);
return src.substr(0, at) + head + (eol == std::string::npos ? std::string() : src.substr(eol + 1));
}
bool one(uint32_t op, Reader& a) {
switch (op) {
case CONTEXT_CREATE: {
// The client API the application selected with eglBindAPI, and the
// version it asked for. Only the guest knows these, which is why they
// are on the wire; see crates/gex-gl/src/proto.rs.
const uint32_t requested_api = a.u32();
const uint32_t requested_major = a.u32();
const uint32_t requested_minor = a.u32();
/* Optional, and added later than the three above. Reading past the
* end sets `ok`, which would make a guest that predates this field
* fail to get a context at all -- so the flag is put back and the
* absent value read as zero, "let the host choose". Staging a stale
* guest driver beside a current host has happened here before. */
const bool had_required_args = a.ok;
const uint32_t requested_profile = a.u32();
if (had_required_args && !a.ok) a.ok = true;
if (!a.ok) return false;
/* Both Mesa backends -- softpipe and gex_wgpu -- make their context
* here, now that the guest has said what it is for. */
{
const int made = create_backend_context(requested_api, requested_major,
requested_minor, requested_profile);
if (made != 0) {
/* Recorded as itself rather than left to be reported as an
* unhandled opcode, which is what a false return from here
* looks like from outside: "opcode 1, unknown opcode" for a
* context that could not be created is a diagnosis that
* sends the reader to the wrong place entirely. */
last_reject_op = op;
last_reject_why = 4;
last_reject_payload = (uint32_t)(-made);
last_reject_client = current_client;
last_reject_detailed = true;
fprintf(stderr, "gexgl: client %u has no context: %d\n",
current_client, made);
return false;
}
}
// CONTEXT_CREATE is the lifetime boundary for a guest process. The WebGPU
// device is intentionally persistent, but GL object names and state are not.
// Keeping these maps across runs creates duplicate reverse aliases whenever
// the host GL recycles a name, sending queries back with another process's
// guest name and retaining all of the old process's GPU resources.
reset_guest_namespace();
const char* vendor = reinterpret_cast<const char*>(glGetString(GL_VENDOR));
const char* renderer = reinterpret_cast<const char*>(glGetString(GL_RENDERER));
// Desktop applications commonly parse GL_VERSION to select driver
// workarounds, and programs such as STK only recognise the
// conventional Mesa, NVIDIA, Intel and ATI layouts. Mesa's own string
// is one of those, so it is passed through unchanged.
const char* version = reinterpret_cast<const char*>(glGetString(GL_VERSION));
/* Said once. An application chooses its renderer by parsing this,
* and a string that arrives empty is invisible from the guest's
* side -- it looks like a driver with no name and no version. */
{
static bool said_strings;
if (!said_strings) {
said_strings = true;
fprintf(stderr, "gexgl: context strings vendor=%s renderer=%s"
" version=%s\n",
vendor ? vendor : "(null)",
renderer ? renderer : "(null)",
version ? version : "(null)");
}
}
put_blob(vendor, vendor ? std::strlen(vendor) : 0);
put_blob(renderer, renderer ? std::strlen(renderer) : 0);
put_blob(version, version ? std::strlen(version) : 0);
break;
}
case SURFACE_RESIZE: {
const int width = a.i32(), height = a.i32();
resize_drawable(width, height);
break;
}
/* Which window on the page's screen the frames after this one belong
* to: see `op::SURFACE_ATTACH`. Kept, not acted on -- the page reads it
* and the compositor does the rest, because what a surface id means is
* the compositor's business and not this decoder's. */
case SURFACE_ATTACH: {
const unsigned kind = a.u32();
const unsigned id = a.u32();
const unsigned token = a.u32();
const unsigned handle = a.u32();
if (!a.ok) break;
attached_kind = kind;
attached_id = id;
attached_token = token;
attached_generation++;
/* A window names the drawable its frames go to, and attaching makes
* that drawable current: this is `eglCreateWindowSurface` finishing,
* and what the client draws next belongs to this window. */
use_surface(handle);
sur->kind = kind;
sur->id = id;
sur->token = token;
++sur->generation;
break;
}
case SURFACE_CURRENT: {
const unsigned handle = a.u32();
if (!a.ok) break;
use_surface(handle);
break;
}
case CONTEXT_DESTROY: {
/* Everything this client's context held goes with it: its objects
* are named in maps that mean nothing to the next context, and the
* next `CONTEXT_CREATE` is then a creation rather than a no-op --
* which is what lets a toolkit read the GL strings through a
* throwaway context and still get the profile it asks for after. */
if (mesa_context) {
st_destroy_context(mesa_context);
mesa_context = nullptr;
}
reset_guest_namespace();
for (auto& entry : surfaces)
for (struct pipe_resource*& attachment : entry.second.drawable.attachments)
pipe_resource_reference(&attachment, nullptr);
break;
}
case SURFACE_DETACH: {
const unsigned handle = a.u32();
if (!a.ok) break;
drop_surface(handle);
/* The last thing the page was told about is gone; saying so keeps a
* compositor from drawing a window's final frame for ever. */
if (attached_id && handle == 0) attached_id = 0;
break;
}
case FINISH: glFinish(); put_u32(0); break;
/* The reply is the number of frames the *page* has put on screen, which
* is not the number the guest has swapped: the backend renders into a
* texture and copies it to the canvas only when the page's animation loop
* calls display(). The guest paces itself against the difference -- see
* eglSwapBuffers -- because without that an application free-runs, and
* the round trips alone starve the loop it is waiting for. */
/* The reply is this *surface's* count of frames shown, which is what the
* guest paces itself against: a client swapping one window must not be
* released by another window's frame reaching the screen. */
case PRESENT:
present_backend();
if (gl_counts()) {
++gl_client_counts[current_client].second;
gl_count_line("present");
}
put_u32(sur ? sur->displayed : displayed);
break;
case CLEAR_COLOR: {
for (float& value : current_clear_color) value = a.f32();
glClearColor(current_clear_color[0], current_clear_color[1],
current_clear_color[2], current_clear_color[3]); break;
}
case CLEAR: glClear(a.u32()); break;
case VIEWPORT: { GLint x=a.i32(), y=a.i32(); GLsizei w=a.i32(), h=a.i32(); glViewport(x,y,w,h); break; }
case SCISSOR: { GLint x=a.i32(), y=a.i32(); GLsizei w=a.i32(), h=a.i32(); glScissor(x,y,w,h); break; }
case ENABLE: glEnable(a.u32()); break;
case DISABLE: glDisable(a.u32()); break;
case BLEND_FUNC: { GLenum src=a.u32(), dst=a.u32(); glBlendFunc(src,dst); break; }
case DEPTH_FUNC: glDepthFunc(a.u32()); break;
case CULL_FACE: glCullFace(a.u32()); break;
case FRONT_FACE: glFrontFace(a.u32()); break;
case PIXEL_STOREI: {
GLenum pname=a.u32(); GLint param=a.i32();
glPixelStorei(pname,param);
(void)gexgl_set_pixel_store(&pack_store, &unpack_store, pname, param);
break;
}
case CLEAR_DEPTH: current_clear_depth=a.f32(); glClearDepthf(current_clear_depth); break;
case DEPTH_MASK: glDepthMask(a.u32()); break;
case HINT: { GLenum target=a.u32(), mode=a.u32(); glHint(target,mode); break; }
case COLOR_MASK: { GLboolean r=a.u32(), g=a.u32(), b=a.u32(), alpha=a.u32(); glColorMask(r,g,b,alpha); break; }
case BLEND_EQUATION: glBlendEquation(a.u32()); break;
case BLEND_COLOR: { GLfloat r=a.f32(), g=a.f32(), b=a.f32(), alpha=a.f32(); glBlendColor(r,g,b,alpha); break; }
case BLEND_EQUATION_SEPARATE: { GLenum rgb=a.u32(), alpha=a.u32(); glBlendEquationSeparate(rgb,alpha); break; }
case CLEAR_BUFFER_FV: {
GLenum b=a.u32(); GLint d=a.i32(); GLfloat v[4]; for(float& x:v)x=a.f32();
glClearBufferfv(b,d,v);
break;
}
case CLEAR_BUFFER_IV: {
GLenum b=a.u32(); GLint d=a.i32(); GLint v[4]; for(int& x:v)x=a.i32();
glClearBufferiv(b,d,v);
break;
}
case CLEAR_BUFFER_UIV: {
GLenum b=a.u32(); GLint d=a.i32(); GLuint v[4]; for(unsigned& x:v)x=a.u32();
glClearBufferuiv(b,d,v);
break;
}
case CLEAR_BUFFER_FI: {
GLenum b=a.u32(); GLint d=a.i32(); GLfloat depth=a.f32(); GLint stencil=a.i32();
glClearBufferfi(b,d,depth,stencil);
break;
}
case STENCIL_FUNC: { GLenum func=a.u32(); GLint ref=a.i32(); GLuint mask=a.u32(); glStencilFunc(func,ref,mask); break; }
case STENCIL_FUNC_SEPARATE: { GLenum face=a.u32(), func=a.u32(); GLint ref=a.i32(); GLuint mask=a.u32(); glStencilFuncSeparate(face,func,ref,mask); break; }
case STENCIL_MASK: glStencilMask(a.u32()); break;
case STENCIL_MASK_SEPARATE: { GLenum face=a.u32(); GLuint mask=a.u32(); glStencilMaskSeparate(face,mask); break; }
case STENCIL_OP: { GLenum fail=a.u32(), zfail=a.u32(), zpass=a.u32(); glStencilOp(fail,zfail,zpass); break; }
case STENCIL_OP_SEPARATE: { GLenum face=a.u32(), fail=a.u32(), zfail=a.u32(), zpass=a.u32(); glStencilOpSeparate(face,fail,zfail,zpass); break; }
case BLEND_FUNC_SEPARATE: { GLenum srgb=a.u32(), drgb=a.u32(), sa=a.u32(), da=a.u32(); glBlendFuncSeparate(srgb,drgb,sa,da); break; }
case POLYGON_OFFSET: { GLfloat factor=a.f32(), units=a.f32(); glPolygonOffset(factor,units); break; }
case LINE_WIDTH: glLineWidth(a.f32()); break;
case CLEAR_STENCIL: current_clear_stencil=a.i32(); glClearStencil(current_clear_stencil); break;
case DEPTH_RANGE: { GLfloat near_value=a.f32(), far_value=a.f32(); glDepthRangef(near_value,far_value); break; }
case GEN_BUFFERS: {
GLsizei n = a.i32();
if (gexgl_negative_count(n, [](GLsizei c, GLuint *p) { glGenBuffers(c, p); })) break;
std::vector<GLuint> ids(n); glGenBuffers(n, ids.data());
for (uint32_t i = 0; i < n; ++i) buffers[a.u32()] = ids[i]; break;
}
case DELETE_BUFFERS: {
GLsizei n = a.i32();
if (gexgl_negative_count(n, [](GLsizei c, GLuint *p) { glDeleteBuffers(c, p); })) break;
std::vector<GLuint> ids; ids.reserve(n);
for (GLsizei i = 0; i < n; ++i) { uint32_t g = a.u32(); ids.push_back(lookup(buffers, g)); buffers.erase(g); }
glDeleteBuffers(n, ids.data()); break;
}
case GEN_QUERIES: {
GLsizei n = a.i32();
if (n < 0) { glGenQueries(n, nullptr); break; }
std::vector<GLuint> ids(static_cast<size_t>(n)); glGenQueries(n, ids.data());
for (GLsizei i = 0; i < n; ++i) queries[a.u32()] = ids[static_cast<size_t>(i)];
break;
}
case DELETE_QUERIES: {
GLsizei n = a.i32();
if (n < 0) { glDeleteQueries(n, nullptr); break; }
std::vector<GLuint> ids; ids.reserve(static_cast<size_t>(n));
for (GLsizei i = 0; i < n; ++i) { uint32_t g=a.u32(); ids.push_back(lookup(queries,g)); queries.erase(g); }
glDeleteQueries(n, ids.data()); break;
}
case IS_QUERY: put_u32(glIsQuery(lookup(queries, a.u32()))); break;
case GEN_TRANSFORM_FEEDBACKS: {
GLsizei n = a.i32();
if (n < 0) { glGenTransformFeedbacks(n, nullptr); break; }
std::vector<GLuint> ids(static_cast<size_t>(n)); glGenTransformFeedbacks(n, ids.data());
for (GLsizei i = 0; i < n; ++i) transform_feedbacks[a.u32()] = ids[static_cast<size_t>(i)];
break;
}
case DELETE_TRANSFORM_FEEDBACKS: {
GLsizei n = a.i32();
if (n < 0) { glDeleteTransformFeedbacks(n, nullptr); break; }
std::vector<GLuint> ids; ids.reserve(static_cast<size_t>(n));
for (GLsizei i = 0; i < n; ++i) { uint32_t g=a.u32(); ids.push_back(lookup(transform_feedbacks,g)); transform_feedbacks.erase(g); }
glDeleteTransformFeedbacks(n, ids.data()); break;
}
case BIND_TRANSFORM_FEEDBACK: {
GLenum target=a.u32(); GLuint id=lookup(transform_feedbacks,a.u32());
glBindTransformFeedback(target,id); break;
}
case IS_TRANSFORM_FEEDBACK: put_u32(glIsTransformFeedback(lookup(transform_feedbacks,a.u32()))); break;
case PAUSE_TRANSFORM_FEEDBACK: glPauseTransformFeedback(); break;
case RESUME_TRANSFORM_FEEDBACK: glResumeTransformFeedback(); break;
case BIND_BUFFER: {
GLenum t = a.u32();
glBindBuffer(t, lookup_bindable(buffers, a.u32()));
break;
}
case BUFFER_DATA: {
GLenum t = a.u32(), usage = a.u32(); uint64_t size = a.u64(); bool hasData = a.u32() != 0;
uint32_t n = 0; const uint8_t* p = a.blob(n);
if (size > static_cast<uint64_t>(std::numeric_limits<GLsizeiptr>::max()) ||
(hasData && size != n) || (!hasData && n != 0)) { a.ok = false; break; }
/* Re-specification of a vertex buffer, per buffer name.
*
* A 44-byte buffer bound with a 44-byte stride and drawn with ten
* indices is either an application asking for exactly one vertex or a
* later, larger glBufferData that did not take effect. Recording the
* size each name is given, and saying so when it changes, separates
* the two. Capped so a buffer re-specified every frame does not become
* the log. */
if (t == GL_ARRAY_BUFFER) {
GLint bound = 0;
glGetIntegerv(GL_ARRAY_BUFFER_BINDING, &bound);
static std::unordered_map<GLint, uint64_t> vb_size;
static unsigned told;
auto prev = vb_size.find(bound);
const bool respec = prev != vb_size.end() && prev->second != size;
if ((respec || prev == vb_size.end()) && told < 24) {
++told;
emscripten_log(EM_LOG_WARN,
"gex-gl: glBufferData ARRAY_BUFFER buf=%d "
"size=%llu (was %lld) data=%d usage=0x%x",
(int)bound, (unsigned long long)size,
respec ? (long long)prev->second : -1,
(int)hasData, usage);
}
vb_size[bound] = size;
}
glBufferData(t, static_cast<GLsizeiptr>(size), hasData ? p : nullptr, usage); break;
}
case BUFFER_SUB_DATA: {
GLenum t = a.u32(); uint64_t offset = a.u64(), size = a.u64();
uint32_t n = 0; const uint8_t* p = a.blob(n);
if (offset > static_cast<uint64_t>(std::numeric_limits<GLintptr>::max()) ||
size > static_cast<uint64_t>(std::numeric_limits<GLsizeiptr>::max()) || size != n) {
a.ok = false; break;
}
glBufferSubData(t, static_cast<GLintptr>(offset), static_cast<GLsizeiptr>(size), p); break;
}
case BIND_BUFFER_BASE: {
GLenum target = a.u32(); GLuint index = a.u32();
glBindBufferBase(target, index, lookup(buffers, a.u32())); break;
}
case GEN_SAMPLERS: {
GLsizei n = a.i32();
if (gexgl_negative_count(n, [](GLsizei c, GLuint *p) { glGenSamplers(c, p); })) break;
std::vector<GLuint> ids(n); glGenSamplers(n, ids.data());
for (uint32_t i = 0; i < n; ++i) samplers[a.u32()] = ids[i]; break;
}
case DELETE_SAMPLERS: {
uint32_t n = a.u32(); std::vector<GLuint> ids; ids.reserve(n);
for (uint32_t i = 0; i < n; ++i) { uint32_t g=a.u32(); ids.push_back(lookup(samplers,g)); samplers.erase(g); }
glDeleteSamplers(n, ids.data()); break;
}
case BIND_SAMPLER: { GLuint unit=a.u32(); glBindSampler(unit, lookup(samplers,a.u32())); break; }
case SAMPLER_PARAMETER_I: {
GLuint sampler = lookup(samplers, a.u32()); GLenum pname = a.u32();
GLint values[4]{}; const size_t count = pname == GL_TEXTURE_BORDER_COLOR ? 4u : 1u;
for (size_t i = 0; i < count; ++i) values[i] = a.i32();
if (count == 4) glSamplerParameteriv(sampler, pname, values);
else glSamplerParameteri(sampler, pname, values[0]);
break;
}
case SAMPLER_PARAMETER_F: {
GLuint sampler = lookup(samplers, a.u32()); GLenum pname = a.u32();
GLfloat values[4]{}; const size_t count = pname == GL_TEXTURE_BORDER_COLOR ? 4u : 1u;
for (size_t i = 0; i < count; ++i) values[i] = a.f32();
if (count == 4) glSamplerParameterfv(sampler, pname, values);
else glSamplerParameterf(sampler, pname, values[0]);
break;
}
case CREATE_SHADER: { uint32_t g = a.u32(); shaders[g] = glCreateShader(a.u32()); break; }
case SHADER_SOURCE: {
GLuint s = lookup(shaders, a.u32()); std::string src = fit_glsl_version(a.string()); const char* p = src.c_str();
shader_source_heads[s] = src.substr(0, 512);
glShaderSource(s, 1, &p, nullptr); break;
}
case COMPILE_SHADER: glCompileShader(lookup(shaders, a.u32())); break;
case DELETE_SHADER: {
/* GL keeps a deleted shader that is still attached to a program,
* and its name stays valid until it is detached: a program may
* attach, delete, and only then compile and link. So the guest name
* keeps its mapping; CREATE_SHADER given the same name replaces it. */
glDeleteShader(lookup(shaders, a.u32()));
break;
}
case CREATE_PROGRAM: { uint32_t g = a.u32(); programs[g] = glCreateProgram(); break; }
case ATTACH_SHADER: { GLuint p = lookup(programs, a.u32()); glAttachShader(p, lookup(shaders, a.u32())); break; }
case DETACH_SHADER: { GLuint p = lookup(programs, a.u32()); glDetachShader(p, lookup(shaders, a.u32())); break; }
case BIND_ATTRIB_LOCATION: { GLuint p = lookup(programs, a.u32()); GLuint i = a.u32(); std::string n = a.string(); glBindAttribLocation(p, i, n.c_str()); break; }
case LINK_PROGRAM: glLinkProgram(lookup(programs, a.u32())); break;
case USE_PROGRAM: {
const uint32_t next = a.u32();
glUseProgram(lookup(programs, next));
const uint32_t previous = current_guest_program;
current_guest_program = next;
if (previous != next && delete_pending_programs.erase(previous) != 0) {
programs.erase(previous);
}
break;
}
case DELETE_PROGRAM: {
const uint32_t g = a.u32();
const auto it = programs.find(g);
glDeleteProgram(it == programs.end() ? 0 : it->second);
if (it != programs.end()) {
if (g == current_guest_program) delete_pending_programs.insert(g);
else programs.erase(it);
}
break;
}
case GET_UNIFORM_LOCATION: { GLuint p = lookup(programs, a.u32()); std::string n = a.string(); put_u32(glGetUniformLocation(p, n.c_str())); break; }
case GET_ATTRIB_LOCATION: { GLuint p = lookup(programs, a.u32()); std::string n = a.string(); put_u32(glGetAttribLocation(p, n.c_str())); break; }
case GET_UNIFORM_BLOCK_INDEX: { GLuint p = lookup(programs, a.u32()); std::string n = a.string(); put_u32(glGetUniformBlockIndex(p, n.c_str())); break; }
case UNIFORM_BLOCK_BINDING: { GLuint p = lookup(programs, a.u32()); GLuint i = a.u32(); glUniformBlockBinding(p, i, a.u32()); break; }
case GET_ATTACHED_SHADERS: {
uint32_t gp = a.u32(), max_count = a.u32(); GLuint p = lookup(programs, gp);
std::vector<GLuint> ids(max_count); GLsizei count = 0;
glGetAttachedShaders(p, max_count, &count, ids.data());
put_u32(count);
for (GLsizei i = 0; i < count; ++i) {
uint32_t guest = 0;
for (const auto &entry : shaders) if (entry.second == ids[i]) { guest = entry.first; break; }
put_u32(guest);
}
break;
}
case GET_SHADER_IV: {
GLuint s = lookup(shaders, a.u32()); GLenum q = a.u32(); GLint v = 0; glGetShaderiv(s, q, &v);
if (q == GL_COMPILE_STATUS && !v) { GLint n=0; glGetShaderiv(s,GL_INFO_LOG_LENGTH,&n); std::vector<char> log(std::max(n,1)); glGetShaderInfoLog(s,log.size(),nullptr,log.data()); emscripten_log(EM_LOG_ERROR,"gex-gl shader compile: %s",log.data());
auto head = shader_source_heads.find(s);
if (head != shader_source_heads.end()) {
/* Line by line, and numbered: the compiler reports "0:LINE(COL)"
* and a single blob makes the reader count. */
unsigned line = 1;
size_t at = 0;
while (at < head->second.size() && line <= 10) {
size_t nl = head->second.find('\n', at);
if (nl == std::string::npos) nl = head->second.size();
emscripten_log(EM_LOG_ERROR, "gex-gl shader src %u: %s",
line, head->second.substr(at, nl - at).c_str());
at = nl + 1;
++line;
}
} }
put_u32(v); break;
}
case GET_PROGRAM_IV: {
GLuint p = lookup(programs, a.u32()); GLenum q = a.u32(); GLint v = 0; glGetProgramiv(p, q, &v);
if (q == GL_LINK_STATUS && !v) { GLint n=0; glGetProgramiv(p,GL_INFO_LOG_LENGTH,&n); std::vector<char> log(std::max(n,1)); glGetProgramInfoLog(p,log.size(),nullptr,log.data()); emscripten_log(EM_LOG_ERROR,"gex-gl program link: %s",log.data()); }
put_u32(v); break;
}
case GET_SHADER_INFO_LOG: {
GLuint s = lookup(shaders, a.u32()); GLint n = 0; glGetShaderiv(s, GL_INFO_LOG_LENGTH, &n);
std::vector<char> out(std::max(n, 1)); GLsizei got = 0; glGetShaderInfoLog(s, out.size(), &got, out.data()); put_blob(out.data(), got); break;
}
case GET_PROGRAM_INFO_LOG: {
GLuint p = lookup(programs, a.u32()); GLint n = 0; glGetProgramiv(p, GL_INFO_LOG_LENGTH, &n);
std::vector<char> out(std::max(n, 1)); GLsizei got = 0; glGetProgramInfoLog(p, out.size(), &got, out.data()); put_blob(out.data(), got); break;
}
case UNIFORM_1I: { GLint l=a.i32(), x=a.i32(); glUniform1i(l,x); break; }
case UNIFORM_2I: { GLint l=a.i32(), x=a.i32(), y=a.i32(); glUniform2i(l,x,y); break; }
case UNIFORM_3I: { GLint l=a.i32(), x=a.i32(), y=a.i32(), z=a.i32(); glUniform3i(l,x,y,z); break; }
case UNIFORM_4I: { GLint l=a.i32(), x=a.i32(), y=a.i32(), z=a.i32(), w=a.i32(); glUniform4i(l,x,y,z,w); break; }
case UNIFORM_1F: { GLint l=a.i32(); GLfloat x=a.f32(); glUniform1f(l,x); break; }
case UNIFORM_2F: { GLint l=a.i32(); GLfloat x=a.f32(), y=a.f32(); glUniform2f(l,x,y); break; }
case UNIFORM_3F: { GLint l=a.i32(); GLfloat x=a.f32(), y=a.f32(), z=a.f32(); glUniform3f(l,x,y,z); break; }
case UNIFORM_4F: { GLint l=a.i32(); GLfloat x=a.f32(), y=a.f32(), z=a.f32(), w=a.f32(); glUniform4f(l,x,y,z,w); break; }
case UNIFORM_MATRIX4FV: {
GLint loc = a.i32(); GLsizei count = a.u32(); GLboolean transpose = a.u32(); uint32_t n; const uint8_t* p = a.blob(n);
glUniformMatrix4fv(loc, count, transpose, reinterpret_cast<const GLfloat*>(p)); break;
}
case GET_ACTIVE_UNIFORM:
case GET_ACTIVE_ATTRIB:
case GET_TRANSFORM_FEEDBACK_VARYING: {
GLuint program = lookup(programs, a.u32()); GLuint index = a.u32();
GLenum length_query = op == GET_ACTIVE_UNIFORM
? GL_ACTIVE_UNIFORM_MAX_LENGTH
: op == GET_ACTIVE_ATTRIB
? GL_ACTIVE_ATTRIBUTE_MAX_LENGTH
: GL_TRANSFORM_FEEDBACK_VARYING_MAX_LENGTH;
GLint max_len = 0;
glGetProgramiv(program, length_query, &max_len);
std::vector<char> name(std::max(max_len, 1)); GLsizei length = 0; GLint size = 0; GLenum type = 0;
if (op == GET_ACTIVE_UNIFORM) glGetActiveUniform(program, index, name.size(), &length, &size, &type, name.data());
else if (op == GET_ACTIVE_ATTRIB) glGetActiveAttrib(program, index, name.size(), &length, &size, &type, name.data());
else glGetTransformFeedbackVarying(program, index, name.size(), &length, &size, &type, name.data());
put_u32(size); put_u32(type); put_blob(name.data(), length); break;
}
case GET_ACTIVE_UNIFORM_NAME:
case GET_ACTIVE_UNIFORM_BLOCK_NAME: {
GLuint program = lookup(programs, a.u32()); GLuint index = a.u32();
GLint max_len = 0;
glGetProgramiv(program,
op == GET_ACTIVE_UNIFORM_NAME
? GL_ACTIVE_UNIFORM_MAX_LENGTH
: GL_ACTIVE_UNIFORM_BLOCK_MAX_NAME_LENGTH,
&max_len);
std::vector<char> name(std::max(max_len, 1));
GLsizei length = 0;
if (op == GET_ACTIVE_UNIFORM_NAME)
glGetActiveUniformName(program, index, name.size(), &length, name.data());
else
glGetActiveUniformBlockName(program, index, name.size(), &length, name.data());
put_blob(name.data(), length); break;
}
case GET_ACTIVE_UNIFORM_BLOCKIV: {
GLuint program = lookup(programs, a.u32());
GLuint index = a.u32();
GLenum pname = a.u32();
/* Only one query returns more than a single integer, and its length is
* itself a query. */
GLint count = 1;
if (pname == GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES)
glGetActiveUniformBlockiv(program, index,
GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS, &count);
std::vector<GLint> params(std::max(count, 1), 0);
glGetActiveUniformBlockiv(program, index, pname, params.data());
put_blob(params.data(), std::max(count, 0) * sizeof(GLint)); break;
}
case GET_ACTIVE_UNIFORMSIV: {
GLuint program = lookup(programs, a.u32());
GLenum pname = a.u32();
uint32_t bytes = 0;
const uint8_t* raw = a.blob(bytes);
const GLsizei count = (GLsizei)(bytes / sizeof(GLuint));
std::vector<GLuint> indices(count);
if (count) memcpy(indices.data(), raw, (size_t)count * sizeof(GLuint));
std::vector<GLint> params(std::max(count, 1), 0);
glGetActiveUniformsiv(program, count, indices.data(), pname, params.data());
put_blob(params.data(), (size_t)count * sizeof(GLint)); break;
}
case GET_UNIFORM_INDICES: {
GLuint program = lookup(programs, a.u32());
const GLsizei count = (GLsizei)a.u32();
uint32_t bytes = 0;
const uint8_t* raw = a.blob(bytes);
/* One blob of NUL-terminated names; walk it rather than trusting a
* separate length array the guest would have had to build. */
std::vector<const GLchar*> names;
const char* at = reinterpret_cast<const char*>(raw);
const char* end = at + bytes;
for (GLsizei i = 0; i < count && at < end; ++i) {
names.push_back(at);
at += strnlen(at, (size_t)(end - at)) + 1;
}
std::vector<GLuint> indices(std::max<size_t>(names.size(), 1),
GL_INVALID_INDEX);
if (!names.empty())
glGetUniformIndices(program, (GLsizei)names.size(), names.data(),
indices.data());
put_blob(indices.data(), names.size() * sizeof(GLuint)); break;
}
case GET_SHADER_SOURCE: {
GLuint shader = lookup(shaders, a.u32());
GLint max_len = 0;
glGetShaderiv(shader, GL_SHADER_SOURCE_LENGTH, &max_len);
std::vector<char> source(std::max(max_len, 1));
GLsizei length = 0;
glGetShaderSource(shader, source.size(), &length, source.data());
put_blob(source.data(), length); break;
}
#define GEX_UNIFORM_VECTOR(OP, FN, TYPE) case OP: { GLint l=a.i32(); GLsizei n=a.i32(); uint32_t bytes; const uint8_t*p=a.blob(bytes); FN(l,n,reinterpret_cast<const TYPE*>(p)); break; }
GEX_UNIFORM_VECTOR(UNIFORM_1IV, glUniform1iv, GLint)
GEX_UNIFORM_VECTOR(UNIFORM_1FV, glUniform1fv, GLfloat)
GEX_UNIFORM_VECTOR(UNIFORM_2FV, glUniform2fv, GLfloat)
GEX_UNIFORM_VECTOR(UNIFORM_3FV, glUniform3fv, GLfloat)
GEX_UNIFORM_VECTOR(UNIFORM_4FV, glUniform4fv, GLfloat)
GEX_UNIFORM_VECTOR(UNIFORM_2IV, glUniform2iv, GLint)
GEX_UNIFORM_VECTOR(UNIFORM_3IV, glUniform3iv, GLint)
GEX_UNIFORM_VECTOR(UNIFORM_4IV, glUniform4iv, GLint)
#undef GEX_UNIFORM_VECTOR
case UNIFORM_MATRIX2FV: {GLint l=a.i32();GLsizei n=a.i32();GLboolean t=a.u32();uint32_t bytes;const uint8_t*p=a.blob(bytes);glUniformMatrix2fv(l,n,t,reinterpret_cast<const GLfloat*>(p));break;}
case UNIFORM_MATRIX3FV: {GLint l=a.i32();GLsizei n=a.i32();GLboolean t=a.u32();uint32_t bytes;const uint8_t*p=a.blob(bytes);glUniformMatrix3fv(l,n,t,reinterpret_cast<const GLfloat*>(p));break;}
case ENABLE_VERTEX_ATTRIB_ARRAY: glEnableVertexAttribArray(a.u32()); break;
case DISABLE_VERTEX_ATTRIB_ARRAY: glDisableVertexAttribArray(a.u32()); break;
case VERTEX_ATTRIB_POINTER: {
GLuint i = a.u32(); GLint size = a.i32(); GLenum type = a.u32(); GLboolean norm = a.u32(); GLsizei stride = a.i32(); uintptr_t off = a.u32();
glVertexAttribPointer(i, size, type, norm, stride, reinterpret_cast<void*>(off)); break;
}
case VERTEX_ATTRIB_I_POINTER: {
GLuint i = a.u32(); GLint size = a.i32(); GLenum type = a.u32();
GLsizei stride = a.i32(); uintptr_t off = a.u32();
glVertexAttribIPointer(i, size, type, stride, reinterpret_cast<void*>(off)); break;
}
case VERTEX_ATTRIB_DIVISOR: { GLuint index=a.u32(), divisor=a.u32();
/* Whether the guest asks for instancing at all. The scene shader reads
* per-instance attributes and Gallium reports every element with
* divisor 0, so either the application never sets one or it is lost on
* the way here. One line per (index, divisor). */
{ static std::unordered_map<uint64_t, unsigned> seen;
if (seen[((uint64_t)index << 32) | divisor]++ == 0)
emscripten_log(EM_LOG_WARN, "gex-gl: glVertexAttribDivisor(%u, %u)",
index, divisor); }
glVertexAttribDivisor(index,divisor); break; }
case VERTEX_ATTRIB_4F: { GLuint index=a.u32(); GLfloat x=a.f32(), y=a.f32(), z=a.f32(), w=a.f32(); glVertexAttrib4f(index,x,y,z,w); break; }
case VERTEX_ATTRIB_I4I: { GLuint index=a.u32(); GLint x=a.i32(), y=a.i32(), z=a.i32(), w=a.i32(); glVertexAttribI4i(index,x,y,z,w); break; }
case GEN_VERTEX_ARRAYS: {
GLsizei n = a.i32();
if (gexgl_negative_count(n, [](GLsizei c, GLuint *p) { glGenVertexArrays(c, p); })) break;
std::vector<GLuint> ids(n); glGenVertexArrays(n, ids.data());
for(uint32_t i=0;i<n;++i) vertex_arrays[a.u32()]=ids[i]; break;
}
case DELETE_VERTEX_ARRAYS: {
uint32_t n=a.u32(); std::vector<GLuint> ids; ids.reserve(n);
for(uint32_t i=0;i<n;++i){uint32_t g=a.u32();ids.push_back(lookup(vertex_arrays,g));vertex_arrays.erase(g);} glDeleteVertexArrays(n,ids.data());break;
}
case BIND_VERTEX_ARRAY: { uint32_t g=a.u32(); glBindVertexArray(g ? lookup(vertex_arrays,g) : default_vao); break; }
case GEN_TEXTURES: {
GLsizei n = a.i32();
if (gexgl_negative_count(n, [](GLsizei c, GLuint *p) { glGenTextures(c, p); })) break;
std::vector<GLuint> ids(n); glGenTextures(n, ids.data());
for (uint32_t i = 0; i < n; ++i) {
const uint32_t g = a.u32();
textures[g] = ids[i];
}
break;
}
case DELETE_TEXTURES: {
uint32_t n = a.u32(); std::vector<GLuint> ids; ids.reserve(n);
for (uint32_t i = 0; i < n; ++i) { uint32_t g = a.u32();
ids.push_back(lookup(textures, g)); textures.erase(g); }
glDeleteTextures(n, ids.data()); break;
}
case BIND_TEXTURE: {
GLenum t = a.u32();
const uint32_t g = a.u32();
/* GL creates a texture object when an unused non-zero name is bound;
only glGen* reserving names is optional. Looking the name up and
binding zero on a miss silently redirected every later call to the
default texture of that target -- uploads appeared to succeed and
the object never existed, which surfaces much later and elsewhere
as an incomplete framebuffer. */
if (g && !textures.count(g)) {
GLuint created = 0;
glGenTextures(1, &created);
textures[g] = created;
}
glBindTexture(t, lookup(textures, g)); break; }
case ACTIVE_TEXTURE: glActiveTexture(a.u32()); break;
case TEX_PARAMETER_I: { GLenum target=a.u32(), pname=a.u32(); GLint param=a.i32(); glTexParameteri(target,pname,param); break; }
case TEX_PARAMETER_F: { GLenum target=a.u32(), pname=a.u32(); GLfloat value=a.f32(); glTexParameterf(target,pname,value); break; }
case TEX_PARAMETER_FV: {
GLenum target=a.u32(), pname=a.u32(); GLfloat value[4]{};
const size_t count = (pname == GL_TEXTURE_BORDER_COLOR || pname == GL_TEXTURE_SWIZZLE_RGBA) ? 4 : 1;
for (size_t i = 0; i < count; ++i) value[i]=a.f32();
glTexParameterfv(target,pname,value);
break;
}
case TEX_PARAMETER_IV: {
GLenum target=a.u32(), pname=a.u32(); GLint value[4]{};
const size_t count = (pname == GL_TEXTURE_BORDER_COLOR || pname == GL_TEXTURE_SWIZZLE_RGBA) ? 4 : 1;
for (size_t i = 0; i < count; ++i) value[i]=a.i32();
glTexParameteriv(target,pname,value);
break;
}
case TEX_IMAGE_2D: {
GLenum target = a.u32(); GLint level = a.i32(), internal = a.i32();
GLsizei w = a.i32(), h = a.i32(); GLint border = a.i32();
GLenum format = a.u32(), type = a.u32(); bool pbo = a.u32() != 0;
uintptr_t pointer = static_cast<uintptr_t>(a.u64());
uint32_t n; const uint8_t* p = a.blob(n);
gexgl_pixel_layout layout{};
const bool valid = gexgl_compute_pixel_layout(format, type, w, h, -1,
&unpack_store, &layout);
if ((pbo && n != 0) || (!pbo && pointer && (!valid || layout.end != n))) {
a.ok = false; break;
}
const void* data = pbo ? reinterpret_cast<const void*>(pointer)
: (pointer ? p : nullptr);
glTexImage2D(target, level, internal, w, h, border, format, type, data);
break;
}
case TEX_SUB_IMAGE_2D: {
GLenum target = a.u32(); GLint level = a.i32(), x = a.i32(), y = a.i32();
GLsizei w = a.i32(), h = a.i32(); GLenum format = a.u32(), type = a.u32();
bool pbo = a.u32() != 0; uintptr_t pointer = static_cast<uintptr_t>(a.u64());
uint32_t n; const uint8_t* p = a.blob(n);
gexgl_pixel_layout layout{};
const bool valid = gexgl_compute_pixel_layout(format, type, w, h, -1,
&unpack_store, &layout);
if ((pbo && n != 0) || (!pbo && pointer && (!valid || layout.end != n))) {
a.ok = false; break;
}
const void* data = pbo ? reinterpret_cast<const void*>(pointer)
: (pointer ? p : nullptr);
glTexSubImage2D(target, level, x, y, w, h, format, type, data);
break;
}
case GEN_FRAMEBUFFERS: {
GLsizei n = a.i32();
if (gexgl_negative_count(n, [](GLsizei c, GLuint *p) { glGenFramebuffers(c, p); })) break;
std::vector<GLuint> ids(n);glGenFramebuffers(n,ids.data());for(GLsizei i=0;i<n;++i)framebuffers[a.u32()]=ids[i];break;
}
case DELETE_FRAMEBUFFERS: {
uint32_t n=a.u32();std::vector<GLuint> ids;ids.reserve(n);for(uint32_t i=0;i<n;++i){uint32_t g=a.u32();ids.push_back(lookup(framebuffers,g));framebuffers.erase(g);}glDeleteFramebuffers(n,ids.data());break;
}
case BIND_FRAMEBUFFER: {GLenum t=a.u32();glBindFramebuffer(t,lookup(framebuffers,a.u32()));break;}
case CHECK_FRAMEBUFFER_STATUS: put_u32(glCheckFramebufferStatus(a.u32())); break;
case FRAMEBUFFER_TEXTURE_2D: {
GLenum t=a.u32(),att=a.u32(),tt=a.u32();
const uint32_t guest_tex=a.u32();
GLuint tex=lookup(textures,guest_tex);
GLint level=a.i32();
/* A name the map does not know looks up as zero, and zero means
*detach* -- so a miss here silently produces a framebuffer with no
attachment, which st/mesa reports as "no teximage" and the
application sees only as GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT. */
if (guest_tex && !tex) {
static int said;
if (said < 4) { said++;
uint32_t lo = 0xffffffffu, hi = 0;
for (const auto& kv : textures) {
if (kv.first < lo) lo = kv.first;
if (kv.first > hi) hi = kv.first;
}
std::fprintf(stderr,
"gexgl: framebuffer texture %u unknown, "
"attaching zero (detach); map has %zu entries "
"spanning %u..%u\n",
guest_tex, textures.size(),
textures.empty() ? 0 : lo,
textures.empty() ? 0 : hi);
}
}
glFramebufferTexture2D(t,att,tt,tex,level);break;}
case GEN_RENDERBUFFERS: {
GLsizei n = a.i32();
if (gexgl_negative_count(n, [](GLsizei c, GLuint *p) { glGenRenderbuffers(c, p); })) break;
std::vector<GLuint> ids(n);glGenRenderbuffers(n,ids.data());for(GLsizei i=0;i<n;++i)renderbuffers[a.u32()]=ids[i];break;
}
case DELETE_RENDERBUFFERS: {
uint32_t n=a.u32();std::vector<GLuint> ids;ids.reserve(n);for(uint32_t i=0;i<n;++i){uint32_t g=a.u32();ids.push_back(lookup(renderbuffers,g));renderbuffers.erase(g);}glDeleteRenderbuffers(n,ids.data());break;
}
case BIND_RENDERBUFFER: {GLenum t=a.u32();glBindRenderbuffer(t,lookup(renderbuffers,a.u32()));break;}
case RENDERBUFFER_STORAGE: { GLenum target=a.u32(), format=a.u32(); GLsizei w=a.i32(), h=a.i32(); glRenderbufferStorage(target,format,w,h); break; }
case DRAW_ARRAYS: { GLenum mode=a.u32(); GLint first=a.i32(); GLsizei count=a.i32(); glDrawArrays(mode,first,count); break; }
case DRAW_ELEMENTS: {
GLenum mode = a.u32(); GLsizei n = a.i32(); GLenum type = a.u32();
uintptr_t off = a.u32();
const bool client = a.u32() != 0;
if (!client) {
glDrawElements(mode, n, type, reinterpret_cast<void*>(off));
break;
}
/* The indices came with the command because the guest had no element
array buffer bound. Passing them straight through keeps the client
array exactly as GL describes it; sending the guest's pointer as an
offset had the host read its own address space instead. */
uint32_t bytes = 0; const uint8_t *wire = a.blob(bytes);
if (!wire) { a.ok = false; break; }
glDrawElements(mode, n, type, wire);
break;
}
case DRAW_BUFFERS: {
GLsizei n = a.i32(); current_draw_buffers.resize(std::max(n, 0));
for (GLsizei i = 0; i < n; ++i) current_draw_buffers[i] = a.u32();
glDrawBuffers(n, current_draw_buffers.data()); break;
}
case DRAW_ARRAYS_INSTANCED: { GLenum mode=a.u32(); GLint first=a.i32(); GLsizei count=a.i32(), instances=a.i32(); glDrawArraysInstanced(mode,first,count,instances); break; }
case DRAW_ELEMENTS_INSTANCED: {
GLenum mode=a.u32(); GLsizei n=a.i32(); GLenum type=a.u32(); uintptr_t off=a.u32(); GLsizei instances=a.i32();
glDrawElementsInstanced(mode,n,type,reinterpret_cast<void*>(off),instances); break;
}
case BLIT_FRAMEBUFFER: {
GLint sx0=a.i32(),sy0=a.i32(),sx1=a.i32(),sy1=a.i32();
GLint dx0=a.i32(),dy0=a.i32(),dx1=a.i32(),dy1=a.i32();
GLbitfield mask=a.u32(); GLenum filter=a.u32();
glBlitFramebuffer(sx0,sy0,sx1,sy1,dx0,dy0,dx1,dy1,mask,filter); break;
}
case FRAMEBUFFER_RENDERBUFFER: {GLenum t=a.u32(),att=a.u32(),rt=a.u32();glFramebufferRenderbuffer(t,att,rt,lookup(renderbuffers,a.u32()));break;}
case GENERATE_MIPMAP: glGenerateMipmap(a.u32()); break;
case TEX_STORAGE_2D: { GLenum target=a.u32(); GLsizei levels=a.i32(); GLenum format=a.u32(); GLsizei w=a.i32(), h=a.i32(); glTexStorage2D(target,levels,format,w,h); break; }
case TEX_STORAGE_3D: { GLenum target=a.u32(); GLsizei levels=a.i32(); GLenum format=a.u32(); GLsizei w=a.i32(), h=a.i32(), d=a.i32(); glTexStorage3D(target,levels,format,w,h,d); break; }
case TEX_IMAGE_3D: {
GLenum t=a.u32();GLint l=a.i32(),in=a.i32();GLsizei w=a.i32(),h=a.i32(),d=a.i32();
GLint border=a.i32();GLenum f=a.u32(),ty=a.u32();bool pbo=a.u32()!=0;
uintptr_t pointer=static_cast<uintptr_t>(a.u64());uint32_t n;const uint8_t* bytes=a.blob(n);
gexgl_pixel_layout layout{};
const bool valid=gexgl_compute_pixel_layout(f,ty,w,h,d,&unpack_store,&layout);
if ((pbo && n != 0) || (!pbo && pointer && (!valid || layout.end != n))) {
a.ok=false;break;
}
const void* data=pbo?reinterpret_cast<const void*>(pointer):(pointer?bytes:nullptr);
glTexImage3D(t,l,in,w,h,d,border,f,ty,data);break;
}
case FRAMEBUFFER_TEXTURE_LAYER: {GLenum t=a.u32(),att=a.u32();GLuint tex=lookup(textures,a.u32());GLint level=a.i32(),layer=a.i32();glFramebufferTextureLayer(t,att,tex,level,layer);break;}
case READ_PIXELS: {
GLint x = a.i32(), y = a.i32(); GLsizei w = a.i32(), h = a.i32();
GLenum format = a.u32(), type = a.u32(); bool pbo = a.u32() != 0;
uintptr_t pointer = static_cast<uintptr_t>(a.u64());
if (pbo) {
glReadPixels(x,y,w,h,format,type,reinterpret_cast<void*>(pointer));
break;
}
gexgl_pixel_layout layout{};
if (!gexgl_compute_pixel_layout(format,type,w,h,-1,&pack_store,&layout) ||
layout.end > UINT32_MAX) {
glReadPixels(x,y,w,h,format,type,nullptr);
put_u32(0); put_blob(nullptr,0); break;
}
std::vector<uint8_t> px(layout.end, 0);
glReadPixels(x,y,w,h,format,type,px.data());
put_u32(static_cast<uint32_t>(layout.offset));
put_blob(px.data()+layout.offset,
static_cast<uint32_t>(layout.end-layout.offset));
break;
}
case GET_STRING: {
GLenum name = a.u32();
/* The real string, not a claim. This used to answer GL_VERSION with a
* fixed "3.3 (Core Profile)" regardless of the context that actually
* came up, which -- combined with the guest substituting its own
* version on top -- meant nothing in the chain reflected what the
* backend could honour. A guest that is told 4.4 and handed an ES 2.0
* context discovers the difference one rejected call at a time. */
const char* s = reinterpret_cast<const char*>(glGetString(name));
put_blob(s, s ? std::strlen(s) : 0);
break;
}
case GET_INTEGERV: {
const GLenum pname = a.u32();
const size_t n = integer_query_components(pname);
/* Room for what GL writes even where the table says less. */
std::vector<GLint> values(std::max<size_t>(n, 16));
if (n) glGetIntegerv(pname, values.data());
put_blob(values.data(), n * sizeof(values[0]));
break;
}
case GET_ERROR: put_u32(glGetError()); break;
case GET_STRING_I: {
GLenum name = a.u32(); GLuint index = a.u32();
const char* s = reinterpret_cast<const char*>(glGetStringi(name, index));
put_blob(s, s ? std::strlen(s) : 0);
break;
}
case GET_UNIFORM_FV:
case GET_UNIFORM_IV: {
GLuint program=lookup_program_or_shader(a.u32()); GLint location=a.i32();
uint32_t components=uniform_components(program,location);
if (op == GET_UNIFORM_FV) {
GLfloat values[16] = {}; glGetUniformfv(program,location,values);
put_blob(values,components*sizeof(values[0]));
} else {
GLint values[16] = {}; glGetUniformiv(program,location,values);
put_blob(values,components*sizeof(values[0]));
}
break;
}
/* Fixed function.
*
* Only reachable in a compatibility profile, which is what a GLX context
* gets -- these entry points do not exist in a core one, and Mesa will
* raise GL_INVALID_OPERATION rather than crash if one arrives anyway.
* There is nothing to translate: the host is real Mesa and has them. */
case MATRIX_MODE: glMatrixMode(a.u32()); break;
case LOAD_IDENTITY: glLoadIdentity(); break;
case PUSH_MATRIX: glPushMatrix(); break;
case POP_MATRIX: glPopMatrix(); break;
case BEGIN: glBegin(a.u32()); break;
case END: glEnd(); break;
case SHADE_MODEL: glShadeModel(a.u32()); break;
case END_LIST: glEndList(); break;
case CALL_LIST: glCallList(a.u32()); break;
case NEW_LIST: { GLuint list = a.u32(); glNewList(list, a.u32()); break; }
case DELETE_LISTS: { GLuint list = a.u32(); glDeleteLists(list, a.i32()); break; }
case TRANSLATEF: {
const GLfloat x = a.f32(), y = a.f32(), z = a.f32();
glTranslatef(x, y, z); break;
}
case ROTATEF: {
const GLfloat angle = a.f32(), x = a.f32(), y = a.f32(), z = a.f32();
glRotatef(angle, x, y, z); break;
}
case VERTEX3F: {
const GLfloat x = a.f32(), y = a.f32(), z = a.f32();
glVertex3f(x, y, z); break;
}
case NORMAL3F: {
const GLfloat x = a.f32(), y = a.f32(), z = a.f32();
glNormal3f(x, y, z); break;
}
case FRUSTUM: {
/* Doubles, two words each: the projection this builds is the one
* place in the set where float range is not enough. */
const GLdouble l = a.f64(), r = a.f64(), b = a.f64();
const GLdouble t = a.f64(), n = a.f64(), f = a.f64();
glFrustum(l, r, b, t, n, f); break;
}
case LIGHTFV:
case MATERIALFV: {
const GLenum target = a.u32();
const GLenum pname = a.u32();
const uint32_t count = a.u32();
/* The guest sends the count the pname calls for; four is the most any
* of them takes, and a larger one is a malformed command. */
if (count > 4 || !a.ok) return false;
GLfloat values[4] = {};
for (uint32_t i = 0; i < count; ++i) values[i] = a.f32();
if (!a.ok) return false;
if (op == LIGHTFV) glLightfv(target, pname, values);
else glMaterialfv(target, pname, values);
break;
}
case GEN_LISTS: put_u32(glGenLists(a.i32())); break;
/* Protocol 11. The array pointers are offsets into the bound
* GL_ARRAY_BUFFER, as VERTEX_ATTRIB_POINTER's are. */
case ENABLE_CLIENT_STATE: glEnableClientState(a.u32()); break;
case DISABLE_CLIENT_STATE: glDisableClientState(a.u32()); break;
case VERTEX_POINTER:
case COLOR_POINTER:
case TEX_COORD_POINTER: {
const GLint size = a.i32(); const GLenum type = a.u32(); const GLsizei stride = a.i32();
const uintptr_t off = static_cast<uintptr_t>(a.u64());
if (!a.ok) return false;
void *p = reinterpret_cast<void*>(off);
if (op == VERTEX_POINTER) glVertexPointer(size, type, stride, p);
else if (op == COLOR_POINTER) glColorPointer(size, type, stride, p);
else glTexCoordPointer(size, type, stride, p);
break;
}
case CLIENT_ACTIVE_TEXTURE: glClientActiveTexture(a.u32()); break;
/* { slot, op, n, n words, bytes }: a client-memory array, as a
* native GL takes them -- a pointer into memory, which st/mesa uploads with its
* stream uploader. The guest used to copy each one into a buffer of its
* own with glBufferData before every draw, and a buffer the pending batch
* already reads cannot be rewritten in place: every such upload became a
* staged copy that split the render pass (one game: 142 a frame, 52 passes
* for 47 draws). `op` is the pointer command (glVertexPointer's,
* glVertexAttribPointer's, ...) and the words are its arguments before
* the pointer, which becomes the bytes' address here; it runs with no
* GL_ARRAY_BUFFER bound, and the guest rebinds its own afterwards. */
case CLIENT_ARRAY: {
const uint32_t slot = a.u32(), inner = a.u32(), n = a.u32();
if (!a.ok || n > 8 || inner == CLIENT_ARRAY) return false;
uint32_t words[8];
for (uint32_t i = 0; i < n; ++i) words[i] = a.u32();
uint32_t len = 0;
const uint8_t* bytes = a.blob(len);
if (!a.ok) return false;
std::vector<uint8_t>& mem = client_arrays[slot];
mem.assign(bytes, bytes + len);
if (mem.empty()) mem.push_back(0);
uint8_t body[8 * 4 + 8];
std::memcpy(body, words, n * 4);
const uint64_t at = reinterpret_cast<uintptr_t>(mem.data());
std::memcpy(body + n * 4, &at, 8);
glBindBuffer(GL_ARRAY_BUFFER, 0);
Reader r{body, body + n * 4 + 8};
return one(inner, r);
}
case ORTHO: {
const GLdouble l = a.f64(), r = a.f64(), b = a.f64();
const GLdouble t = a.f64(), n = a.f64(), f = a.f64();
glOrtho(l, r, b, t, n, f); break;
}
case SCALEF: {
const GLfloat x = a.f32(), y = a.f32(), z = a.f32();
glScalef(x, y, z); break;
}
case LOAD_MATRIXF:
case MULT_MATRIXF: {
GLfloat m[16];
for (int i = 0; i < 16; ++i) m[i] = a.f32();
if (!a.ok) return false;
if (op == LOAD_MATRIXF) glLoadMatrixf(m); else glMultMatrixf(m);
break;
}
case COLOR4F: {
const GLfloat r = a.f32(), g = a.f32(), b = a.f32(), al = a.f32();
glColor4f(r, g, b, al); break;
}
case TEX_ENVI: {
const GLenum target = a.u32(), pname = a.u32(); const GLint param = a.i32();
glTexEnvi(target, pname, param); break;
}
case ALPHA_FUNC: {
const GLenum func = a.u32(); const GLfloat ref = a.f32();
glAlphaFunc(func, ref); break;
}
case PUSH_ATTRIB: glPushAttrib(a.u32()); break;
case POP_ATTRIB: glPopAttrib(); break;
#include "gl44-generated-cases.inc"
#include "gl-compat-generated-cases.inc"
default:
/* Named, once. A command the host does not know aborts the whole
* batch -- the guest discards every queued byte, including the
* PRESENT at the end of the frame -- and the guest's own message can
* only say that it happened, not what caused it. */
{
static bool said;
if (!said) {
said = true;
fprintf(stderr, "gexgl: unknown opcode %u; the batch carrying "
"it is discarded\n", (unsigned)op);
}
}
return false;
}
if (!a.ok) {
static bool said;
if (!said) {
said = true;
fprintf(stderr, "gexgl: opcode %u ran out of arguments; the batch "
"carrying it is discarded\n", (unsigned)op);
}
}
return a.ok;
}
} // namespace
extern "C" {
/* Select the client API before gex_gl_init. `api` is 0 for GLES and 1 for a
* desktop core profile; the version is what the context asks for, and st/mesa
* returns the latest it can satisfy. */
EMSCRIPTEN_KEEPALIVE void gex_gl_set_api(int api, int major, int minor) {
backend_profile = static_cast<int>(api == 1 ? API_OPENGL_CORE
: API_OPENGLES2);
backend_major = major > 0 ? major : (api == 1 ? 3 : 3);
backend_minor = major > 0 ? minor : (api == 1 ? 3 : 0);
/* And for every client that has not appeared yet. */
device_profile = backend_profile;
device_major = backend_major;
device_minor = backend_minor;
}
EMSCRIPTEN_KEEPALIVE int gex_gl_init(int width, int height) {
const int result = initialize_backend(width, height);
if (result != 0) return result;
// GLES has a usable default vertex-array object. A core desktop context's
// VAO 0 cannot carry attribute state, so materialize the GLES default once
// at context creation.
glGenVertexArrays(1, &default_vao);
glBindVertexArray(default_vao);
return 0;
}
/* `client` names the guest process whose commands these are: its own GL
* context, its own object names, its own windows. Callers that predate it --
* the smoke and conformance harnesses, which are the only client there is --
* pass nothing and get client zero. */
EMSCRIPTEN_KEEPALIVE int gex_gl_submit(const uint8_t* data, uint32_t len,
uint32_t client) {
use_client(client);
if (gl_counts() && (++gl_client_counts[current_client].first % 256) == 0)
gl_count_line("batch");
reply.clear(); uint32_t at = 0;
while (at < len) {
if (len - at < 8) return -1;
uint32_t op, size; std::memcpy(&op, data + at, 4); std::memcpy(&size, data + at + 4, 4);
if (size < 8 || size > len - at) return -1;
Reader args{data + at + 8, data + at + size};
/* Every opcode has one canonical payload shape. Reject both short
* reads and trailing data: silently accepting a prefix makes version
* skew indistinguishable from a valid command and lets the guest and
* worker advance through the stream with different assumptions. */
if (!one(op, args) || !args.ok || args.p != args.end) {
/* Which opcode, and which of the three ways it went wrong. A bare
-2 says a batch was rejected and the caller then latches, so the
application stops dead with no indication of what it asked for
-- and the opcode is the whole diagnosis. */
/* Kept, not only printed: a page reads this back and says it
* where the failure is visible, which stderr in a worker is
* not. One rejected batch is a client that stops dead, and the
* opcode is the whole diagnosis. */
if (!last_reject_detailed) {
last_reject_op = op;
last_reject_why = !args.ok ? 1 : args.p != args.end ? 2 : 3;
last_reject_payload = size - 8;
last_reject_client = current_client;
}
last_reject_detailed = false;
static bool said;
if (!said) {
said = true;
std::fprintf(stderr,
"gexgl: batch rejected at opcode %u (%s) "
"payload=%u client=%u\n", op,
!args.ok ? "short read"
: args.p != args.end ? "trailing data"
: "unhandled opcode",
size - 8, current_client);
}
return -2;
}
gl_error_check(op);
at += size;
}
return 0;
}
/* Set one host-side environment variable, as "NAME=VALUE".
*
* The driver's diagnostics are all getenv-gated (GEX_WGPU_DUMP_WGSL and the
* rest), and in the browser getenv answers NULL for every one of them unless
* the page sets them here, before gex_gl_init. The Mesa fork's gex-wasm
* targets export it. */
EMSCRIPTEN_KEEPALIVE void gex_gl_setenv(const char *pair) {
if (!pair) return;
const char *equals = std::strchr(pair, '=');
if (!equals || equals == pair) return;
const std::string name(pair, (size_t)(equals - pair));
setenv(name.c_str(), equals + 1, 1);
}
EMSCRIPTEN_KEEPALIVE uint32_t gex_gl_reply_len() { return reply.size(); }
EMSCRIPTEN_KEEPALIVE const uint8_t* gex_gl_reply_data() { return reply.data(); }
/* gexd3d-mesa.cpp, when the D3D device is built in. */
extern "C" __attribute__((weak)) void gex_d3d_display_tick();
/* Show one frame of every window that has one waiting.
*
* Not just the current one: a page frame shows every window, and a client
* whose window is not the one that submitted last must not be left waiting
* for a count that only moves when it is. */
EMSCRIPTEN_KEEPALIVE uint32_t gex_gl_display() {
/* The D3D device's, when it is built in (gexd3d-mesa.cpp). */
if (gex_d3d_display_tick) gex_d3d_display_tick();
const uint32_t was_client = current_client;
const uint32_t was_surface = current_surface;
std::vector<uint32_t> ids;
ids.push_back(was_client);
for (const auto& entry : clients)
if (entry.first != was_client) ids.push_back(entry.first);
for (uint32_t id : ids) {
use_client(id);
/* A client with no GL context may still have frames: the D3D
* device's (gex_gl_external_frame). */
if (!mesa_context && surfaces.empty()) continue;
std::vector<uint32_t> pending;
for (const auto& entry : surfaces)
if (display_pending(entry.second))
pending.push_back(entry.first);
for (uint32_t handle : pending) {
use_surface(handle);
display_backend();
}
}
use_client(was_client);
use_surface(was_surface);
return displayed;
}
/* The windows the host is drawing, as the page reads them.
*
* One entry per attached surface across every client: the page pairs each
* with the window it belongs to (a `wl_surface` id, or an X drawable) and
* asks the renderer for that surface's own frame. A snapshot is taken when
* the count is asked for, so the indices below are stable for one read. */
struct surface_ref {
uint32_t client, handle, uid, kind, id, token, width, height;
};
std::vector<surface_ref> surface_list;
void collect_surfaces() {
surface_list.clear();
for (const auto& entry : surfaces)
if (entry.second.kind || entry.second.id)
surface_list.push_back({current_client, entry.first,
entry.second.uid, entry.second.kind,
entry.second.id, entry.second.token,
entry.second.drawable.width,
entry.second.drawable.height});
/* The other clients' surfaces are in their saved state, and readable
* there without swapping anything in. */
for (const auto& client : clients) {
if (client.first == current_client) continue;
for (const auto& entry : client.second.surfaces)
if (entry.second.kind || entry.second.id)
surface_list.push_back({client.first, entry.first,
entry.second.uid, entry.second.kind,
entry.second.id, entry.second.token,
entry.second.drawable.width,
entry.second.drawable.height});
}
}
/* How many windows the host is drawing, and what each one is. Call the count
* first: it takes the snapshot the rest read. */
EMSCRIPTEN_KEEPALIVE uint32_t gex_gl_surface_count() {
collect_surfaces();
return (uint32_t)surface_list.size();
}
EMSCRIPTEN_KEEPALIVE uint32_t gex_gl_surface_kind(uint32_t i) {
return i < surface_list.size() ? surface_list[i].kind : 0;
}
EMSCRIPTEN_KEEPALIVE uint32_t gex_gl_surface_id(uint32_t i) {
return i < surface_list.size() ? surface_list[i].id : 0;
}
EMSCRIPTEN_KEEPALIVE uint32_t gex_gl_surface_token(uint32_t i) {
return i < surface_list.size() ? surface_list[i].token : 0;
}
EMSCRIPTEN_KEEPALIVE uint32_t gex_gl_surface_client(uint32_t i) {
return i < surface_list.size() ? surface_list[i].client : 0;
}
/* What the renderer keys this surface's frame by. */
EMSCRIPTEN_KEEPALIVE uint32_t gex_gl_surface_frame(uint32_t i) {
return i < surface_list.size() ? surface_list[i].uid : 0;
}
EMSCRIPTEN_KEEPALIVE uint32_t gex_gl_surface_width(uint32_t i) {
return i < surface_list.size() ? surface_list[i].width : 0;
}
EMSCRIPTEN_KEEPALIVE uint32_t gex_gl_surface_height(uint32_t i) {
return i < surface_list.size() ? surface_list[i].height : 0;
}
/* Bumped whenever a surface is attached or one goes away, so a page can tell
* a changed list from the same list read twice. */
EMSCRIPTEN_KEEPALIVE uint32_t gex_gl_surface_generation() {
return attached_generation + surface_gone_generation;
}
/* This client is gone: its process exited, or the page dropped it.
*
* A client that exits says nothing -- it is not there to say it -- so the
* page, which does notice, says it instead. Everything the client owned goes
* back: its windows' colour and depth buffers, and the GL context that holds
* its textures, its programs and its buffers. Without this a session that ran
* two GL clients in turn leaked a context per client, and the names the dead
* one had allocated were still in the maps the next one drew with. */
EMSCRIPTEN_KEEPALIVE void gex_gl_forget_client(uint32_t client) {
const bool known = clients.find(client) != clients.end();
if (!known && client != current_client) return;
const uint32_t was = current_client;
use_client(client);
/* The context first, while the drawables it was built on still exist,
* and then the surfaces with nothing to point at instead. Dropping the
* current surface the usual way points the context at another one, and
* with none left that is surface 0, made anew -- so `surfaces` was never
* empty and this looped for ever in the compositor's Worker, which then
* drew nothing and answered nothing (the second glxgears of a session,
* whichever way it was started). */
if (mesa_context) {
st_destroy_context(mesa_context);
mesa_context = nullptr;
}
sur = nullptr;
while (!surfaces.empty()) drop_surface(surfaces.begin()->first, false);
current_surface = 0;
/* Back to whoever was running, unless that was this one. */
use_client(was == client ? 0 : was);
clients.erase(client);
}
/* A frame for one of a client's windows that this decoder did not draw:
* the D3D device's (gexd3d-mesa.cpp), which shares this table of windows so
* that its frames are shown, listed and forgotten as a GL client's are.
*
* `handle` names the drawable, from a range the client's EGL does not use;
* `image` becomes its back buffer, held until the next frame replaces it. */
EMSCRIPTEN_KEEPALIVE void gex_gl_external_frame(uint32_t client, uint32_t handle,
unsigned kind, unsigned id,
unsigned token,
struct pipe_resource *image,
unsigned width, unsigned height) {
const uint32_t was = current_client;
use_client(client);
gex_surface *s = surface_for(handle);
if (s->kind != kind || s->id != id || s->token != token) {
s->kind = kind;
s->id = id;
s->token = token;
++s->generation;
attached_generation++;
}
s->external = true;
pipe_resource_reference(&s->drawable.attachments[ST_ATTACHMENT_BACK_LEFT], image);
s->drawable.width = width;
s->drawable.height = height;
++produced;
++s->produced;
use_client(was);
}
/* Why the last batch was refused: see `last_reject_op`. */
EMSCRIPTEN_KEEPALIVE uint32_t gex_gl_reject_op() { return last_reject_op; }
EMSCRIPTEN_KEEPALIVE uint32_t gex_gl_reject_why() { return last_reject_why; }
EMSCRIPTEN_KEEPALIVE uint32_t gex_gl_reject_payload() { return last_reject_payload; }
EMSCRIPTEN_KEEPALIVE uint32_t gex_gl_reject_client() { return last_reject_client; }
EMSCRIPTEN_KEEPALIVE uint32_t gex_gl_attached_kind() { return attached_kind; }
EMSCRIPTEN_KEEPALIVE uint32_t gex_gl_attached_id() { return attached_id; }
EMSCRIPTEN_KEEPALIVE uint32_t gex_gl_attached_token() { return attached_token; }
EMSCRIPTEN_KEEPALIVE uint32_t gex_gl_attached_generation() { return attached_generation; }
EMSCRIPTEN_KEEPALIVE uint32_t gex_gl_presented() { return displayed; }
/* Surface `uid` (`gex_gl_surface_frame`) has a canvas of its own in the page,
* with the id `gex-gl-<uid>`; `on` zero when it is gone. */
EMSCRIPTEN_KEEPALIVE void gex_gl_surface_canvas(uint32_t uid, uint32_t on) {
set_window_canvas(uid, on != 0);
}
EMSCRIPTEN_KEEPALIVE uint32_t gex_gl_produced() { return produced; }
}