mirror of
https://github.com/kovidgoyal/kitty.git
synced 2026-02-01 11:34:59 +01:00
This was needed to fix various corner cases when doing blending of colors in linear space. The new architecture has the same performance as the old in the common case of opaque rendering with no UI layers or images. In the case of only positive z-index images there is a performance decrease as the OS Window is now rendered to a offscreen texture and then blitted to screen. However, in the future when we move to Vulkan or I can figure out how to get Wayland to accept buffers with colors in linear space, this performance penalty can be removed. The performance penalty was not significant on my system but this is highly GPU dependent. Modern GPUs are supposedly optimised for rendering to offscreen buffers, so we will see. The awrit project might be a good test case. Now either we have 1-shot rendering for the case of opaque with only ext or all the various pieces are rendered in successive draw calls into an offscreen buffer that is blitted to the output buffer after all drawing is done. Fixes #8869
2475 lines
103 KiB
C
2475 lines
103 KiB
C
/*
|
|
* graphics.c
|
|
* Copyright (C) 2017 Kovid Goyal <kovid at kovidgoyal.net>
|
|
*
|
|
* Distributed under terms of the GPL3 license.
|
|
*/
|
|
|
|
#define GRAPHICS_INTERNAL_APIS
|
|
#include "graphics.h"
|
|
#include "state.h"
|
|
#include "disk-cache.h"
|
|
#include "iqsort.h"
|
|
#include "safe-wrappers.h"
|
|
|
|
#include <sys/types.h>
|
|
#include <sys/stat.h>
|
|
#include <fcntl.h>
|
|
#include <sys/mman.h>
|
|
#include <stdlib.h>
|
|
|
|
#include <zlib.h>
|
|
#include <structmember.h>
|
|
#include "png-reader.h"
|
|
PyTypeObject GraphicsManager_Type;
|
|
|
|
#define DEFAULT_STORAGE_LIMIT 320u * (1024u * 1024u)
|
|
#define REPORT_ERROR(...) { log_error(__VA_ARGS__); }
|
|
#define RAII_CoalescedFrameData(name, initializer) __attribute__((cleanup(cfd_free))) CoalescedFrameData name = initializer
|
|
|
|
// caching {{{
|
|
#define member_size(type, member) sizeof(((type *)0)->member)
|
|
#define CACHE_KEY_BUFFER_SIZE (member_size(ImageAndFrame, image_id) + member_size(ImageAndFrame, frame_id))
|
|
|
|
static size_t
|
|
cache_key(const ImageAndFrame x, char *key) {
|
|
memcpy(key, &x.image_id, sizeof(x.image_id));
|
|
memcpy(key + sizeof(x.image_id), &x.frame_id, sizeof(x.frame_id));
|
|
return CACHE_KEY_BUFFER_SIZE;
|
|
}
|
|
#define CK(x) key, cache_key(x, key)
|
|
|
|
static bool
|
|
add_to_cache(GraphicsManager *self, const ImageAndFrame x, const void *data, const size_t sz) {
|
|
char key[CACHE_KEY_BUFFER_SIZE];
|
|
return add_to_disk_cache(self->disk_cache, CK(x), data, sz);
|
|
}
|
|
|
|
static bool
|
|
remove_from_cache(GraphicsManager *self, const ImageAndFrame x) {
|
|
char key[CACHE_KEY_BUFFER_SIZE];
|
|
return remove_from_disk_cache(self->disk_cache, CK(x));
|
|
}
|
|
|
|
static bool
|
|
read_from_cache(const GraphicsManager *self, const ImageAndFrame x, void **data, size_t *sz) {
|
|
char key[CACHE_KEY_BUFFER_SIZE];
|
|
return read_from_disk_cache_simple(self->disk_cache, CK(x), data, sz, false);
|
|
}
|
|
|
|
static size_t
|
|
cache_size(const GraphicsManager *self) { return disk_cache_total_size(self->disk_cache); }
|
|
#undef CK
|
|
// }}}
|
|
|
|
|
|
static inline id_type
|
|
next_id(id_type *counter) {
|
|
id_type ans = ++(*counter);
|
|
if (UNLIKELY(ans == 0)) ans = ++(*counter);
|
|
return ans;
|
|
}
|
|
static const unsigned PARENT_DEPTH_LIMIT = 8;
|
|
|
|
GraphicsManager*
|
|
grman_alloc(bool for_paused_rendering) {
|
|
GraphicsManager *self = (GraphicsManager *)GraphicsManager_Type.tp_alloc(&GraphicsManager_Type, 0);
|
|
self->render_data.capacity = 64;
|
|
self->render_data.item = calloc(self->render_data.capacity, sizeof(self->render_data.item[0]));
|
|
self->storage_limit = DEFAULT_STORAGE_LIMIT;
|
|
if (self->render_data.item == NULL) {
|
|
PyErr_NoMemory();
|
|
Py_CLEAR(self); return NULL;
|
|
}
|
|
if (!for_paused_rendering) {
|
|
self->disk_cache = create_disk_cache();
|
|
if (!self->disk_cache) { Py_CLEAR(self); return NULL; }
|
|
}
|
|
vt_init(&self->images_by_internal_id);
|
|
return self;
|
|
}
|
|
|
|
#define iter_refs(img) vt_create_for_loop(ref_map_itr, i, &((img)->refs_by_internal_id))
|
|
|
|
static void
|
|
free_refs_data(Image *img) {
|
|
iter_refs(img) free(i.data->val);
|
|
vt_cleanup(&img->refs_by_internal_id);
|
|
}
|
|
|
|
static void
|
|
free_load_data(LoadData *ld) {
|
|
free(ld->buf); ld->buf_used = 0; ld->buf_capacity = 0; ld->buf = NULL;
|
|
if (ld->mapped_file) munmap(ld->mapped_file, ld->mapped_file_sz);
|
|
ld->mapped_file = NULL; ld->mapped_file_sz = 0;
|
|
ld->loading_for = (const ImageAndFrame){0};
|
|
}
|
|
|
|
static void*
|
|
clear_texture_ref(TextureRef **x) {
|
|
if (*x) {
|
|
if ((*x)->refcnt < 2) {
|
|
if ((*x)->id) free_texture(&(*x)->id);
|
|
free(*x); *x = NULL;
|
|
} else (*x)->refcnt--;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static TextureRef*
|
|
incref_texture_ref(TextureRef *ref) {
|
|
if (ref) ref->refcnt++;
|
|
return ref;
|
|
}
|
|
|
|
static TextureRef*
|
|
new_texture_ref(void) {
|
|
TextureRef *ans = calloc(1, sizeof(TextureRef));
|
|
if (!ans) fatal("Out of memory allocating a TextureRef");
|
|
ans->refcnt = 1;
|
|
return ans;
|
|
}
|
|
|
|
static uint32_t
|
|
texture_id_for_img(Image *img) {
|
|
return img->texture ? img->texture->id : 0;
|
|
}
|
|
|
|
static void
|
|
free_image_resources(GraphicsManager *self, Image *img) {
|
|
clear_texture_ref(&img->texture);
|
|
if (self->disk_cache) {
|
|
ImageAndFrame key = { .image_id=img->internal_id, .frame_id = img->root_frame.id };
|
|
if (!remove_from_cache(self, key) && PyErr_Occurred()) PyErr_Print();
|
|
for (unsigned i = 0; i < img->extra_framecnt; i++) {
|
|
key.frame_id = img->extra_frames[i].id;
|
|
if (!remove_from_cache(self, key) && PyErr_Occurred()) PyErr_Print();
|
|
}
|
|
}
|
|
if (img->extra_frames) {
|
|
free(img->extra_frames);
|
|
img->extra_frames = NULL;
|
|
}
|
|
free_refs_data(img);
|
|
self->used_storage = img->used_storage <= self->used_storage ? self->used_storage - img->used_storage : 0;
|
|
}
|
|
|
|
static void
|
|
free_image(GraphicsManager *self, Image *img) {
|
|
free_image_resources(self, img);
|
|
free(img);
|
|
}
|
|
|
|
#define iter_images(grman) vt_create_for_loop(image_map_itr, i, &((grman)->images_by_internal_id))
|
|
|
|
static void
|
|
free_all_images(GraphicsManager *self) {
|
|
iter_images(self) free_image(self, i.data->val);
|
|
vt_cleanup(&self->images_by_internal_id);
|
|
}
|
|
|
|
static void
|
|
dealloc(GraphicsManager* self) {
|
|
free_all_images(self);
|
|
free(self->render_data.item);
|
|
Py_CLEAR(self->disk_cache);
|
|
Py_TYPE(self)->tp_free((PyObject*)self);
|
|
}
|
|
|
|
static Image*
|
|
img_by_internal_id(const GraphicsManager *self, id_type id) {
|
|
image_map_itr i = vt_get((image_map*)&self->images_by_internal_id, id);
|
|
return vt_is_end(i) ? NULL : i.data->val;
|
|
}
|
|
|
|
static Image*
|
|
img_by_client_id(const GraphicsManager *self, uint32_t id) {
|
|
iter_images(((GraphicsManager*)self)) if (i.data->val->client_id == id) return i.data->val;
|
|
return NULL;
|
|
}
|
|
|
|
static Image*
|
|
img_by_client_number(const GraphicsManager *self, uint32_t number) {
|
|
// get the newest image with the specified number
|
|
Image *ans = NULL;
|
|
iter_images(((GraphicsManager*)self)) {
|
|
Image *img = i.data->val;
|
|
if (img->client_number == number && (!ans || img->internal_id > ans->internal_id)) ans = img;
|
|
}
|
|
return ans;
|
|
}
|
|
|
|
static ImageRef*
|
|
ref_by_internal_id(const Image *img, id_type id) {
|
|
ref_map_itr i = vt_get(&((Image *)img)->refs_by_internal_id, id);
|
|
return vt_is_end(i) ? NULL : i.data->val;
|
|
}
|
|
|
|
|
|
static ImageRef*
|
|
ref_by_client_id(const Image *img, uint32_t id) {
|
|
iter_refs((Image*)img) if (i.data->val->client_id == id) return i.data->val;
|
|
return NULL;
|
|
}
|
|
|
|
static void
|
|
set_layers_dirty(GraphicsManager *self) {
|
|
self->layers_dirty = true;
|
|
}
|
|
|
|
static image_map_itr
|
|
remove_image_itr(GraphicsManager *self, image_map_itr i) {
|
|
free_image(self, i.data->val);
|
|
set_layers_dirty(self);
|
|
return vt_erase_itr(&self->images_by_internal_id, i);
|
|
}
|
|
|
|
static void
|
|
remove_image(GraphicsManager *self, Image *img) {
|
|
image_map_itr i = vt_get(&self->images_by_internal_id, img->internal_id);
|
|
if (!vt_is_end(i)) remove_image_itr(self, i);
|
|
}
|
|
|
|
static void
|
|
remove_images(GraphicsManager *self, bool(*predicate)(Image*), id_type skip_image_internal_id) {
|
|
for (image_map_itr i = vt_first(&self->images_by_internal_id); !vt_is_end(i);) {
|
|
Image *img = i.data->val;
|
|
if (img->internal_id != skip_image_internal_id && predicate(img)) i = remove_image_itr(self, i);
|
|
else i = vt_next(i);
|
|
}
|
|
}
|
|
|
|
void
|
|
grman_pause_rendering(GraphicsManager *self, GraphicsManager *dest) {
|
|
make_window_context_current(dest->window_id);
|
|
free_all_images(dest);
|
|
dest->render_data.count = 0;
|
|
if (self == NULL) return;
|
|
dest->window_id = self->window_id;
|
|
dest->layers_dirty = true;
|
|
dest->last_scrolled_by = 0;
|
|
|
|
iter_images(self) {
|
|
Image *clone = calloc(1, sizeof(Image)), *img = i.data->val;
|
|
if (!clone) continue;
|
|
memcpy(clone, img, sizeof(*clone));
|
|
memset(&clone->refs_by_internal_id, 0, sizeof(clone->refs_by_internal_id));
|
|
vt_init(&clone->refs_by_internal_id);
|
|
clone->extra_frames = NULL;
|
|
iter_refs(img) {
|
|
ImageRef *cr = malloc(sizeof(ImageRef));
|
|
if (cr) {
|
|
memcpy(cr, i.data->val, sizeof(*cr));
|
|
vt_insert(&clone->refs_by_internal_id, cr->internal_id, cr);
|
|
}
|
|
}
|
|
clone->texture = incref_texture_ref(img->texture);
|
|
vt_insert(&dest->images_by_internal_id, clone->internal_id, clone);
|
|
}
|
|
}
|
|
|
|
// Loading image data {{{
|
|
|
|
static bool
|
|
trim_predicate(Image *img) {
|
|
return !img->root_frame_data_loaded || !vt_size(&img->refs_by_internal_id);
|
|
}
|
|
|
|
static void
|
|
apply_storage_quota(GraphicsManager *self, size_t storage_limit, id_type currently_added_image_internal_id) {
|
|
// First remove unreferenced images, even if they have an id
|
|
remove_images(self, trim_predicate, currently_added_image_internal_id);
|
|
if (self->used_storage < storage_limit) return;
|
|
size_t num_images = vt_size(&self->images_by_internal_id);
|
|
RAII_ALLOC(Image*, sorted, malloc(num_images * sizeof(Image*)));
|
|
if (!sorted) fatal("Out of memory");
|
|
Image **p = sorted;
|
|
iter_images(self) { *p++ = i.data->val; }
|
|
#define oldest_img_first(a, b) ((*a)->atime < (*b)->atime)
|
|
QSORT(Image*, sorted, num_images, oldest_img_first);
|
|
#undef oldest_img_first
|
|
|
|
for (p = sorted; self->used_storage > storage_limit && num_images; p++, num_images--) remove_image(self, *p);
|
|
if (!num_images || !vt_size(&self->images_by_internal_id)) self->used_storage = 0; // sanity check
|
|
}
|
|
|
|
static char command_response[512] = {0};
|
|
|
|
static void
|
|
set_command_failed_response(const char *code, const char *fmt, ...) {
|
|
va_list args;
|
|
va_start(args, fmt);
|
|
const size_t sz = sizeof(command_response)/sizeof(command_response[0]);
|
|
const int num = snprintf(command_response, sz, "%s:", code);
|
|
vsnprintf(command_response + num, sz - num, fmt, args);
|
|
va_end(args);
|
|
}
|
|
|
|
// Decode formats {{{
|
|
#define ABRT(code, ...) { set_command_failed_response(#code, __VA_ARGS__); goto err; }
|
|
|
|
static bool
|
|
mmap_img_file(GraphicsManager *self, int fd, size_t sz, off_t offset) {
|
|
if (!sz) {
|
|
struct stat s;
|
|
if (fstat(fd, &s) != 0) ABRT(EBADF, "Failed to fstat() the fd: %d file with error: [%d] %s", fd, errno, strerror(errno));
|
|
sz = s.st_size;
|
|
}
|
|
void *addr = mmap(0, sz, PROT_READ, MAP_SHARED, fd, offset);
|
|
if (addr == MAP_FAILED) ABRT(EBADF, "Failed to map image file fd: %d at offset: %zd with size: %zu with error: [%d] %s", fd, offset, sz, errno, strerror(errno));
|
|
self->currently_loading.mapped_file = addr;
|
|
self->currently_loading.mapped_file_sz = sz;
|
|
return true;
|
|
err:
|
|
return false;
|
|
}
|
|
|
|
|
|
static const char*
|
|
zlib_strerror(int ret) {
|
|
#define Z(x) case x: return #x;
|
|
static char buf[128];
|
|
switch(ret) {
|
|
case Z_ERRNO:
|
|
return strerror(errno);
|
|
default:
|
|
snprintf(buf, sizeof(buf)/sizeof(buf[0]), "Unknown error: %d", ret);
|
|
return buf;
|
|
Z(Z_STREAM_ERROR);
|
|
Z(Z_DATA_ERROR);
|
|
Z(Z_MEM_ERROR);
|
|
Z(Z_BUF_ERROR);
|
|
Z(Z_VERSION_ERROR);
|
|
}
|
|
#undef Z
|
|
}
|
|
|
|
static bool
|
|
inflate_zlib(LoadData *load_data, uint8_t *buf, size_t bufsz) {
|
|
bool ok = false;
|
|
z_stream z;
|
|
uint8_t *decompressed = malloc(load_data->data_sz);
|
|
if (decompressed == NULL) fatal("Out of memory allocating decompression buffer");
|
|
z.zalloc = Z_NULL;
|
|
z.zfree = Z_NULL;
|
|
z.opaque = Z_NULL;
|
|
z.avail_in = bufsz;
|
|
z.next_in = (Bytef*)buf;
|
|
z.avail_out = load_data->data_sz;
|
|
z.next_out = decompressed;
|
|
int ret;
|
|
if ((ret = inflateInit(&z)) != Z_OK) ABRT(ENOMEM, "Failed to initialize inflate with error: %s", zlib_strerror(ret));
|
|
if ((ret = inflate(&z, Z_FINISH)) != Z_STREAM_END) ABRT(EINVAL, "Failed to inflate image data with error: %s", zlib_strerror(ret));
|
|
if (z.avail_out) ABRT(EINVAL, "Image data size post inflation does not match expected size");
|
|
free_load_data(load_data);
|
|
load_data->buf_capacity = load_data->data_sz;
|
|
load_data->buf = decompressed;
|
|
load_data->buf_used = load_data->data_sz;
|
|
ok = true;
|
|
err:
|
|
inflateEnd(&z);
|
|
if (!ok) free(decompressed);
|
|
return ok;
|
|
}
|
|
|
|
static void
|
|
png_error_handler(png_read_data *d UNUSED, const char *code, const char *msg) {
|
|
set_command_failed_response(code, "%s", msg);
|
|
}
|
|
|
|
static bool
|
|
inflate_png(LoadData *load_data, uint8_t *buf, size_t bufsz) {
|
|
png_read_data d = {.err_handler=png_error_handler};
|
|
inflate_png_inner(&d, buf, bufsz);
|
|
if (d.ok) {
|
|
free_load_data(load_data);
|
|
load_data->buf = d.decompressed;
|
|
load_data->buf_capacity = d.sz;
|
|
load_data->buf_used = d.sz;
|
|
load_data->data_sz = d.sz;
|
|
load_data->width = d.width; load_data->height = d.height;
|
|
}
|
|
else free(d.decompressed);
|
|
free(d.row_pointers);
|
|
return d.ok;
|
|
}
|
|
#undef ABRT
|
|
// }}}
|
|
|
|
static bool
|
|
add_trim_predicate(Image *img) {
|
|
return !img->root_frame_data_loaded || (!img->client_id && !vt_size(&img->refs_by_internal_id));
|
|
}
|
|
|
|
static void
|
|
print_png_read_error(png_read_data *d, const char *code, const char* msg) {
|
|
if (d->error.used >= d->error.capacity) {
|
|
size_t cap = MAX(2 * d->error.capacity, 1024 + d->error.used);
|
|
d->error.buf = realloc(d->error.buf, cap);
|
|
if (!d->error.buf) return;
|
|
d->error.capacity = cap;
|
|
}
|
|
d->error.used += snprintf(d->error.buf + d->error.used, d->error.capacity - d->error.used, "%s: %s ", code, msg);
|
|
}
|
|
|
|
bool
|
|
png_from_data(void *png_data, size_t png_data_sz, const char *path_for_error_messages, uint8_t** data, unsigned int* width, unsigned int* height, size_t* sz) {
|
|
png_read_data d = {.err_handler=print_png_read_error};
|
|
inflate_png_inner(&d, png_data, png_data_sz);
|
|
if (!d.ok) {
|
|
log_error("Failed to decode PNG image at: %s with error: %s", path_for_error_messages, d.error.used > 0 ? d.error.buf : "");
|
|
free(d.decompressed); free(d.row_pointers); free(d.error.buf);
|
|
return false;
|
|
}
|
|
*data = d.decompressed;
|
|
free(d.row_pointers); free(d.error.buf);
|
|
*sz = d.sz;
|
|
*height = d.height; *width = d.width;
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
png_from_file_pointer(FILE *fp, const char *path_for_error_messages, uint8_t** data, unsigned int* width, unsigned int* height, size_t* sz) {
|
|
size_t capacity = 16*1024, pos = 0;
|
|
unsigned char *buf = malloc(capacity);
|
|
if (!buf) { log_error("Out of memory reading PNG file at: %s", path_for_error_messages); fclose(fp); return false; }
|
|
while (!feof(fp)) {
|
|
if (capacity - pos < 1024) {
|
|
capacity *= 2;
|
|
unsigned char *new_buf = realloc(buf, capacity);
|
|
if (!new_buf) {
|
|
free(buf);
|
|
log_error("Out of memory reading PNG file at: %s", path_for_error_messages); fclose(fp); return false;
|
|
}
|
|
buf = new_buf;
|
|
}
|
|
pos += fread(buf + pos, sizeof(char), capacity - pos, fp);
|
|
int saved_errno = errno;
|
|
if (ferror(fp) && saved_errno != EINTR) {
|
|
log_error("Failed while reading from file: %s with error: %s", path_for_error_messages, strerror(saved_errno));
|
|
free(buf);
|
|
return false;
|
|
}
|
|
}
|
|
bool ret = png_from_data(buf, pos, path_for_error_messages, data, width, height, sz);
|
|
free(buf);
|
|
return ret;
|
|
}
|
|
|
|
bool
|
|
png_path_to_bitmap(const char* path, uint8_t** data, unsigned int* width, unsigned int* height, size_t* sz) {
|
|
FILE* fp = fopen(path, "r");
|
|
if (fp == NULL) {
|
|
log_error("The PNG image: %s could not be opened with error: %s", path, strerror(errno));
|
|
return false;
|
|
}
|
|
bool ret = png_from_file_pointer(fp, path, data, width, height, sz);
|
|
fclose(fp); fp = NULL;
|
|
return ret;
|
|
}
|
|
|
|
bool
|
|
image_path_to_bitmap(const char *path, uint8_t** data, unsigned int* width, unsigned int* height, size_t* sz) {
|
|
*data = NULL; *sz = 0; *width = 0; *height = 0;
|
|
RAII_PyObject(module, PyImport_ImportModule("kitty.render_cache"));
|
|
#define fail_on_python_error { log_error("Failed to convert image at %s to bitmap with python error:", path); PyErr_Print(); return false; }
|
|
if (!module) fail_on_python_error;
|
|
RAII_PyObject(irc, PyObject_GetAttrString(module, "default_image_render_cache"));
|
|
if (!irc) fail_on_python_error;
|
|
RAII_PyObject(ret, PyObject_CallFunction(irc, "s", path));
|
|
if (!ret) fail_on_python_error;
|
|
size_t w = PyLong_AsSize_t(PyTuple_GET_ITEM(ret, 0));
|
|
size_t h = PyLong_AsSize_t(PyTuple_GET_ITEM(ret, 1));
|
|
int fd = PyLong_AsLong(PyTuple_GET_ITEM(ret, 2));
|
|
#undef fail_on_python_error
|
|
size_t data_size = 8 + w * h * 4;
|
|
*data = mmap(NULL, data_size, PROT_READ, MAP_PRIVATE, fd, 0);
|
|
int saved_errno = errno;
|
|
safe_close(fd, __FILE__, __LINE__);
|
|
if (*data == MAP_FAILED) {
|
|
log_error("Failed to mmap bitmap data for image at %s with error: %s", path, strerror(saved_errno));
|
|
return false;
|
|
}
|
|
*sz = data_size; *width = w; *height = h;
|
|
return true;
|
|
}
|
|
|
|
static Image*
|
|
find_or_create_image(GraphicsManager *self, uint32_t id, bool *existing) {
|
|
if (id) {
|
|
Image *img = img_by_client_id(self, id);
|
|
if (img) {
|
|
*existing = true;
|
|
return img;
|
|
}
|
|
}
|
|
*existing = false;
|
|
Image *ans = calloc(1, sizeof(Image));
|
|
if (!ans) fatal("Out of memory allocating Image object");
|
|
ans->internal_id = next_id(&self->image_id_counter);
|
|
ans->texture = new_texture_ref();
|
|
vt_init(&ans->refs_by_internal_id);
|
|
if (vt_is_end(vt_insert(&self->images_by_internal_id, ans->internal_id, ans))) fatal("Out of memory");
|
|
return ans;
|
|
}
|
|
|
|
static uint32_t
|
|
get_free_client_id(const GraphicsManager *self) {
|
|
size_t num_images = vt_size(&((GraphicsManager*)self)->images_by_internal_id);
|
|
if (!num_images) return 1;
|
|
RAII_ALLOC(uint32_t, client_ids, malloc(num_images * sizeof(uint32_t)));
|
|
if (!client_ids) fatal("Out of memory");
|
|
size_t count = 0;
|
|
iter_images((GraphicsManager*)self) {
|
|
Image *img = i.data->val;
|
|
if (img->client_id) client_ids[count++] = img->client_id;
|
|
}
|
|
if (!count) return 1;
|
|
#define int_lt(a, b) ((*a)<(*b))
|
|
QSORT(uint32_t, client_ids, count, int_lt)
|
|
#undef int_lt
|
|
uint32_t prev_id = 0, ans = 1;
|
|
for (size_t i = 0; i < count; i++) {
|
|
if (client_ids[i] == prev_id) continue;
|
|
prev_id = client_ids[i];
|
|
if (client_ids[i] != ans) break;
|
|
ans = client_ids[i] + 1;
|
|
}
|
|
return ans;
|
|
}
|
|
|
|
#define ABRT(code, ...) { set_command_failed_response(code, __VA_ARGS__); self->currently_loading.loading_completed_successfully = false; free_load_data(&self->currently_loading); return NULL; }
|
|
|
|
#define MAX_DATA_SZ (4u * 100000000u)
|
|
enum FORMATS { RGB=24, RGBA=32, PNG=100 };
|
|
|
|
static Image*
|
|
load_image_data(GraphicsManager *self, Image *img, const GraphicsCommand *g, const unsigned char transmission_type, const uint32_t data_fmt, const uint8_t *payload) {
|
|
int fd;
|
|
static char fname[2056] = {0};
|
|
LoadData *load_data = &self->currently_loading;
|
|
|
|
switch(transmission_type) {
|
|
case 'd': // direct
|
|
if (load_data->buf_capacity - load_data->buf_used < g->payload_sz) {
|
|
if (load_data->buf_used + g->payload_sz > MAX_DATA_SZ || data_fmt != PNG) ABRT("EFBIG", "Too much data");
|
|
load_data->buf_capacity = MIN(2 * load_data->buf_capacity, MAX_DATA_SZ);
|
|
load_data->buf = realloc(load_data->buf, load_data->buf_capacity);
|
|
if (load_data->buf == NULL) {
|
|
load_data->buf_capacity = 0; load_data->buf_used = 0;
|
|
ABRT("ENOMEM", "Out of memory");
|
|
}
|
|
}
|
|
memcpy(load_data->buf + load_data->buf_used, payload, g->payload_sz);
|
|
load_data->buf_used += g->payload_sz;
|
|
if (!g->more) { load_data->loading_completed_successfully = true; load_data->loading_for = (const ImageAndFrame){0}; }
|
|
break;
|
|
case 'f': // file
|
|
case 't': // temporary file
|
|
case 's': // POSIX shared memory
|
|
if (g->payload_sz > 2048) ABRT("EINVAL", "Filename too long");
|
|
snprintf(fname, sizeof(fname)/sizeof(fname[0]), "%.*s", (int)g->payload_sz, payload);
|
|
if (transmission_type == 's') fd = safe_shm_open(fname, O_RDONLY, 0);
|
|
else fd = safe_open(fname, O_CLOEXEC | O_RDONLY | O_NONBLOCK, 0); // O_NONBLOCK so that opening a FIFO pipe does not block
|
|
if (fd == -1) ABRT("EBADF", "Failed to open file for graphics transmission with error: [%d] %s", errno, strerror(errno));
|
|
if (global_state.boss && transmission_type != 's') {
|
|
RAII_PyObject(cret_, PyObject_CallMethod(global_state.boss, "is_ok_to_read_image_file", "si", fname, fd));
|
|
if (cret_ == NULL) {
|
|
PyErr_Print();
|
|
ABRT("EBADF", "Failed to check file for read permission");
|
|
}
|
|
if (cret_ != Py_True) {
|
|
log_error("Refusing to read image file as permission was denied");
|
|
ABRT("EPERM", "Permission denied to read image file");
|
|
}
|
|
}
|
|
load_data->loading_completed_successfully = mmap_img_file(self, fd, g->data_sz, g->data_offset);
|
|
safe_close(fd, __FILE__, __LINE__);
|
|
if (transmission_type == 't' && strstr(fname, "tty-graphics-protocol") != NULL) {
|
|
if (global_state.boss) { call_boss(safe_delete_temp_file, "s", fname); }
|
|
else unlink(fname);
|
|
}
|
|
else if (transmission_type == 's') shm_unlink(fname);
|
|
if (!load_data->loading_completed_successfully) return NULL;
|
|
break;
|
|
default:
|
|
ABRT("EINVAL", "Unknown transmission type: %c", g->transmission_type);
|
|
}
|
|
return img;
|
|
}
|
|
|
|
static Image*
|
|
process_image_data(GraphicsManager *self, Image* img, const GraphicsCommand *g, const unsigned char transmission_type, const uint32_t data_fmt) {
|
|
bool needs_processing = g->compressed || data_fmt == PNG;
|
|
if (needs_processing) {
|
|
uint8_t *buf; size_t bufsz;
|
|
#define IB { if (self->currently_loading.buf) { buf = self->currently_loading.buf; bufsz = self->currently_loading.buf_used; } else { buf = self->currently_loading.mapped_file; bufsz = self->currently_loading.mapped_file_sz; } }
|
|
switch(g->compressed) {
|
|
case 'z':
|
|
IB;
|
|
if (!inflate_zlib(&self->currently_loading, buf, bufsz)) {
|
|
self->currently_loading.loading_completed_successfully = false; return NULL;
|
|
}
|
|
break;
|
|
case 0:
|
|
break;
|
|
default:
|
|
ABRT("EINVAL", "Unknown image compression: %c", g->compressed);
|
|
}
|
|
switch(data_fmt) {
|
|
case PNG:
|
|
IB;
|
|
if (!inflate_png(&self->currently_loading, buf, bufsz)) {
|
|
self->currently_loading.loading_completed_successfully = false; return NULL;
|
|
}
|
|
break;
|
|
default: break;
|
|
}
|
|
#undef IB
|
|
self->currently_loading.data = self->currently_loading.buf;
|
|
if (self->currently_loading.buf_used < self->currently_loading.data_sz) {
|
|
ABRT("ENODATA", "Insufficient image data: %zu < %zu", self->currently_loading.buf_used, self->currently_loading.data_sz);
|
|
}
|
|
if (self->currently_loading.mapped_file) {
|
|
munmap(self->currently_loading.mapped_file, self->currently_loading.mapped_file_sz);
|
|
self->currently_loading.mapped_file = NULL; self->currently_loading.mapped_file_sz = 0;
|
|
}
|
|
} else {
|
|
if (transmission_type == 'd') {
|
|
if (self->currently_loading.buf_used < self->currently_loading.data_sz) {
|
|
ABRT("ENODATA", "Insufficient image data: %zu < %zu", self->currently_loading.buf_used, self->currently_loading.data_sz);
|
|
} else self->currently_loading.data = self->currently_loading.buf;
|
|
} else {
|
|
if (self->currently_loading.mapped_file_sz < self->currently_loading.data_sz) {
|
|
ABRT("ENODATA", "Insufficient image data: %zu < %zu", self->currently_loading.mapped_file_sz, self->currently_loading.data_sz);
|
|
} else self->currently_loading.data = self->currently_loading.mapped_file;
|
|
}
|
|
self->currently_loading.loading_completed_successfully = true;
|
|
}
|
|
return img;
|
|
}
|
|
|
|
static Image*
|
|
initialize_load_data(GraphicsManager *self, const GraphicsCommand *g, Image *img, const unsigned char transmission_type, const uint32_t data_fmt, const uint32_t frame_id) {
|
|
free_load_data(&self->currently_loading);
|
|
self->currently_loading = (const LoadData){0};
|
|
self->currently_loading.start_command = *g;
|
|
self->currently_loading.width = g->data_width; self->currently_loading.height = g->data_height;
|
|
switch(data_fmt) {
|
|
case PNG:
|
|
if (g->data_sz > MAX_DATA_SZ) ABRT("EINVAL", "PNG data size too large");
|
|
self->currently_loading.is_4byte_aligned = true;
|
|
self->currently_loading.is_opaque = false;
|
|
self->currently_loading.data_sz = g->data_sz ? g->data_sz : 1024 * 100;
|
|
break;
|
|
case RGB:
|
|
case RGBA:
|
|
self->currently_loading.data_sz = (size_t)g->data_width * g->data_height * (data_fmt / 8);
|
|
if (!self->currently_loading.data_sz) ABRT("EINVAL", "Zero width/height not allowed");
|
|
self->currently_loading.is_4byte_aligned = data_fmt == RGBA || (self->currently_loading.width % 4 == 0);
|
|
self->currently_loading.is_opaque = data_fmt == RGB;
|
|
break;
|
|
default:
|
|
ABRT("EINVAL", "Unknown image format: %u", data_fmt);
|
|
}
|
|
self->currently_loading.loading_for.image_id = img->internal_id;
|
|
self->currently_loading.loading_for.frame_id = frame_id;
|
|
if (transmission_type == 'd') {
|
|
self->currently_loading.buf_capacity = self->currently_loading.data_sz + (g->compressed ? 1024 : 10); // compression header
|
|
self->currently_loading.buf = malloc(self->currently_loading.buf_capacity);
|
|
self->currently_loading.buf_used = 0;
|
|
if (self->currently_loading.buf == NULL) {
|
|
self->currently_loading.buf_capacity = 0; self->currently_loading.buf_used = 0;
|
|
ABRT("ENOMEM", "Out of memory");
|
|
}
|
|
}
|
|
return img;
|
|
}
|
|
|
|
#define INIT_CHUNKED_LOAD { \
|
|
self->currently_loading.start_command.more = g->more; \
|
|
self->currently_loading.start_command.payload_sz = g->payload_sz; \
|
|
g = &self->currently_loading.start_command; \
|
|
tt = g->transmission_type ? g->transmission_type : 'd'; \
|
|
fmt = g->format ? g->format : RGBA; \
|
|
}
|
|
#define MAX_IMAGE_DIMENSION 10000u
|
|
|
|
static void
|
|
upload_to_gpu(GraphicsManager *self, Image *img, const bool is_opaque, const bool is_4byte_aligned, const uint8_t *data) {
|
|
if (!self->context_made_current_for_this_command) {
|
|
if (!self->window_id) return;
|
|
if (!make_window_context_current(self->window_id)) return;
|
|
self->context_made_current_for_this_command = true;
|
|
}
|
|
if (img->texture) {
|
|
send_image_to_gpu(&img->texture->id, data, img->width, img->height, is_opaque, is_4byte_aligned, true, REPEAT_CLAMP);
|
|
}
|
|
}
|
|
|
|
static Image*
|
|
handle_add_command(GraphicsManager *self, const GraphicsCommand *g, const uint8_t *payload, bool *is_dirty, uint32_t iid, bool is_query) {
|
|
bool existing, init_img = true;
|
|
Image *img = NULL;
|
|
unsigned char tt = g->transmission_type ? g->transmission_type : 'd';
|
|
uint32_t fmt = g->format ? g->format : RGBA;
|
|
if (tt == 'd' && self->currently_loading.loading_for.image_id) init_img = false;
|
|
if (init_img) {
|
|
self->currently_loading.loading_for = (const ImageAndFrame){0};
|
|
if (g->data_width > MAX_IMAGE_DIMENSION || g->data_height > MAX_IMAGE_DIMENSION) ABRT("EINVAL", "Image too large, width or height greater than %u", MAX_IMAGE_DIMENSION);
|
|
remove_images(self, add_trim_predicate, 0);
|
|
img = find_or_create_image(self, iid, &existing);
|
|
if (existing) {
|
|
free_image_resources(self, img);
|
|
img->texture = new_texture_ref();
|
|
img->root_frame_data_loaded = false;
|
|
img->is_drawn = false;
|
|
img->current_frame_shown_at = 0;
|
|
img->extra_framecnt = 0;
|
|
*is_dirty = true;
|
|
set_layers_dirty(self);
|
|
} else {
|
|
img->client_id = iid;
|
|
img->client_number = g->image_number;
|
|
if (!img->client_id && img->client_number) {
|
|
img->client_id = get_free_client_id(self);
|
|
iid = img->client_id;
|
|
}
|
|
}
|
|
img->atime = monotonic(); img->used_storage = 0;
|
|
if (!initialize_load_data(self, g, img, tt, fmt, 0)) return NULL;
|
|
self->currently_loading.start_command.id = iid;
|
|
} else {
|
|
INIT_CHUNKED_LOAD;
|
|
img = img_by_internal_id(self, self->currently_loading.loading_for.image_id);
|
|
if (img == NULL) {
|
|
self->currently_loading.loading_for = (const ImageAndFrame){0};
|
|
ABRT("EILSEQ", "More payload loading refers to non-existent image");
|
|
}
|
|
}
|
|
img = load_image_data(self, img, g, tt, fmt, payload);
|
|
if (!img || !self->currently_loading.loading_completed_successfully) return NULL;
|
|
self->currently_loading.loading_for = (const ImageAndFrame){0};
|
|
img = process_image_data(self, img, g, tt, fmt);
|
|
if (!img) return NULL;
|
|
size_t required_sz = (size_t)(self->currently_loading.is_opaque ? 3 : 4) * self->currently_loading.width * self->currently_loading.height;
|
|
if (self->currently_loading.data_sz != required_sz) ABRT("EINVAL", "Image dimensions: %ux%u do not match data size: %zu, expected size: %zu", self->currently_loading.width, self->currently_loading.height, self->currently_loading.data_sz, required_sz);
|
|
if (self->currently_loading.loading_completed_successfully) {
|
|
img->width = self->currently_loading.width;
|
|
img->height = self->currently_loading.height;
|
|
if (img->root_frame.id) remove_from_cache(self, (const ImageAndFrame){.image_id=img->internal_id, .frame_id=img->root_frame.id});
|
|
img->root_frame = (const Frame){
|
|
.id = ++img->frame_id_counter,
|
|
.is_opaque = self->currently_loading.is_opaque,
|
|
.is_4byte_aligned = self->currently_loading.is_4byte_aligned,
|
|
.width = img->width, .height = img->height,
|
|
};
|
|
if (!is_query) {
|
|
if (!add_to_cache(self, (const ImageAndFrame){.image_id = img->internal_id, .frame_id=img->root_frame.id}, self->currently_loading.data, self->currently_loading.data_sz)) {
|
|
if (PyErr_Occurred()) PyErr_Print();
|
|
ABRT("ENOSPC", "Failed to store image data in disk cache");
|
|
}
|
|
upload_to_gpu(self, img, img->root_frame.is_opaque, img->root_frame.is_4byte_aligned, self->currently_loading.data);
|
|
self->used_storage += required_sz;
|
|
img->used_storage = required_sz;
|
|
}
|
|
img->root_frame_data_loaded = true;
|
|
}
|
|
return img;
|
|
#undef MAX_DATA_SZ
|
|
}
|
|
|
|
static const char*
|
|
finish_command_response(const GraphicsCommand *g, bool data_loaded) {
|
|
static char rbuf[sizeof(command_response)/sizeof(command_response[0]) + 128];
|
|
bool is_ok_response = !command_response[0];
|
|
if (g->quiet) {
|
|
if (is_ok_response || g->quiet > 1) return NULL;
|
|
}
|
|
if (g->id || g->image_number) {
|
|
if (is_ok_response) {
|
|
if (!data_loaded) return NULL;
|
|
snprintf(command_response, 10, "OK");
|
|
}
|
|
size_t pos = 0;
|
|
rbuf[pos++] = 'G';
|
|
#define print(fmt, ...) if (arraysz(rbuf) - 1 > pos) pos += snprintf(rbuf + pos, arraysz(rbuf) - 1 - pos, fmt, __VA_ARGS__)
|
|
if (g->id) print("i=%u", g->id);
|
|
if (g->image_number) print(",I=%u", g->image_number);
|
|
if (g->placement_id) print(",p=%u", g->placement_id);
|
|
if (g->num_lines && (g->action == 'f' || g->action == 'a')) print(",r=%u", g->num_lines);
|
|
print(";%s", command_response);
|
|
return rbuf;
|
|
#undef print
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
// }}}
|
|
|
|
// Displaying images {{{
|
|
|
|
static void
|
|
update_src_rect(ImageRef *ref, Image *img) {
|
|
// The src rect in OpenGL co-ords [0, 1] with origin at top-left corner of image
|
|
ref->src_rect.left = (float)ref->src_x / (float)img->width;
|
|
ref->src_rect.right = (float)(ref->src_x + ref->src_width) / (float)img->width;
|
|
ref->src_rect.top = (float)ref->src_y / (float)img->height;
|
|
ref->src_rect.bottom = (float)(ref->src_y + ref->src_height) / (float)img->height;
|
|
}
|
|
|
|
static void
|
|
update_dest_rect(ImageRef *ref, uint32_t num_cols, uint32_t num_rows, CellPixelSize cell) {
|
|
uint32_t t;
|
|
if (num_cols == 0) {
|
|
if (num_rows == 0) {
|
|
t = (uint32_t)(ref->src_width + ref->cell_x_offset);
|
|
num_cols = t / cell.width;
|
|
if (t > num_cols * cell.width) num_cols += 1;
|
|
} else {
|
|
double height_px = cell.height * num_rows + ref->cell_y_offset;
|
|
double width_px = height_px * ref->src_width / (double) ref->src_height;
|
|
num_cols = (uint32_t)ceil(width_px / cell.width);
|
|
}
|
|
}
|
|
if (num_rows == 0) {
|
|
if (num_cols == 0) {
|
|
t = (uint32_t)(ref->src_height + ref->cell_y_offset);
|
|
num_rows = t / cell.height;
|
|
if (t > num_rows * cell.height) num_rows += 1;
|
|
} else {
|
|
double width_px = cell.width * num_cols + ref->cell_x_offset;
|
|
double height_px = width_px * ref->src_height / (double)ref->src_width;
|
|
num_rows = (uint32_t)ceil(height_px / cell.height);
|
|
}
|
|
}
|
|
ref->effective_num_rows = num_rows;
|
|
ref->effective_num_cols = num_cols;
|
|
}
|
|
|
|
static ImageRef*
|
|
create_ref(Image *img, ImageRef *clone_from) {
|
|
ImageRef *ans = calloc(1, sizeof(ImageRef));
|
|
if (!ans) fatal("Out of memory creating ImageRef");
|
|
if (clone_from) *ans = *clone_from;
|
|
ans->internal_id = next_id(&img->ref_id_counter);
|
|
if (vt_is_end(vt_insert(&img->refs_by_internal_id, ans->internal_id, ans))) fatal("Out of memory");
|
|
return ans;
|
|
}
|
|
|
|
static inline bool
|
|
is_cell_image(const ImageRef *self) { return self->virtual_ref_id != 0; }
|
|
|
|
// Create a real image ref for a virtual image ref (placement) positioned in the
|
|
// given cells. This is used for images positioned using Unicode placeholders.
|
|
//
|
|
// The image is resized to fit a box of cells with dimensions
|
|
// `image_ref->columns` by `image_ref->rows`. The parameters `img_col`,
|
|
// `img_row, `columns`, `rows` describe a part of this box that we want to
|
|
// display.
|
|
//
|
|
// Parameters:
|
|
// - `self` - the graphics manager
|
|
// - `screen_row` - the starting row of the screen
|
|
// - `screen_col` - the starting column of the screen
|
|
// - `image_id` - the id of the image
|
|
// - `placement_id` - the id of the placement (0 to find it automatically), it
|
|
// must be a virtual placement
|
|
// - `img_col` - the column of the image box we want to start with (base 0)
|
|
// - `img_row` - the row of the image box we want to start with (base 0)
|
|
// - `columns` - the number of columns we want to display
|
|
// - `rows` - the number of rows we want to display
|
|
// - `cell` - the size of a screen cell
|
|
void grman_put_cell_image(GraphicsManager *self, uint32_t screen_row,
|
|
uint32_t screen_col, uint32_t image_id,
|
|
uint32_t placement_id, uint32_t img_col,
|
|
uint32_t img_row, uint32_t columns, uint32_t rows,
|
|
CellPixelSize cell) {
|
|
Image *img = img_by_client_id(self, image_id);
|
|
if (img == NULL) return;
|
|
|
|
ImageRef *virt_img_ref = NULL;
|
|
if (placement_id) {
|
|
// Find the placement by the id. It must be a virtual placement.
|
|
iter_refs(img) { ImageRef *r = i.data->val;
|
|
if (r->is_virtual_ref && r->client_id == placement_id) {
|
|
virt_img_ref = r;
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
// Find the first virtual image placement.
|
|
iter_refs(img) { ImageRef *r = i.data->val;
|
|
if (r->is_virtual_ref) {
|
|
virt_img_ref = r;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!virt_img_ref) return;
|
|
|
|
// Create the ref structure on stack first. We will not create a real
|
|
// reference if the image is completely out of bounds.
|
|
ImageRef ref = {0};
|
|
ref.virtual_ref_id = virt_img_ref->internal_id;
|
|
|
|
uint32_t img_rows = virt_img_ref->num_rows;
|
|
uint32_t img_columns = virt_img_ref->num_cols;
|
|
// If the number of columns or rows for the image is not set, compute them
|
|
// in such a way that the image is as close as possible to its natural size.
|
|
if (img_columns == 0)
|
|
img_columns = (img->width + cell.width - 1) / cell.width;
|
|
if (img_rows == 0) img_rows = (img->height + cell.height - 1) / cell.height;
|
|
|
|
ref.start_row = screen_row;
|
|
ref.start_column = screen_col;
|
|
ref.num_cols = columns;
|
|
ref.num_rows = rows;
|
|
|
|
// The image is fit to the destination box of size
|
|
// (cell.width * img_columns) by (cell.height * img_rows)
|
|
// The conversion from source (image) coordinates to destination (box)
|
|
// coordinates is done by the following formula:
|
|
// x_dst = x_src * x_scale + x_offset
|
|
// y_dst = y_src * y_scale + y_offset
|
|
float x_offset, y_offset, x_scale, y_scale;
|
|
|
|
// Fit the image to the box while preserving aspect ratio
|
|
if (img->width * img_rows * cell.height > img->height * img_columns * cell.width) {
|
|
// Fit to width and center vertically.
|
|
x_offset = 0;
|
|
x_scale = (float)(img_columns * cell.width) / MAX(1u, img->width);
|
|
y_scale = x_scale;
|
|
y_offset = (img_rows * cell.height - img->height * y_scale) / 2;
|
|
} else {
|
|
// Fit to height and center horizontally.
|
|
y_offset = 0;
|
|
y_scale = (float)(img_rows * cell.height) / MAX(1u, img->height);
|
|
x_scale = y_scale;
|
|
x_offset = (img_columns * cell.width - img->width * x_scale) / 2;
|
|
}
|
|
|
|
// Now we can compute source (image) coordinates from destination (box)
|
|
// coordinates by formula:
|
|
// x_src = (x_dst - x_offset) / x_scale
|
|
// y_src = (y_dst - y_offset) / y_scale
|
|
|
|
// Destination (box) coordinates of the rectangle we want to display.
|
|
uint32_t x_dst = img_col * cell.width;
|
|
uint32_t y_dst = img_row * cell.height;
|
|
uint32_t w_dst = columns * cell.width;
|
|
uint32_t h_dst = rows * cell.height;
|
|
|
|
// Compute the source coordinates of the rectangle.
|
|
ref.src_x = (x_dst - x_offset) / x_scale;
|
|
ref.src_y = (y_dst - y_offset) / y_scale;
|
|
ref.src_width = w_dst / x_scale;
|
|
ref.src_height = h_dst / y_scale;
|
|
|
|
// If the top left corner is out of bounds of the source image, we can
|
|
// adjust cell offsets and the starting row/column. And if the rectangle is
|
|
// completely out of bounds, we can avoid creating a real reference. This
|
|
// is just an optimization, the image will be displayed correctly even if we
|
|
// do not do this.
|
|
if (ref.src_x < 0) {
|
|
ref.src_width += ref.src_x;
|
|
ref.cell_x_offset = (uint32_t)(-ref.src_x * x_scale);
|
|
ref.src_x = 0;
|
|
uint32_t col_offset = ref.cell_x_offset / cell.width;
|
|
ref.cell_x_offset %= cell.width;
|
|
ref.start_column += col_offset;
|
|
if (ref.num_cols <= col_offset) return;
|
|
ref.num_cols -= col_offset;
|
|
}
|
|
if (ref.src_y < 0) {
|
|
ref.src_height += ref.src_y;
|
|
ref.cell_y_offset = (uint32_t)(-ref.src_y * y_scale);
|
|
ref.src_y = 0;
|
|
uint32_t row_offset = ref.cell_y_offset / cell.height;
|
|
ref.cell_y_offset %= cell.height;
|
|
ref.start_row += row_offset;
|
|
if (ref.num_rows <= row_offset) return;
|
|
ref.num_rows -= row_offset;
|
|
}
|
|
|
|
// For the bottom right corner we can remove only completely empty rows and
|
|
// columns.
|
|
if (ref.src_x + ref.src_width > img->width) {
|
|
float redundant_w = ref.src_x + ref.src_width - img->width;
|
|
uint32_t redundant_cols = (uint32_t)(redundant_w * x_scale) / cell.width;
|
|
if (ref.num_cols <= redundant_cols) return;
|
|
ref.src_width -= redundant_cols * cell.width / x_scale;
|
|
ref.num_cols -= redundant_cols;
|
|
}
|
|
if (ref.src_y + ref.src_height > img->height) {
|
|
float redundant_h = ref.src_y + ref.src_height - img->height;
|
|
uint32_t redundant_rows = (uint32_t)(redundant_h * y_scale) / cell.height;
|
|
if (ref.num_rows <= redundant_rows) return;
|
|
ref.src_height -= redundant_rows * cell.height / y_scale;
|
|
ref.num_rows -= redundant_rows;
|
|
}
|
|
|
|
// The cursor will be drawn on top of the image.
|
|
ref.z_index = -1;
|
|
|
|
// Create a real ref.
|
|
ImageRef *real_ref = create_ref(img, &ref);
|
|
|
|
img->atime = monotonic();
|
|
set_layers_dirty(self);
|
|
|
|
update_src_rect(real_ref, img);
|
|
update_dest_rect(real_ref, ref.num_cols, ref.num_rows, cell);
|
|
}
|
|
|
|
static void remove_ref(Image *img, ImageRef *ref);
|
|
static ref_map_itr remove_ref_itr(Image *img, ref_map_itr x);
|
|
|
|
static bool
|
|
has_good_ancestry(GraphicsManager *self, ImageRef *ref) {
|
|
ImageRef *r = ref;
|
|
unsigned depth = 0;
|
|
while (r->parent.img) {
|
|
if (r == ref && depth) {
|
|
set_command_failed_response("ECYCLE", "This parent reference creates a cycle");
|
|
return false;
|
|
}
|
|
if (depth++ >= PARENT_DEPTH_LIMIT) {
|
|
set_command_failed_response("ETOODEEP", "Too many levels of parent references");
|
|
return false;
|
|
}
|
|
Image *parent = img_by_internal_id(self, r->parent.img);
|
|
if (!parent) {
|
|
set_command_failed_response("ENOENT", "One of the ancestors of this ref with image id: %llu not found", r->parent.img);
|
|
return false;
|
|
}
|
|
ImageRef *parent_ref = ref_by_internal_id(parent, r->parent.ref);
|
|
if (!parent_ref) {
|
|
set_command_failed_response("ENOENT", "One of the ancestors of this ref with image id: %llu and ref id: %llu not found", r->parent.img, r->parent.ref);
|
|
return false;
|
|
}
|
|
r = parent_ref;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static uint32_t
|
|
handle_put_command(GraphicsManager *self, const GraphicsCommand *g, Cursor *c, bool *is_dirty, Image *img, CellPixelSize cell) {
|
|
if (g->unicode_placement && g->parent_id) {
|
|
set_command_failed_response("EINVAL", "Put command creating a virtual placement cannot refer to a parent"); return g->id;
|
|
}
|
|
if (img == NULL) {
|
|
if (g->id) img = img_by_client_id(self, g->id);
|
|
else if (g->image_number) img = img_by_client_number(self, g->image_number);
|
|
if (img == NULL) { set_command_failed_response("ENOENT", "Put command refers to non-existent image with id: %u and number: %u", g->id, g->image_number); return g->id; }
|
|
}
|
|
if (!img->root_frame_data_loaded) { set_command_failed_response("ENOENT", "Put command refers to image with id: %u that could not load its data", g->id); return img->client_id; }
|
|
id_type parent_id = 0, parent_placement_id = 0;
|
|
if (g->parent_id) {
|
|
Image *parent = img_by_client_id(self, g->parent_id);
|
|
if (!parent) {
|
|
set_command_failed_response("ENOPARENT", "Put command refers to a parent image with id: %u that does not exist", g->parent_id);
|
|
return g->id;
|
|
}
|
|
if (!vt_size(&parent->refs_by_internal_id)) {
|
|
set_command_failed_response("ENOPARENT", "Put command refers to a parent image with id: %u that has no placements", g->parent_id);
|
|
return g->id;
|
|
}
|
|
ImageRef *parent_ref = vt_first(&parent->refs_by_internal_id).data->val;
|
|
if (g->parent_placement_id) {
|
|
parent_ref = ref_by_client_id(parent, g->parent_placement_id);
|
|
if (!parent_ref) {
|
|
set_command_failed_response("ENOPARENT", "Put command refers to a parent image placement with id: %u and placement id: %u that does not exist", g->parent_id, g->parent_placement_id);
|
|
return g->id;
|
|
}
|
|
}
|
|
parent_id = parent->internal_id;
|
|
parent_placement_id = parent_ref->internal_id;
|
|
}
|
|
ImageRef *ref = NULL;
|
|
if (g->placement_id && img->client_id) {
|
|
iter_refs(img) { ImageRef *r = i.data->val;
|
|
if (r->client_id == g->placement_id) {
|
|
ref = r;
|
|
if (parent_id && parent_id == img->internal_id && parent_placement_id && parent_placement_id == r->internal_id) {
|
|
set_command_failed_response("EINVAL", "Put command refers to itself as its own parent");
|
|
return g->id;
|
|
}
|
|
if (parent_id && parent_placement_id) {
|
|
id_type rp = ref->parent.img, rpp = ref->parent.ref;
|
|
ref->parent.img = parent_id; ref->parent.ref = parent_placement_id;
|
|
bool ok = has_good_ancestry(self, ref);
|
|
ref->parent.img = rp; ref->parent.ref = rpp;
|
|
if (!ok) return g->id;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (ref == NULL) ref = create_ref(img, NULL);
|
|
|
|
*is_dirty = true;
|
|
set_layers_dirty(self);
|
|
img->atime = monotonic();
|
|
ref->src_x = g->x_offset; ref->src_y = g->y_offset; ref->src_width = g->width ? g->width : img->width; ref->src_height = g->height ? g->height : img->height;
|
|
ref->src_width = MIN(ref->src_width, img->width - ((float)img->width > ref->src_x ? ref->src_x : (float)img->width));
|
|
ref->src_height = MIN(ref->src_height, img->height - ((float)img->height > ref->src_y ? ref->src_y : (float)img->height));
|
|
ref->z_index = g->z_index;
|
|
ref->start_row = c->y; ref->start_column = c->x;
|
|
ref->cell_x_offset = MIN(g->cell_x_offset, cell.width - 1);
|
|
ref->cell_y_offset = MIN(g->cell_y_offset, cell.height - 1);
|
|
ref->num_cols = g->num_cells; ref->num_rows = g->num_lines;
|
|
if (img->client_id) ref->client_id = g->placement_id;
|
|
update_src_rect(ref, img);
|
|
update_dest_rect(ref, g->num_cells, g->num_lines, cell);
|
|
ref->parent.img = parent_id;
|
|
ref->parent.ref = parent_placement_id;
|
|
ref->parent.offset.x = g->offset_from_parent_x;
|
|
ref->parent.offset.y = g->offset_from_parent_y;
|
|
ref->is_virtual_ref = false;
|
|
if (g->unicode_placement) {
|
|
ref->is_virtual_ref = true;
|
|
ref->start_row = ref->start_column = 0;
|
|
}
|
|
if (ref->parent.img) {
|
|
if (!has_good_ancestry(self, ref)) {
|
|
remove_ref(img, ref);
|
|
return g->id;
|
|
}
|
|
} else {
|
|
// Move the cursor, the screen will take care of ensuring it is in bounds
|
|
if (g->cursor_movement != 1 && !g->unicode_placement) {
|
|
c->x += ref->effective_num_cols;
|
|
if (ref->effective_num_rows) c->y += ref->effective_num_rows - 1;
|
|
}
|
|
}
|
|
return img->client_id;
|
|
}
|
|
|
|
void
|
|
gpu_data_for_image(ImageRenderData *ans, float left, float top, float right, float bottom) {
|
|
// For dest rect: x-axis is from -1 to 1, y axis is from 1 to -1
|
|
static const ImageRef source_rect = { .src_rect = { .left=0, .top=0, .bottom=1, .right=1 }};
|
|
ans->src_rect = source_rect.src_rect;
|
|
ans->dest_rect = (ImageRect){ .left = left, .right = right, .top = top, .bottom = bottom };
|
|
ans->group_count = 1;
|
|
}
|
|
|
|
static bool
|
|
resolve_cell_ref(const Image *img, id_type virt_ref_id, int32_t *start_row, int32_t *start_column) {
|
|
*start_row = 0; *start_column = 0;
|
|
bool found = false;
|
|
iter_refs((Image*)img) { ImageRef *ref = i.data->val;
|
|
if (ref->virtual_ref_id == virt_ref_id) {
|
|
if (!found || ref->start_row < *start_row) *start_row = ref->start_row;
|
|
if (!found || ref->start_column < *start_column) *start_column = ref->start_column;
|
|
found = true;
|
|
}
|
|
}
|
|
return found;
|
|
}
|
|
|
|
static bool
|
|
resolve_parent_offset(const GraphicsManager *self, const ImageRef *ref, int32_t *start_row, int32_t *start_column, bool *has_virtual_ancestor) {
|
|
*start_row = 0; *start_column = 0; *has_virtual_ancestor = false;
|
|
int32_t x = 0, y = 0;
|
|
unsigned depth = 0;
|
|
ImageRef cell_ref = {0};
|
|
while (ref->parent.img) {
|
|
if (depth++ >= PARENT_DEPTH_LIMIT) return false; // either a cycle or too many ancestors
|
|
Image *img = img_by_internal_id(self, ref->parent.img);
|
|
if (!img) return false;
|
|
ImageRef *parent = ref_by_internal_id(img, ref->parent.ref);
|
|
if (!parent) return false;
|
|
if (parent->is_virtual_ref) {
|
|
*has_virtual_ancestor = true;
|
|
if (!resolve_cell_ref(img, parent->internal_id, &cell_ref.start_row, &cell_ref.start_column)) return false;
|
|
parent = &cell_ref;
|
|
}
|
|
x += ref->parent.offset.x;
|
|
y += ref->parent.offset.y;
|
|
ref = parent;
|
|
}
|
|
*start_row = ref->start_row + y;
|
|
*start_column = ref->start_column + x;
|
|
return true;
|
|
}
|
|
|
|
|
|
bool
|
|
grman_update_layers(GraphicsManager *self, unsigned int scrolled_by, float screen_left, float screen_top, float dx, float dy, unsigned int num_cols, unsigned int num_rows, CellPixelSize cell) {
|
|
if (self->last_scrolled_by != scrolled_by) set_layers_dirty(self);
|
|
self->last_scrolled_by = scrolled_by;
|
|
if (!self->layers_dirty) return false;
|
|
self->layers_dirty = false;
|
|
size_t i;
|
|
self->num_of_below_refs = 0;
|
|
self->num_of_negative_refs = 0;
|
|
self->num_of_positive_refs = 0;
|
|
ImageRect r;
|
|
float screen_width = dx * num_cols, screen_height = dy * num_rows;
|
|
float screen_bottom = screen_top - screen_height;
|
|
float screen_width_px = num_cols * cell.width;
|
|
float screen_height_px = num_rows * cell.height;
|
|
float y0 = screen_top - dy * scrolled_by;
|
|
|
|
// Iterate over all visible refs and create render data
|
|
self->render_data.count = 0;
|
|
|
|
for (image_map_itr imgitr = vt_first(&self->images_by_internal_id); !vt_is_end(imgitr); ) {
|
|
Image *img = imgitr.data->val;
|
|
bool was_drawn = img->is_drawn, ref_removed = false;
|
|
img->is_drawn = false;
|
|
|
|
for (ref_map_itr refitr = vt_first(&img->refs_by_internal_id); !vt_is_end(refitr); ) {
|
|
ImageRef *ref = refitr.data->val;
|
|
if (ref->is_virtual_ref) { refitr = vt_next(refitr); continue; }
|
|
int32_t start_row = ref->start_row, start_column = ref->start_column;
|
|
if (ref->parent.img) {
|
|
bool has_virtual_ancestor;
|
|
if (!resolve_parent_offset(self, ref, &start_row, &start_column, &has_virtual_ancestor)) {
|
|
if (!has_virtual_ancestor) {
|
|
refitr = remove_ref_itr(img, refitr);
|
|
ref_removed = true;
|
|
} else refitr = vt_next(refitr);
|
|
continue;
|
|
}
|
|
}
|
|
r.top = y0 - start_row * dy - dy * (float)ref->cell_y_offset / (float)cell.height;
|
|
r.left = screen_left + start_column * dx + dx * (float)ref->cell_x_offset / (float) cell.width;
|
|
|
|
int32_t nr = ref->num_rows, nc = ref->num_cols;
|
|
if (nr) {
|
|
r.bottom = y0 - (start_row + nr) * dy;
|
|
if (nc) r.right = screen_left + (start_column + nc) * dx;
|
|
else {
|
|
double height_px = (((double)r.top - r.bottom) / screen_height) * screen_height_px;
|
|
double width_px = height_px * ref->src_width / (double) ref->src_height;
|
|
r.right = r.left + (float)((width_px / screen_width_px) * screen_width);
|
|
}
|
|
} else {
|
|
if (nc) r.right = screen_left + (start_column + nc) * dx;
|
|
else r.right = r.left + screen_width * (float)ref->src_width / screen_width_px;
|
|
double width_px = (((double)r.right - r.left) / screen_width) * screen_width_px;
|
|
double height_px = width_px * ref->src_height / (double)ref->src_width;
|
|
r.bottom = r.top - (float)((height_px / screen_height_px) * screen_height);
|
|
}
|
|
|
|
if (r.top <= screen_bottom || r.bottom >= screen_top) { refitr = vt_next(refitr); continue; } // not visible
|
|
|
|
if (ref->z_index < ((int32_t)INT32_MIN/2))
|
|
self->num_of_below_refs++;
|
|
else if (ref->z_index < 0)
|
|
self->num_of_negative_refs++;
|
|
else
|
|
self->num_of_positive_refs++;
|
|
ensure_space_for(&(self->render_data), item, ImageRenderData, self->render_data.count + 1, capacity, 64, true);
|
|
ImageRenderData *rd = self->render_data.item + self->render_data.count;
|
|
zero_at_ptr(rd);
|
|
rd->dest_rect = r; rd->src_rect = ref->src_rect;
|
|
self->render_data.count++;
|
|
rd->z_index = ref->z_index; rd->image_id = img->internal_id; rd->ref_id = ref->internal_id;
|
|
rd->texture_id = texture_id_for_img(img);
|
|
img->is_drawn = true;
|
|
refitr = vt_next(refitr);
|
|
}
|
|
if (ref_removed && !vt_size(&img->refs_by_internal_id)) {
|
|
imgitr = remove_image_itr(self, imgitr);
|
|
continue;
|
|
}
|
|
if (img->is_drawn && !was_drawn && img->animation_state != ANIMATION_STOPPED && img->extra_framecnt && img->animation_duration) {
|
|
self->has_images_needing_animation = true;
|
|
global_state.check_for_active_animated_images = true;
|
|
}
|
|
imgitr = vt_next(imgitr);
|
|
}
|
|
if (!self->render_data.count) return false;
|
|
// Sort visible refs in draw order (z-index, img, ref)
|
|
#define lt(a, b) ( (a)->z_index < (b)->z_index || ((a)->z_index == (b)->z_index && ( \
|
|
(a)->image_id < (b)->image_id || ((a)->image_id == (b)->image_id && a->ref_id < b->ref_id))) )
|
|
QSORT(ImageRenderData, self->render_data.item, self->render_data.count, lt);
|
|
#undef lt
|
|
// Calculate the group counts
|
|
i = 0;
|
|
while (i < self->render_data.count) {
|
|
id_type num_identical = 1, image_id = self->render_data.item[i].image_id, start = i;
|
|
while (++i < self->render_data.count) {
|
|
if (self->render_data.item[i].image_id != image_id) break;
|
|
num_identical++;
|
|
}
|
|
while (num_identical > 0) {
|
|
self->render_data.item[start++].group_count = num_identical--;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// }}}
|
|
|
|
// Animation {{{
|
|
#define DEFAULT_GAP 40
|
|
|
|
static Frame*
|
|
current_frame(Image *img) {
|
|
if (img->current_frame_index > img->extra_framecnt) return NULL;
|
|
return img->current_frame_index ? img->extra_frames + img->current_frame_index - 1 : &img->root_frame;
|
|
}
|
|
|
|
static Frame*
|
|
frame_for_id(Image *img, const uint32_t frame_id) {
|
|
if (img->root_frame.id == frame_id) return &img->root_frame;
|
|
for (unsigned i = 0; i < img->extra_framecnt; i++) {
|
|
if (img->extra_frames[i].id == frame_id) return img->extra_frames + i;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static Frame*
|
|
frame_for_number(Image *img, const uint32_t frame_number) {
|
|
switch(frame_number) {
|
|
case 1:
|
|
return &img->root_frame;
|
|
case 0:
|
|
return NULL;
|
|
default:
|
|
if (frame_number - 2 < img->extra_framecnt) return img->extra_frames + frame_number - 2;
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
static void
|
|
change_gap(Image *img, Frame *f, int32_t gap) {
|
|
uint32_t prev_gap = f->gap;
|
|
f->gap = MAX(0, gap);
|
|
img->animation_duration = prev_gap < img->animation_duration ? img->animation_duration - prev_gap : 0;
|
|
img->animation_duration += f->gap;
|
|
}
|
|
|
|
typedef struct {
|
|
uint8_t *buf;
|
|
bool is_4byte_aligned, is_opaque;
|
|
} CoalescedFrameData;
|
|
|
|
static void
|
|
blend_on_opaque(uint8_t *under_px, const uint8_t *over_px) {
|
|
const float alpha = (float)over_px[3] / 255.f;
|
|
const float alpha_op = 1.f - alpha;
|
|
for (unsigned i = 0; i < 3; i++) under_px[i] = (uint8_t)(over_px[i] * alpha + under_px[i] * alpha_op);
|
|
}
|
|
|
|
static void
|
|
alpha_blend(uint8_t *dest_px, const uint8_t *src_px) {
|
|
if (src_px[3]) {
|
|
const float dest_a = (float)dest_px[3] / 255.f, src_a = (float)src_px[3] / 255.f;
|
|
const float alpha = src_a + dest_a * (1.f - src_a);
|
|
dest_px[3] = (uint8_t)(255 * alpha);
|
|
if (!dest_px[3]) { dest_px[0] = 0; dest_px[1] = 0; dest_px[2] = 0; return; }
|
|
for (unsigned i = 0; i < 3; i++) dest_px[i] = (uint8_t)((src_px[i] * src_a + dest_px[i] * dest_a * (1.f - src_a))/alpha);
|
|
}
|
|
}
|
|
|
|
typedef struct {
|
|
bool needs_blending;
|
|
uint32_t over_px_sz, under_px_sz;
|
|
uint32_t over_width, over_height, under_width, under_height, over_offset_x, over_offset_y, under_offset_x, under_offset_y;
|
|
uint32_t stride;
|
|
} ComposeData;
|
|
|
|
#define COPY_RGB under_px[0] = over_px[0]; under_px[1] = over_px[1]; under_px[2] = over_px[2];
|
|
#define COPY_PIXELS \
|
|
if (d.needs_blending) { \
|
|
if (d.under_px_sz == 3) { \
|
|
ROW_ITER PIX_ITER blend_on_opaque(under_px, over_px); }} \
|
|
} else { \
|
|
ROW_ITER PIX_ITER alpha_blend(under_px, over_px); }} \
|
|
} \
|
|
} else { \
|
|
if (d.under_px_sz == 4) { \
|
|
if (d.over_px_sz == 4) { \
|
|
ROW_ITER PIX_ITER COPY_RGB under_px[3] = over_px[3]; }} \
|
|
} else { \
|
|
ROW_ITER PIX_ITER COPY_RGB under_px[3] = 255; }} \
|
|
} \
|
|
} else { \
|
|
ROW_ITER PIX_ITER COPY_RGB }} \
|
|
} \
|
|
} \
|
|
|
|
|
|
static void
|
|
compose_rectangles(const ComposeData d, uint8_t *under_data, const uint8_t *over_data) {
|
|
// compose two equal sized, non-overlapping rectangles at different offsets
|
|
// does not do bounds checking on the data arrays
|
|
const bool can_copy_rows = !d.needs_blending && d.over_px_sz == d.under_px_sz;
|
|
const unsigned min_width = MIN(d.under_width, d.over_width);
|
|
#define ROW_ITER for (unsigned y = 0; y < d.under_height && y < d.over_height; y++) { \
|
|
uint8_t *under_row = under_data + (y + d.under_offset_y) * d.under_px_sz * d.stride + (d.under_offset_x * d.under_px_sz); \
|
|
const uint8_t *over_row = over_data + (y + d.over_offset_y) * d.over_px_sz * d.stride + (d.over_offset_x * d.over_px_sz);
|
|
if (can_copy_rows) {
|
|
ROW_ITER memcpy(under_row, over_row, (size_t)d.over_px_sz * min_width);}
|
|
return;
|
|
}
|
|
#define PIX_ITER for (unsigned x = 0; x < min_width; x++) { \
|
|
uint8_t *under_px = under_row + (d.under_px_sz * x); \
|
|
const uint8_t *over_px = over_row + (d.over_px_sz * x);
|
|
COPY_PIXELS
|
|
#undef PIX_ITER
|
|
#undef ROW_ITER
|
|
}
|
|
|
|
static void
|
|
compose(const ComposeData d, uint8_t *under_data, const uint8_t *over_data) {
|
|
const bool can_copy_rows = !d.needs_blending && d.over_px_sz == d.under_px_sz;
|
|
unsigned min_row_sz = d.over_offset_x < d.under_width ? d.under_width - d.over_offset_x : 0;
|
|
min_row_sz = MIN(min_row_sz, d.over_width);
|
|
#define ROW_ITER for (unsigned y = 0; y + d.over_offset_y < d.under_height && y < d.over_height; y++) { \
|
|
uint8_t *under_row = under_data + (y + d.over_offset_y) * d.under_px_sz * d.under_width + d.under_px_sz * d.over_offset_x; \
|
|
const uint8_t *over_row = over_data + y * d.over_px_sz * d.over_width;
|
|
#define END_ITER }
|
|
if (can_copy_rows) {
|
|
ROW_ITER memcpy(under_row, over_row, (size_t)d.over_px_sz * min_row_sz); END_ITER
|
|
return;
|
|
}
|
|
#define PIX_ITER for (unsigned x = 0; x < min_row_sz; x++) { \
|
|
uint8_t *under_px = under_row + (d.under_px_sz * x); \
|
|
const uint8_t *over_px = over_row + (d.over_px_sz * x);
|
|
COPY_PIXELS
|
|
#undef COPY_RGB
|
|
#undef PIX_ITER
|
|
#undef ROW_ITER
|
|
#undef END_ITER
|
|
}
|
|
|
|
static CoalescedFrameData
|
|
get_coalesced_frame_data_standalone(const Image *img, const Frame *f, uint8_t *frame_data) {
|
|
CoalescedFrameData ans = {0};
|
|
bool is_full_frame = f->width == img->width && f->height == img->height && !f->x && !f->y;
|
|
if (is_full_frame) {
|
|
ans.buf = frame_data;
|
|
ans.is_4byte_aligned = f->is_4byte_aligned;
|
|
ans.is_opaque = f->is_opaque;
|
|
return ans;
|
|
}
|
|
const unsigned bytes_per_pixel = f->is_opaque ? 3 : 4;
|
|
uint8_t *base;
|
|
if (f->bgcolor) {
|
|
base = malloc((size_t)img->width * img->height * bytes_per_pixel);
|
|
if (base) {
|
|
uint8_t *p = base;
|
|
const uint8_t r = (f->bgcolor >> 24) & 0xff,
|
|
g = (f->bgcolor >> 16) & 0xff, b = (f->bgcolor >> 8) & 0xff, a = f->bgcolor & 0xff;
|
|
if (bytes_per_pixel == 4) {
|
|
for (uint32_t i = 0; i < img->width * img->height; i++) {
|
|
*(p++) = r; *(p++) = g; *(p++) = b; *(p++) = a;
|
|
}
|
|
} else {
|
|
for (uint32_t i = 0; i < img->width * img->height; i++) {
|
|
*(p++) = r; *(p++) = g; *(p++) = b;
|
|
}
|
|
}
|
|
}
|
|
} else base = calloc((size_t)img->width * img->height, bytes_per_pixel);
|
|
if (!base) { free(frame_data); return ans; }
|
|
ComposeData d = {
|
|
.over_px_sz = bytes_per_pixel, .under_px_sz = bytes_per_pixel,
|
|
.over_width = f->width, .over_height = f->height, .over_offset_x = f->x, .over_offset_y = f->y,
|
|
.under_width = img->width, .under_height = img->height,
|
|
.needs_blending = f->alpha_blend && !f->is_opaque
|
|
};
|
|
compose(d, base, frame_data);
|
|
ans.buf = base;
|
|
ans.is_4byte_aligned = bytes_per_pixel == 4 || (img->width % 4) == 0;
|
|
ans.is_opaque = f->is_opaque;
|
|
free(frame_data);
|
|
return ans;
|
|
}
|
|
|
|
|
|
static CoalescedFrameData
|
|
get_coalesced_frame_data_impl(GraphicsManager *self, Image *img, const Frame *f, unsigned count) {
|
|
CoalescedFrameData ans = {0};
|
|
if (count > 32) return ans; // prevent stack overflows, infinite recursion
|
|
size_t frame_data_sz; void *frame_data;
|
|
ImageAndFrame key = {.image_id = img->internal_id, .frame_id = f->id};
|
|
if (!read_from_cache(self, key, &frame_data, &frame_data_sz)) return ans;
|
|
if (!f->base_frame_id) return get_coalesced_frame_data_standalone(img, f, frame_data);
|
|
Frame *base = frame_for_id(img, f->base_frame_id);
|
|
if (!base) { free(frame_data); return ans; }
|
|
CoalescedFrameData base_data = get_coalesced_frame_data_impl(self, img, base, count + 1);
|
|
if (!base_data.buf) { free(frame_data); return ans; }
|
|
ComposeData d = {
|
|
.over_px_sz = f->is_opaque ? 3 : 4,
|
|
.under_px_sz = base_data.is_opaque ? 3 : 4,
|
|
.over_width = f->width, .over_height = f->height, .over_offset_x = f->x, .over_offset_y = f->y,
|
|
.under_width = img->width, .under_height = img->height,
|
|
.needs_blending = f->alpha_blend && !f->is_opaque
|
|
};
|
|
compose(d, base_data.buf, frame_data);
|
|
free(frame_data);
|
|
return base_data;
|
|
}
|
|
|
|
static CoalescedFrameData
|
|
get_coalesced_frame_data(GraphicsManager *self, Image *img, const Frame *f) {
|
|
return get_coalesced_frame_data_impl(self, img, f, 0);
|
|
}
|
|
|
|
static void
|
|
update_current_frame(GraphicsManager *self, Image *img, const CoalescedFrameData *data) {
|
|
bool needs_load = data == NULL;
|
|
CoalescedFrameData cfd;
|
|
if (needs_load) {
|
|
Frame *f = current_frame(img);
|
|
if (f == NULL) return;
|
|
cfd = get_coalesced_frame_data(self, img, f);
|
|
if (!cfd.buf) {
|
|
if (PyErr_Occurred()) PyErr_Print();
|
|
return;
|
|
}
|
|
data = &cfd;
|
|
}
|
|
upload_to_gpu(self, img, data->is_opaque, data->is_4byte_aligned, data->buf);
|
|
if (needs_load) free(data->buf);
|
|
img->current_frame_shown_at = monotonic();
|
|
}
|
|
|
|
static bool
|
|
reference_chain_too_large(Image *img, const Frame *frame) {
|
|
uint32_t limit = img->width * img->height * 2;
|
|
uint32_t drawn_area = frame->width * frame->height;
|
|
unsigned num = 1;
|
|
while (drawn_area < limit && num < 5) {
|
|
if (!frame->base_frame_id || !(frame = frame_for_id(img, frame->base_frame_id))) break;
|
|
drawn_area += frame->width * frame->height;
|
|
num++;
|
|
}
|
|
return num >= 5 || drawn_area >= limit;
|
|
}
|
|
|
|
static Image*
|
|
handle_animation_frame_load_command(GraphicsManager *self, GraphicsCommand *g, Image *img, const uint8_t *payload, bool *is_dirty) {
|
|
uint32_t frame_number = g->frame_number, fmt = g->format ? g->format : RGBA;
|
|
if (!frame_number || frame_number > img->extra_framecnt + 2) frame_number = img->extra_framecnt + 2;
|
|
bool is_new_frame = frame_number == img->extra_framecnt + 2;
|
|
g->frame_number = frame_number;
|
|
unsigned char tt = g->transmission_type ? g->transmission_type : 'd';
|
|
if (tt == 'd' && self->currently_loading.loading_for.image_id == img->internal_id) {
|
|
INIT_CHUNKED_LOAD;
|
|
} else {
|
|
self->currently_loading.loading_for = (const ImageAndFrame){0};
|
|
if (g->data_width > MAX_IMAGE_DIMENSION || g->data_height > MAX_IMAGE_DIMENSION) ABRT("EINVAL", "Image too large, width or height greater than %u", MAX_IMAGE_DIMENSION);
|
|
if (!initialize_load_data(self, g, img, tt, fmt, frame_number - 1)) return NULL;
|
|
}
|
|
LoadData *load_data = &self->currently_loading;
|
|
img = load_image_data(self, img, g, tt, fmt, payload);
|
|
if (!img || !load_data->loading_completed_successfully) return NULL;
|
|
self->currently_loading.loading_for = (const ImageAndFrame){0};
|
|
img = process_image_data(self, img, g, tt, fmt);
|
|
if (!img || !load_data->loading_completed_successfully) return img;
|
|
|
|
const unsigned long bytes_per_pixel = load_data->is_opaque ? 3 : 4;
|
|
if (load_data->data_sz < bytes_per_pixel * load_data->width * load_data->height)
|
|
ABRT("ENODATA", "Insufficient image data %zu < %zu", load_data->data_sz, bytes_per_pixel * g->data_width, g->data_height);
|
|
if (load_data->width > img->width)
|
|
ABRT("EINVAL", "Frame width %u larger than image width: %u", load_data->width, img->width);
|
|
if (load_data->height > img->height)
|
|
ABRT("EINVAL", "Frame height %u larger than image height: %u", load_data->height, img->height);
|
|
if (is_new_frame && cache_size(self) + load_data->data_sz > self->storage_limit * 5) {
|
|
remove_images(self, trim_predicate, img->internal_id);
|
|
if (cache_size(self) + load_data->data_sz > self->storage_limit * 5)
|
|
ABRT("ENOSPC", "Cache size exceeded cannot add new frames");
|
|
}
|
|
|
|
Frame transmitted_frame = {
|
|
.width = load_data->width, .height = load_data->height,
|
|
.x = g->x_offset, .y = g->y_offset,
|
|
.is_4byte_aligned = load_data->is_4byte_aligned,
|
|
.is_opaque = load_data->is_opaque,
|
|
.alpha_blend = g->blend_mode != 1 && !load_data->is_opaque,
|
|
.gap = g->gap > 0 ? g->gap : (g->gap < 0) ? 0 : DEFAULT_GAP,
|
|
.bgcolor = g->bgcolor,
|
|
};
|
|
Frame *frame;
|
|
if (is_new_frame) {
|
|
transmitted_frame.id = ++img->frame_id_counter;
|
|
Frame *frames = realloc(img->extra_frames, sizeof(img->extra_frames[0]) * (img->extra_framecnt + 1));
|
|
if (!frames) ABRT("ENOMEM", "Out of memory");
|
|
img->extra_frames = frames;
|
|
img->extra_framecnt++;
|
|
frame = img->extra_frames + frame_number - 2;
|
|
const ImageAndFrame key = { .image_id = img->internal_id, .frame_id = transmitted_frame.id };
|
|
if (g->other_frame_number) {
|
|
Frame *other_frame = frame_for_number(img, g->other_frame_number);
|
|
if (!other_frame) {
|
|
img->extra_framecnt--;
|
|
ABRT("EINVAL", "No frame with number: %u found", g->other_frame_number);
|
|
}
|
|
if (other_frame->base_frame_id && reference_chain_too_large(img, other_frame)) {
|
|
// since there is a long reference chain to render this frame, make
|
|
// it a fully coalesced key frame, for performance
|
|
CoalescedFrameData cfd = get_coalesced_frame_data(self, img, other_frame);
|
|
if (!cfd.buf) ABRT("EINVAL", "Failed to get data from frame referenced by frame: %u", frame_number);
|
|
ComposeData d = {
|
|
.over_px_sz = transmitted_frame.is_opaque ? 3 : 4, .under_px_sz = cfd.is_opaque ? 3: 4,
|
|
.over_width = transmitted_frame.width, .over_height = transmitted_frame.height,
|
|
.over_offset_x = transmitted_frame.x, .over_offset_y = transmitted_frame.y,
|
|
.under_width = img->width, .under_height = img->height,
|
|
.needs_blending = transmitted_frame.alpha_blend && !transmitted_frame.is_opaque
|
|
};
|
|
compose(d, cfd.buf, load_data->data);
|
|
free_load_data(load_data);
|
|
load_data->data = cfd.buf; load_data->data_sz = (size_t)img->width * img->height * d.under_px_sz;
|
|
transmitted_frame.width = img->width; transmitted_frame.height = img->height;
|
|
transmitted_frame.x = 0; transmitted_frame.y = 0;
|
|
transmitted_frame.is_4byte_aligned = cfd.is_4byte_aligned;
|
|
transmitted_frame.is_opaque = cfd.is_opaque;
|
|
} else {
|
|
transmitted_frame.base_frame_id = other_frame->id;
|
|
}
|
|
}
|
|
*frame = transmitted_frame;
|
|
if (!add_to_cache(self, key, load_data->data, load_data->data_sz)) {
|
|
img->extra_framecnt--;
|
|
if (PyErr_Occurred()) PyErr_Print();
|
|
ABRT("ENOSPC", "Failed to cache data for image frame");
|
|
}
|
|
img->animation_duration += frame->gap;
|
|
if (img->animation_state == ANIMATION_LOADING) {
|
|
self->has_images_needing_animation = true;
|
|
global_state.check_for_active_animated_images = true;
|
|
}
|
|
} else {
|
|
frame = frame_for_number(img, frame_number);
|
|
if (!frame) ABRT("EINVAL", "No frame with number: %u found", frame_number);
|
|
if (g->gap != 0) change_gap(img, frame, transmitted_frame.gap);
|
|
CoalescedFrameData cfd = get_coalesced_frame_data(self, img, frame);
|
|
if (!cfd.buf) ABRT("EINVAL", "No data associated with frame number: %u", frame_number);
|
|
frame->alpha_blend = false; frame->base_frame_id = 0; frame->bgcolor = 0;
|
|
frame->is_opaque = cfd.is_opaque; frame->is_4byte_aligned = cfd.is_4byte_aligned;
|
|
frame->x = 0; frame->y = 0; frame->width = img->width; frame->height = img->height;
|
|
const unsigned bytes_per_pixel = frame->is_opaque ? 3: 4;
|
|
ComposeData d = {
|
|
.over_px_sz = transmitted_frame.is_opaque ? 3 : 4, .under_px_sz = bytes_per_pixel,
|
|
.over_width = transmitted_frame.width, .over_height = transmitted_frame.height,
|
|
.over_offset_x = transmitted_frame.x, .over_offset_y = transmitted_frame.y,
|
|
.under_width = frame->width, .under_height = frame->height,
|
|
.needs_blending = transmitted_frame.alpha_blend && !transmitted_frame.is_opaque
|
|
};
|
|
compose(d, cfd.buf, load_data->data);
|
|
const ImageAndFrame key = { .image_id = img->internal_id, .frame_id = frame->id };
|
|
bool added = add_to_cache(self, key, cfd.buf, (size_t)bytes_per_pixel * frame->width * frame->height);
|
|
if (added && frame == current_frame(img)) {
|
|
update_current_frame(self, img, &cfd);
|
|
*is_dirty = true;
|
|
}
|
|
free(cfd.buf);
|
|
if (!added) {
|
|
if (PyErr_Occurred()) PyErr_Print();
|
|
ABRT("ENOSPC", "Failed to cache data for image frame");
|
|
}
|
|
}
|
|
return img;
|
|
}
|
|
|
|
#undef ABRT
|
|
|
|
static Image*
|
|
handle_delete_frame_command(GraphicsManager *self, const GraphicsCommand *g, bool *is_dirty) {
|
|
if (!g->id && !g->image_number) {
|
|
REPORT_ERROR("Delete frame data command without image id or number");
|
|
return NULL;
|
|
}
|
|
Image *img = g->id ? img_by_client_id(self, g->id) : img_by_client_number(self, g->image_number);
|
|
if (!img) {
|
|
REPORT_ERROR("Animation command refers to non-existent image with id: %u and number: %u", g->id, g->image_number);
|
|
return NULL;
|
|
}
|
|
uint32_t frame_number = MIN(img->extra_framecnt + 1, g->frame_number);
|
|
if (!frame_number) frame_number = 1;
|
|
if (!img->extra_framecnt) return g->delete_action == 'F' ? img : NULL;
|
|
*is_dirty = true;
|
|
ImageAndFrame key = {.image_id=img->internal_id};
|
|
bool remove_root = frame_number == 1;
|
|
uint32_t removed_gap = 0;
|
|
if (remove_root) {
|
|
key.frame_id = img->root_frame.id;
|
|
remove_from_cache(self, key);
|
|
if (PyErr_Occurred()) PyErr_Print();
|
|
removed_gap = img->root_frame.gap;
|
|
img->root_frame = img->extra_frames[0];
|
|
}
|
|
unsigned removed_idx = remove_root ? 0 : frame_number - 2;
|
|
if (!remove_root) {
|
|
key.frame_id = img->extra_frames[removed_idx].id;
|
|
removed_gap = img->extra_frames[removed_idx].gap;
|
|
remove_from_cache(self, key);
|
|
}
|
|
img->animation_duration = removed_gap < img->animation_duration ? img->animation_duration - removed_gap : 0;
|
|
if (PyErr_Occurred()) PyErr_Print();
|
|
if (removed_idx < img->extra_framecnt - 1) memmove(img->extra_frames + removed_idx, img->extra_frames + removed_idx + 1, sizeof(img->extra_frames[0]) * (img->extra_framecnt - 1 - removed_idx));
|
|
img->extra_framecnt--;
|
|
if (img->current_frame_index > img->extra_framecnt) {
|
|
img->current_frame_index = img->extra_framecnt;
|
|
update_current_frame(self, img, NULL);
|
|
return NULL;
|
|
}
|
|
if (removed_idx == img->current_frame_index) update_current_frame(self, img, NULL);
|
|
else if (removed_idx < img->current_frame_index) img->current_frame_index--;
|
|
return NULL;
|
|
}
|
|
|
|
static void
|
|
handle_animation_control_command(GraphicsManager *self, bool *is_dirty, const GraphicsCommand *g, Image *img) {
|
|
if (g->frame_number) {
|
|
uint32_t frame_idx = g->frame_number - 1;
|
|
if (frame_idx <= img->extra_framecnt) {
|
|
Frame *f = frame_idx ? img->extra_frames + frame_idx - 1 : &img->root_frame;
|
|
if (g->gap) change_gap(img, f, g->gap);
|
|
}
|
|
}
|
|
if (g->other_frame_number) {
|
|
uint32_t frame_idx = g->other_frame_number - 1;
|
|
if (frame_idx != img->current_frame_index && frame_idx <= img->extra_framecnt) {
|
|
img->current_frame_index = frame_idx;
|
|
*is_dirty = true;
|
|
update_current_frame(self, img, NULL);
|
|
}
|
|
}
|
|
if (g->animation_state) {
|
|
AnimationState old_state = img->animation_state;
|
|
switch(g->animation_state) {
|
|
case 1:
|
|
img->animation_state = ANIMATION_STOPPED; break;
|
|
case 2:
|
|
img->animation_state = ANIMATION_LOADING; break;
|
|
case 3:
|
|
img->animation_state = ANIMATION_RUNNING; break;
|
|
default:
|
|
break;
|
|
}
|
|
if (img->animation_state == ANIMATION_STOPPED) {
|
|
img->current_loop = 0;
|
|
} else {
|
|
if (old_state == ANIMATION_STOPPED) { img->current_frame_shown_at = monotonic(); img->is_drawn = true; }
|
|
self->has_images_needing_animation = true;
|
|
global_state.check_for_active_animated_images = true;
|
|
}
|
|
img->current_loop = 0;
|
|
}
|
|
if (g->loop_count) {
|
|
img->max_loops = g->loop_count - 1;
|
|
global_state.check_for_active_animated_images = true;
|
|
}
|
|
}
|
|
|
|
static bool
|
|
image_is_animatable(const Image *img) {
|
|
return img->animation_state != ANIMATION_STOPPED && img->extra_framecnt && img->is_drawn && img->animation_duration && (
|
|
!img->max_loops || img->current_loop < img->max_loops);
|
|
}
|
|
|
|
bool
|
|
scan_active_animations(GraphicsManager *self, const monotonic_t now, monotonic_t *minimum_gap, bool os_window_context_set) {
|
|
bool dirtied = false;
|
|
*minimum_gap = MONOTONIC_T_MAX;
|
|
if (!self->has_images_needing_animation) return dirtied;
|
|
self->has_images_needing_animation = false;
|
|
self->context_made_current_for_this_command = os_window_context_set;
|
|
iter_images(self) { Image *img = i.data->val;
|
|
if (image_is_animatable(img)) {
|
|
Frame *f = current_frame(img);
|
|
if (f) {
|
|
self->has_images_needing_animation = true;
|
|
monotonic_t next_frame_at = img->current_frame_shown_at + ms_to_monotonic_t(f->gap);
|
|
if (now >= next_frame_at) {
|
|
do {
|
|
uint32_t next = (img->current_frame_index + 1) % (img->extra_framecnt + 1);
|
|
if (!next) {
|
|
if (img->animation_state == ANIMATION_LOADING) goto skip_image;
|
|
if (++img->current_loop >= img->max_loops && img->max_loops) goto skip_image;
|
|
}
|
|
img->current_frame_index = next;
|
|
} while (!current_frame(img)->gap);
|
|
dirtied = true;
|
|
update_current_frame(self, img, NULL);
|
|
f = current_frame(img);
|
|
next_frame_at = img->current_frame_shown_at + ms_to_monotonic_t(f->gap);
|
|
}
|
|
if (next_frame_at > now && next_frame_at - now < *minimum_gap) *minimum_gap = next_frame_at - now;
|
|
}
|
|
}
|
|
skip_image:;
|
|
}
|
|
return dirtied;
|
|
}
|
|
// }}}
|
|
|
|
// {{{ composition a=c
|
|
static void
|
|
cfd_free(CoalescedFrameData *p) { free((p)->buf); p->buf = NULL; }
|
|
|
|
static void
|
|
handle_compose_command(GraphicsManager *self, bool *is_dirty, const GraphicsCommand *g, Image *img) {
|
|
Frame *src_frame = frame_for_number(img, g->frame_number);
|
|
if (!src_frame) {
|
|
set_command_failed_response("ENOENT", "No source frame number %u exists in image id: %u\n", g->frame_number, img->client_id);
|
|
return;
|
|
}
|
|
Frame *dest_frame = frame_for_number(img, g->other_frame_number);
|
|
if (!dest_frame) {
|
|
set_command_failed_response("ENOENT", "No destination frame number %u exists in image id: %u\n", g->other_frame_number, img->client_id);
|
|
return;
|
|
}
|
|
const unsigned int width = g->width ? g->width : img->width;
|
|
const unsigned int height = g->height ? g->height : img->height;
|
|
const unsigned int dest_x = g->x_offset, dest_y = g->y_offset, src_x = g->cell_x_offset, src_y = g->cell_y_offset;
|
|
if (dest_x + width > img->width || dest_y + height > img->height) {
|
|
set_command_failed_response("EINVAL", "The destination rectangle is out of bounds");
|
|
return;
|
|
}
|
|
if (src_x + width > img->width || src_y + height > img->height) {
|
|
set_command_failed_response("EINVAL", "The source rectangle is out of bounds");
|
|
return;
|
|
}
|
|
if (src_frame == dest_frame) {
|
|
bool x_overlaps = MAX(src_x, dest_x) < (MIN(src_x, dest_x) + width);
|
|
bool y_overlaps = MAX(src_y, dest_y) < (MIN(src_y, dest_y) + height);
|
|
if (x_overlaps && y_overlaps) {
|
|
set_command_failed_response("EINVAL", "The source and destination rectangles overlap and the src and destination frames are the same");
|
|
return;
|
|
}
|
|
}
|
|
|
|
RAII_CoalescedFrameData(src_data, get_coalesced_frame_data(self, img, src_frame));
|
|
if (!src_data.buf) {
|
|
set_command_failed_response("EINVAL", "Failed to get data for src frame: %u", g->frame_number - 1);
|
|
return;
|
|
}
|
|
RAII_CoalescedFrameData(dest_data, get_coalesced_frame_data(self, img, dest_frame));
|
|
if (!dest_data.buf) {
|
|
set_command_failed_response("EINVAL", "Failed to get data for destination frame: %u", g->other_frame_number - 1);
|
|
return;
|
|
}
|
|
ComposeData d = {
|
|
.over_px_sz = src_data.is_opaque ? 3 : 4, .under_px_sz = dest_data.is_opaque ? 3: 4,
|
|
.needs_blending = !g->compose_mode && !src_data.is_opaque,
|
|
.over_offset_x = src_x, .over_offset_y = src_y,
|
|
.under_offset_x = dest_x, .under_offset_y = dest_y,
|
|
.over_width = width, .over_height = height, .under_width = width, .under_height = height,
|
|
.stride = img->width
|
|
};
|
|
compose_rectangles(d, dest_data.buf, src_data.buf);
|
|
const ImageAndFrame key = { .image_id = img->internal_id, .frame_id = dest_frame->id };
|
|
if (!add_to_cache(self, key, dest_data.buf, ((size_t)(dest_data.is_opaque ? 3 : 4)) * img->width * img->height)) {
|
|
if (PyErr_Occurred()) PyErr_Print();
|
|
set_command_failed_response("ENOSPC", "Failed to store image data in disk cache");
|
|
}
|
|
// frame is now a fully coalesced frame
|
|
dest_frame->x = 0; dest_frame->y = 0; dest_frame->width = img->width; dest_frame->height = img->height;
|
|
dest_frame->base_frame_id = 0; dest_frame->bgcolor = 0;
|
|
*is_dirty = (g->other_frame_number - 1) == img->current_frame_index;
|
|
if (*is_dirty) update_current_frame(self, img, &dest_data);
|
|
}
|
|
// }}}
|
|
|
|
// Image lifetime/scrolling {{{
|
|
|
|
static ref_map_itr
|
|
remove_ref_itr(Image *img, ref_map_itr x) {
|
|
free(x.data->val);
|
|
return vt_erase_itr(&img->refs_by_internal_id, x);
|
|
}
|
|
|
|
|
|
static void
|
|
remove_ref(Image *img, ImageRef *ref) {
|
|
ref_map_itr i = vt_get(&img->refs_by_internal_id, ref->internal_id);
|
|
if (vt_is_end(i)) return;
|
|
remove_ref_itr(img, i);
|
|
}
|
|
|
|
static void
|
|
filter_refs(GraphicsManager *self, const void* data, bool free_images, bool (*filter_func)(const ImageRef*, Image*, const void*, CellPixelSize), CellPixelSize cell, bool only_first_image, bool free_only_matched) {
|
|
for (image_map_itr ii = vt_first(&self->images_by_internal_id); !vt_is_end(ii); ) { Image *img = ii.data->val;
|
|
bool matched = false;
|
|
for (ref_map_itr ri = vt_first(&img->refs_by_internal_id); !vt_is_end(ri); ) { ImageRef *ref = ri.data->val;
|
|
if (filter_func(ref, img, data, cell)) {
|
|
ri = remove_ref_itr(img, ri);
|
|
set_layers_dirty(self);
|
|
matched = true;
|
|
} else ri = vt_next(ri);
|
|
}
|
|
if ((!free_only_matched || matched) && !vt_size(&img->refs_by_internal_id) && (free_images || img->client_id == 0)) ii = remove_image_itr(self, ii);
|
|
else ii = vt_next(ii);
|
|
if (only_first_image && matched) break;
|
|
}
|
|
}
|
|
|
|
|
|
static void
|
|
modify_refs(GraphicsManager *self, const void* data, bool (*filter_func)(ImageRef*, Image*, const void*, CellPixelSize), CellPixelSize cell) {
|
|
for (image_map_itr ii = vt_first(&self->images_by_internal_id); !vt_is_end(ii); ) { Image *img = ii.data->val;
|
|
for (ref_map_itr ri = vt_first(&img->refs_by_internal_id); !vt_is_end(ri); ) { ImageRef *ref = ri.data->val;
|
|
if (filter_func(ref, img, data, cell)) ri = remove_ref_itr(img, ri);
|
|
else ri = vt_next(ri);
|
|
}
|
|
if (!vt_size(&img->refs_by_internal_id) && img->client_id == 0 && img->client_number == 0) {
|
|
// references have all scrolled off the history buffer and the image has no way to reference it
|
|
// to create new references so remove it.
|
|
ii = remove_image_itr(self, ii);
|
|
} else ii = vt_next(ii);
|
|
}
|
|
}
|
|
|
|
|
|
static bool
|
|
scroll_filter_func(ImageRef *ref, Image UNUSED *img, const void *data, CellPixelSize cell UNUSED) {
|
|
if (ref->is_virtual_ref) return false;
|
|
ScrollData *d = (ScrollData*)data;
|
|
ref->start_row += d->amt;
|
|
return ref->start_row + (int32_t)ref->effective_num_rows <= d->limit;
|
|
}
|
|
|
|
static bool
|
|
ref_within_region(const ImageRef *ref, index_type margin_top, index_type margin_bottom) {
|
|
return ref->start_row >= (int32_t)margin_top && ref->start_row + (int32_t)ref->effective_num_rows - 1 <= (int32_t)margin_bottom;
|
|
}
|
|
|
|
static bool
|
|
ref_outside_region(const ImageRef *ref, index_type margin_top, index_type margin_bottom) {
|
|
return ref->start_row + (int32_t)ref->effective_num_rows <= (int32_t)margin_top || ref->start_row > (int32_t)margin_bottom;
|
|
}
|
|
|
|
static bool
|
|
scroll_filter_margins_func(ImageRef* ref, Image* img, const void* data, CellPixelSize cell) {
|
|
if (ref->is_virtual_ref) return false;
|
|
ScrollData *d = (ScrollData*)data;
|
|
if (ref_within_region(ref, d->margin_top, d->margin_bottom)) {
|
|
ref->start_row += d->amt;
|
|
if (ref_outside_region(ref, d->margin_top, d->margin_bottom)) return true;
|
|
// Clip the image if scrolling has resulted in part of it being outside the page area
|
|
uint32_t clip_amt, clipped_rows;
|
|
if (ref->start_row < (int32_t)d->margin_top) {
|
|
// image moved up
|
|
clipped_rows = d->margin_top - ref->start_row;
|
|
clip_amt = cell.height * clipped_rows;
|
|
if (ref->src_height <= clip_amt) return true;
|
|
ref->src_y += clip_amt; ref->src_height -= clip_amt;
|
|
ref->effective_num_rows -= clipped_rows;
|
|
update_src_rect(ref, img);
|
|
ref->start_row += clipped_rows;
|
|
} else if (ref->start_row + (int32_t)ref->effective_num_rows - 1 > (int32_t)d->margin_bottom) {
|
|
// image moved down
|
|
clipped_rows = ref->start_row + ref->effective_num_rows - 1 - d->margin_bottom;
|
|
clip_amt = cell.height * clipped_rows;
|
|
if (ref->src_height <= clip_amt) return true;
|
|
ref->src_height -= clip_amt;
|
|
ref->effective_num_rows -= clipped_rows;
|
|
update_src_rect(ref, img);
|
|
}
|
|
return ref_outside_region(ref, d->margin_top, d->margin_bottom);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void
|
|
grman_scroll_images(GraphicsManager *self, const ScrollData *data, CellPixelSize cell) {
|
|
if (vt_size(&self->images_by_internal_id)) {
|
|
set_layers_dirty(self);
|
|
modify_refs(self, data, data->has_margins ? scroll_filter_margins_func : scroll_filter_func, cell);
|
|
}
|
|
}
|
|
|
|
static bool
|
|
cell_image_row_filter_func(const ImageRef *ref, Image UNUSED *img, const void *data, CellPixelSize cell UNUSED) {
|
|
if (ref->is_virtual_ref || !is_cell_image(ref))
|
|
return false;
|
|
int32_t top = *(int32_t *)data;
|
|
int32_t bottom = *((int32_t *)data + 1);
|
|
return ref_within_region(ref, top, bottom);
|
|
}
|
|
|
|
static bool
|
|
cell_image_filter_func(const ImageRef *ref, Image UNUSED *img, const void *data UNUSED, CellPixelSize cell UNUSED) {
|
|
return !ref->is_virtual_ref && is_cell_image(ref);
|
|
}
|
|
|
|
// Remove cell images within the given region.
|
|
void
|
|
grman_remove_cell_images(GraphicsManager *self, int32_t top, int32_t bottom) {
|
|
CellPixelSize dummy = {0};
|
|
int32_t data[] = {top, bottom};
|
|
filter_refs(self, data, false, cell_image_row_filter_func, dummy, false, true);
|
|
}
|
|
|
|
void
|
|
grman_remove_all_cell_images(GraphicsManager *self) {
|
|
CellPixelSize dummy = {0};
|
|
filter_refs(self, NULL, false, cell_image_filter_func, dummy, false, true);
|
|
}
|
|
|
|
|
|
static bool
|
|
clear_filter_func(const ImageRef *ref, Image UNUSED *img, const void UNUSED *data, CellPixelSize cell UNUSED) {
|
|
if (ref->is_virtual_ref) return false;
|
|
return ref->start_row + (int32_t)ref->effective_num_rows > 0;
|
|
}
|
|
|
|
static bool
|
|
clear_filter_func_noncell(const ImageRef *ref, Image UNUSED *img, const void UNUSED *data, CellPixelSize cell UNUSED) {
|
|
if (ref->is_virtual_ref || is_cell_image(ref)) return false;
|
|
return ref->start_row + (int32_t)ref->effective_num_rows > 0;
|
|
}
|
|
|
|
static bool
|
|
clear_all_filter_func(const ImageRef *ref UNUSED, Image UNUSED *img, const void UNUSED *data, CellPixelSize cell UNUSED) {
|
|
if (ref->is_virtual_ref) return false;
|
|
return true;
|
|
}
|
|
|
|
void
|
|
grman_clear(GraphicsManager *self, bool all, CellPixelSize cell) {
|
|
filter_refs(self, NULL, true, all ? clear_all_filter_func : clear_filter_func, cell, false, false);
|
|
}
|
|
|
|
static bool
|
|
id_filter_func(const ImageRef *ref, Image *img, const void *data, CellPixelSize cell UNUSED) {
|
|
const GraphicsCommand *g = data;
|
|
if (g->id && img->client_id == g->id) return !g->placement_id || ref->client_id == g->placement_id;
|
|
return false;
|
|
}
|
|
|
|
static bool
|
|
id_range_filter_func(const ImageRef *ref UNUSED, Image *img, const void *data, CellPixelSize cell UNUSED) {
|
|
const GraphicsCommand *g = data;
|
|
return img->client_id && g->x_offset <= img->client_id && img->client_id <= g->y_offset;
|
|
}
|
|
|
|
|
|
static bool
|
|
x_filter_func(const ImageRef *ref, Image UNUSED *img, const void *data, CellPixelSize cell UNUSED) {
|
|
if (ref->is_virtual_ref || is_cell_image(ref)) return false;
|
|
const GraphicsCommand *g = data;
|
|
return ref->start_column <= (int32_t)g->x_offset - 1 && ((int32_t)g->x_offset - 1) < ((int32_t)(ref->start_column + ref->effective_num_cols));
|
|
}
|
|
|
|
static bool
|
|
y_filter_func(const ImageRef *ref, Image UNUSED *img, const void *data, CellPixelSize cell UNUSED) {
|
|
if (ref->is_virtual_ref || is_cell_image(ref)) return false;
|
|
const GraphicsCommand *g = data;
|
|
return ref->start_row <= (int32_t)g->y_offset - 1 && ((int32_t)g->y_offset - 1) < ((int32_t)(ref->start_row + ref->effective_num_rows));
|
|
}
|
|
|
|
static bool
|
|
z_filter_func(const ImageRef *ref, Image UNUSED *img, const void *data, CellPixelSize cell UNUSED) {
|
|
if (ref->is_virtual_ref || is_cell_image(ref)) return false;
|
|
const GraphicsCommand *g = data;
|
|
return ref->z_index == g->z_index;
|
|
}
|
|
|
|
|
|
static bool
|
|
point_filter_func(const ImageRef *ref, Image *img, const void *data, CellPixelSize cell) {
|
|
if (ref->is_virtual_ref || is_cell_image(ref)) return false;
|
|
return x_filter_func(ref, img, data, cell) && y_filter_func(ref, img, data, cell);
|
|
}
|
|
|
|
static bool
|
|
point3d_filter_func(const ImageRef *ref, Image *img, const void *data, CellPixelSize cell) {
|
|
if (ref->is_virtual_ref || is_cell_image(ref)) return false;
|
|
return z_filter_func(ref, img, data, cell) && point_filter_func(ref, img, data, cell);
|
|
}
|
|
|
|
|
|
static void
|
|
handle_delete_command(GraphicsManager *self, const GraphicsCommand *g, Cursor *c, bool *is_dirty, CellPixelSize cell) {
|
|
if (self->currently_loading.loading_for.image_id) free_load_data(&self->currently_loading);
|
|
GraphicsCommand d;
|
|
if (!g->placement_id) {
|
|
// special case freeing of images with no refs by id or number as
|
|
// filter_refs doesnt handle this
|
|
Image *img = NULL;
|
|
switch(g->delete_action) {
|
|
case 'I': img = img_by_client_id(self, g->id); break;
|
|
case 'N': img = img_by_client_number(self, g->image_number); break;
|
|
case 'R': {
|
|
for (image_map_itr ii = vt_first(&self->images_by_internal_id); !vt_is_end(ii); ) {
|
|
img = ii.data->val;
|
|
if (id_range_filter_func(NULL, img, g, cell) && !vt_size(&img->refs_by_internal_id)) ii = remove_image_itr(self, ii);
|
|
else ii = vt_next(ii);
|
|
}
|
|
} img = NULL; break;
|
|
}
|
|
if (img && !vt_size(&img->refs_by_internal_id)) { remove_image(self, img); goto end; }
|
|
}
|
|
switch (g->delete_action) {
|
|
#define I(u, data, func) filter_refs(self, data, g->delete_action == u, func, cell, false, true); *is_dirty = true; break
|
|
#define D(l, u, data, func) case l: case u: I(u, data, func)
|
|
#define G(l, u, func) D(l, u, g, func)
|
|
case 0:
|
|
D('a', 'A', NULL, clear_filter_func_noncell);
|
|
G('i', 'I', id_filter_func);
|
|
G('r', 'R', id_range_filter_func);
|
|
G('p', 'P', point_filter_func);
|
|
G('q', 'Q', point3d_filter_func);
|
|
G('x', 'X', x_filter_func);
|
|
G('y', 'Y', y_filter_func);
|
|
G('z', 'Z', z_filter_func);
|
|
case 'c':
|
|
case 'C':
|
|
d.x_offset = c->x + 1; d.y_offset = c->y + 1;
|
|
I('C', &d, point_filter_func);
|
|
case 'n':
|
|
case 'N': {
|
|
Image *img = img_by_client_number(self, g->image_number);
|
|
if (img) {
|
|
for (ref_map_itr ri = vt_first(&img->refs_by_internal_id); !vt_is_end(ri); ) { ImageRef *ref = ri.data->val;
|
|
if (!g->placement_id || g->placement_id == ref->client_id) {
|
|
ri = remove_ref_itr(img, ri);
|
|
set_layers_dirty(self);
|
|
} else ri = vt_next(ri);
|
|
}
|
|
if (!vt_size(&img->refs_by_internal_id) && (g->delete_action == 'N' || img->client_id == 0)) remove_image(self, img);
|
|
}
|
|
} break;
|
|
case 'f':
|
|
case 'F': {
|
|
Image *img = handle_delete_frame_command(self, g, is_dirty);
|
|
if (img != NULL) {
|
|
remove_image(self, img);
|
|
*is_dirty = true;
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
REPORT_ERROR("Unknown graphics command delete action: %c", g->delete_action);
|
|
break;
|
|
#undef G
|
|
#undef D
|
|
#undef I
|
|
}
|
|
end:
|
|
if (!vt_size(&self->images_by_internal_id) && self->render_data.count) self->render_data.count = 0;
|
|
}
|
|
|
|
// }}}
|
|
|
|
void
|
|
grman_resize(GraphicsManager *self, index_type old_lines UNUSED, index_type lines UNUSED, index_type old_columns, index_type columns, index_type num_content_lines_before, index_type num_content_lines_after) {
|
|
ImageRef *ref; Image *img;
|
|
set_layers_dirty(self);
|
|
if (columns == old_columns && num_content_lines_before > num_content_lines_after) {
|
|
const unsigned int vertical_shrink_size = num_content_lines_before - num_content_lines_after;
|
|
iter_images(self) { img = i.data->val;
|
|
iter_refs(img) { ref = i.data->val;
|
|
if (ref->is_virtual_ref || is_cell_image(ref)) continue;
|
|
ref->start_row -= vertical_shrink_size;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
grman_rescale(GraphicsManager *self, CellPixelSize cell) {
|
|
ImageRef *ref; Image *img;
|
|
set_layers_dirty(self);
|
|
iter_images(self) { img = i.data->val;
|
|
iter_refs(img) { ref = i.data->val;
|
|
if (ref->is_virtual_ref || is_cell_image(ref)) continue;
|
|
ref->cell_x_offset = MIN(ref->cell_x_offset, cell.width - 1);
|
|
ref->cell_y_offset = MIN(ref->cell_y_offset, cell.height - 1);
|
|
update_dest_rect(ref, ref->num_cols, ref->num_rows, cell);
|
|
}
|
|
}
|
|
}
|
|
|
|
const char*
|
|
grman_handle_command(GraphicsManager *self, const GraphicsCommand *g, const uint8_t *payload, Cursor *c, bool *is_dirty, CellPixelSize cell) {
|
|
const char *ret = NULL;
|
|
command_response[0] = 0;
|
|
self->context_made_current_for_this_command = false;
|
|
|
|
if (g->id && g->image_number) {
|
|
set_command_failed_response("EINVAL", "Must not specify both image id and image number");
|
|
return finish_command_response(g, false);
|
|
}
|
|
|
|
switch(g->action) {
|
|
case 0:
|
|
case 't':
|
|
case 'T':
|
|
case 'q': {
|
|
uint32_t iid = g->id, q_iid = iid;
|
|
bool is_query = g->action == 'q';
|
|
if (is_query) { iid = 0; if (!q_iid) { REPORT_ERROR("Query graphics command without image id"); break; } }
|
|
Image *image = handle_add_command(self, g, payload, is_dirty, iid, is_query);
|
|
if (!self->currently_loading.loading_for.image_id) free_load_data(&self->currently_loading);
|
|
GraphicsCommand *lg = &self->currently_loading.start_command;
|
|
if (g->quiet) lg->quiet = g->quiet;
|
|
if (is_query) ret = finish_command_response(&(const GraphicsCommand){.id=q_iid, .quiet=g->quiet}, image != NULL);
|
|
else ret = finish_command_response(lg, image != NULL);
|
|
if (lg->action == 'T' && image && image->root_frame_data_loaded) handle_put_command(self, lg, c, is_dirty, image, cell);
|
|
id_type added_image_id = image ? image->internal_id : 0;
|
|
if (g->action == 'q') remove_images(self, add_trim_predicate, 0);
|
|
if (self->used_storage > self->storage_limit) apply_storage_quota(self, self->storage_limit, added_image_id);
|
|
break;
|
|
}
|
|
case 'a':
|
|
case 'f': {
|
|
if (!g->id && !g->image_number && !self->currently_loading.loading_for.image_id) {
|
|
REPORT_ERROR("Add frame data command without image id or number");
|
|
break;
|
|
}
|
|
Image *img;
|
|
if (self->currently_loading.loading_for.image_id) img = img_by_internal_id(self, self->currently_loading.loading_for.image_id);
|
|
else img = g->id ? img_by_client_id(self, g->id) : img_by_client_number(self, g->image_number);
|
|
if (!img) {
|
|
set_command_failed_response("ENOENT", "Animation command refers to non-existent image with id: %u and number: %u", g->id, g->image_number);
|
|
ret = finish_command_response(g, false);
|
|
} else {
|
|
GraphicsCommand ag = *g;
|
|
if (ag.action == 'f') {
|
|
img = handle_animation_frame_load_command(self, &ag, img, payload, is_dirty);
|
|
if (!self->currently_loading.loading_for.image_id) free_load_data(&self->currently_loading);
|
|
if (g->quiet) ag.quiet = g->quiet;
|
|
else ag.quiet = self->currently_loading.start_command.quiet;
|
|
ret = finish_command_response(&ag, img != NULL);
|
|
} else if (ag.action == 'a') {
|
|
handle_animation_control_command(self, is_dirty, &ag, img);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case 'p': {
|
|
if (!g->id && !g->image_number) {
|
|
REPORT_ERROR("Put graphics command without image id or number");
|
|
break;
|
|
}
|
|
uint32_t image_id = handle_put_command(self, g, c, is_dirty, NULL, cell);
|
|
GraphicsCommand rg = *g; rg.id = image_id;
|
|
ret = finish_command_response(&rg, true);
|
|
break;
|
|
}
|
|
case 'd':
|
|
handle_delete_command(self, g, c, is_dirty, cell);
|
|
break;
|
|
case 'c':
|
|
if (!g->id && !g->image_number) {
|
|
REPORT_ERROR("Compose frame data command without image id or number");
|
|
break;
|
|
}
|
|
Image *img = g->id ? img_by_client_id(self, g->id) : img_by_client_number(self, g->image_number);
|
|
if (!img) {
|
|
set_command_failed_response("ENOENT", "Animation command refers to non-existent image with id: %u and number: %u", g->id, g->image_number);
|
|
ret = finish_command_response(g, false);
|
|
} else {
|
|
handle_compose_command(self, is_dirty, g, img);
|
|
ret = finish_command_response(g, true);
|
|
}
|
|
break;
|
|
default:
|
|
REPORT_ERROR("Unknown graphics command action: %c", g->action);
|
|
break;
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
|
|
// Boilerplate {{{
|
|
static PyObject *
|
|
new_graphicsmanager_object(PyTypeObject UNUSED *type, PyObject UNUSED *args, PyObject UNUSED *kwds) {
|
|
PyObject *ans = (PyObject*)grman_alloc(false);
|
|
if (ans == NULL) PyErr_NoMemory();
|
|
return ans;
|
|
}
|
|
|
|
static PyObject*
|
|
image_as_dict(GraphicsManager *self, Image *img) {
|
|
#define U(x) #x, (unsigned int)(img->x)
|
|
#define B(x) #x, img->x ? Py_True : Py_False
|
|
PyObject *frames = PyTuple_New(img->extra_framecnt);
|
|
for (unsigned i = 0; i < img->extra_framecnt; i++) {
|
|
Frame *f = img->extra_frames + i;
|
|
CoalescedFrameData cfd = get_coalesced_frame_data(self, img, f);
|
|
if (!cfd.buf) { PyErr_SetString(PyExc_RuntimeError, "Failed to get data for frame"); return NULL; }
|
|
PyTuple_SET_ITEM(frames, i, Py_BuildValue(
|
|
"{sI sI sy#}",
|
|
"gap", f->gap,
|
|
"id", f->id,
|
|
"data", cfd.buf, (Py_ssize_t)((cfd.is_opaque ? 3 : 4) * img->width * img->height)
|
|
));
|
|
free(cfd.buf);
|
|
if (PyErr_Occurred()) { Py_CLEAR(frames); return NULL; }
|
|
}
|
|
CoalescedFrameData cfd = get_coalesced_frame_data(self, img, &img->root_frame);
|
|
if (!cfd.buf) { PyErr_SetString(PyExc_RuntimeError, "Failed to get data for root frame"); return NULL; }
|
|
PyObject *ans = Py_BuildValue("{sI sI sI sI sI sI sI " "sO sI sO " "sI sI sI " "sI sy# sN}",
|
|
"texture_id", texture_id_for_img(img), U(client_id), U(width), U(height), U(internal_id),
|
|
"refs.count", (unsigned int)vt_size(&img->refs_by_internal_id), U(client_number),
|
|
|
|
B(root_frame_data_loaded), U(animation_state), "is_4byte_aligned", img->root_frame.is_4byte_aligned ? Py_True : Py_False,
|
|
|
|
U(current_frame_index), "root_frame_gap", img->root_frame.gap, U(current_frame_index),
|
|
|
|
U(animation_duration), "data", cfd.buf, (Py_ssize_t)((cfd.is_opaque ? 3 : 4) * img->width * img->height), "extra_frames", frames
|
|
);
|
|
free(cfd.buf);
|
|
return ans;
|
|
#undef B
|
|
#undef U
|
|
}
|
|
|
|
#define W(x) static PyObject* py##x(GraphicsManager UNUSED *self, PyObject *args)
|
|
#define PA(fmt, ...) if(!PyArg_ParseTuple(args, fmt, __VA_ARGS__)) return NULL;
|
|
|
|
W(image_for_client_id) {
|
|
unsigned long id = PyLong_AsUnsignedLong(args);
|
|
bool existing = false;
|
|
Image *img = find_or_create_image(self, id, &existing);
|
|
if (!existing) { Py_RETURN_NONE; }
|
|
return image_as_dict(self, img);
|
|
}
|
|
|
|
W(image_for_client_number) {
|
|
unsigned long num = PyLong_AsUnsignedLong(args);
|
|
Image *img = img_by_client_number(self, num);
|
|
if (!img) Py_RETURN_NONE;
|
|
return image_as_dict(self, img);
|
|
}
|
|
|
|
W(shm_write) {
|
|
const char *name, *data;
|
|
Py_ssize_t sz;
|
|
PA("ss#", &name, &data, &sz);
|
|
int fd = shm_open(name, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR);
|
|
if (fd == -1) { PyErr_SetFromErrnoWithFilename(PyExc_OSError, name); return NULL; }
|
|
int ret = ftruncate(fd, sz);
|
|
if (ret != 0) { safe_close(fd, __FILE__, __LINE__); PyErr_SetFromErrnoWithFilename(PyExc_OSError, name); return NULL; }
|
|
void *addr = mmap(0, sz, PROT_WRITE, MAP_SHARED, fd, 0);
|
|
if (addr == MAP_FAILED) { safe_close(fd, __FILE__, __LINE__); PyErr_SetFromErrnoWithFilename(PyExc_OSError, name); return NULL; }
|
|
memcpy(addr, data, sz);
|
|
if (munmap(addr, sz) != 0) { safe_close(fd, __FILE__, __LINE__); PyErr_SetFromErrnoWithFilename(PyExc_OSError, name); return NULL; }
|
|
safe_close(fd, __FILE__, __LINE__);
|
|
Py_RETURN_NONE;
|
|
}
|
|
|
|
W(shm_unlink) {
|
|
char *name;
|
|
PA("s", &name);
|
|
int ret = shm_unlink(name);
|
|
if (ret == -1) { PyErr_SetFromErrnoWithFilename(PyExc_OSError, name); return NULL; }
|
|
Py_RETURN_NONE;
|
|
}
|
|
|
|
W(update_layers) {
|
|
unsigned int scrolled_by, sx, sy; float xstart, ystart, dx, dy;
|
|
CellPixelSize cell;
|
|
PA("IffffIIII", &scrolled_by, &xstart, &ystart, &dx, &dy, &sx, &sy, &cell.width, &cell.height);
|
|
grman_update_layers(self, scrolled_by, xstart, ystart, dx, dy, sx, sy, cell);
|
|
PyObject *ans = PyTuple_New(self->render_data.count);
|
|
for (size_t i = 0; i < self->render_data.count; i++) {
|
|
ImageRenderData *r = self->render_data.item + i;
|
|
#define R(which) Py_BuildValue("{sf sf sf sf}", "left", r->which.left, "top", r->which.top, "right", r->which.right, "bottom", r->which.bottom)
|
|
PyTuple_SET_ITEM(ans, i,
|
|
Py_BuildValue("{sN sN sI si sK sK}", "src_rect", R(src_rect), "dest_rect", R(dest_rect), "group_count", r->group_count, "z_index", r->z_index, "image_id", r->image_id, "ref_id", r->ref_id)
|
|
);
|
|
#undef R
|
|
}
|
|
return ans;
|
|
}
|
|
|
|
#define M(x, va) {#x, (PyCFunction)py##x, va, ""}
|
|
|
|
static PyMethodDef methods[] = {
|
|
M(image_for_client_id, METH_O),
|
|
M(image_for_client_number, METH_O),
|
|
M(update_layers, METH_VARARGS),
|
|
{NULL} /* Sentinel */
|
|
};
|
|
|
|
static PyObject*
|
|
get_image_count(GraphicsManager *self, void* closure UNUSED) {
|
|
return PyLong_FromSize_t(vt_size(&self->images_by_internal_id));
|
|
}
|
|
|
|
static PyGetSetDef getsets[] = {
|
|
{"image_count", (getter)get_image_count, NULL, NULL, NULL},
|
|
{NULL},
|
|
};
|
|
|
|
static PyMemberDef members[] = {
|
|
{"storage_limit", T_PYSSIZET, offsetof(GraphicsManager, storage_limit), 0, "storage_limit"},
|
|
{"disk_cache", T_OBJECT_EX, offsetof(GraphicsManager, disk_cache), READONLY, "disk_cache"},
|
|
{NULL},
|
|
};
|
|
|
|
PyTypeObject GraphicsManager_Type = {
|
|
PyVarObject_HEAD_INIT(NULL, 0)
|
|
.tp_name = "fast_data_types.GraphicsManager",
|
|
.tp_basicsize = sizeof(GraphicsManager),
|
|
.tp_dealloc = (destructor)dealloc,
|
|
.tp_flags = Py_TPFLAGS_DEFAULT,
|
|
.tp_doc = "GraphicsManager",
|
|
.tp_new = new_graphicsmanager_object,
|
|
.tp_methods = methods,
|
|
.tp_members = members,
|
|
.tp_getset = getsets,
|
|
};
|
|
|
|
static PyObject*
|
|
pycreate_canvas(PyObject *self UNUSED, PyObject *args) {
|
|
unsigned int bytes_per_pixel;
|
|
unsigned int over_width, width, height, x, y;
|
|
Py_ssize_t over_sz;
|
|
const uint8_t *over_data;
|
|
if (!PyArg_ParseTuple(args, "y#IIIIII", &over_data, &over_sz, &over_width, &x, &y, &width, &height, &bytes_per_pixel)) return NULL;
|
|
size_t canvas_sz = (size_t)width * height * bytes_per_pixel;
|
|
PyObject *ans = PyBytes_FromStringAndSize(NULL, canvas_sz);
|
|
if (!ans) return NULL;
|
|
|
|
uint8_t* canvas = (uint8_t*)PyBytes_AS_STRING(ans);
|
|
memset(canvas, 0, canvas_sz);
|
|
ComposeData cd = {
|
|
.needs_blending = bytes_per_pixel == 4,
|
|
.over_width = over_width, .over_height = over_sz / (bytes_per_pixel * over_width),
|
|
.under_width = width, .under_height = height,
|
|
.over_px_sz = bytes_per_pixel, .under_px_sz = bytes_per_pixel,
|
|
.over_offset_x = x, .over_offset_y = y
|
|
};
|
|
compose(cd, canvas, over_data);
|
|
|
|
return ans;
|
|
}
|
|
|
|
static PyMethodDef module_methods[] = {
|
|
M(shm_write, METH_VARARGS),
|
|
M(shm_unlink, METH_VARARGS),
|
|
M(create_canvas, METH_VARARGS),
|
|
{NULL, NULL, 0, NULL} /* Sentinel */
|
|
};
|
|
|
|
|
|
bool
|
|
init_graphics(PyObject *module) {
|
|
if (PyType_Ready(&GraphicsManager_Type) < 0) return false;
|
|
if (PyModule_AddObject(module, "GraphicsManager", (PyObject *)&GraphicsManager_Type) != 0) return false;
|
|
if (PyModule_AddFunctions(module, module_methods) != 0) return false;
|
|
if (PyModule_AddIntMacro(module, IMAGE_PLACEHOLDER_CHAR) != 0) return false;
|
|
Py_INCREF(&GraphicsManager_Type);
|
|
return true;
|
|
}
|
|
|
|
void grman_mark_layers_dirty(GraphicsManager *self) { set_layers_dirty(self); }
|
|
void grman_set_window_id(GraphicsManager *self, id_type id) { self->window_id = id; }
|
|
bool grman_has_images(GraphicsManager *self) { return self->num_of_below_refs + self->num_of_negative_refs + self->num_of_positive_refs > 0; }
|
|
GraphicsRenderData grman_render_data(GraphicsManager *self) {
|
|
GraphicsRenderData ans = {
|
|
.count=self->render_data.count, .capacity=self->render_data.capacity, .images=self->render_data.item,
|
|
.num_of_below_refs=self->num_of_below_refs, .num_of_negative_refs=self->num_of_negative_refs,
|
|
.num_of_positive_refs=self->num_of_positive_refs,
|
|
};
|
|
return ans;
|
|
}
|
|
// }}}
|