Refactor to store frame deltas instead of fully coalesced frames
Trades CPU usage for cache/memory space
This commit is contained in:
parent
c152624634
commit
19f0e2303d
@ -554,6 +554,12 @@ Key Value Default Description
|
|||||||
``z`` 32-bit integer ``0`` The gap (in milliseconds) of this frame from the next one. A value of
|
``z`` 32-bit integer ``0`` The gap (in milliseconds) of this frame from the next one. A value of
|
||||||
zero is ignored. Negative values create a *gapless* frame. If not specified,
|
zero is ignored. Negative values create a *gapless* frame. If not specified,
|
||||||
frames have a default gap of ``40ms``. The root frame defaults to zero gap.
|
frames have a default gap of ``40ms``. The root frame defaults to zero gap.
|
||||||
|
``X`` Positive integer ``0`` The composition mode for blending pixels when creating a new frame or
|
||||||
|
editing a frame's data. The default is full alpha blending. ``1`` means a
|
||||||
|
simple overwrite.
|
||||||
|
``Y`` Positive integer ``0`` The background color for pixels not
|
||||||
|
specified in the frame data. Must be in 32-bit RGBA format
|
||||||
|
|
||||||
|
|
||||||
**Keys for animation control**
|
**Keys for animation control**
|
||||||
-----------------------------------------------------------
|
-----------------------------------------------------------
|
||||||
|
|||||||
556
kitty/graphics.c
556
kitty/graphics.c
@ -24,6 +24,8 @@ PyTypeObject GraphicsManager_Type;
|
|||||||
|
|
||||||
#define DEFAULT_STORAGE_LIMIT 320u * (1024u * 1024u)
|
#define DEFAULT_STORAGE_LIMIT 320u * (1024u * 1024u)
|
||||||
#define REPORT_ERROR(...) { log_error(__VA_ARGS__); }
|
#define REPORT_ERROR(...) { log_error(__VA_ARGS__); }
|
||||||
|
|
||||||
|
// caching {{{
|
||||||
#define CACHE_KEY_BUFFER_SIZE 32
|
#define CACHE_KEY_BUFFER_SIZE 32
|
||||||
|
|
||||||
static inline size_t
|
static inline size_t
|
||||||
@ -59,6 +61,7 @@ read_from_cache(const GraphicsManager *self, const ImageAndFrame x, void **data,
|
|||||||
static inline size_t
|
static inline size_t
|
||||||
cache_size(const GraphicsManager *self) { return disk_cache_total_size(self->disk_cache); }
|
cache_size(const GraphicsManager *self) { return disk_cache_total_size(self->disk_cache); }
|
||||||
#undef CK
|
#undef CK
|
||||||
|
// }}}
|
||||||
|
|
||||||
|
|
||||||
GraphicsManager*
|
GraphicsManager*
|
||||||
@ -88,6 +91,7 @@ free_load_data(LoadData *ld) {
|
|||||||
free(ld->buf); ld->buf_used = 0; ld->buf_capacity = 0; ld->buf = NULL;
|
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);
|
if (ld->mapped_file) munmap(ld->mapped_file, ld->mapped_file_sz);
|
||||||
ld->mapped_file = NULL; ld->mapped_file_sz = 0;
|
ld->mapped_file = NULL; ld->mapped_file_sz = 0;
|
||||||
|
ld->loading_for = (const ImageAndFrame){0};
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline void
|
static inline void
|
||||||
@ -104,7 +108,6 @@ free_image(GraphicsManager *self, Image *img) {
|
|||||||
img->extra_frames = NULL;
|
img->extra_frames = NULL;
|
||||||
}
|
}
|
||||||
free_refs_data(img);
|
free_refs_data(img);
|
||||||
free_load_data(&(img->load_data));
|
|
||||||
self->used_storage -= img->used_storage;
|
self->used_storage -= img->used_storage;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -206,7 +209,7 @@ set_command_failed_response(const char *code, const char *fmt, ...) {
|
|||||||
#define ABRT(code, ...) { set_command_failed_response(#code, __VA_ARGS__); goto err; }
|
#define ABRT(code, ...) { set_command_failed_response(#code, __VA_ARGS__); goto err; }
|
||||||
|
|
||||||
static inline bool
|
static inline bool
|
||||||
mmap_img_file(GraphicsManager UNUSED *self, Image *img, int fd, size_t sz, off_t offset) {
|
mmap_img_file(GraphicsManager *self, int fd, size_t sz, off_t offset) {
|
||||||
if (!sz) {
|
if (!sz) {
|
||||||
struct stat s;
|
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));
|
if (fstat(fd, &s) != 0) ABRT(EBADF, "Failed to fstat() the fd: %d file with error: [%d] %s", fd, errno, strerror(errno));
|
||||||
@ -214,8 +217,8 @@ mmap_img_file(GraphicsManager UNUSED *self, Image *img, int fd, size_t sz, off_t
|
|||||||
}
|
}
|
||||||
void *addr = mmap(0, sz, PROT_READ, MAP_SHARED, fd, offset);
|
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));
|
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));
|
||||||
img->load_data.mapped_file = addr;
|
self->currently_loading.mapped_file = addr;
|
||||||
img->load_data.mapped_file_sz = sz;
|
self->currently_loading.mapped_file_sz = sz;
|
||||||
return true;
|
return true;
|
||||||
err:
|
err:
|
||||||
return false;
|
return false;
|
||||||
@ -242,26 +245,26 @@ zlib_strerror(int ret) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static inline bool
|
static inline bool
|
||||||
inflate_zlib(Image *img, uint8_t *buf, size_t bufsz) {
|
inflate_zlib(LoadData *load_data, uint8_t *buf, size_t bufsz) {
|
||||||
bool ok = false;
|
bool ok = false;
|
||||||
z_stream z;
|
z_stream z;
|
||||||
uint8_t *decompressed = malloc(img->load_data.data_sz);
|
uint8_t *decompressed = malloc(load_data->data_sz);
|
||||||
if (decompressed == NULL) fatal("Out of memory allocating decompression buffer");
|
if (decompressed == NULL) fatal("Out of memory allocating decompression buffer");
|
||||||
z.zalloc = Z_NULL;
|
z.zalloc = Z_NULL;
|
||||||
z.zfree = Z_NULL;
|
z.zfree = Z_NULL;
|
||||||
z.opaque = Z_NULL;
|
z.opaque = Z_NULL;
|
||||||
z.avail_in = bufsz;
|
z.avail_in = bufsz;
|
||||||
z.next_in = (Bytef*)buf;
|
z.next_in = (Bytef*)buf;
|
||||||
z.avail_out = img->load_data.data_sz;
|
z.avail_out = load_data->data_sz;
|
||||||
z.next_out = decompressed;
|
z.next_out = decompressed;
|
||||||
int ret;
|
int ret;
|
||||||
if ((ret = inflateInit(&z)) != Z_OK) ABRT(ENOMEM, "Failed to initialize inflate with error: %s", zlib_strerror(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 ((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");
|
if (z.avail_out) ABRT(EINVAL, "Image data size post inflation does not match expected size");
|
||||||
free_load_data(&img->load_data);
|
free_load_data(load_data);
|
||||||
img->load_data.buf_capacity = img->load_data.data_sz;
|
load_data->buf_capacity = load_data->data_sz;
|
||||||
img->load_data.buf = decompressed;
|
load_data->buf = decompressed;
|
||||||
img->load_data.buf_used = img->load_data.data_sz;
|
load_data->buf_used = load_data->data_sz;
|
||||||
ok = true;
|
ok = true;
|
||||||
err:
|
err:
|
||||||
inflateEnd(&z);
|
inflateEnd(&z);
|
||||||
@ -275,16 +278,16 @@ png_error_handler(const char *code, const char *msg) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static inline bool
|
static inline bool
|
||||||
inflate_png(Image *img, uint8_t *buf, size_t bufsz) {
|
inflate_png(LoadData *load_data, uint8_t *buf, size_t bufsz) {
|
||||||
png_read_data d = {.err_handler=png_error_handler};
|
png_read_data d = {.err_handler=png_error_handler};
|
||||||
inflate_png_inner(&d, buf, bufsz);
|
inflate_png_inner(&d, buf, bufsz);
|
||||||
if (d.ok) {
|
if (d.ok) {
|
||||||
free_load_data(&img->load_data);
|
free_load_data(load_data);
|
||||||
img->load_data.buf = d.decompressed;
|
load_data->buf = d.decompressed;
|
||||||
img->load_data.buf_capacity = d.sz;
|
load_data->buf_capacity = d.sz;
|
||||||
img->load_data.buf_used = d.sz;
|
load_data->buf_used = d.sz;
|
||||||
img->load_data.data_sz = d.sz;
|
load_data->data_sz = d.sz;
|
||||||
img->width = d.width; img->height = d.height;
|
load_data->width = d.width; load_data->height = d.height;
|
||||||
}
|
}
|
||||||
else free(d.decompressed);
|
else free(d.decompressed);
|
||||||
free(d.row_pointers);
|
free(d.row_pointers);
|
||||||
@ -383,7 +386,7 @@ get_free_client_id(const GraphicsManager *self) {
|
|||||||
return ans;
|
return ans;
|
||||||
}
|
}
|
||||||
|
|
||||||
#define ABRT(code, ...) { set_command_failed_response(code, __VA_ARGS__); self->currently_loading_data_for = (const ImageAndFrame){0}; if (img) img->data_loaded = false; return NULL; }
|
#define ABRT(code, ...) { set_command_failed_response(code, __VA_ARGS__); if (img) img->data_loaded = false; free_load_data(&self->currently_loading); return NULL; }
|
||||||
|
|
||||||
#define MAX_DATA_SZ (4u * 100000000u)
|
#define MAX_DATA_SZ (4u * 100000000u)
|
||||||
enum FORMATS { RGB=24, RGBA=32, PNG=100 };
|
enum FORMATS { RGB=24, RGBA=32, PNG=100 };
|
||||||
@ -392,21 +395,22 @@ 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) {
|
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;
|
int fd;
|
||||||
static char fname[2056] = {0};
|
static char fname[2056] = {0};
|
||||||
|
LoadData *load_data = &self->currently_loading;
|
||||||
|
|
||||||
switch(transmission_type) {
|
switch(transmission_type) {
|
||||||
case 'd': // direct
|
case 'd': // direct
|
||||||
if (img->load_data.buf_capacity - img->load_data.buf_used < g->payload_sz) {
|
if (load_data->buf_capacity - load_data->buf_used < g->payload_sz) {
|
||||||
if (img->load_data.buf_used + g->payload_sz > MAX_DATA_SZ || data_fmt != PNG) ABRT("EFBIG", "Too much data");
|
if (load_data->buf_used + g->payload_sz > MAX_DATA_SZ || data_fmt != PNG) ABRT("EFBIG", "Too much data");
|
||||||
img->load_data.buf_capacity = MIN(2 * img->load_data.buf_capacity, MAX_DATA_SZ);
|
load_data->buf_capacity = MIN(2 * load_data->buf_capacity, MAX_DATA_SZ);
|
||||||
img->load_data.buf = realloc(img->load_data.buf, img->load_data.buf_capacity);
|
load_data->buf = realloc(load_data->buf, load_data->buf_capacity);
|
||||||
if (img->load_data.buf == NULL) {
|
if (load_data->buf == NULL) {
|
||||||
img->load_data.buf_capacity = 0; img->load_data.buf_used = 0;
|
load_data->buf_capacity = 0; load_data->buf_used = 0;
|
||||||
ABRT("ENOMEM", "Out of memory");
|
ABRT("ENOMEM", "Out of memory");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
memcpy(img->load_data.buf + img->load_data.buf_used, payload, g->payload_sz);
|
memcpy(load_data->buf + load_data->buf_used, payload, g->payload_sz);
|
||||||
img->load_data.buf_used += g->payload_sz;
|
load_data->buf_used += g->payload_sz;
|
||||||
if (!g->more) { img->data_loaded = true; self->currently_loading_data_for = (const ImageAndFrame){0}; }
|
if (!g->more) { img->data_loaded = true; load_data->loading_for = (const ImageAndFrame){0}; }
|
||||||
break;
|
break;
|
||||||
case 'f': // file
|
case 'f': // file
|
||||||
case 't': // temporary file
|
case 't': // temporary file
|
||||||
@ -416,7 +420,7 @@ load_image_data(GraphicsManager *self, Image *img, const GraphicsCommand *g, con
|
|||||||
if (transmission_type == 's') fd = safe_shm_open(fname, O_RDONLY, 0);
|
if (transmission_type == 's') fd = safe_shm_open(fname, O_RDONLY, 0);
|
||||||
else fd = safe_open(fname, O_CLOEXEC | O_RDONLY, 0);
|
else fd = safe_open(fname, O_CLOEXEC | O_RDONLY, 0);
|
||||||
if (fd == -1) ABRT("EBADF", "Failed to open file for graphics transmission with error: [%d] %s", errno, strerror(errno));
|
if (fd == -1) ABRT("EBADF", "Failed to open file for graphics transmission with error: [%d] %s", errno, strerror(errno));
|
||||||
img->data_loaded = mmap_img_file(self, img, fd, g->data_sz, g->data_offset);
|
img->data_loaded = mmap_img_file(self, fd, g->data_sz, g->data_offset);
|
||||||
safe_close(fd, __FILE__, __LINE__);
|
safe_close(fd, __FILE__, __LINE__);
|
||||||
if (transmission_type == 't') {
|
if (transmission_type == 't') {
|
||||||
if (global_state.boss) { call_boss(safe_delete_temp_file, "s", fname); }
|
if (global_state.boss) { call_boss(safe_delete_temp_file, "s", fname); }
|
||||||
@ -436,11 +440,11 @@ process_image_data(GraphicsManager *self, Image* img, const GraphicsCommand *g,
|
|||||||
bool needs_processing = g->compressed || data_fmt == PNG;
|
bool needs_processing = g->compressed || data_fmt == PNG;
|
||||||
if (needs_processing) {
|
if (needs_processing) {
|
||||||
uint8_t *buf; size_t bufsz;
|
uint8_t *buf; size_t bufsz;
|
||||||
#define IB { if (img->load_data.buf) { buf = img->load_data.buf; bufsz = img->load_data.buf_used; } else { buf = img->load_data.mapped_file; bufsz = img->load_data.mapped_file_sz; } }
|
#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) {
|
switch(g->compressed) {
|
||||||
case 'z':
|
case 'z':
|
||||||
IB;
|
IB;
|
||||||
if (!inflate_zlib(img, buf, bufsz)) {
|
if (!inflate_zlib(&self->currently_loading, buf, bufsz)) {
|
||||||
img->data_loaded = false; return NULL;
|
img->data_loaded = false; return NULL;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -452,62 +456,67 @@ process_image_data(GraphicsManager *self, Image* img, const GraphicsCommand *g,
|
|||||||
switch(data_fmt) {
|
switch(data_fmt) {
|
||||||
case PNG:
|
case PNG:
|
||||||
IB;
|
IB;
|
||||||
if (!inflate_png(img, buf, bufsz)) {
|
if (!inflate_png(&self->currently_loading, buf, bufsz)) {
|
||||||
img->data_loaded = false; return NULL;
|
img->data_loaded = false; return NULL;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default: break;
|
default: break;
|
||||||
}
|
}
|
||||||
#undef IB
|
#undef IB
|
||||||
img->load_data.data = img->load_data.buf;
|
self->currently_loading.data = self->currently_loading.buf;
|
||||||
if (img->load_data.buf_used < img->load_data.data_sz) {
|
if (self->currently_loading.buf_used < self->currently_loading.data_sz) {
|
||||||
ABRT("ENODATA", "Insufficient image data: %zu < %zu", img->load_data.buf_used, img->load_data.data_sz);
|
ABRT("ENODATA", "Insufficient image data: %zu < %zu", self->currently_loading.buf_used, self->currently_loading.data_sz);
|
||||||
}
|
}
|
||||||
if (img->load_data.mapped_file) {
|
if (self->currently_loading.mapped_file) {
|
||||||
munmap(img->load_data.mapped_file, img->load_data.mapped_file_sz);
|
munmap(self->currently_loading.mapped_file, self->currently_loading.mapped_file_sz);
|
||||||
img->load_data.mapped_file = NULL; img->load_data.mapped_file_sz = 0;
|
self->currently_loading.mapped_file = NULL; self->currently_loading.mapped_file_sz = 0;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (transmission_type == 'd') {
|
if (transmission_type == 'd') {
|
||||||
if (img->load_data.buf_used < img->load_data.data_sz) {
|
if (self->currently_loading.buf_used < self->currently_loading.data_sz) {
|
||||||
ABRT("ENODATA", "Insufficient image data: %zu < %zu", img->load_data.buf_used, img->load_data.data_sz);
|
ABRT("ENODATA", "Insufficient image data: %zu < %zu", self->currently_loading.buf_used, self->currently_loading.data_sz);
|
||||||
} else img->load_data.data = img->load_data.buf;
|
} else self->currently_loading.data = self->currently_loading.buf;
|
||||||
} else {
|
} else {
|
||||||
if (img->load_data.mapped_file_sz < img->load_data.data_sz) {
|
if (self->currently_loading.mapped_file_sz < self->currently_loading.data_sz) {
|
||||||
ABRT("ENODATA", "Insufficient image data: %zu < %zu", img->load_data.mapped_file_sz, img->load_data.data_sz);
|
ABRT("ENODATA", "Insufficient image data: %zu < %zu", self->currently_loading.mapped_file_sz, self->currently_loading.data_sz);
|
||||||
} else img->load_data.data = img->load_data.mapped_file;
|
} else self->currently_loading.data = self->currently_loading.mapped_file;
|
||||||
}
|
}
|
||||||
|
img->data_loaded = true;
|
||||||
}
|
}
|
||||||
return img;
|
return img;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Image*
|
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) {
|
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) {
|
||||||
img->load_data = (const LoadData){0};
|
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) {
|
switch(data_fmt) {
|
||||||
case PNG:
|
case PNG:
|
||||||
if (g->data_sz > MAX_DATA_SZ) ABRT("EINVAL", "PNG data size too large");
|
if (g->data_sz > MAX_DATA_SZ) ABRT("EINVAL", "PNG data size too large");
|
||||||
img->load_data.is_4byte_aligned = true;
|
self->currently_loading.is_4byte_aligned = true;
|
||||||
img->load_data.is_opaque = false;
|
self->currently_loading.is_opaque = false;
|
||||||
img->load_data.data_sz = g->data_sz ? g->data_sz : 1024 * 100;
|
self->currently_loading.data_sz = g->data_sz ? g->data_sz : 1024 * 100;
|
||||||
break;
|
break;
|
||||||
case RGB:
|
case RGB:
|
||||||
case RGBA:
|
case RGBA:
|
||||||
img->load_data.data_sz = (size_t)g->data_width * g->data_height * (data_fmt / 8);
|
self->currently_loading.data_sz = (size_t)g->data_width * g->data_height * (data_fmt / 8);
|
||||||
if (!img->load_data.data_sz) ABRT("EINVAL", "Zero width/height not allowed");
|
if (!self->currently_loading.data_sz) ABRT("EINVAL", "Zero width/height not allowed");
|
||||||
img->load_data.is_4byte_aligned = data_fmt == RGBA || (img->width % 4 == 0);
|
self->currently_loading.is_4byte_aligned = data_fmt == RGBA || (self->currently_loading.width % 4 == 0);
|
||||||
img->load_data.is_opaque = data_fmt == RGB;
|
self->currently_loading.is_opaque = data_fmt == RGB;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
ABRT("EINVAL", "Unknown image format: %u", data_fmt);
|
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') {
|
if (transmission_type == 'd') {
|
||||||
if (g->more) self->currently_loading_data_for = (ImageAndFrame){.image_id = img->internal_id, .frame_id = frame_id};
|
self->currently_loading.buf_capacity = self->currently_loading.data_sz + (g->compressed ? 1024 : 10); // compression header
|
||||||
img->load_data.buf_capacity = img->load_data.data_sz + (g->compressed ? 1024 : 10); // compression header
|
self->currently_loading.buf = malloc(self->currently_loading.buf_capacity);
|
||||||
img->load_data.buf = malloc(img->load_data.buf_capacity);
|
self->currently_loading.buf_used = 0;
|
||||||
img->load_data.buf_used = 0;
|
if (self->currently_loading.buf == NULL) {
|
||||||
if (img->load_data.buf == NULL) {
|
self->currently_loading.buf_capacity = 0; self->currently_loading.buf_used = 0;
|
||||||
img->load_data.buf_capacity = 0; img->load_data.buf_used = 0;
|
|
||||||
ABRT("ENOMEM", "Out of memory");
|
ABRT("ENOMEM", "Out of memory");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -515,22 +524,22 @@ initialize_load_data(GraphicsManager *self, const GraphicsCommand *g, Image *img
|
|||||||
}
|
}
|
||||||
|
|
||||||
#define INIT_CHUNKED_LOAD { \
|
#define INIT_CHUNKED_LOAD { \
|
||||||
self->last_transmit_graphics_command.more = g->more; \
|
self->currently_loading.start_command.more = g->more; \
|
||||||
self->last_transmit_graphics_command.payload_sz = g->payload_sz; \
|
self->currently_loading.start_command.payload_sz = g->payload_sz; \
|
||||||
g = &self->last_transmit_graphics_command; \
|
g = &self->currently_loading.start_command; \
|
||||||
tt = g->transmission_type ? g->transmission_type : 'd'; \
|
tt = g->transmission_type ? g->transmission_type : 'd'; \
|
||||||
fmt = g->format ? g->format : RGBA; \
|
fmt = g->format ? g->format : RGBA; \
|
||||||
}
|
}
|
||||||
#define MAX_IMAGE_DIMENSION 10000u
|
#define MAX_IMAGE_DIMENSION 10000u
|
||||||
|
|
||||||
static void
|
static void
|
||||||
upload_to_gpu(GraphicsManager *self, Image *img, void *data) {
|
upload_to_gpu(GraphicsManager *self, Image *img, const bool is_opaque, const bool is_4byte_aligned, const void *data) {
|
||||||
if (!self->context_made_current_for_this_command) {
|
if (!self->context_made_current_for_this_command) {
|
||||||
if (!self->window_id) return;
|
if (!self->window_id) return;
|
||||||
if (!make_window_context_current(self->window_id)) return;
|
if (!make_window_context_current(self->window_id)) return;
|
||||||
self->context_made_current_for_this_command = true;
|
self->context_made_current_for_this_command = true;
|
||||||
}
|
}
|
||||||
send_image_to_gpu(&img->texture_id, data, img->width, img->height, img->is_opaque, img->is_4byte_aligned, false, REPEAT_CLAMP);
|
send_image_to_gpu(&img->texture_id, data, img->width, img->height, is_opaque, is_4byte_aligned, false, REPEAT_CLAMP);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Image*
|
static Image*
|
||||||
@ -539,16 +548,13 @@ handle_add_command(GraphicsManager *self, const GraphicsCommand *g, const uint8_
|
|||||||
Image *img = NULL;
|
Image *img = NULL;
|
||||||
unsigned char tt = g->transmission_type ? g->transmission_type : 'd';
|
unsigned char tt = g->transmission_type ? g->transmission_type : 'd';
|
||||||
uint32_t fmt = g->format ? g->format : RGBA;
|
uint32_t fmt = g->format ? g->format : RGBA;
|
||||||
if (tt == 'd' && self->currently_loading_data_for.image_id) init_img = false;
|
if (tt == 'd' && self->currently_loading.loading_for.image_id) init_img = false;
|
||||||
if (init_img) {
|
if (init_img) {
|
||||||
self->last_transmit_graphics_command = *g;
|
self->currently_loading.loading_for = (const ImageAndFrame){0};
|
||||||
self->currently_loading_data_for = (const ImageAndFrame){0};
|
|
||||||
if (g->data_width > MAX_IMAGE_DIMENSION || g->data_height > MAX_IMAGE_DIMENSION) ABRT("EINVAL", "Image too large");
|
if (g->data_width > MAX_IMAGE_DIMENSION || g->data_height > MAX_IMAGE_DIMENSION) ABRT("EINVAL", "Image too large");
|
||||||
self->last_transmit_graphics_command.id = iid;
|
|
||||||
remove_images(self, add_trim_predicate, 0);
|
remove_images(self, add_trim_predicate, 0);
|
||||||
img = find_or_create_image(self, iid, &existing);
|
img = find_or_create_image(self, iid, &existing);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
free_load_data(&img->load_data);
|
|
||||||
img->data_loaded = false;
|
img->data_loaded = false;
|
||||||
img->is_drawn = false;
|
img->is_drawn = false;
|
||||||
img->current_frame_shown_at = 0;
|
img->current_frame_shown_at = 0;
|
||||||
@ -561,38 +567,42 @@ handle_add_command(GraphicsManager *self, const GraphicsCommand *g, const uint8_
|
|||||||
img->client_number = g->image_number;
|
img->client_number = g->image_number;
|
||||||
if (!img->client_id && img->client_number) {
|
if (!img->client_id && img->client_number) {
|
||||||
img->client_id = get_free_client_id(self);
|
img->client_id = get_free_client_id(self);
|
||||||
self->last_transmit_graphics_command.id = img->client_id;
|
iid = img->client_id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
img->atime = monotonic(); img->used_storage = 0;
|
img->atime = monotonic(); img->used_storage = 0;
|
||||||
img->width = g->data_width; img->height = g->data_height;
|
|
||||||
if (!initialize_load_data(self, g, img, tt, fmt, 0)) return NULL;
|
if (!initialize_load_data(self, g, img, tt, fmt, 0)) return NULL;
|
||||||
|
self->currently_loading.start_command.id = iid;
|
||||||
} else {
|
} else {
|
||||||
INIT_CHUNKED_LOAD;
|
INIT_CHUNKED_LOAD;
|
||||||
img = img_by_internal_id(self, self->currently_loading_data_for.image_id);
|
img = img_by_internal_id(self, self->currently_loading.loading_for.image_id);
|
||||||
if (img == NULL) {
|
if (img == NULL) {
|
||||||
self->currently_loading_data_for = (const ImageAndFrame){0};
|
self->currently_loading.loading_for = (const ImageAndFrame){0};
|
||||||
ABRT("EILSEQ", "More payload loading refers to non-existent image");
|
ABRT("EILSEQ", "More payload loading refers to non-existent image");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
img = load_image_data(self, img, g, tt, fmt, payload);
|
img = load_image_data(self, img, g, tt, fmt, payload);
|
||||||
if (!img || !img->data_loaded) return NULL; // !data_loaded without an error implies chunked load
|
if (!img || !img->data_loaded) return NULL;
|
||||||
self->currently_loading_data_for = (const ImageAndFrame){0};
|
self->currently_loading.loading_for = (const ImageAndFrame){0};
|
||||||
img = process_image_data(self, img, g, tt, fmt);
|
img = process_image_data(self, img, g, tt, fmt);
|
||||||
if (!img) return NULL;
|
if (!img) return NULL;
|
||||||
size_t required_sz = (size_t)(img->load_data.is_opaque ? 3 : 4) * img->width * img->height;
|
size_t required_sz = (size_t)(self->currently_loading.is_opaque ? 3 : 4) * self->currently_loading.width * self->currently_loading.height;
|
||||||
if (img->load_data.data_sz != required_sz) ABRT("EINVAL", "Image dimensions: %ux%u do not match data size: %zu, expected size: %zu", img->width, img->height, img->load_data.data_sz, required_sz);
|
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 (img->data_loaded) {
|
if (img->data_loaded) {
|
||||||
img->is_opaque = img->load_data.is_opaque;
|
img->width = self->currently_loading.width;
|
||||||
img->is_4byte_aligned = img->load_data.is_4byte_aligned;
|
img->height = self->currently_loading.height;
|
||||||
upload_to_gpu(self, img, img->load_data.data);
|
|
||||||
if (img->root_frame.id) remove_from_cache(self, (const ImageAndFrame){.image_id=img->internal_id, .frame_id=img->root_frame.id});
|
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.id = ++img->frame_id_counter;
|
img->root_frame = (const Frame){
|
||||||
if (!add_to_cache(self, (const ImageAndFrame){.image_id = img->internal_id, .frame_id=img->root_frame.id}, img->load_data.data, img->load_data.data_sz)) {
|
.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 (!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();
|
if (PyErr_Occurred()) PyErr_Print();
|
||||||
ABRT("ENOSPC", "Failed to store image data in disk cache");
|
ABRT("ENOSPC", "Failed to store image data in disk cache");
|
||||||
}
|
}
|
||||||
free_load_data(&img->load_data);
|
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;
|
self->used_storage += required_sz;
|
||||||
img->used_storage = required_sz;
|
img->used_storage = required_sz;
|
||||||
}
|
}
|
||||||
@ -797,6 +807,8 @@ grman_update_layers(GraphicsManager *self, unsigned int scrolled_by, float scree
|
|||||||
#define _other_frame_number num_cells
|
#define _other_frame_number num_cells
|
||||||
#define _gap z_index
|
#define _gap z_index
|
||||||
#define _animation_enabled data_width
|
#define _animation_enabled data_width
|
||||||
|
#define _blend_mode cell_x_offset
|
||||||
|
#define _bgcolor cell_y_offset
|
||||||
|
|
||||||
static inline Frame*
|
static inline Frame*
|
||||||
current_frame(Image *img) {
|
current_frame(Image *img) {
|
||||||
@ -804,21 +816,26 @@ current_frame(Image *img) {
|
|||||||
return img->current_frame_index ? img->extra_frames + img->current_frame_index - 1 : &img->root_frame;
|
return img->current_frame_index ? img->extra_frames + img->current_frame_index - 1 : &img->root_frame;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static inline Frame*
|
||||||
update_current_frame(GraphicsManager *self, Image *img, void *data) {
|
frame_for_id(Image *img, const uint32_t frame_id) {
|
||||||
bool needs_load = data == NULL;
|
if (img->root_frame.id == frame_id) return &img->root_frame;
|
||||||
Frame *f = current_frame(img);
|
for (unsigned i = 0; i < img->extra_framecnt; i++) {
|
||||||
if (f == NULL) return;
|
if (img->extra_frames[i].id == frame_id) return img->extra_frames + i;
|
||||||
if (needs_load) {
|
|
||||||
size_t data_sz;
|
|
||||||
if (!read_from_cache(self, (const ImageAndFrame){.image_id=img->internal_id, .frame_id=f->id}, &data, &data_sz)) {
|
|
||||||
if (PyErr_Occurred()) PyErr_Print();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline 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;
|
||||||
}
|
}
|
||||||
upload_to_gpu(self, img, data);
|
|
||||||
if (needs_load) free(data);
|
|
||||||
img->current_frame_shown_at = monotonic();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline void
|
static inline void
|
||||||
@ -829,6 +846,159 @@ change_gap(Image *img, Frame *f, int32_t gap) {
|
|||||||
img->animation_duration += f->gap;
|
img->animation_duration += f->gap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t *buf;
|
||||||
|
bool is_4byte_aligned, is_opaque;
|
||||||
|
} CoalescedFrameData;
|
||||||
|
|
||||||
|
static inline 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 inline void
|
||||||
|
alpha_blend(uint8_t *dest_px, const uint8_t *src_px) {
|
||||||
|
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;
|
||||||
|
unsigned over_px_sz, under_px_sz;
|
||||||
|
size_t over_width, over_height, under_width, under_height, over_offset_x, over_offset_y;
|
||||||
|
} ComposeData;
|
||||||
|
|
||||||
|
static inline 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;
|
||||||
|
if (can_copy_rows) {
|
||||||
|
ROW_ITER memcpy(under_row, over_row, d.over_px_sz * min_row_sz);}
|
||||||
|
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);
|
||||||
|
#define COPY_RGB under_px[0] = over_px[0]; under_px[1] = over_px[1]; under_px[2] = over_px[2];
|
||||||
|
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 }}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#undef COPY_RGB
|
||||||
|
#undef PIX_ITER
|
||||||
|
#undef ROW_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(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 (size_t i = 0; i < img->width * img->height; i++) {
|
||||||
|
*(p++) = r; *(p++) = g; *(p++) = b; *(p++) = a;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (size_t i = 0; i < img->width * img->height; i++) {
|
||||||
|
*(p++) = r; *(p++) = g; *(p++) = b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else base = calloc(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(GraphicsManager *self, Image *img, const Frame *f) {
|
||||||
|
CoalescedFrameData ans = {0};
|
||||||
|
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(self, img, base);
|
||||||
|
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 = base->width, .under_height = base->height,
|
||||||
|
.needs_blending = f->alpha_blend && !f->is_opaque
|
||||||
|
};
|
||||||
|
compose(d, base_data.buf, frame_data);
|
||||||
|
free(frame_data);
|
||||||
|
return base_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
if (needs_load) free(data->buf);
|
||||||
|
img->current_frame_shown_at = monotonic();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static Image*
|
static Image*
|
||||||
handle_animation_frame_load_command(GraphicsManager *self, GraphicsCommand *g, Image *img, const uint8_t *payload, bool *is_dirty) {
|
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;
|
uint32_t frame_number = g->_frame_number, fmt = g->format ? g->format : RGBA;
|
||||||
@ -836,112 +1006,95 @@ handle_animation_frame_load_command(GraphicsManager *self, GraphicsCommand *g, I
|
|||||||
bool is_new_frame = frame_number == img->extra_framecnt + 2;
|
bool is_new_frame = frame_number == img->extra_framecnt + 2;
|
||||||
g->_frame_number = frame_number;
|
g->_frame_number = frame_number;
|
||||||
unsigned char tt = g->transmission_type ? g->transmission_type : 'd';
|
unsigned char tt = g->transmission_type ? g->transmission_type : 'd';
|
||||||
size_t w = img->width, h = img->height;
|
if (tt == 'd' && self->currently_loading.loading_for.image_id == img->internal_id) {
|
||||||
if (tt == 'd' && self->currently_loading_data_for.image_id == img->internal_id) {
|
|
||||||
INIT_CHUNKED_LOAD;
|
INIT_CHUNKED_LOAD;
|
||||||
} else {
|
} else {
|
||||||
self->last_transmit_graphics_command = *g;
|
self->currently_loading.loading_for = (const ImageAndFrame){0};
|
||||||
self->currently_loading_data_for = (const ImageAndFrame){0};
|
|
||||||
if (g->data_width > MAX_IMAGE_DIMENSION || g->data_height > MAX_IMAGE_DIMENSION) ABRT("EINVAL", "Image too large");
|
if (g->data_width > MAX_IMAGE_DIMENSION || g->data_height > MAX_IMAGE_DIMENSION) ABRT("EINVAL", "Image too large");
|
||||||
free_load_data(&img->load_data);
|
|
||||||
if (!initialize_load_data(self, g, img, tt, fmt, frame_number - 1)) return NULL;
|
if (!initialize_load_data(self, g, img, tt, fmt, frame_number - 1)) return NULL;
|
||||||
}
|
}
|
||||||
img = load_image_data(self, img, g, tt, fmt, payload);
|
img = load_image_data(self, img, g, tt, fmt, payload);
|
||||||
if (!img || !img->data_loaded) return NULL; // !data_loaded without an error implies chunked load
|
if (!img || !img->data_loaded) return NULL;
|
||||||
self->currently_loading_data_for = (const ImageAndFrame){0};
|
self->currently_loading.loading_for = (const ImageAndFrame){0};
|
||||||
img = process_image_data(self, img, g, tt, fmt);
|
img = process_image_data(self, img, g, tt, fmt);
|
||||||
if (!img) return NULL;
|
if (!img || !img->data_loaded) return img;
|
||||||
img->width = w; img->height = h;
|
|
||||||
#define FAIL(errno, ...) { free_load_data(&img->load_data); ABRT(errno, __VA_ARGS__); }
|
|
||||||
if (img->data_loaded) {
|
|
||||||
const unsigned bytes_per_pixel = img->is_opaque ? 3 : 4;
|
|
||||||
const size_t expected_data_sz = img->width * img->height * bytes_per_pixel;
|
|
||||||
|
|
||||||
if (img->load_data.is_opaque != img->is_opaque)
|
LoadData *load_data = &self->currently_loading;
|
||||||
FAIL("EINVAL", "Transparency for frames must match that of the base image");
|
const unsigned bytes_per_pixel = load_data->is_opaque ? 3 : 4;
|
||||||
if (img->load_data.is_4byte_aligned != img->is_4byte_aligned)
|
if (load_data->data_sz < bytes_per_pixel * load_data->width * load_data->height)
|
||||||
FAIL("EINVAL", "Data type for frames must match that of the base image");
|
ABRT("ENODATA", "Insufficient image data %zu < %zu", load_data->data_sz, bytes_per_pixel * g->data_width, g->data_height);
|
||||||
if (img->load_data.data_sz < bytes_per_pixel * g->data_width * g->data_height)
|
if (load_data->width > img->width)
|
||||||
FAIL("ENODATA", "Insufficient image data %zu < %zu", img->load_data.data_sz, bytes_per_pixel * g->data_width, g->data_height);
|
ABRT("EINVAL", "Frame width %u larger than image width: %u", load_data->width, img->width);
|
||||||
if (is_new_frame && cache_size(self) + expected_data_sz > self->storage_limit * 5) {
|
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);
|
remove_images(self, trim_predicate, img->internal_id);
|
||||||
if (is_new_frame && cache_size(self) + expected_data_sz > self->storage_limit * 5)
|
if (is_new_frame && cache_size(self) + load_data->data_sz > self->storage_limit * 5)
|
||||||
FAIL("ENOSPC", "Cache size exceeded cannot add new frames");
|
ABRT("ENOSPC", "Cache size exceeded cannot add new frames");
|
||||||
}
|
}
|
||||||
|
|
||||||
void *base_data = NULL;
|
Frame transmitted_frame = {
|
||||||
size_t data_sz = 0;
|
.width = load_data->width, .height = load_data->height,
|
||||||
bool needs_send_to_gpu = false;
|
.x = g->x_offset, .y = g->y_offset,
|
||||||
ImageAndFrame key = { .image_id = img->internal_id };
|
.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) {
|
if (is_new_frame) {
|
||||||
key.frame_id = ++img->frame_id_counter;
|
transmitted_frame.id = ++img->frame_id_counter;
|
||||||
if (!key.frame_id) key.frame_id = ++img->frame_id_counter;
|
Frame *frames = realloc(img->extra_frames, sizeof(img->extra_frames[0]) * (img->extra_framecnt + 1));
|
||||||
if (g->_other_frame_number) {
|
|
||||||
ImageAndFrame other = { .image_id = img->internal_id, .frame_id = img->root_frame.id };
|
|
||||||
if (g->_other_frame_number > 1) {
|
|
||||||
other.frame_id = g->_other_frame_number - 2;
|
|
||||||
if (other.frame_id >= img->extra_framecnt) {
|
|
||||||
FAIL("ENODATA", "No data for frame with number: %u found in image: %u", g->_other_frame_number, img->client_id);
|
|
||||||
}
|
|
||||||
other.frame_id = img->extra_frames[other.frame_id].id;
|
|
||||||
}
|
|
||||||
if (!read_from_cache(self, other, &base_data, &data_sz)) {
|
|
||||||
FAIL("ENODATA", "No data for frame with number: %u found in image: %u", g->_other_frame_number, img->client_id);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
base_data = calloc(1, expected_data_sz);
|
|
||||||
if (!base_data) { FAIL("ENOMEM", "Out of memory"); }
|
|
||||||
data_sz = expected_data_sz;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (frame_number > 1) key.frame_id = img->extra_frames[frame_number - 2].id;
|
|
||||||
else key.frame_id = img->root_frame.id;
|
|
||||||
if (!read_from_cache(self, key, &base_data, &data_sz)) {
|
|
||||||
FAIL("ENODATA", "No data for frame with number: %u found in image: %u", frame_number, img->client_id);
|
|
||||||
}
|
|
||||||
Frame *f = current_frame(img);
|
|
||||||
if (f && f->id == key.frame_id) needs_send_to_gpu = true;
|
|
||||||
}
|
|
||||||
if (data_sz != expected_data_sz) {
|
|
||||||
free(base_data);
|
|
||||||
FAIL("EINVAL", "Cached data sz: %zu != expected data sz: %zu", data_sz, expected_data_sz);
|
|
||||||
}
|
|
||||||
if (data_sz == img->load_data.data_sz && !g->x_offset && !g->y_offset && !g->width && !g->height) {
|
|
||||||
memcpy(base_data, img->load_data.data, data_sz);
|
|
||||||
} else {
|
|
||||||
const size_t dest_width = img->width > g->x_offset ? img->width - g->x_offset : 0;
|
|
||||||
const size_t stride = MIN(g->data_width, dest_width) * bytes_per_pixel;
|
|
||||||
for (size_t src_y = 0, dest_y = g->y_offset; src_y < g->data_height && dest_y < img->height; src_y++, dest_y++) {
|
|
||||||
memcpy(
|
|
||||||
(uint8_t*)base_data + dest_y * bytes_per_pixel * dest_width,
|
|
||||||
img->load_data.data + src_y * bytes_per_pixel * g->data_width,
|
|
||||||
stride
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#undef FAIL
|
|
||||||
|
|
||||||
free_load_data(&img->load_data);
|
|
||||||
bool added = add_to_cache(self, key, base_data, data_sz);
|
|
||||||
if (needs_send_to_gpu) {
|
|
||||||
update_current_frame(self, img, base_data);
|
|
||||||
*is_dirty = true;
|
|
||||||
}
|
|
||||||
free(base_data);
|
|
||||||
if (!added) {
|
|
||||||
PyErr_Print();
|
|
||||||
ABRT("ENOSPC", "Failed to cache data for image frame");
|
|
||||||
}
|
|
||||||
if (is_new_frame) {
|
|
||||||
Frame *frames = realloc(img->extra_frames, sizeof(img->extra_frames[0]) * img->extra_framecnt + 1);
|
|
||||||
if (!frames) ABRT("ENOMEM", "Out of memory");
|
if (!frames) ABRT("ENOMEM", "Out of memory");
|
||||||
img->extra_frames = frames;
|
img->extra_frames = frames;
|
||||||
img->extra_framecnt++;
|
img->extra_framecnt++;
|
||||||
img->extra_frames[frame_number - 2].gap = DEFAULT_GAP;
|
frame = img->extra_frames + frame_number - 2;
|
||||||
img->extra_frames[frame_number - 2].id = key.frame_id;
|
const ImageAndFrame key = { .image_id = img->internal_id, .frame_id = transmitted_frame.id };
|
||||||
img->animation_duration += DEFAULT_GAP;
|
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);
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
} 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, 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");
|
||||||
}
|
}
|
||||||
if (g->_gap > 0) change_gap(img, img->extra_frames + frame_number - 2, g->_gap);
|
|
||||||
}
|
}
|
||||||
return img;
|
return img;
|
||||||
}
|
}
|
||||||
@ -981,7 +1134,7 @@ handle_delete_frame_command(GraphicsManager *self, const GraphicsCommand *g, boo
|
|||||||
}
|
}
|
||||||
img->animation_duration = removed_gap < img->animation_duration ? img->animation_duration - removed_gap : 0;
|
img->animation_duration = removed_gap < img->animation_duration ? img->animation_duration - removed_gap : 0;
|
||||||
if (PyErr_Occurred()) PyErr_Print();
|
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);
|
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--;
|
img->extra_framecnt--;
|
||||||
if (img->current_frame_index > img->extra_framecnt) {
|
if (img->current_frame_index > img->extra_framecnt) {
|
||||||
img->current_frame_index = img->extra_framecnt;
|
img->current_frame_index = img->extra_framecnt;
|
||||||
@ -1284,7 +1437,8 @@ grman_handle_command(GraphicsManager *self, const GraphicsCommand *g, const uint
|
|||||||
bool is_query = g->action == 'q';
|
bool is_query = g->action == 'q';
|
||||||
if (is_query) { iid = 0; if (!q_iid) { REPORT_ERROR("Query graphics command without image id"); break; } }
|
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);
|
Image *image = handle_add_command(self, g, payload, is_dirty, iid);
|
||||||
GraphicsCommand *lg = &self->last_transmit_graphics_command;
|
if (!self->currently_loading.loading_for.image_id) free_load_data(&self->currently_loading);
|
||||||
|
GraphicsCommand *lg = &self->currently_loading.start_command;
|
||||||
lg->quiet = g->quiet;
|
lg->quiet = g->quiet;
|
||||||
if (is_query) ret = finish_command_response(&(const GraphicsCommand){.id=q_iid, .quiet=g->quiet}, image != NULL);
|
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);
|
else ret = finish_command_response(lg, image != NULL);
|
||||||
@ -1308,6 +1462,7 @@ grman_handle_command(GraphicsManager *self, const GraphicsCommand *g, const uint
|
|||||||
GraphicsCommand ag = *g;
|
GraphicsCommand ag = *g;
|
||||||
if (ag.action == 'f') {
|
if (ag.action == 'f') {
|
||||||
img = handle_animation_frame_load_command(self, &ag, img, payload, is_dirty);
|
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);
|
||||||
ret = finish_command_response(&ag, img != NULL);
|
ret = finish_command_response(&ag, img != NULL);
|
||||||
} else if (ag.action == 'a') {
|
} else if (ag.action == 'a') {
|
||||||
handle_animation_control_command(self, is_dirty, &ag, img);
|
handle_animation_control_command(self, is_dirty, &ag, img);
|
||||||
@ -1348,22 +1503,33 @@ static inline PyObject*
|
|||||||
image_as_dict(GraphicsManager *self, Image *img) {
|
image_as_dict(GraphicsManager *self, Image *img) {
|
||||||
#define U(x) #x, img->x
|
#define U(x) #x, img->x
|
||||||
#define B(x) #x, img->x ? Py_True : Py_False
|
#define B(x) #x, img->x ? Py_True : Py_False
|
||||||
ImageAndFrame key = {.image_id = img->internal_id};
|
|
||||||
PyObject *frames = PyTuple_New(img->extra_framecnt);
|
PyObject *frames = PyTuple_New(img->extra_framecnt);
|
||||||
for (unsigned i = 0; i < img->extra_framecnt; i++) {
|
for (unsigned i = 0; i < img->extra_framecnt; i++) {
|
||||||
key.frame_id = img->extra_frames[i].id;
|
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(
|
PyTuple_SET_ITEM(frames, i, Py_BuildValue(
|
||||||
"{sI sI sN}", "gap", img->extra_frames[i].gap, "id", key.frame_id, "data", read_from_cache_python(self, key)));
|
"{sI sI sy#}",
|
||||||
|
"gap", f->gap,
|
||||||
|
"id", f->id,
|
||||||
|
"data", cfd.buf, (cfd.is_opaque ? 3 : 4) * img->width * img->height
|
||||||
|
));
|
||||||
|
free(cfd.buf);
|
||||||
if (PyErr_Occurred()) { Py_CLEAR(frames); return NULL; }
|
if (PyErr_Occurred()) { Py_CLEAR(frames); return NULL; }
|
||||||
}
|
}
|
||||||
key.frame_id = img->root_frame.id;
|
CoalescedFrameData cfd = get_coalesced_frame_data(self, img, &img->root_frame);
|
||||||
return Py_BuildValue("{sI sI sI sI sK sI sI sO sO sO sI sI sI sI sN sN}",
|
if (!cfd.buf) { PyErr_SetString(PyExc_RuntimeError, "Failed to get data for root frame"); return NULL; }
|
||||||
|
PyObject *ans = Py_BuildValue("{sI sI sI sI sK sI sI " "sO sO sO " "sI sI sI " "sI sy# sN}",
|
||||||
U(texture_id), U(client_id), U(width), U(height), U(internal_id), U(refcnt), U(client_number),
|
U(texture_id), U(client_id), U(width), U(height), U(internal_id), U(refcnt), U(client_number),
|
||||||
B(data_loaded), B(is_4byte_aligned), B(animation_enabled),
|
|
||||||
|
B(data_loaded), B(animation_enabled), "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(current_frame_index), "root_frame_gap", img->root_frame.gap, U(current_frame_index),
|
||||||
U(animation_duration),
|
|
||||||
"data", read_from_cache_python(self, key), "extra_frames", frames
|
U(animation_duration), "data", cfd.buf, (cfd.is_opaque ? 3 : 4) * img->width * img->height, "extra_frames", frames
|
||||||
);
|
);
|
||||||
|
free(cfd.buf);
|
||||||
|
return ans;
|
||||||
#undef B
|
#undef B
|
||||||
#undef U
|
#undef U
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,19 +16,6 @@ typedef struct {
|
|||||||
size_t payload_sz;
|
size_t payload_sz;
|
||||||
} GraphicsCommand;
|
} GraphicsCommand;
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
uint8_t *buf;
|
|
||||||
size_t buf_capacity, buf_used;
|
|
||||||
|
|
||||||
uint8_t *mapped_file;
|
|
||||||
size_t mapped_file_sz;
|
|
||||||
|
|
||||||
size_t data_sz;
|
|
||||||
uint8_t *data;
|
|
||||||
bool is_4byte_aligned;
|
|
||||||
bool is_opaque;
|
|
||||||
} LoadData;
|
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
float left, top, right, bottom;
|
float left, top, right, bottom;
|
||||||
} ImageRect;
|
} ImageRect;
|
||||||
@ -43,7 +30,8 @@ typedef struct {
|
|||||||
} ImageRef;
|
} ImageRef;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uint32_t gap, id;
|
uint32_t gap, id, width, height, x, y, base_frame_id, bgcolor;
|
||||||
|
bool is_opaque, is_4byte_aligned, alpha_blend;
|
||||||
} Frame;
|
} Frame;
|
||||||
|
|
||||||
|
|
||||||
@ -52,8 +40,6 @@ typedef struct {
|
|||||||
id_type internal_id;
|
id_type internal_id;
|
||||||
|
|
||||||
bool data_loaded;
|
bool data_loaded;
|
||||||
LoadData load_data;
|
|
||||||
|
|
||||||
ImageRef *refs;
|
ImageRef *refs;
|
||||||
Frame *extra_frames, root_frame;
|
Frame *extra_frames, root_frame;
|
||||||
uint32_t current_frame_index, frame_id_counter;
|
uint32_t current_frame_index, frame_id_counter;
|
||||||
@ -61,7 +47,7 @@ typedef struct {
|
|||||||
size_t refcnt, refcap, extra_framecnt;
|
size_t refcnt, refcap, extra_framecnt;
|
||||||
monotonic_t atime;
|
monotonic_t atime;
|
||||||
size_t used_storage;
|
size_t used_storage;
|
||||||
bool is_opaque, is_4byte_aligned, animation_enabled, is_drawn;
|
bool animation_enabled, is_drawn;
|
||||||
monotonic_t current_frame_shown_at;
|
monotonic_t current_frame_shown_at;
|
||||||
} Image;
|
} Image;
|
||||||
|
|
||||||
@ -84,12 +70,27 @@ typedef struct {
|
|||||||
uint32_t frame_id;
|
uint32_t frame_id;
|
||||||
} ImageAndFrame;
|
} ImageAndFrame;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t *buf;
|
||||||
|
size_t buf_capacity, buf_used;
|
||||||
|
|
||||||
|
uint8_t *mapped_file;
|
||||||
|
size_t mapped_file_sz;
|
||||||
|
|
||||||
|
size_t data_sz;
|
||||||
|
uint8_t *data;
|
||||||
|
bool is_4byte_aligned;
|
||||||
|
bool is_opaque;
|
||||||
|
uint32_t width, height;
|
||||||
|
GraphicsCommand start_command;
|
||||||
|
ImageAndFrame loading_for;
|
||||||
|
} LoadData;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
PyObject_HEAD
|
PyObject_HEAD
|
||||||
|
|
||||||
size_t image_count, images_capacity, storage_limit;
|
size_t image_count, images_capacity, storage_limit;
|
||||||
ImageAndFrame currently_loading_data_for;
|
LoadData currently_loading;
|
||||||
GraphicsCommand last_transmit_graphics_command;
|
|
||||||
Image *images;
|
Image *images;
|
||||||
size_t count, capacity;
|
size_t count, capacity;
|
||||||
ImageRenderData *render_data;
|
ImageRenderData *render_data;
|
||||||
|
|||||||
@ -604,14 +604,11 @@ class TestGraphics(BaseTest):
|
|||||||
# test error on send frame for non-existent image
|
# test error on send frame for non-existent image
|
||||||
self.assertEqual(li().code, 'ENOENT')
|
self.assertEqual(li().code, 'ENOENT')
|
||||||
|
|
||||||
# test error sending incompatible data formats
|
# create image
|
||||||
self.assertEqual(li(a='t').code, 'OK')
|
self.assertEqual(li(a='t').code, 'OK')
|
||||||
self.assertEqual(g.disk_cache.total_size, 36)
|
self.assertEqual(g.disk_cache.total_size, 36)
|
||||||
res = li(s=3, v=3, f=32)
|
|
||||||
self.assertEqual(res.code, 'EINVAL')
|
|
||||||
self.assertIn('ransparen', res.msg)
|
|
||||||
|
|
||||||
# simple new frame
|
# simple new frame (width=4, height=3)
|
||||||
t(payload='2' * 36, z=77)
|
t(payload='2' * 36, z=77)
|
||||||
img = g.image_for_client_id(1)
|
img = g.image_for_client_id(1)
|
||||||
self.assertEqual(img['extra_frames'], ({'gap': 77, 'id': 2, 'data': b'2' * 36},))
|
self.assertEqual(img['extra_frames'], ({'gap': 77, 'id': 2, 'data': b'2' * 36},))
|
||||||
@ -622,10 +619,17 @@ class TestGraphics(BaseTest):
|
|||||||
# test editing part of a frame
|
# test editing part of a frame
|
||||||
t(payload='4' * 12, r=2, s=2, v=2)
|
t(payload='4' * 12, r=2, s=2, v=2)
|
||||||
img = g.image_for_client_id(1)
|
img = g.image_for_client_id(1)
|
||||||
self.assertEqual(img['extra_frames'], ({'gap': 77, 'id': 2, 'data': b'444444333333444444333333333333333333'},))
|
|
||||||
|
def expand(*rows):
|
||||||
|
ans = []
|
||||||
|
for r in rows:
|
||||||
|
ans.append(''.join(x * 3 for x in str(r)))
|
||||||
|
return ''.join(ans).encode('ascii')
|
||||||
|
|
||||||
|
self.assertEqual(img['extra_frames'], ({'gap': 77, 'id': 2, 'data': expand(4433, 4433, 3333)},))
|
||||||
t(payload='5' * 12, r=2, s=2, v=2, x=1, y=1)
|
t(payload='5' * 12, r=2, s=2, v=2, x=1, y=1)
|
||||||
img = g.image_for_client_id(1)
|
img = g.image_for_client_id(1)
|
||||||
self.assertEqual(img['extra_frames'], ({'gap': 77, 'id': 2, 'data': b'444444333555555444555555333333333333'},))
|
self.assertEqual(img['extra_frames'], ({'gap': 77, 'id': 2, 'data': expand(4433, 4553, 3553)},))
|
||||||
t(payload='3' * 36, r=2)
|
t(payload='3' * 36, r=2)
|
||||||
img = g.image_for_client_id(1)
|
img = g.image_for_client_id(1)
|
||||||
self.assertEqual(img['extra_frames'], ({'gap': 77, 'id': 2, 'data': b'3' * 36},))
|
self.assertEqual(img['extra_frames'], ({'gap': 77, 'id': 2, 'data': b'3' * 36},))
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user