graphics: add memory-only storage for graphics data

Add a new graphics protocol key, N=1, to request that transmitted
image/frame data is kept only in memory and not written to the graphics
disk cache file.

This is useful for transient high-frequency updates such as video-like
streams, where the latest frame is the only useful data and persisting
each frame to the disk cache causes unnecessary write traffic.

The implementation keeps the existing graphics cache abstraction intact:
memory-only entries can still be read back by animation, composition, and
frame coalescing paths. Only persistence to the disk cache file is skipped.

The default behavior is unchanged when N is omitted or set to zero.
This commit is contained in:
Matsumoto Kotaro 2026-05-30 18:46:24 +09:00
parent 6bd62a5242
commit cc2d7a1789
9 changed files with 63 additions and 18 deletions

View file

@ -1051,6 +1051,8 @@ Key Value Default Description
``o`` Single character. ``null`` The type of data compression. ``o`` Single character. ``null`` The type of data compression.
``only z`` ``only z``
``m`` zero or one ``0`` Whether there is more chunked data available. ``m`` zero or one ``0`` Whether there is more chunked data available.
``N`` zero or one ``0`` If set to ``1``, keep the transmitted image or frame data in memory only,
without writing it to the graphics disk cache.
**Keys for image display** **Keys for image display**
----------------------------------------------------------- -----------------------------------------------------------

View file

@ -313,6 +313,7 @@ def parsers() -> None:
'U': ('unicode_placement', 'uint'), 'U': ('unicode_placement', 'uint'),
'P': ('parent_id', 'uint'), 'P': ('parent_id', 'uint'),
'Q': ('parent_placement_id', 'uint'), 'Q': ('parent_placement_id', 'uint'),
'N': ('no_disk_cache', 'uint'),
'H': ('offset_from_parent_x', 'int'), 'H': ('offset_from_parent_x', 'int'),
'V': ('offset_from_parent_y', 'int'), 'V': ('offset_from_parent_y', 'int'),
} }

View file

@ -33,7 +33,7 @@ typedef struct CacheKey {
typedef struct { typedef struct {
uint8_t *data; uint8_t *data;
size_t data_sz; size_t data_sz;
bool written_to_disk, uses_encryption; bool written_to_disk, uses_encryption, memory_only;
off_t pos_in_cache_file; off_t pos_in_cache_file;
uint8_t encryption_key[64]; uint8_t encryption_key[64];
} CacheValue; } CacheValue;
@ -593,7 +593,7 @@ create_cache_entry(void) {
} }
bool bool
add_to_disk_cache(PyObject *self_, const void *key, size_t key_sz, const void *data, size_t data_sz) { add_to_disk_cache(PyObject *self_, const void *key, size_t key_sz, const void *data, size_t data_sz, bool memory_only) {
DiskCache *self = (DiskCache*)self_; DiskCache *self = (DiskCache*)self_;
if (!ensure_state(self)) return false; if (!ensure_state(self)) return false;
if (key_sz > MAX_KEY_SIZE) { PyErr_SetString(PyExc_KeyError, "cache key is too long"); return false; } if (key_sz > MAX_KEY_SIZE) { PyErr_SetString(PyExc_KeyError, "cache key is too long"); return false; }
@ -618,11 +618,14 @@ add_to_disk_cache(PyObject *self_, const void *key, size_t key_sz, const void *d
if (s->data) free(s->data); if (s->data) free(s->data);
} }
s->data = copied_data; s->data_sz = data_sz; copied_data = NULL; s->data = copied_data; s->data_sz = data_sz; copied_data = NULL;
s->memory_only = memory_only;
s->written_to_disk = memory_only;
if (memory_only) s->pos_in_cache_file = -1;
self->total_size += s->data_sz; self->total_size += s->data_sz;
end: end:
mutex(unlock); mutex(unlock);
if (PyErr_Occurred()) return false; if (PyErr_Occurred()) return false;
wakeup_write_loop(self); if (!memory_only) wakeup_write_loop(self);
return true; return true;
} }
@ -740,7 +743,7 @@ disk_cache_clear_from_ram(PyObject *self_, bool(matches)(void*, void *key, unsig
mutex(lock); mutex(lock);
cache_map_for_loop(i) { cache_map_for_loop(i) {
CacheValue *s = i.data->val; CacheValue *s = i.data->val;
if (s->written_to_disk && s->data && matches(data, i.data->key.hash_key, i.data->key.hash_keylen)) { if (s->written_to_disk && !s->memory_only && s->data && matches(data, i.data->key.hash_key, i.data->key.hash_keylen)) {
free(s->data); s->data = NULL; free(s->data); s->data = NULL;
ans++; ans++;
} }
@ -848,7 +851,7 @@ add(PyObject *self, PyObject *args) {
const char *key, *data; const char *key, *data;
Py_ssize_t keylen, datalen; Py_ssize_t keylen, datalen;
PA("y#y#", &key, &keylen, &data, &datalen); PA("y#y#", &key, &keylen, &data, &datalen);
if (!add_to_disk_cache(self, key, keylen, data, datalen)) return NULL; if (!add_to_disk_cache(self, key, keylen, data, datalen, false)) return NULL;
Py_RETURN_NONE; Py_RETURN_NONE;
} }

View file

@ -9,7 +9,7 @@
#include "data-types.h" #include "data-types.h"
PyObject* create_disk_cache(void); PyObject* create_disk_cache(void);
bool add_to_disk_cache(PyObject *self, const void *key, size_t key_sz, const void *data, size_t data_sz); bool add_to_disk_cache(PyObject *self, const void *key, size_t key_sz, const void *data, size_t data_sz, bool memory_only);
bool remove_from_disk_cache(PyObject *self_, const void *key, size_t key_sz); bool remove_from_disk_cache(PyObject *self_, const void *key, size_t key_sz);
void* read_from_disk_cache(PyObject *self_, const void *key, size_t key_sz, void*(allocator)(void*, size_t), void*, bool); void* read_from_disk_cache(PyObject *self_, const void *key, size_t key_sz, void*(allocator)(void*, size_t), void*, bool);
PyObject* read_from_disk_cache_python(PyObject *self_, const void *key, size_t key_sz, bool); PyObject* read_from_disk_cache_python(PyObject *self_, const void *key, size_t key_sz, bool);

View file

@ -41,9 +41,9 @@ cache_key(const ImageAndFrame x, char *key) {
#define CK(x) key, cache_key(x, key) #define CK(x) key, cache_key(x, key)
static bool static bool
add_to_cache(GraphicsManager *self, const ImageAndFrame x, const void *data, const size_t sz) { add_to_cache(GraphicsManager *self, const ImageAndFrame x, const void *data, const size_t sz, bool memory_only) {
char key[CACHE_KEY_BUFFER_SIZE]; char key[CACHE_KEY_BUFFER_SIZE];
return add_to_disk_cache(self->disk_cache, CK(x), data, sz); return add_to_disk_cache(self->disk_cache, CK(x), data, sz, memory_only);
} }
static bool static bool
@ -768,11 +768,12 @@ handle_add_command(GraphicsManager *self, const GraphicsCommand *g, const uint8_
.is_opaque = self->currently_loading.is_opaque, .is_opaque = self->currently_loading.is_opaque,
.is_4byte_aligned = self->currently_loading.is_4byte_aligned, .is_4byte_aligned = self->currently_loading.is_4byte_aligned,
.width = img->width, .height = img->height, .width = img->width, .height = img->height,
.memory_only = !!g->no_disk_cache,
}; };
if (!is_query) { 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 (!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, img->root_frame.memory_only)) {
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 cache");
} }
upload_to_gpu(self, img, img->root_frame.is_opaque, img->root_frame.is_4byte_aligned, self->currently_loading.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;
@ -1595,6 +1596,7 @@ handle_animation_frame_load_command(GraphicsManager *self, GraphicsCommand *g, I
.alpha_blend = g->compose_mode != 1 && !load_data->is_opaque, .alpha_blend = g->compose_mode != 1 && !load_data->is_opaque,
.gap = g->gap > 0 ? g->gap : (g->gap < 0) ? 0 : DEFAULT_GAP, .gap = g->gap > 0 ? g->gap : (g->gap < 0) ? 0 : DEFAULT_GAP,
.bgcolor = g->bgcolor, .bgcolor = g->bgcolor,
.memory_only = !!g->no_disk_cache,
}; };
Frame *frame; Frame *frame;
if (is_new_frame) { if (is_new_frame) {
@ -1635,7 +1637,7 @@ handle_animation_frame_load_command(GraphicsManager *self, GraphicsCommand *g, I
} }
} }
*frame = transmitted_frame; *frame = transmitted_frame;
if (!add_to_cache(self, key, load_data->data, load_data->data_sz)) { if (!add_to_cache(self, key, load_data->data, load_data->data_sz, frame->memory_only)) {
img->extra_framecnt--; img->extra_framecnt--;
if (PyErr_Occurred()) PyErr_Print(); if (PyErr_Occurred()) PyErr_Print();
ABRT("ENOSPC", "Failed to cache data for image frame"); ABRT("ENOSPC", "Failed to cache data for image frame");
@ -1651,6 +1653,7 @@ handle_animation_frame_load_command(GraphicsManager *self, GraphicsCommand *g, I
if (g->gap != 0) change_gap(img, frame, transmitted_frame.gap); if (g->gap != 0) change_gap(img, frame, transmitted_frame.gap);
CoalescedFrameData cfd = get_coalesced_frame_data(self, img, frame); CoalescedFrameData cfd = get_coalesced_frame_data(self, img, frame);
if (!cfd.buf) ABRT("EINVAL", "No data associated with frame number: %u", frame_number); if (!cfd.buf) ABRT("EINVAL", "No data associated with frame number: %u", frame_number);
frame->memory_only = transmitted_frame.memory_only;
frame->alpha_blend = false; frame->base_frame_id = 0; frame->bgcolor = 0; 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->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; frame->x = 0; frame->y = 0; frame->width = img->width; frame->height = img->height;
@ -1664,7 +1667,7 @@ handle_animation_frame_load_command(GraphicsManager *self, GraphicsCommand *g, I
}; };
compose(d, cfd.buf, load_data->data); compose(d, cfd.buf, load_data->data);
const ImageAndFrame key = { .image_id = img->internal_id, .frame_id = frame->id }; 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); bool added = add_to_cache(self, key, cfd.buf, (size_t)bytes_per_pixel * frame->width * frame->height, frame->memory_only);
if (added && frame == current_frame(img)) { if (added && frame == current_frame(img)) {
update_current_frame(self, img, &cfd); update_current_frame(self, img, &cfd);
*is_dirty = true; *is_dirty = true;
@ -1868,9 +1871,9 @@ handle_compose_command(GraphicsManager *self, bool *is_dirty, const GraphicsComm
}; };
compose_rectangles(d, dest_data.buf, src_data.buf); compose_rectangles(d, dest_data.buf, src_data.buf);
const ImageAndFrame key = { .image_id = img->internal_id, .frame_id = dest_frame->id }; 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 (!add_to_cache(self, key, dest_data.buf, ((size_t)(dest_data.is_opaque ? 3 : 4)) * img->width * img->height, dest_frame->memory_only)) {
if (PyErr_Occurred()) PyErr_Print(); if (PyErr_Occurred()) PyErr_Print();
set_command_failed_response("ENOSPC", "Failed to store image data in disk cache"); set_command_failed_response("ENOSPC", "Failed to store image data in cache");
} }
// frame is now a fully coalesced frame // 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->x = 0; dest_frame->y = 0; dest_frame->width = img->width; dest_frame->height = img->height;

View file

@ -10,7 +10,7 @@
typedef struct { typedef struct {
unsigned char action, transmission_type, compressed, delete_action; unsigned char action, transmission_type, compressed, delete_action;
uint32_t format, more, id, image_number, data_sz, data_offset, placement_id, quiet, parent_id, parent_placement_id; uint32_t format, more, id, image_number, data_sz, data_offset, placement_id, quiet, parent_id, parent_placement_id, no_disk_cache;
uint32_t width, height, x_offset, y_offset; uint32_t width, height, x_offset, y_offset;
union { uint32_t cursor_movement, compose_mode; }; union { uint32_t cursor_movement, compose_mode; };
union { uint32_t cell_x_offset; }; union { uint32_t cell_x_offset; };
@ -71,7 +71,7 @@ typedef struct {
typedef struct { typedef struct {
uint32_t gap, id, width, height, x, y, base_frame_id, bgcolor; uint32_t gap, id, width, height, x, y, base_frame_id, bgcolor;
bool is_opaque, is_4byte_aligned, alpha_blend; bool is_opaque, is_4byte_aligned, alpha_blend, memory_only;
} Frame; } Frame;
typedef enum { ANIMATION_STOPPED = 0, ANIMATION_LOADING = 1, ANIMATION_RUNNING = 2} AnimationState; typedef enum { ANIMATION_STOPPED = 0, ANIMATION_LOADING = 1, ANIMATION_RUNNING = 2} AnimationState;

View file

@ -46,6 +46,7 @@ static inline void parse_graphics_code(PS *self, uint8_t *parser_buf,
unicode_placement = 'U', unicode_placement = 'U',
parent_id = 'P', parent_id = 'P',
parent_placement_id = 'Q', parent_placement_id = 'Q',
no_disk_cache = 'N',
offset_from_parent_x = 'H', offset_from_parent_x = 'H',
offset_from_parent_y = 'V' offset_from_parent_y = 'V'
}; };
@ -141,6 +142,9 @@ static inline void parse_graphics_code(PS *self, uint8_t *parser_buf,
case parent_placement_id: case parent_placement_id:
value_state = UINT; value_state = UINT;
break; break;
case no_disk_cache:
value_state = UINT;
break;
case offset_from_parent_x: case offset_from_parent_x:
value_state = INT; value_state = INT;
break; break;
@ -299,6 +303,7 @@ static inline void parse_graphics_code(PS *self, uint8_t *parser_buf,
U(unicode_placement); U(unicode_placement);
U(parent_id); U(parent_id);
U(parent_placement_id); U(parent_placement_id);
U(no_disk_cache);
default: default:
break; break;
} }
@ -359,7 +364,7 @@ static inline void parse_graphics_code(PS *self, uint8_t *parser_buf,
REPORT_VA_COMMAND( REPORT_VA_COMMAND(
"K s {sc sc sc sc sI sI sI sI sI sI sI sI sI sI sI sI sI sI sI sI sI sI " "K s {sc sc sc sc sI sI sI sI sI sI sI sI sI sI sI sI sI sI sI sI sI sI "
"sI sI sI sI si si si ss#}", "sI sI sI sI sI si si si ss#}",
self->window_id, "graphics_command", self->window_id, "graphics_command",
"action", g.action, "delete_action", g.delete_action, "transmission_type", "action", g.action, "delete_action", g.delete_action, "transmission_type",
@ -379,6 +384,7 @@ static inline void parse_graphics_code(PS *self, uint8_t *parser_buf,
(unsigned int)g.cursor_movement, "unicode_placement", (unsigned int)g.cursor_movement, "unicode_placement",
(unsigned int)g.unicode_placement, "parent_id", (unsigned int)g.parent_id, (unsigned int)g.unicode_placement, "parent_id", (unsigned int)g.parent_id,
"parent_placement_id", (unsigned int)g.parent_placement_id, "parent_placement_id", (unsigned int)g.parent_placement_id,
"no_disk_cache", (unsigned int)g.no_disk_cache,
"z_index", (int)g.z_index, "offset_from_parent_x", "z_index", (int)g.z_index, "offset_from_parent_x",
(int)g.offset_from_parent_x, "offset_from_parent_y", (int)g.offset_from_parent_x, "offset_from_parent_y",

View file

@ -386,6 +386,20 @@ def test_suppressing_gr_command_responses(self):
self.assertIsNone(li(payload='2' * 12, z=77, m=1, q=2)) self.assertIsNone(li(payload='2' * 12, z=77, m=1, q=2))
self.assertIsNone(li(payload='2' * 12)) self.assertIsNone(li(payload='2' * 12))
def test_no_disk_cache_graphics_image(self):
s, g, pl, sl = load_helpers(self)
self.assertEqual(g.disk_cache.end_of_data_offset(), 0)
self.ae(pl('abc', s=1, v=1, f=24, N=1), 'OK')
self.assertTrue(g.disk_cache.wait_for_write())
self.assertEqual(g.disk_cache.end_of_data_offset(), 0)
img = g.image_for_client_id(1)
self.assertIsNotNone(img)
self.ae(img['data'], b'abc')
self.ae(pl('def', s=1, v=1, f=24, i=2), 'OK')
self.assertTrue(g.disk_cache.wait_for_write())
self.assertGreater(g.disk_cache.end_of_data_offset(), 0)
def test_load_images(self): def test_load_images(self):
s, g, pl, sl = load_helpers(self) s, g, pl, sl = load_helpers(self)
self.assertEqual(g.disk_cache.total_size, 0) self.assertEqual(g.disk_cache.total_size, 0)

View file

@ -143,7 +143,7 @@ type GraphicsCommand struct {
d GRT_d d GRT_d
U GRT_U U GRT_U
s, v, S, O, x, y, w, h, X, Y, c, r uint64 s, v, S, O, x, y, w, h, X, Y, c, r, N uint64
i, I, p uint32 i, I, p uint32
@ -176,6 +176,7 @@ func (self *GraphicsCommand) serialize_non_default_fields() (ans []string) {
write_key('U', self.U, null.U) write_key('U', self.U, null.U)
write_key('d', self.d, null.d) write_key('d', self.d, null.d)
write_key('N', self.N, null.N)
write_key('s', self.s, null.s) write_key('s', self.s, null.s)
write_key('v', self.v, null.v) write_key('v', self.v, null.v)
write_key('S', self.S, null.S) write_key('S', self.S, null.S)
@ -376,6 +377,8 @@ func (self *GraphicsCommand) SetString(key byte, value string) (err error) {
err = set_val(&self.U, GRT_U_from_string, value) err = set_val(&self.U, GRT_U_from_string, value)
case 'd': case 'd':
err = set_val(&self.d, GRT_d_from_string, value) err = set_val(&self.d, GRT_d_from_string, value)
case 'N':
err = set_uval(&self.N, value)
case 's': case 's':
err = set_uval(&self.s, value) err = set_uval(&self.s, value)
case 'v': case 'v':
@ -753,6 +756,19 @@ func (self *GraphicsCommand) SetFrameToMakeCurrent(c uint64) *GraphicsCommand {
return self return self
} }
func (self *GraphicsCommand) NoDiskCache() bool {
return self.N != 0
}
func (self *GraphicsCommand) SetNoDiskCache(noDiskCache bool) *GraphicsCommand {
if noDiskCache {
self.N = 1
} else {
self.N = 0
}
return self
}
func (self *GraphicsCommand) ImageId() uint32 { func (self *GraphicsCommand) ImageId() uint32 {
return self.i return self.i
} }