mirror of
https://github.com/kovidgoyal/kitty.git
synced 2026-02-01 11:34:59 +01:00
2477 lines
103 KiB
C
2477 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;
|
|
self->layout_or_image_data_change_count++;
|
|
}
|
|
|
|
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;
|
|
dest->layout_or_image_data_change_count = 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);
|
|
self->layout_or_image_data_change_count++;
|
|
}
|
|
}
|
|
|
|
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; }
|
|
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, .change_count=self->layout_or_image_data_change_count,
|
|
};
|
|
return ans;
|
|
}
|
|
// }}}
|