feat(graphics): add offscreen render targets (Phase 1)
Adds make-render-target, with-render-target macro, render-target->texture, and draw-render-target — a minimal offscreen render-to-texture surface that lets downstream apps build post-processing pipelines (cinder-cantata v0.3 bloom/chromatic aberration is the immediate consumer).
Implementation: - GfxRenderTarget wraps an sgimage color attachment, an attachment view (for rendering INTO), a texture view + sampler (for sampling AS texture), and a paired depth-stencil image + view. The depth attachment is required because sgp's default pipelines bake in the swap-chain depth-stencil format; offscreen passes must match. - with-render-target uses sokolgp's nested sgpbegin/sgpend via the internal state stack, so the offscreen pass composes cleanly with begin-frame/end-frame: each render target owns its own sgpbegin/sgbeginpass/sgpflush/sgendpass/sgpend envelope inside the outer frame. - GfxTexture grows an ownsresources flag so render-target->texture can return a borrowed wrapper without double-freeing the rt's resources on GC. - with-render-target uses dynamic-wind so the offscreen pass closes cleanly on non-local exit, keeping the sgp/sg pass stacks balanced.
Build / runtime: - Install slogfunc as sgsetup's logger so validation failures print a diagnostic before sokol's abort. The implementation already lives in sigil-app's archive (sigil-app's sokol-app.c picks up SOKOLLOGIMPL via SOKOL_IMPL); graphics.c just declares it. - Add manifest.scm so guix shell -m manifest.scm can build + launch the test apps (mesa, libglvnd, libx11/xi/xcursor, alsa-lib, libogg/ libvorbis — mirrors cinder-cantata's runtime surface). - Bump sigil: ^0.13 → ^0.14 since the only sigil binary on PATH is 0.14.3; drop the explicit sigil-stdlib pin (auto-derived in 0.14).
Smoke test: test/test-render-target/ builds a 256×256 offscreen scene and composites it onto an 800×600 swap chain at four positions/sizes, including a tinted copy via set-color modulation. Clean exit; no sokol validation panics.
The cinder-side wiring at the phase boundary is deliberately not in this commit — it's throwaway test wiring that lives on a temp branch in the cinder repo per the task brief's "cinder integration" section.
manifest.scm | 28 +++++++++
package.sgl | 2 +-
sigil.lock | 15 +++--
src/c/graphics.c | 470 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
src/c/sokol-graphics.c | 3 +
src/sigil/graphics.sgl | 29 ++++++++-
test/test-render-target/dev-redirects.sgl | 8 +++
test/test-render-target/package.sgl | 28 +++++++++
test/test-render-target/src/test-render-target/main.sgl | 90 ++++++++++++++++++++++++++
9 files changed, 657 insertions(+), 16 deletions(-)manifest.scmadded
;; sigil-graphics - Development Environment;; Use with: guix shell -m manifest.scm;;;; Mirrors cinder-cantata's runtime surface (mesa, X11 stack, alsa,;; ogg/vorbis) so the smoke tests under test/ launch cleanly under;; sokol_app on Guix. libglvnd is what supplies <GL/gl.h> on modern;; Guix; mesa alone is not enough for headers, only for the runtime;; libGL.(specifications->manifest '("gcc-toolchain" "pkg-config" "binutils" ;; Graphics (sokol_gfx + sokol_app on Linux) "mesa" ; OpenGL implementation (libGL) "libglvnd" ; OpenGL dispatcher; provides GL/gl.h on modern Guix "libx11" "libxi" "libxcursor" ;; Audio (sokol_audio runtime — sigil-app pulls it in transitively) "alsa-lib" ;; OGG Vorbis (carried for parity with cinder's manifest, since ;; downstream test apps may pull sigil-audio) "libvorbis" "libogg"))package.sglmodified
(package name: "sigil-graphics" version: "0.9.3" sigil: "^0.14" description: "2D graphics, image loading, and font rendering for Sigil" url: "https://codeberg.org/sigil/sigil-graphics" license: "BSD-3-Clause" features: '(release cross windows))) dependencies: (list (from-git url: "codeberg:sigil/sigil" package: "sigil-stdlib" version: "^0.13.0") (from-git url: "codeberg:sigil/sigil-app" version: "^0.8.2")) libraries: (listsigil.lockmodified
;; Auto-generated by sigil deps install. Do not edit.(lock (package name: "sigil-lib" url: "codeberg:sigil/sigil-lang" ref: "^0.14" sha: "ff67dfe2033e03cb3d71ba3d801418ba0729094f" package-selector: "sigil-lib" version: "0.14.4") (package name: "sigil-stdlib" url: "codeberg:sigil/sigil" ref: "^0.13.0" sha: "2e95b0343b976057a7655b41f08f0930d6abb628" package-selector: "sigil-stdlib") url: "codeberg:sigil/sigil-lang" ref: "^0.14" sha: "ff67dfe2033e03cb3d71ba3d801418ba0729094f" package-selector: "sigil-stdlib" version: "0.14.4") (package name: "sigil-app" url: "codeberg:sigil/sigil-app" ref: "^0.8.2"src/c/graphics.cmodified
#include "sokol_gfx.h"#include "sokol_glue.h"#include "sokol_gp.h"#include "sokol_log.h"#include <stdio.h>#include <stdlib.h>/* Texture type tag (initialized at module init) */static Value texture_type_tag = SIGIL_UNDEFINED;/* Texture structure *//* Render target type tag (initialized at module init) */static Value rt_type_tag = SIGIL_UNDEFINED;/* Texture structure. * * `owns_resources` is false for textures that borrow their handles from * another owner (e.g., from a render target via render-target->texture). * Borrowed wrappers must not destroy the underlying sokol resources; * the original owner does that. */typedef struct { sg_image handle; sg_sampler sampler; sg_view view; int width; int height; bool owns_resources;} GfxTexture;/* Render target structure. * * Holds a color image, a color-attachment view (for rendering INTO), * a texture view (for sampling AS texture), a sampler, and a * depth-stencil image + view. The depth-stencil attachment is only * needed because sgp's default pipelines bake in the swap chain's * depth-stencil format; offscreen passes must provide a matching * attachment so pipeline validation passes. We don't actually use the * depth buffer for 2D rendering. */typedef struct { sg_image color_img; sg_view color_att_view; sg_view tex_view; sg_sampler sampler; sg_image depth_img; sg_view depth_att_view; int width; int height; bool freed;} GfxRenderTarget;/* Current draw color */static float draw_color[4] = {1.0f, 1.0f, 1.0f, 1.0f}; return SIGIL_NIL; } /* Initialize sokol_gfx */ /* Initialize sokol_gfx. Install slog_func so validation failures * print a useful diagnostic before sokol aborts. */ sg_desc desc = { .environment = sglue_environment(), .logger.func = slog_func, }; sg_setup(&desc);{ GfxTexture *tex = (GfxTexture *)data; if (tex) { if (tex->view.id != SG_INVALID_ID) { sg_destroy_view(tex->view); } if (tex->handle.id != SG_INVALID_ID) { sg_destroy_image(tex->handle); } if (tex->sampler.id != SG_INVALID_ID) { sg_destroy_sampler(tex->sampler); if (tex->owns_resources) { if (tex->view.id != SG_INVALID_ID) { sg_destroy_view(tex->view); } if (tex->handle.id != SG_INVALID_ID) { sg_destroy_image(tex->handle); } if (tex->sampler.id != SG_INVALID_ID) { sg_destroy_sampler(tex->sampler); } } free(tex); } tex->view = view; tex->width = width; tex->height = height; tex->owns_resources = true; ensure_texture_type(vm); return sigil_make_foreign(vm, texture_type_tag, tex, texture_destructor, return SIGIL_NIL;}/* ============================================================ * RENDER TARGETS * ============================================================ */static void ensure_rt_type(SigilVM *vm){ if (sigil_is_undefined(rt_type_tag)) { rt_type_tag = sigil_intern_symbol(vm, "sigil-graphics-rt", 17); }}static GfxRenderTarget *get_rt(SigilVM *vm, Value v){ if (!sigil_is_foreign(v)) return NULL; ensure_rt_type(vm); if (sigil_foreign_type(v) != rt_type_tag) return NULL; return (GfxRenderTarget *)sigil_foreign_data(v);}/* Free a render target's GPU resources. Idempotent. */static void rt_free_resources(GfxRenderTarget *rt){ if (!rt || rt->freed) return; if (rt->color_att_view.id != SG_INVALID_ID) { sg_destroy_view(rt->color_att_view); } if (rt->tex_view.id != SG_INVALID_ID) { sg_destroy_view(rt->tex_view); } if (rt->depth_att_view.id != SG_INVALID_ID) { sg_destroy_view(rt->depth_att_view); } if (rt->color_img.id != SG_INVALID_ID) { sg_destroy_image(rt->color_img); } if (rt->depth_img.id != SG_INVALID_ID) { sg_destroy_image(rt->depth_img); } if (rt->sampler.id != SG_INVALID_ID) { sg_destroy_sampler(rt->sampler); } rt->freed = true;}static void rt_destructor(void *data){ GfxRenderTarget *rt = (GfxRenderTarget *)data; if (rt) { rt_free_resources(rt); free(rt); }}/* * (%make-render-target width height) -> <render-target> or #f * * Creates an offscreen color render target backed by an sg_image with * color_attachment usage, plus the views and sampler needed to render * into it and sample it as a texture. */static Value native_make_render_target(SigilVM *vm, int argc, Value *args){ if (argc < 2) { sigil__vm_error(vm, SIGIL_ERR_ARITY, "make-render-target: requires width, height arguments"); return SIGIL_UNDEFINED; } int w = (int)value_to_float(args[0]); int h = (int)value_to_float(args[1]); if (w <= 0 || h <= 0) { sigil__vm_set_error(vm, SIGIL_ERR_TYPE, "make-render-target: width and height must be positive"); return SIGIL_FALSE; } /* Pixel format and sample count default to sg_environment.defaults * (i.e., the swap chain's color format / sample count). Matching them * is required so sgp's default pipelines validate against this pass — * sgp pipelines bake in the format/sample count from sgp_setup. */ sg_image_desc img_desc = { .usage = { .color_attachment = true }, .width = w, .height = h, }; sg_image img = sg_make_image(&img_desc); if (img.id == SG_INVALID_ID) { sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME, "make-render-target: failed to create color image"); return SIGIL_FALSE; } sg_view_desc att_desc = { .color_attachment = { .image = img }, .label = "sigil-rt-color-attachment", }; sg_view att_view = sg_make_view(&att_desc); if (att_view.id == SG_INVALID_ID) { sg_destroy_image(img); sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME, "make-render-target: failed to create color attachment view"); return SIGIL_FALSE; } sg_view tex_view = sgp_make_texture_view_from_image(img, "sigil-rt-texture"); if (tex_view.id == SG_INVALID_ID) { sg_destroy_view(att_view); sg_destroy_image(img); sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME, "make-render-target: failed to create texture view"); return SIGIL_FALSE; } /* Depth-stencil attachment — needed only for pipeline-validation * compatibility with sgp's default pipelines, which bake in the * swap chain's depth-stencil format. */ sg_image_desc depth_desc = { .usage = { .depth_stencil_attachment = true }, .width = w, .height = h, }; sg_image depth_img = sg_make_image(&depth_desc); if (depth_img.id == SG_INVALID_ID) { sg_destroy_view(tex_view); sg_destroy_view(att_view); sg_destroy_image(img); sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME, "make-render-target: failed to create depth image"); return SIGIL_FALSE; } sg_view_desc depth_att_desc = { .depth_stencil_attachment = { .image = depth_img }, .label = "sigil-rt-depth-attachment", }; sg_view depth_att_view = sg_make_view(&depth_att_desc); if (depth_att_view.id == SG_INVALID_ID) { sg_destroy_image(depth_img); sg_destroy_view(tex_view); sg_destroy_view(att_view); sg_destroy_image(img); sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME, "make-render-target: failed to create depth attachment view"); return SIGIL_FALSE; } sg_sampler_desc smp_desc = { .min_filter = SG_FILTER_LINEAR, .mag_filter = SG_FILTER_LINEAR, .wrap_u = SG_WRAP_CLAMP_TO_EDGE, .wrap_v = SG_WRAP_CLAMP_TO_EDGE, }; sg_sampler smp = sg_make_sampler(&smp_desc); GfxRenderTarget *rt = malloc(sizeof(GfxRenderTarget)); if (!rt) { sg_destroy_sampler(smp); sg_destroy_view(depth_att_view); sg_destroy_image(depth_img); sg_destroy_view(tex_view); sg_destroy_view(att_view); sg_destroy_image(img); sigil__vm_set_error(vm, SIGIL_ERR_MEMORY, "make-render-target: out of memory"); return SIGIL_FALSE; } rt->color_img = img; rt->color_att_view = att_view; rt->tex_view = tex_view; rt->sampler = smp; rt->depth_img = depth_img; rt->depth_att_view = depth_att_view; rt->width = w; rt->height = h; rt->freed = false; ensure_rt_type(vm); return sigil_make_foreign(vm, rt_type_tag, rt, rt_destructor, sizeof(GfxRenderTarget));}/* * (render-target? obj) -> boolean */static Value native_render_target_p(SigilVM *vm, int argc, Value *args){ (void)argc; return get_rt(vm, args[0]) ? SIGIL_TRUE : SIGIL_FALSE;}/* * (render-target-width rt) -> integer */static Value native_render_target_width(SigilVM *vm, int argc, Value *args){ (void)argc; GfxRenderTarget *rt = get_rt(vm, args[0]); if (!rt) { sigil__vm_set_error(vm, SIGIL_ERR_TYPE, "render-target-width: expected render-target"); return SIGIL_UNDEFINED; } return sigil_fixnum(rt->width);}/* * (render-target-height rt) -> integer */static Value native_render_target_height(SigilVM *vm, int argc, Value *args){ (void)argc; GfxRenderTarget *rt = get_rt(vm, args[0]); if (!rt) { sigil__vm_set_error(vm, SIGIL_ERR_TYPE, "render-target-height: expected render-target"); return SIGIL_UNDEFINED; } return sigil_fixnum(rt->height);}/* * (render-target-free! rt) - Explicit cleanup of GPU resources * * Idempotent. After this call the render target is unusable; the * destructor on GC will be a no-op. */static Value native_render_target_free(SigilVM *vm, int argc, Value *args){ (void)argc; GfxRenderTarget *rt = get_rt(vm, args[0]); if (!rt) { sigil__vm_set_error(vm, SIGIL_ERR_TYPE, "render-target-free!: expected render-target"); return SIGIL_UNDEFINED; } rt_free_resources(rt); return SIGIL_NIL;}/* * (%begin-rt-pass rt) - Push a new sokol_gp queue + sokol_gfx pass that * targets the render target. All subsequent draws land in rt's color * image until %end-rt-pass is called. */static Value native_begin_rt_pass(SigilVM *vm, int argc, Value *args){ (void)argc; GfxRenderTarget *rt = get_rt(vm, args[0]); if (!rt) { sigil__vm_set_error(vm, SIGIL_ERR_TYPE, "%begin-rt-pass: expected render-target"); return SIGIL_UNDEFINED; } if (rt->freed) { sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME, "%begin-rt-pass: render target has been freed"); return SIGIL_UNDEFINED; } /* Push an inner sgp state on the stack with the render target's size. * sgp's state stack ensures inner sgp_flush only emits commands queued * between this sgp_begin and its matching sgp_end. */ sgp_begin(rt->width, rt->height); sgp_viewport(0, 0, rt->width, rt->height); sgp_project(0, (float)rt->width, 0, (float)rt->height); sg_pass_action act = { .colors[0] = { .load_action = SG_LOADACTION_CLEAR, .store_action = SG_STOREACTION_STORE, .clear_value = {0.0f, 0.0f, 0.0f, 0.0f}, }, .depth = { .load_action = SG_LOADACTION_CLEAR, .store_action = SG_STOREACTION_DONTCARE, .clear_value = 1.0f, }, }; sg_pass pass = { .action = act, .attachments = { .colors[0] = rt->color_att_view, .depth_stencil = rt->depth_att_view, }, }; sg_begin_pass(&pass); return SIGIL_NIL;}/* * (%end-rt-pass) - Flush queued commands to the current render target, * end the sokol_gfx pass, and pop the inner sgp state. * * Must balance a prior %begin-rt-pass call. */static Value native_end_rt_pass(SigilVM *vm, int argc, Value *args){ (void)vm; (void)argc; (void)args; sgp_flush(); sg_end_pass(); sgp_end(); return SIGIL_NIL;}/* * (render-target->texture rt) -> <texture> * * Returns a texture wrapper that borrows the render target's image, * texture view, and sampler. The returned texture is invalid after the * render target is freed; callers must keep the render target alive for * the lifetime of the wrapper. */static Value native_render_target_to_texture(SigilVM *vm, int argc, Value *args){ (void)argc; GfxRenderTarget *rt = get_rt(vm, args[0]); if (!rt) { sigil__vm_set_error(vm, SIGIL_ERR_TYPE, "render-target->texture: expected render-target"); return SIGIL_UNDEFINED; } GfxTexture *tex = malloc(sizeof(GfxTexture)); if (!tex) { sigil__vm_set_error(vm, SIGIL_ERR_MEMORY, "render-target->texture: out of memory"); return SIGIL_FALSE; } tex->handle = rt->color_img; tex->sampler = rt->sampler; tex->view = rt->tex_view; tex->width = rt->width; tex->height = rt->height; tex->owns_resources = false; ensure_texture_type(vm); return sigil_make_foreign(vm, texture_type_tag, tex, texture_destructor, sizeof(GfxTexture));}/* * (draw-render-target rt x y w h) - Draw the render target's color image * as a textured rect on the current pass. * * Convenience over (draw-texture (render-target->texture rt) x y w h): * skips the texture wrapper allocation. */static Value native_draw_render_target(SigilVM *vm, int argc, Value *args){ if (argc < 5) { sigil__vm_error(vm, SIGIL_ERR_ARITY, "draw-render-target: requires rt, x, y, w, h"); return SIGIL_UNDEFINED; } GfxRenderTarget *rt = get_rt(vm, args[0]); if (!rt) { sigil__vm_set_error(vm, SIGIL_ERR_TYPE, "draw-render-target: expected render-target"); return SIGIL_UNDEFINED; } float x = value_to_float(args[1]); float y = value_to_float(args[2]); float w = value_to_float(args[3]); float h = value_to_float(args[4]); sgp_set_view(0, rt->tex_view); sgp_set_sampler(0, rt->sampler); sgp_rect dest = {x, y, w, h}; sgp_rect src = {0, 0, (float)rt->width, (float)rt->height}; sgp_draw_textured_rect(0, dest, src); sgp_reset_view(0); sgp_reset_sampler(0); return SIGIL_NIL;}/* ============================================================ * MODULE INITIALIZATION * ============================================================ */ sigil_module_register_native(vm, "draw-texture-region", native_draw_texture_region, SIGIL_ARITY_EXACT(9), "Draw texture region"); /* Render targets — %begin-rt-pass / %end-rt-pass are raw primitives * wrapped by the with-render-target macro for stack discipline. */ sigil_module_register_native(vm, "%make-render-target", native_make_render_target, SIGIL_ARITY_EXACT(2), "Create offscreen render target");Showing the first 500 of 536 diff lines for this file. This diff is INCOMPLETE; read the file or clone the repository for the rest.
src/c/sokol-graphics.cmodified
#include "sokol_gfx.h"#include "sokol_glue.h"#include "sokol_gp.h"/* slog_func is implemented in sigil-app's archive (sigil-app's * sokol-app.c picks up SOKOL_LOG_IMPL via SOKOL_IMPL). graphics.c * just declares it via sokol_log.h. */src/sigil/graphics.sglmodified
texture? texture-width texture-height draw-texture draw-texture-region ;; Render targets (offscreen render-to-texture) make-render-target render-target? render-target-width render-target-height render-target-free! render-target->texture draw-render-target with-render-target ;; Re-export image functions for convenience load-image image? image-width image-height image-channels (define-syntax with-additive-blend (syntax-rules () ((_ body ...) (with-blend-mode 'additive body ...)))))) (with-blend-mode 'additive body ...)))) ;; Public render-target constructor — wraps the raw native to keep ;; the surface name additive (the existing C native is %make-render-target). (define (make-render-target width height) (%make-render-target width height)) ;; Run body with the render target bound as the active draw surface. ;; All draws inside body land in rt's color image. Uses dynamic-wind ;; so the offscreen pass is properly closed even on non-local exit ;; (exception, continuation invocation), keeping the sgp/sg pass ;; stacks balanced for the next frame. (define-syntax with-render-target (syntax-rules () ((_ rt body ...) (let ((target rt)) (dynamic-wind (lambda () (%begin-rt-pass target)) (lambda () body ...) (lambda () (%end-rt-pass)))))))))test/test-render-target/dev-redirects.sgladded
;; Local sigil-graphics is the in-progress version under development —;; the offscreen render target work this test exercises lives two;; directories up. sigil-app uses the resolved registry version.(redirects repos: (list (for-repo url: "codeberg:sigil/sigil-graphics" use: (from-path dir: "../.."))))test/test-render-target/package.sgladded
;;; test-render-target — smoke test for sigil-graphics render targets;;;;;; Renders a coloured quad into a 256×256 offscreen render target, then;;; composites the target back onto the swap chain at four different;;; positions and sizes. Quits after ~4 seconds or when escape is pressed.(package name: "test-render-target" version: "0.0.1" sigil: "^0.14" description: "Smoke test: offscreen render target end-to-end" license: "BSD-3-Clause" authors: (list "David Wilson <[email protected]>") entry: '(test-render-target main) bundle-name: "test-render-target" configs: (list (config name: 'dev output-dir: "build/dev" debug?: #t optimize: 0 bundle?: #t)) dependencies: (list (from-git url: "codeberg:sigil/sigil-app" version: "^0.8.2") (from-git url: "codeberg:sigil/sigil-graphics" version: "^0.9.3")))test/test-render-target/src/test-render-target/main.sgladded
;;; test-render-target / main — render target end-to-end smoke test;;;;;; Validates the offscreen render target API by building a small scene;;; (a magenta backdrop with a green and a red filled rect plus a yellow;;; circle) into a 256×256 render target, then drawing that render target;;; back onto the swap chain at four positions and sizes. If render;;; targets are wired correctly the four composited copies are visually;;; identical and contain the scene drawn at offscreen coordinates.(define-library (test-render-target main) (import (sigil core) (sigil math) (sigil app) (sigil graphics)) (export main) (begin (define WINDOW-W 800) (define WINDOW-H 600) (define RT-SIZE 256) ;; Renders the offscreen scene into the active draw target. ;; Coordinates are in render-target space (0,0)..(RT-SIZE,RT-SIZE). (define (draw-rt-scene) ;; Magenta backdrop so the offscreen surface is obvious if it's ;; ever sampled un-filled. (set-color 0.6 0.1 0.6 1.0) (draw-filled-rect 0.0 0.0 (inexact RT-SIZE) (inexact RT-SIZE)) ;; Green and red rectangles to give the composited image an ;; identifiable orientation. (set-color 0.1 0.9 0.2 1.0) (draw-filled-rect 32.0 32.0 96.0 64.0) (set-color 0.95 0.2 0.2 1.0) (draw-filled-rect 128.0 96.0 96.0 96.0) ;; Yellow circle in the bottom-right so we can see that ;; circle drawing also works inside an offscreen pass. (set-color 1.0 0.92 0.1 1.0) (draw-filled-circle 200.0 210.0 28.0 32)) (define (game-loop) (gfx-setup) (set-viewport WINDOW-W WINDOW-H) (let* ((rt (make-render-target RT-SIZE RT-SIZE))) ;; Sanity: surface predicates and accessors agree with the ;; constructor before we start rendering with the target. (when (not (render-target? rt)) (display "FAIL: render-target? returned false\n")) (when (not (= (render-target-width rt) RT-SIZE)) (display "FAIL: render-target-width mismatch\n")) (when (not (= (render-target-height rt) RT-SIZE)) (display "FAIL: render-target-height mismatch\n")) (let loop ((elapsed 0.0)) (let ((dt (wait-frame))) ;; Refresh the offscreen target every frame — proves the ;; pass-restart path is stable, not just a one-shot. (with-render-target rt (draw-rt-scene)) (with-frame ;; Dim swap-chain background so the four RT copies stand ;; out against negative space. (clear-screen 0.05 0.05 0.08 1.0) ;; Native size at the top-left corner. (draw-render-target rt 16.0 16.0 (inexact RT-SIZE) (inexact RT-SIZE)) ;; Half-size top-right. (draw-render-target rt 528.0 16.0 128.0 128.0) ;; Stretched wide on the bottom row. (draw-render-target rt 16.0 320.0 384.0 256.0) ;; Tinted by the current draw color (set-color is a ;; modulator on textured draws); pale blue tint. (set-color 0.6 0.85 1.0 1.0) (draw-render-target rt 432.0 320.0 256.0 256.0) (set-color 1.0 1.0 1.0 1.0)) (cond ((key-pressed? 'escape) (request-quit)) ((quit-requested?) #t) ((> elapsed 4.0) (request-quit)) (else (loop (+ elapsed dt)))))) (render-target-free! rt)) (gfx-shutdown)) (define (main . _args) (run-game "sigil-graphics render-target smoke test" WINDOW-W WINDOW-H game-loop) 0)))