""" Headless GLSL shader renderer using ModernGL. Renders Shadertoy-dialect GLSL fragment shaders to pixel buffers via an EGL-backed OpenGL context (no display required). Can be imported directly or run as a subprocess (used by harness.py): echo '{"code":"void mainImage(out vec4 c,in vec2 f){c=vec4(1,0,0,1);}"}' | python render.py """ import json import platform import re import sys from base64 import b64encode VERTEX = """\ #version 330 in vec2 vert; void main() { gl_Position = vec4(vert, 0.0, 1.0); } """ # Shadertoy-compatible preamble. Declares all standard uniforms, # forward-declares mainImage, and defines main() which calls it. # User code (containing the mainImage definition) is appended after this. PREAMBLE = """\ #version 330 uniform vec3 iResolution; uniform float iTime; uniform float iTimeDelta; uniform int iFrame; uniform float iFrameRate; uniform vec4 iMouse; uniform vec4 iDate; uniform float iSampleRate; uniform vec3 iChannelResolution[4]; uniform float iChannelTime[4]; uniform sampler2D iChannel0; uniform sampler2D iChannel1; uniform sampler2D iChannel2; uniform sampler2D iChannel3; #define HW_PERFORMANCE 1 void mainImage(out vec4, in vec2); out vec4 _fragcolor; void main() { vec4 color = vec4(0.0, 0.0, 0.0, 1.0); mainImage(color, gl_FragCoord.xy); _fragcolor = color; } """ PREAMBLE_LINES = PREAMBLE.count("\n") # Fullscreen quad: 2 triangles covering [-1,1] x [-1,1]. QUAD = [ -1.0, -1.0, 1.0, -1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0, ] def preprocess(code): """Strip #version, precision, and #extension directives from user shader code.""" code = re.sub(r"^\s*#version\s+.*$", "", code, flags=re.MULTILINE) code = re.sub(r"^\s*precision\s+\w+\s+\w+\s*;", "", code, flags=re.MULTILINE) code = re.sub(r"^\s*#extension\s+.*$", "", code, flags=re.MULTILINE) return code def _set(prog, name, value): """Set a uniform only if it exists in the compiled program.""" if prog.get(name, None) is not None: prog[name] = value def _adjust_errors(msg): """Extract error lines from ModernGL output and adjust line numbers.""" lines = msg.strip().splitlines() errors = [] for line in lines: line = line.strip() if not line or line.startswith("="): continue if line in ("GLSL Compiler failed", "vertex_shader", "fragment_shader"): continue # Mesa format: "0:LINE(COL): error: ..." line = re.sub( r"^(\d+):(\d+)", lambda m: f"{m.group(1)}:{max(1, int(m.group(2)) - PREAMBLE_LINES)}", line, ) # NVIDIA format: "0(LINE) : error ..." line = re.sub( r"^(\d+)\((\d+)\)", lambda m: f"{m.group(1)}({max(1, int(m.group(2)) - PREAMBLE_LINES)})", line, ) errors.append(line) return errors if errors else [msg.strip()] def _release(*objs): for obj in objs: try: obj.release() except Exception: pass def render(code, resolution=(1280, 720), time=0.0, frame=0, mouse=(0.0, 0.0, 0.0, 0.0)): """ Render a Shadertoy-dialect GLSL shader headlessly. Returns a dict: compiled: bool rendered: bool errors: list[str] (line numbers adjusted to user code) frame: bytes | None (raw RGBA, width*height*4, top-left origin) width: int height: int """ try: import moderngl import numpy as np except ImportError as e: return {"compiled": False, "rendered": False, "errors": [f"missing dependency: {e}"], "frame": None, "width": 0, "height": 0} w, h = resolution fail = {"compiled": False, "rendered": False, "errors": [], "frame": None, "width": w, "height": h} fragment = PREAMBLE + preprocess(code) # Context try: backend = "egl" if platform.system() == "Linux" else None kwargs = {"backend": backend} if backend else {} ctx = moderngl.create_context(standalone=True, require=330, **kwargs) except Exception as e: fail["errors"] = [f"context: {e}"] return fail # Compile try: prog = ctx.program(vertex_shader=VERTEX, fragment_shader=fragment) except Exception as e: fail["errors"] = _adjust_errors(str(e)) ctx.release() return fail # Setup framebuffer + geometry fbo = ctx.simple_framebuffer((w, h), components=4) fbo.use() vbo = ctx.buffer(np.array(QUAD, dtype="f4").tobytes()) vao = ctx.vertex_array(prog, [(vbo, "2f", "vert")]) # Uniforms _set(prog, "iResolution", (float(w), float(h), 1.0)) _set(prog, "iTime", float(time)) _set(prog, "iTimeDelta", 1.0 / 60.0) _set(prog, "iFrame", int(frame)) _set(prog, "iFrameRate", 60.0) _set(prog, "iMouse", tuple(float(v) for v in mouse)) _set(prog, "iDate", (2026.0, 1.0, 1.0, 0.0)) _set(prog, "iSampleRate", 44100.0) # Render try: fbo.clear(0.0, 0.0, 0.0, 1.0) vao.render(moderngl.TRIANGLES) except Exception as e: _release(vao, vbo, fbo, prog, ctx) fail["compiled"] = True fail["errors"] = [f"render: {e}"] return fail # Read pixels try: raw = fbo.read(components=4) pixels = np.frombuffer(raw, dtype=np.uint8).reshape((h, w, 4)) pixels = np.flipud(pixels) frame_bytes = pixels.tobytes() except Exception as e: _release(vao, vbo, fbo, prog, ctx) fail["compiled"] = True fail["errors"] = [f"readback: {e}"] return fail _release(vao, vbo, fbo, prog, ctx) return {"compiled": True, "rendered": True, "errors": [], "frame": frame_bytes, "width": w, "height": h} if __name__ == "__main__": try: request = json.loads(sys.stdin.read()) code = request.pop("code") result = render(code, **request) if result["frame"] is not None: result["frame"] = b64encode(result["frame"]).decode("ascii") json.dump(result, sys.stdout) except Exception as e: json.dump({"compiled": False, "rendered": False, "errors": [str(e)], "frame": None, "width": 0, "height": 0}, sys.stdout)