bug#80281: nil
Divya Ranjan Pattanaik via "Bug reports for GNU Emacs, the Swiss army knife of text editors" <[email protected]> Sat, 01 Aug 2026 14:17:15 +0000
| Newsgroups | gmane.emacs.bugs |
|---|---|
| Message-ID | <[email protected]> |
Hello Eli and others, Daniel and I have been working on a revision of the canvas patch over the last few months, which we submit here. Several of Eli's and others' suggestions have been incorporated, such as the following: - Creating a canvas image from Elisp via create-image or directly building the image spec - Canvas can be loaded from :file and :data via create-image - Supported and tested on multiple GNU/Linux ports (PGTK, X11, Lucid), MacOS and Windows. - The redrawing/refreshing logic of canvases has been made more robust. Specifically, if the frame is garbaged we wait for next full redisplay. Apart from the above, the patch also contains a test suite, and more documentation. We have tried to keep the changes minimal and as non-invasive as possible. Looking forward to hear your thoughts. Regards, -- Divya Ranjan Pattanaik, Philosophy, Mathematics, Libre Software. PGP Fingerprint: F0B3 1A69 8006 8FB8 096A 2F12 B245 10C6 108C 8D4A
canvas-32.patch
(text/x-patch, 48.8 KB)
From f15487b3398c5d60910adc0210faeda95b762d8c Mon Sep 17 00:00:00 2001 From: Divya Ranjan <[email protected]> Date: Wed, 8 Jul 2026 07:20:39 +0000 Subject: [PATCH] New image type canvas Canvas images support efficient updating and refreshing. An image specifier with ':type canvas' creates a canvas image. The canvas has an associated writable pixel buffer. After changing the ':data' vector or writing to the pixel buffer a call to 'canvas-refresh' redraws the canvas image. See bug#80281. Co-Authored-By: Divya Ranjan <[email protected]> Co-Authored-By: Daniel Mendler <[email protected]> Canvas changes: * doc/lispref/display.texi (Images): Add Canvas Images to menu. (Canvas Images): New node documenting canvas image objects, the `:data-width', `:data-height', `:data' and `:file' properties, and `canvas-refresh'. * doc/lispref/elisp.texi (Top): Reference new Canvas Images node. * doc/lispref/internals.texi (Writing Dynamic Modules): Add Module Canvas API to menu. (Module Canvas API): New node documenting `canvas_data' module function. * etc/NEWS: Announce the addition. * lisp/image.el (create-image): Add `:id' to image specification of canvas images. * src/dispextern.h (struct image): Add `refresh', `original_width' and `original_height' fields. (redraw_image_glyphs, canvas_data): New prototypes. * src/emacs-module.c (module_canvas_data): New function. (initialize_environment): Register `canvas_data' module function. * src/image.c (struct canvas): New struct for canvas. (canvas_prepare_for_display, canvas_free_unused): New forward declarations. (prepare_image_for_display): Call `canvas_prepare_for_display' when the canvas refresh counter has changed. Set image `original_width' and `original_height'. (filter_image_spec): Exclude `:data' from the image cache key for canvas images, so mutating the data vector in place does not create a new cache entry. (Fclear_image_cache): Call `canvas_free_unused'. (image_set_transform): Use CAIRO_FILTER_GOOD instead of CAIRO_FILTER_BEST for canvas images, for performance. Do not set image `original_width' and `original_height'. (canvas_image_p, canvas_free_unused, canvas_apply_data, canvas_get) (canvas_parse, canvas_load, canvas_prepare_for_display, canvas_data): New functions implementing canvas creation, resizing, and reloading of `:data' or `:file' content. (Fcanvas_refresh): New defun `canvas-refresh', with an optional RELOAD-DATA argument to reread `:data' or `:file'. (initialize_image_type, syms_of_image): Register the `canvas' image type and `canvas-refresh'. * src/module-env-32.h (canvas_data): New prototype. * src/nsimage.m (ns_image_reset): New function to recreate an NS pixmap to update the canvas. * src/nsterm.h: Add prototype for `ns_image_reset'. * src/xdisp.c (redraw_image_glyphs_window) (redraw_image_glyphs_window_tree, redraw_image_glyphs): New functions to redraw only the glyphs referencing a given canvas image spec, falling back to a full frame redraw if the frame is garbaged. * test/src/emacs-module-resources/mod-test.c (mod-test-canvas-read) (mod-test-canvas-write, mod-test-canvas-invalid): New helper functions. * test/src/emacs-module-tests.el (test-canvas-gen-file) (mod-test-canvas/valid, mod-test-canvas/invalid, mod-test-canvas/vector) (mod-test-canvas/vector-reload, mod-test-canvas/unibyte) (mod-test-canvas/file, mod-test-canvas/gc-stress): New tests. --- doc/lispref/display.texi | 78 +++ doc/lispref/elisp.texi | 1 + doc/lispref/internals.texi | 47 ++ etc/NEWS | 11 + lisp/image.el | 3 + src/dispextern.h | 22 +- src/emacs-module.c | 10 + src/image.c | 536 ++++++++++++++++++++- src/module-env-32.h | 9 + src/nsimage.m | 7 + src/nsterm.h | 1 + src/xdisp.c | 71 +++ test/src/emacs-module-resources/mod-test.c | 115 +++++ test/src/emacs-module-tests.el | 117 +++++ 14 files changed, 1008 insertions(+), 20 deletions(-) diff --git a/doc/lispref/display.texi b/doc/lispref/display.texi index 0d0899e418c..29c1e2b5609 100644 --- a/doc/lispref/display.texi +++ b/doc/lispref/display.texi @@ -6024,6 +6024,7 @@ displayed (@pxref{Display Feature Testing}). * Defining Images:: Convenient ways to define an image for later use. * Showing Images:: Convenient ways to display an image once it is defined. * Multi-Frame Images:: Some images contain more than one frame. +* Canvas Images:: Easily modifiable image with a pixel buffer. * Image Cache:: Internal mechanisms of image display. @end menu @@ -7475,6 +7476,83 @@ This function returns the timer responsible for animating @var{image}, if there is one. @end defun +@node Canvas Images +@subsection Canvas Images +@cindex canvas image + +Canvas images can be updated and displayed efficiently with high frame rates, +for example by document viewers, visualization tools, dynamically computed +animations or games. Canvas images have an associated writeable pixel +buffer in ARGB32 format on all platforms. Unlike other image types supported +in Emacs, canvas images do not require creating a new Emacs Lisp object +every time they get updated. + +An image specifier with @code{:type canvas} creates a canvas, for instance +as follows: + +@lisp +(defvar my-canvas + '(image :type canvas + :id my-canvas + :data-width 800 + :data-height 600)) +@end lisp + +The image specification object uniquely (with respect to @code{eq}) +identifies a canvas image object. The properties @code{:data-width}, +@code{:data-height} and @code{:id} are mandatory. The width and height +must both be positive integers and the id must be a symbol. + +Canvases can be created by either providing ARGB32 data as a vector of +fixnums or as an unibyte string in @code{:data}. Path to a file +containing the ARGB32 data in @code{:file} can also be provided. +Furthermore @code{create-image} can create canvas images as follows, +where the data is initially loaded from a file, or from a vector: + +@lisp +(setq my-file-canvas (create-image "/path/to/file" + 'canvas nil + :data-width 800 + :data-height 600)) +(setq my-vec (make-vector (* 800 600) #xFFFF0000)) +(setq my-vec-canvas (create-image my-vec + 'canvas t + :data-width 800 + :data-height 600)) +@end lisp + +If the values for dimensions change (i.e., @code{:data-height} or +@code{:data-width}), then Emacs will automatically adjust the underlying +pixel buffer and resize the canvas. + +Other image properties like @code{:scale}, @code{:margin}, @code{:relief}, +@code{:transform-smoothing} and @code{:map} are supported. +@xref{Image Descriptors}, for further details on these properties. + +In order to display the canvas, specify it as the display property of a +string or overlay: + +@lisp +(insert (propertize "#" 'display my-canvas)) +(overlay-put some-overlay 'display my-canvas) +@end lisp + +Once a canvas image object has been created and displayed, it can be +``refreshed'' via @code{canvas-refresh}. If an optional non-@code{nil} +argument is passed to @code{canvas-refresh} then Emacs will reload the +@code{:data} or @code{:file} before redrawing the canvas. + +The following code refreshes the canvas by reloading the data or file: + +@lisp +;; Draw pixels +(aset my-vec <index> <color>) +;; Reload and refresh canvas images +(canvas-refresh my-vec-canvas 'reload-data) +@end lisp + +Canvas images can be manipulated and refreshed via dynamic modules. +@xref{Module Canvas API}, for further details. @node Image Cache @subsection Image Cache diff --git a/doc/lispref/elisp.texi b/doc/lispref/elisp.texi index 2944b598178..45c110bb5a0 100644 --- a/doc/lispref/elisp.texi +++ b/doc/lispref/elisp.texi @@ -1577,6 +1577,7 @@ Images * Showing Images:: Convenient ways to display an image once it is defined. * Multi-Frame Images:: Some images contain more than one frame. +* Canvas Images:: Easily modifiable image with a pixel buffer. * Image Cache:: Internal mechanisms of image display. Buttons diff --git a/doc/lispref/internals.texi b/doc/lispref/internals.texi index 0cb3e0ccb1d..202a5cad010 100644 --- a/doc/lispref/internals.texi +++ b/doc/lispref/internals.texi @@ -1117,6 +1117,7 @@ option. @xref{Initial Options,,,emacs, The GNU Emacs Manual}. * Module Functions:: * Module Values:: * Module Misc:: +* Module Canvas API:: * Module Nonlocal:: @end menu @@ -2020,6 +2021,52 @@ done, close the file descriptor using @code{close}. @ref{Low-Level I/O,,,libc}. @end deftypefun +@node Module Canvas API +@subsection Using Canvas API With Modules + +Emacs supports creating image objects called ``canvas'' which have a +pixel buffer associated with them. These image objects can be accessed +and updated natively via dynamic modules. @xref{Canvas Images}, for what +they are. + +The following functions allow you to operate on canvas image objects +with dynamic modules: + +@anchor{canvas_data} +@deftypefn Function uint32_t *canvas_data (emacs_env *@var{env}, emacs_value @var{canvas}) +The function gives access to the pixel buffer of @var{canvas}. The +pixel buffer is in row-major order with a size @var{width} * @var{height}. +The pixel format is ARGB32 on all platforms. Return @code{NULL} in case +of error. +@end deftypefn + +Note that the pixel buffer is only valid as the canvas object is alive +and its dimensions have not been changed. If it has changed, Emacs will +create a new pixel buffer and automatically resize the canvas. + +Once we update the canvas pixel buffer via dynamic modules, we also have +to call @code{canvas-refresh} for Emacs to redisplay the canvas. This +can be done from the module via an Elisp funcall. + +@example +/* Access the pixel buffer */ +uint32_t *pixel = env->canvas_data (env, canvas); +/* Write to the pixel buffer */ +... +/* Redraw the canvas */ +env->funcall(env, env->intern(env, "canvas-refresh"), 1, &canvas); +@end example + +Note that @code{canvas-refresh} has an optional boolean argument to +reload the @code{:data} vector or string from the canvas image +specification. When called from dynamic modules, the argument should +usually be @code{nil}, thus not provided. + +Thus, one can simply define a module function that takes as an argument +a canvas image object, following the specification as laid down in +@xref{Canvas Images}, and then use the above code to manipulate the pixel +buffer of the canvas and redraw it. + @node Module Nonlocal @subsection Nonlocal Exits in Modules @cindex nonlocal exits, in modules diff --git a/etc/NEWS b/etc/NEWS index 13810f81054..a20f967b478 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -30,6 +30,8 @@ applies, and please also update docstrings as needed. * Changes in Emacs 32.1 ++++ + --- ** Emacs no longer kills child processes after EPIPE. Previously, Emacs would immediately kill a child process and set its @@ -250,6 +252,15 @@ older '(HIGH LOW USEC PSEC)' form. * Lisp Changes in Emacs 32.1 ++++ +** Support for canvas image objects. +Canvas images support efficient updating and refreshing. An image +specifier with ':type canvas' creates a canvas image. The canvas has an +associated writable pixel buffer. The dynamic module function 'canvas_data' +provides access to the pixel buffer. After changing the ':data' vector or +writing to the pixel buffer a call to 'canvas-refresh' redraws the canvas +image. + +++ ** 'ignore' is now also a place, acting as a "blackhole" like "/dev/null". E.g., '(push (new-elem) (pcase (foo) (0 var1) (1 var2) (_ (ignore))))'. diff --git a/lisp/image.el b/lisp/image.el index dbfbc266445..5f069292544 100644 --- a/lisp/image.el +++ b/lisp/image.el @@ -562,6 +562,9 @@ Images should not be larger than specified by `max-image-size'." (not (plist-get props :map))) (setq image (nconc image (list :map (image--compute-map image))))) + ;; Add unique canvas id if not already present + (when (and (eq type 'canvas) (not (plist-get props :id))) + (setq image (nconc image (list :id (gensym))))) image))) (defun image--default-smoothing (image) diff --git a/src/dispextern.h b/src/dispextern.h index 357504dd989..904746ae65e 100644 --- a/src/dispextern.h +++ b/src/dispextern.h @@ -3233,10 +3233,6 @@ struct image # if !defined USE_CAIRO && defined HAVE_XRENDER /* Picture versions of pixmap and mask for compositing. */ Picture picture, mask_picture; - - /* We need to store the original image dimensions in case we have to - call XGetImage. */ - int original_width, original_height; # endif #endif /* HAVE_X_WINDOWS */ #ifdef HAVE_ANDROID @@ -3254,9 +3250,6 @@ struct image /* The affine transformation to apply to this image. */ double transform[3][3]; - /* The original width and height of the image. */ - int original_width, original_height; - /* Whether or not bilinear filtering should be used to "smooth" the image. */ bool use_bilinear_filtering; @@ -3296,7 +3289,17 @@ struct image valid, respectively. */ bool_bf background_valid : 1, background_transparent_valid : 1; - /* Width and height of the image. */ + /* Refresh counter reflecting the current version of the image. + Always larger than zero for images which may need refreshing. + Right now it is only used by canvas images. */ + uint32_t refresh; + + /* The original width and height of the image before transformations + like scaling or rotation. */ + int original_width, original_height; + + /* Width and height of the image. These values depend on + the :scale or :rotation image parameters. */ int width, height; /* The scale factor applied to the image. */ @@ -3627,6 +3630,7 @@ extern void get_font_ascent_descent (struct font *, int *, int *); #ifdef HAVE_WINDOW_SYSTEM +extern void redraw_image_glyphs (Lisp_Object); extern void gui_get_glyph_overhangs (struct glyph *, struct frame *, int *, int *); extern struct font *font_for_underline_metrics (struct glyph_string *); @@ -3716,6 +3720,8 @@ extern void update_redisplay_ticks (int, struct window *); /* Defined in image.c */ +extern uint32_t* canvas_data (Lisp_Object); + #ifdef HAVE_WINDOW_SYSTEM extern void clear_image_cache (struct frame *, Lisp_Object); diff --git a/src/emacs-module.c b/src/emacs-module.c index ce6c292dbb5..26980008d27 100644 --- a/src/emacs-module.c +++ b/src/emacs-module.c @@ -962,6 +962,15 @@ module_vec_size (emacs_env *env, emacs_value vector) return ASIZE (lisp); } +static uint32_t* +module_canvas_data (emacs_env *env, emacs_value canvas) +{ + MODULE_FUNCTION_BEGIN (NULL); + uint32_t* data = canvas_data (value_to_lisp (canvas)); + MODULE_INTERNAL_CLEANUP (); + return data; +} + /* This function should return true if and only if maybe_quit would quit. */ static bool @@ -1612,6 +1621,7 @@ initialize_environment (emacs_env *env, struct emacs_env_private *priv) env->open_channel = module_open_channel; env->make_interactive = module_make_interactive; env->make_unibyte_string = module_make_unibyte_string; + env->canvas_data = module_canvas_data; return env; } diff --git a/src/image.c b/src/image.c index 1086f474eec..701818fa87e 100644 --- a/src/image.c +++ b/src/image.c @@ -110,6 +110,21 @@ static unsigned long image_alloc_image_color (struct frame *, struct image *, # define DONT_CREATE_TRANSFORMED_IMAGEMAGICK_IMAGE #endif +struct canvas +{ + /* Linked list of canvases, see `canvas_list'. */ + struct canvas *next; + /* Pixel memory buffer in ARGB32 format across all platforms. */ + uint32_t *data; + /* Incremented if the canvas should be redrawn. Always larger than 0. */ + uint32_t refresh; + /* Dimension of the canvas. */ + int width, height; +}; + +static void canvas_prepare_for_display (struct frame *f, struct image *img); +static void canvas_free_unused (void); + #ifdef HAVE_NTGUI /* We need (or want) w32.h only when we're _not_ compiling for Cygwin. */ @@ -221,6 +236,7 @@ static unsigned long *colors_in_color_table (int *n); #ifdef HAVE_NTGUI static HBITMAP w32_create_pixmap_from_bitmap_data (int, int, char *); +static void XPutPixel (XImage *, int, int, COLORREF); #endif @@ -1877,6 +1893,13 @@ prepare_image_for_display (struct frame *f, struct image *img) unblock_input (); } #endif + + /* Update image pixmap from canvas pixel buffer if refresh counter has + been updated. For canvases the refresh counter is always >= 1. */ + if (img->refresh + && EQ (image_spec_value (img->spec, QCtype, NULL), Qcanvas) + && img->refresh != ((struct canvas *) XFIXNUMPTR (img->lisp_data))->refresh) + canvas_prepare_for_display (f, img); } @@ -2260,6 +2283,8 @@ filter_image_spec (Lisp_Object spec) { Lisp_Object out = Qnil; + bool is_canvas = EQ (image_spec_value (spec, QCtype, NULL), Qcanvas); + /* Skip past the `image' element. */ if (CONSP (spec)) spec = XCDR (spec); @@ -2274,10 +2299,14 @@ filter_image_spec (Lisp_Object spec) spec = XCDR (spec); /* Some animation-related data doesn't affect display, but - breaks the image cache. Filter those out. */ - if (!(EQ (key, QCanimate_buffer) - || EQ (key, QCanimate_tardiness) - || EQ (key, QCanimate_position))) + breaks the image cache. Furthermore for canvases do not + check the data for equality, such that data can be changed + via mutation to refresh the canvas. Filter these keys out. + */ + if (!(is_canvas ? EQ (key, QCdata) : + (EQ (key, QCanimate_buffer) + || EQ (key, QCanimate_tardiness) + || EQ (key, QCanimate_position)))) { out = Fcons (value, out); out = Fcons (key, out); @@ -2468,6 +2497,8 @@ but the image is still displayed. */) /* Also clear the animation caches. */ image_prune_animation_caches (true); + canvas_free_unused (); + return Qnil; } @@ -3033,8 +3064,6 @@ image_set_transform (struct frame *f, struct image *img) { 0, 0, 1 }, }; - img->original_width = img->width; - img->original_height = img->height; img->use_bilinear_filtering = false; memcpy (&img->transform, identity, sizeof identity); @@ -3058,10 +3087,6 @@ image_set_transform (struct frame *f, struct image *img) # if !defined USE_CAIRO && defined HAVE_XRENDER if (!img->picture) return; - - /* Store the original dimensions as we'll overwrite them later. */ - img->original_width = img->width; - img->original_height = img->height; # endif /* Determine size. */ @@ -3364,8 +3389,16 @@ image_set_transform (struct frame *f, struct image *img) matrix[1][1], matrix[2][0], matrix[2][1]}; cairo_pattern_t *pattern = cairo_pattern_create_rgb (0, 0, 0); cairo_pattern_set_matrix (pattern, &cr_matrix); - cairo_pattern_set_filter (pattern, smoothing - ? CAIRO_FILTER_BEST : CAIRO_FILTER_NEAREST); + + /* Performance degrades with CAIRO_FILTER_BEST when using canvas, and possibly + we get HW acceleration from CAIRO_FILTER_GOOD */ + int cairo_filter; + if (EQ (image_spec_value (img->spec, QCtype, NULL), Qcanvas)) + cairo_filter = smoothing ? CAIRO_FILTER_GOOD : CAIRO_FILTER_NEAREST; + else + cairo_filter = smoothing ? CAIRO_FILTER_BEST : CAIRO_FILTER_NEAREST; + cairo_pattern_set_filter (pattern, cairo_filter); + /* Dummy solid color pattern just to record pattern matrix. */ img->cr_data = pattern; # elif defined (HAVE_XRENDER) @@ -3624,6 +3657,10 @@ lookup_image (struct frame *f, Lisp_Object spec, int face_id) } } + /* Store the original width and height before transforming the image. */ + img->original_width = img->width; + img->original_height = img->height; + /* Do image transformations and compute masks, unless we don't have the image yet. */ if (!EQ (builtin_lisp_symbol (img->type->type), Qpostscript)) @@ -5410,7 +5447,474 @@ xbm_load (struct frame *f, struct image *img) return success_p; } + +/*********************************************************************** + Canvas + ***********************************************************************/ +/* Indices of image specification fields in canvas_format, below. */ + +enum canvas_keyword_index +{ + CANVAS_TYPE, + CANVAS_ID, + CANVAS_FILE, + CANVAS_DATA, + CANVAS_WIDTH, + CANVAS_HEIGHT, + CANVAS_ASCENT, + CANVAS_MARGIN, + CANVAS_RELIEF, + CANVAS_LAST +}; + +/* Vector of image_keyword structures describing the format + of valid user-defined image specifications. */ + +static const struct image_keyword canvas_format[CANVAS_LAST] = +{ + {":type", IMAGE_SYMBOL_VALUE, 1}, + {":id", IMAGE_SYMBOL_VALUE, 1}, + {":file", IMAGE_STRING_VALUE, 0}, + {":data", IMAGE_DONT_CHECK_VALUE_TYPE, 0}, + {":data-width", IMAGE_POSITIVE_INTEGER_VALUE, 1}, + {":data-height", IMAGE_POSITIVE_INTEGER_VALUE, 1}, + {":ascent", IMAGE_ASCENT_VALUE, 0}, + {":margin", IMAGE_NON_NEGATIVE_INTEGER_VALUE_OR_PAIR, 0}, + {":relief", IMAGE_INTEGER_VALUE, 0}, +}; + +/* Weak hash map associating the image spec with the canvas object. */ + +static Lisp_Object canvas_map; + +/* Linked list of all canvases. */ + +static struct canvas* canvas_list = 0; + +/* Parse canvas specification OBJECT and return true if valid. */ + +static bool +canvas_parse (Lisp_Object object, struct image_keyword *fmt) +{ + memcpy (fmt, canvas_format, sizeof canvas_format); + + /* Check that only one of :data or :file is present. */ + if (!parse_image_spec (object, fmt, CANVAS_LAST, Qcanvas) + || fmt[CANVAS_FILE].count + fmt[CANVAS_DATA].count > 1) + return false; + + ptrdiff_t w = XFIXNAT (fmt[CANVAS_WIDTH].value); + ptrdiff_t h = XFIXNAT (fmt[CANVAS_HEIGHT].value); + + /* Check that w*h*4 does not overflow */ + return w <= INT_MAX / 4 / h; +} + +/* Return true if OBJECT is a valid canvas image specification. */ + +static bool +canvas_image_p (Lisp_Object object) +{ + struct image_keyword fmt[CANVAS_LAST]; + return canvas_parse (object, fmt); +} + +/* Clear canvas list. All canvases which are not referenced anymore in + the weak hash table canvas_map are freed. */ + +static void +canvas_free_unused (void) +{ + /* Mark all referenced canvases as used (negative width). */ + DOHASH (XHASH_TABLE (canvas_map), k, v) + ((struct canvas *)XFIXNUMPTR (v))->width *= -1; + + /* Free unreferenced canvases and remove them from the list. */ + struct canvas **p = &canvas_list; + while (*p) + { + struct canvas *c = *p; + if (c->width < 0) + { + p = &c->next; + c->width *= -1; + } + else + { + *p = c->next; + xfree (c->data); + xfree (c); + } + } +} + +/* Copy pixel data into canvas C from a parsed image keyword array FMT. + + :data must be an unibyte string of exactly 4*WIDTH*HEIGHT bytes, or a + vector of size WIDTH*HEIGHT in row-major order, where each element is + a 32 bit integer. :file names a binary file with size 4*WIDTH*HEIGHT + bytes. */ + +static void +canvas_apply_data (struct canvas *c, struct image_keyword *fmt) +{ + ptrdiff_t expected_size = (ptrdiff_t) c->width * c->height; + + Lisp_Object data = fmt[CANVAS_DATA].value; + Lisp_Object file = fmt[CANVAS_FILE].value; + + if (STRINGP (data)) /* Unibyte string data in ARGB32 format. */ + { + if (STRING_MULTIBYTE (data)) + { + image_error ("Canvas :data string must be unibyte"); + return; + } + + if (SBYTES (data) != 4 * expected_size) + { + image_error ("Canvas :data size mismatch"); + return; + } + + const uint32_t *buf = (const uint32_t *) SDATA (data); +#ifdef WORDS_BIGENDIAN + for (ptrdiff_t i = 0; i < expected_size; ++i) + c->data[i] = bswap_32 (buf[i]); +#else + for (ptrdiff_t i = 0; i < expected_size; ++i) + c->data[i] = buf[i]; +#endif + } + else if (VECTORP (data)) /* Vector of ARGB32 integers. */ + { + if (ASIZE (data) != expected_size) + { + image_error ("Canvas :data size mismatch"); + return; + } + + for (ptrdiff_t i = 0; i < expected_size; ++i) + { + Lisp_Object pixel = AREF (data, i); + if (!FIXNUMP (pixel)) + { + image_error ("Expected fixnum in the canvas :data vector"); + return; + } + c->data[i] = (uint32_t) XFIXNUM (pixel); + } + } + else if (STRINGP (file)) /* Binary file with ARGB32 data. */ + { + Lisp_Object found = image_find_image_file (file); + if (NILP (found)) + { + image_error ("Cannot find image :file to load for canvas %s", file); + return; + } + + Lisp_Object encoded = ENCODE_FILE (found); + int fd = emacs_open (SSDATA (encoded), O_RDONLY | O_BINARY, 0); + if (fd < 0) + { + image_error ("Cannot open image :file for canvas %s", file); + return; + } + + ptrdiff_t nbytes; + uint32_t *buf = (uint32_t *) slurp_file (fd, &nbytes); + emacs_close (fd); + if (!buf) + { + image_error ("Cannot read image :file for canvas %s", file); + return; + } + + if (nbytes != 4 * expected_size) + { + image_error ("Canvas :file size mismatch for %s", file); + xfree (buf); + return; + } + +#ifdef WORDS_BIGENDIAN + for (ptrdiff_t i = 0; i < expected_size; ++i) + c->data[i] = bswap_32 (buf[i]); +#else + for (ptrdiff_t i = 0; i < expected_size; ++i) + c->data[i] = buf[i]; +#endif + + xfree (buf); + } +} + +/* Get canvas object for IMAGE specification. Return NULL on error. */ + +static struct canvas* +canvas_get (Lisp_Object image, struct image_keyword *fmt) +{ + if (!canvas_parse (image, fmt)) + { + image_error ("Not a canvas image specification"); + return NULL; + } + + Lisp_Object canvas_ptr = Fgethash (image, canvas_map, Qnil); + struct canvas* c = NILP (canvas_ptr) ? 0 : XFIXNUMPTR (canvas_ptr); + int width = XFIXNAT (fmt[CANVAS_WIDTH].value), + height = XFIXNAT (fmt[CANVAS_HEIGHT].value); + + if (!c) + { + /* Free old canvases now, when allocating a new one, to keep + memory usage low. */ + canvas_free_unused (); + + c = xzalloc (sizeof (struct canvas)); + c->refresh = 2; /* 2 in order to enforce first refresh */ + c->width = width; + c->height = height; + c->data = xzalloc (4 * width * height); + + /* Register the canvas in the list and the map. */ + c->next = canvas_list; + canvas_list = c; + Fputhash (image, make_pointer_integer (c), canvas_map); + + /* Initialize pixel buffer from :data or :file if supplied. */ + canvas_apply_data (c, fmt); + } + else if (c->width != width || c->height != height) + { + /* Resize canvas. */ + c->width = width; + c->height = height; + c->data = xrealloc (c->data, 4 * width * height); + memset (c->data, 0, 4 * width * height); + + /* Initialize pixel buffer from :data or :file if supplied. */ + canvas_apply_data (c, fmt); + } + + return c; +} + +/* Create canvas IMG in frame F. Value is true if successful. */ + +static bool +canvas_load (struct frame *f, struct image *img) +{ + struct image_keyword fmt[CANVAS_LAST]; + struct canvas* c = canvas_get (img->spec, fmt); + + if (!c) + return false; + + img->lisp_data = make_pointer_integer (c); + img->refresh = 1; /* refresh is always > 0 for canvas images */ + img->width = c->width; + img->height = c->height; + img->background_valid = 1; + img->background_transparent_valid = 1; + + Emacs_Pix_Container ximg; + if (!image_create_x_image_and_pixmap (f, img, c->width, c->height, 0, &ximg, 0)) + return false; + + image_put_x_image (f, img, ximg, 0); + + return true; +} + +/* Prepare image IMG from canvas for display. */ + +static void +canvas_prepare_for_display (struct frame *f, struct image *img) +{ + struct canvas *c = XFIXNUMPTR (img->lisp_data); + img->refresh = c->refresh; + uint32_t* src = c->data; + int width = c->width, height = c->height; + + /* If canvas has been resized in the meantime and the image is stale, + mark frame as garbaged and wait for redisplay. See also + uncache_image which handles stale images. */ + if (width != img->original_width || height != img->original_height) + { + SET_FRAME_GARBAGED (f); + return; + } + + block_input (); + +#ifdef USE_CAIRO + /* Cairo: Optimized canvas reloading. Reuse the existing Cairo surface. */ + cairo_surface_t* surface; + if (img->cr_data + /* prepare_image_for_display ensures that cr_data is a surface pattern */ + && cairo_pattern_get_type (img->cr_data) == CAIRO_PATTERN_TYPE_SURFACE + && !cairo_pattern_get_surface (img->cr_data, &surface)) + { + cairo_surface_flush (surface); + int stride = cairo_image_surface_get_stride (surface); + unsigned char *dst = cairo_image_surface_get_data (surface); + /* Alpha channel is preserved here. Potentially preserve it when + drawing the image in x_draw_image_glyph_string? */ + if (stride == 4 * width) /* Fast path */ + { + memcpy (dst, src, stride * height); + } + else + { + for (int y = 0; y < height; ++y) + memcpy (dst + (y * stride), src + (y * width), 4 * width); + } + cairo_surface_mark_dirty (surface); + } +#elif defined HAVE_X_WINDOWS + /* X11: Optimized canvas reloading. Reuse the existing pixmap. */ + int depth = FRAME_DISPLAY_INFO (f)->n_planes; + XImage *ximg = XCreateImage (FRAME_X_DISPLAY (f), FRAME_X_VISUAL (f), + depth, ZPixmap, 0, NULL, width, height, + depth > 16 ? 32 : depth > 8 ? 16 : 8, 0); + if (ximg) + { + ximg->data = xmalloc (ximg->bytes_per_line * height); + for (int y = 0; y < height; ++y) + { + for (int x = 0; x < width; ++x) + { + uint32_t c = src[y * width + x], + r = (c >> 16) & 255, + g = (c >> 8) & 255, + b = c & 255; + PUT_PIXEL (ximg, x, y, lookup_rgb_color (f, r << 8, g << 8, b << 8)); + } + } + gui_put_x_image (f, ximg, img->pixmap, width, height); + x_destroy_x_image (ximg); + } +#elif defined HAVE_ANDROID + /* Android: Optimized canvas reloading. Reuse the existing pixmap. */ + struct android_image *ximg = android_create_image (FRAME_DISPLAY_INFO (f)->n_planes, + ANDROID_Z_PIXMAP, NULL, width, height); + if (ximg) + { + ximg->data = xmalloc (ximg->bytes_per_line * height); + for (int y = 0; y < height; ++y) + for (int x = 0; x < width; ++x) + PUT_PIXEL (ximg, x, y, src[y * width + x] & 0x00FFFFFF); + gui_put_x_image (f, ximg, img->pixmap, width, height); + image_destroy_x_image (ximg); + } +#elif defined HAVE_NS + /* MacOS: Recreates and fills the pixmap */ + img->pixmap = ns_image_reset(img->pixmap, width, height); + for (int y = 0; y < height; ++y) + for (int x = 0; x < width; ++x) + PUT_PIXEL (img->pixmap, x, y, src[y * width + x]); +#else + /* Platform independent canvas reloading. Less efficient, since it recreates images and pixmaps. */ + FRAME_TERMINAL (f)->free_pixmap (f, img->pixmap); + img->pixmap = NO_PIXMAP; + Emacs_Pix_Container ximg; + if (image_create_x_image_and_pixmap (f, img, width, height, 0, &ximg, 0)) + { + for (int y = 0; y < height; ++y) + for (int x = 0; x < width; ++x) + { + uint32_t c = src[y * width + x], + r = (c >> 16) & 255, + g = (c >> 8) & 255, + b = c & 255; + PUT_PIXEL (ximg, x, y, lookup_rgb_color (f, r << 8, g << 8, b << 8)); + } + image_put_x_image (f, img, ximg, 0); + } +#endif + + unblock_input (); +} + +/* Access canvas buffer. Note that the pixel buffer + is valid only as long as its dimensions have not been changed.*/ + +uint32_t* +canvas_data (Lisp_Object image) +{ + struct image_keyword fmt[CANVAS_LAST]; + struct canvas* c = canvas_get (image, fmt); + + if (!c) + error ("Not a canvas"); + + return c->data; +} + +DEFUN ("canvas-refresh", Fcanvas_refresh, Scanvas_refresh, 1, 2, 0, + doc: /* Refresh canvas IMAGE and update it on screen. + +If RELOAD-DATA is non-nil, reload the :data from the image +specification. + +`redisplay' must be called after `canvas-refresh' such that the updated +canvas is displayed even if double buffering is enabled. It can be +updated in a loop. + +Example: + + (setq canvas (create-image (make-vector (* 100 100) 0) 'canvas t + :data-width 100 :data-height 100)) + (insert (propertize "#" 'display canvas)) + (dotimes (i (* 100 100)) + (aset (plist-get (cdr canvas) :data) i #xFF) + (canvas-refresh canvas 'reload-data) + (redisplay)) + +When `canvas-refresh' is called from a timer or a command, `redisplay' +will be called implicitly after the timer or command. + +Example: + + (let ((i 0)) + (run-at-time nil 0.03 (lambda () + (when (< i (* 100 100)) + (aset (plist-get (cdr canvas) :data) i #xFF) + (incf i) + (canvas-refresh canvas 'reload-data))))) */) + (Lisp_Object image, Lisp_Object reload_data) +{ + struct image_keyword fmt[CANVAS_LAST]; + struct canvas* c = canvas_get (image, fmt); + + if (!c) + error ("Not a canvas"); + + /* Reload :data or :file from the image specification. */ + if (!NILP (reload_data)) + canvas_apply_data (c, fmt); + + /* Increment refresh counter; reset to one on overflow, since the + refresh counter must always be greater than zero. */ + if (++c->refresh == 0) + c->refresh = 1; + +#ifdef HAVE_WINDOW_SYSTEM + /* Redraw all image glyphs. */ + block_input (); + redraw_image_glyphs (image); + /* We do not call `redisplay' or `flush_frame' here. This means the + canvas images are not updated immediately on screen, since the + double buffer won't be flipped immediately. The next call to + `redisplay' will flip the double buffer. */ + unblock_input (); +#endif + + return Qnil; +} /*********************************************************************** @@ -12977,6 +13481,7 @@ static struct image_type const image_types[] = #endif { SYMBOL_INDEX (Qxbm), xbm_image_p, xbm_load, image_clear_image }, { SYMBOL_INDEX (Qpbm), pbm_image_p, pbm_load, image_clear_image }, + { SYMBOL_INDEX (Qcanvas), canvas_image_p, canvas_load, image_clear_image }, }; #if HAVE_NATIVE_IMAGE_API @@ -13127,6 +13632,13 @@ non-numeric, there is no explicit limit on the size of images. */); ); #endif + DEFSYM (Qcanvas, "canvas"); + add_image_type (Qcanvas); + + canvas_map = make_hash_table (&hashtest_eq, DEFAULT_HASH_SIZE, Weak_Key); + staticpro (&canvas_map); + defsubr (&Scanvas_refresh); + DEFSYM (Qpbm, "pbm"); add_image_type (Qpbm); diff --git a/src/module-env-32.h b/src/module-env-32.h index 8051066bf88..3ab37f9001b 100644 --- a/src/module-env-32.h +++ b/src/module-env-32.h @@ -1,3 +1,12 @@ /* Add module environment functions newly added in Emacs 32 here. Before Emacs 32 is released, remove this comment and start module-env-33.h on master (see admin/release-branch.txt). */ + + /* Get pointer to the pixel buffer of CANVAS. The buffer is in row + major order and has the size width * height. The pixel format is + ARGB32 on all platforms. The pointer will be valid as long as + CANVAS is alive, and as long as its dimensions have not been + changed. Return NULL in case of error. */ + + uint32_t* (*canvas_data) (emacs_env *env, emacs_value canvas) + EMACS_ATTRIBUTE_NONNULL(1); diff --git a/src/nsimage.m b/src/nsimage.m index 426ce20eb05..f396d424ffe 100644 --- a/src/nsimage.m +++ b/src/nsimage.m @@ -256,6 +256,13 @@ Updated by Christian Limpach ([email protected]) [(EmacsImage *)img setAlphaAtX: x Y: y to: a]; } +void * +ns_image_reset (void *img, int width, int height) +{ + [(EmacsImage *)img release]; + return ns_image_for_XPM (width, height, 32); +} + size_t ns_image_size_in_bytes (void *img) { diff --git a/src/nsterm.h b/src/nsterm.h index b21dd519e4b..69f565d51f9 100644 --- a/src/nsterm.h +++ b/src/nsterm.h @@ -1215,6 +1215,7 @@ extern void ns_image_set_smoothing (void *img, bool smooth); extern unsigned long ns_get_pixel (void *img, int x, int y); extern void ns_put_pixel (void *img, int x, int y, unsigned long argb); extern void ns_set_alpha (void *img, int x, int y, unsigned char a); +extern void *ns_image_reset (void *img, int width, int height); extern int ns_display_pixel_height (struct ns_display_info *); extern int ns_display_pixel_width (struct ns_display_info *); diff --git a/src/xdisp.c b/src/xdisp.c index caea8e77a9d..908575fe552 100644 --- a/src/xdisp.c +++ b/src/xdisp.c @@ -32824,6 +32824,77 @@ append_stretch_glyph (struct it *it, Lisp_Object object, IT_EXPAND_MATRIX_WIDTH (it, area); } +static void +redraw_image_glyphs_window (struct window *w, Lisp_Object spec) +{ + if (w->current_matrix == NULL) + return; + + struct frame* f = WINDOW_XFRAME (w); + + if (w->must_be_updated_p) + { + SET_FRAME_GARBAGED (f); + return; + } + + for (int y = 0; y < w->current_matrix->nrows; ++y) + { + struct glyph_row *row = w->current_matrix->rows + y; + if (row->enabled_p) + { + int start = row->x; + for (int x = 0; x < row->used[TEXT_AREA]; ++x) + { + struct glyph *glyph = row->glyphs[TEXT_AREA] + x; + if (glyph->type == IMAGE_GLYPH) + { + struct image* img = IMAGE_OPT_FROM_ID (f, glyph->u.img_id); + if (img && EQ (img->spec, spec)) + { + prepare_image_for_display (f, img); + draw_glyphs (w, start, row, TEXT_AREA, x, x + 1, DRAW_NORMAL_TEXT, 0); + } + } + start += glyph->pixel_width; + } + } + } +} + +static void +redraw_image_glyphs_window_tree (struct window *w, Lisp_Object spec) +{ + while (w) + { + if (WINDOWP (w->contents)) + redraw_image_glyphs_window_tree (XWINDOW (w->contents), spec); + else + redraw_image_glyphs_window (w, spec); + w = NILP (w->next) ? NULL : XWINDOW (w->next); + } +} + +/* redraw_image_glyphs: Redraw only the image glyphs. Image redrawing is similar to the + handling of Expose or GraphicsExpose events in xterm.c. + GraphicsExpose event + -> expose_frame -> expose_window_tree -> expose_window + -> expose_line -> expose_area -> draw_glyphs */ + +void +redraw_image_glyphs (Lisp_Object spec) +{ + Lisp_Object tail, frame; + FOR_EACH_FRAME (tail, frame) + { + /* When the frame is garbaged, wait for full redisplay. Only use + the fast path when the frame is in a consistent state. */ + struct frame* f = XFRAME (frame); + if (!FRAME_GARBAGED_P (f)) + redraw_image_glyphs_window_tree (XWINDOW (f->root_window), spec); + } +} + #endif /* HAVE_WINDOW_SYSTEM */ /* Produce a stretch glyph for iterator IT. IT->object is the value diff --git a/test/src/emacs-module-resources/mod-test.c b/test/src/emacs-module-resources/mod-test.c index 3dabca1aa43..68e05eceed7 100644 --- a/test/src/emacs-module-resources/mod-test.c +++ b/test/src/emacs-module-resources/mod-test.c @@ -752,6 +752,118 @@ Fmod_test_make_string (emacs_env *env, ptrdiff_t nargs, return ret; } +/* djb2 hash over all the pixels */ + +static uint32_t +canvas_hash (uint32_t *buf, int width, int height) +{ + uint32_t hash = 5381; + + for (int i = 0; i < width * height; i++) + hash = hash * 33 ^ buf[i]; + + return hash; +} + +static emacs_value +Fmod_test_canvas_read (emacs_env *env, ptrdiff_t nargs, + emacs_value *args, void *data) +{ + assert (nargs == 3); + uint32_t* buf = env->canvas_data (env, args[0]); + + if (!buf) + { + signal_error(env, "Not a valid canvas"); + return env->intern (env, "nil"); + } + + int width = (int) env->extract_integer (env, args[1]); + int height = (int) env->extract_integer (env, args[2]); + + /* Validate dimensions against the canvas image spec. */ + emacs_value Qimage_property = env->intern (env, "image-property"); + emacs_value Qdata_width = env->intern (env, ":data-width"); + emacs_value Qdata_height = env->intern (env, ":data-height"); + emacs_value iw_val = env->funcall (env, Qimage_property, 2, + (emacs_value[]){args[0], Qdata_width}); + emacs_value ih_val = env->funcall (env, Qimage_property, 2, + (emacs_value[]){args[0], Qdata_height}); + int iw = (int) env->extract_integer (env, iw_val); + int ih = (int) env->extract_integer (env, ih_val); + + if (width != iw || height != ih) + { + signal_error (env, "Canvas size mismatch"); + return env->intern (env, "nil"); + } + + uint32_t hash = canvas_hash (buf, width, height); + + return env->make_integer (env, (intmax_t) hash); +} + +static emacs_value +Fmod_test_canvas_write (emacs_env *env, ptrdiff_t nargs, + emacs_value *args, void *data) +{ + assert (nargs == 3); + uint32_t* buf = env->canvas_data (env, args[0]); + + if (!buf) + { + signal_error(env, "Not a valid canvas"); + return env->intern (env, "nil"); + } + + int width = (int) env->extract_integer (env, args[1]); + int height = (int) env->extract_integer (env, args[2]); + + /* Validate dimensions against the canvas image spec. */ + emacs_value Qimage_property = env->intern (env, "image-property"); + emacs_value Qdata_width = env->intern (env, ":data-width"); + emacs_value Qdata_height = env->intern (env, ":data-height"); + emacs_value iw_val = env->funcall (env, Qimage_property, 2, + (emacs_value[]){args[0], Qdata_width}); + emacs_value ih_val = env->funcall (env, Qimage_property, 2, + (emacs_value[]){args[0], Qdata_height}); + int iw = (int) env->extract_integer (env, iw_val); + int ih = (int) env->extract_integer (env, ih_val); + + if (width != iw || height != ih) + { + signal_error (env, "Canvas size mismatch"); + return env->intern (env, "nil"); + } + + for (int i = 0; i < width * height; i++) + buf[i] = ~buf[i]; // invert the pixels + + emacs_value canvas_args[2] = {args[0], env->intern (env, "nil")}; + env->funcall (env, env->intern (env, "canvas-refresh"), 2, canvas_args); + + return env->intern (env, "t"); +} + +static emacs_value +Fmod_test_canvas_invalid (emacs_env *env, ptrdiff_t nargs, + emacs_value *args, void *data) +{ + assert (nargs == 1); + uint32_t *buf = env->canvas_data (env, args[0]); + + if (env->non_local_exit_check (env) != emacs_funcall_exit_return) + env->non_local_exit_clear (env); // don't propagate the error to ERT + + if (buf) + { + signal_error (env, "Expected invalid canvas, but canvas_data returned non-NULL"); + return env->intern (env, "nil"); + } + + return env->intern (env, "t"); +} + /* Lisp utilities for easier readability (simple wrappers). */ /* Provide FEATURE to Emacs. */ @@ -853,6 +965,9 @@ emacs_module_init (struct emacs_runtime *ert) DEFUN ("mod-test-funcall", Fmod_test_funcall, 1, emacs_variadic_function, NULL, NULL); DEFUN ("mod-test-make-string", Fmod_test_make_string, 2, 2, NULL, NULL); + DEFUN ("mod-test-canvas-read", Fmod_test_canvas_read, 3, 3, NULL, NULL); + DEFUN ("mod-test-canvas-write", Fmod_test_canvas_write, 3, 3, NULL, NULL); + DEFUN ("mod-test-canvas-invalid", Fmod_test_canvas_invalid, 1, 1, NULL, NULL); #undef DEFUN diff --git a/test/src/emacs-module-tests.el b/test/src/emacs-module-tests.el index e678fba3873..c10029b5fd2 100644 --- a/test/src/emacs-module-tests.el +++ b/test/src/emacs-module-tests.el @@ -588,4 +588,121 @@ See Bug#36226." (should (string-equal first second)) (should-not (eq first second)))))) +;;; Canvas tests + +(defun test-canvas-gen-file (width height pixel) + (let* ((bytes (unibyte-string (logand pixel #xff) + (logand (ash pixel -8) #xff) + (logand (ash pixel -16) #xff) + (logand (ash pixel -24) #xff))) + (coding-system-for-write 'binary)) + (with-temp-file "data/image/canvas-argb" + (set-buffer-multibyte nil) + (dotimes (_ (* width height)) + (insert bytes))) + t)) + +(ert-deftest mod-test-canvas/valid () + (let* ((width 128) (height 215) + (canvas `(image :type canvas :id test-canvas + :data-width ,width :data-height ,height)) + (hash-before (mod-test-canvas-read canvas width height))) + (should (integerp hash-before)) + (should (mod-test-canvas-write canvas width height)) + (let ((hash-after (mod-test-canvas-read canvas width height))) + (should (integerp hash-after)) + (should (not (eql hash-before hash-after)))))) + +(ert-deftest mod-test-canvas/invalid () + (should-error (mod-test-canvas-read nil nil nil)) + (should-error (mod-test-canvas-write nil nil nil)) + (should (mod-test-canvas-invalid nil)) + (should (mod-test-canvas-invalid '(image :type xbm :data ""))) + (should-error (mod-test-canvas-read '(image :type canvas) 256 527)) + (should-error (mod-test-canvas-write '(image :type canvas) 256 527))) + +(ert-deftest mod-test-canvas/vector () + (let* ((width 327) (height 98) + (canvas `(image :type canvas :id test-canvas + :data-width ,width :data-height ,height + :data ,(make-vector (* width height) #xFFFF0000))) + (hash-before (mod-test-canvas-read canvas width height))) + (should (integerp hash-before)) + (should (mod-test-canvas-write canvas width height)) + (should (not (eql (mod-test-canvas-read canvas width height) hash-before)))) + ;; Mismatched sizes: passing wrong width/height should error. + (let* ((width 327) (height 98) + (canvas `(image :type canvas :id test-canvas + :data-width ,width :data-height ,height + :data ,(make-vector (* width height) #xFFFF0000)))) + (should-error (mod-test-canvas-read canvas 501 72)) + (should-error (mod-test-canvas-write canvas 187 210)))) + +(ert-deftest mod-test-canvas/vector-reload () + (let* ((width 198) (height 720) + (test-vector (make-vector (* width height) #xFFFF0000)) + (canvas `(image :type canvas :id test-canvas + :data-width ,width :data-height ,height + :data ,test-vector)) + (hash-initial (mod-test-canvas-read canvas width height))) + (should (integerp hash-initial)) + (dotimes (i 50) + (aset test-vector i #xFFFFFFFF)) + (canvas-refresh canvas t) + (let ((hash-mutated (mod-test-canvas-read canvas width height))) + (should (integerp hash-mutated)) + (should (not (eql hash-initial hash-mutated)))))) + +(ert-deftest mod-test-canvas/unibyte () + (let* ((width 458) (height 278) + (pixel (unibyte-string #xFF #x80 #x40 #x80)) + (string-data (apply #'concat (make-list (* width height) pixel))) + (canvas `(image :type canvas :id test-canvas + :data-width ,width :data-height ,height + :data ,string-data)) + (hash-before (mod-test-canvas-read canvas width height))) + (should (integerp hash-before)) + (should (mod-test-canvas-write canvas width height)) + (should (not (eql (mod-test-canvas-read canvas width height) hash-before)))) + ;; Mismatched sizes: passing wrong width/height should error. + (let* ((width 458) (height 278) + (pixel (unibyte-string #xFF #x80 #x40 #x80)) + (string-data (apply #'concat (make-list (* width height) pixel))) + (canvas `(image :type canvas :id test-canvas + :data-width ,width :data-height ,height + :data ,string-data))) + (should-error (mod-test-canvas-read canvas 76 38)) + (should-error (mod-test-canvas-write canvas 378 453)))) + +(ert-deftest mod-test-canvas/file () + ;; Generate the canvas data file + (test-canvas-gen-file 128 98 #x80800000) + (let* ((width 128) (height 98) + (canvas (create-image "../data/image/canvas-argb" + 'canvas nil + :data-width width :data-height height)) + (hash-before (mod-test-canvas-read canvas width height))) + (should (integerp hash-before)) + (should (mod-test-canvas-write canvas width height)) + (should (not (eql (mod-test-canvas-read canvas width height) hash-before)))) + ;; Mismatched sizes: passing wrong width/height should error. + (let ((canvas (create-image "../data/image/canvas-argb" + 'canvas nil + :data-width 128 :data-height 98))) + (should-error (mod-test-canvas-read canvas 28 76)) + (should-error (mod-test-canvas-write canvas 398 712)))) + +(ert-deftest mod-test-canvas/gc-stress () + "Allocate canvases in batches with GC between batches. +Verifies that canvas pixel buffers are freed correctly and do not +cause use-after-free crashes or GC assertion failures." + (dotimes (_ 10) + (dotimes (x 20) + (let* ((canvas `(image :type canvas :id test-canvas + :data-width ,(+ x 10) + :data-height ,(+ x 20))) + (hash (mod-test-canvas-read canvas (+ x 10) (+ x 20)))) + (should (integerp hash)))) + (garbage-collect))) + ;;; emacs-module-tests.el ends here -- 2.47.3