graphics: make N a transient usage-hints bitmask

Change the graphics protocol N key from a boolean into a usage-hints
bitmask. Define the first bit as a transient hint, allowing the terminal
to treat the image data as short-lived and apply optimizations such as
skipping disk cache writes.

Propagate the transient hint through frame coalescing and composition, so
a composed frame is transient if any contributing frame is transient.
This commit is contained in:
Matsumoto Kotaro 2026-06-19 13:18:41 +09:00
parent cc2d7a1789
commit 89946ebc07
8 changed files with 54 additions and 28 deletions

View file

@ -1051,8 +1051,16 @@ Key Value Default Description
``o`` Single character. ``null`` The type of data compression.
``only z``
``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.
``N`` bitmask ``0`` Usage hints from the client to the terminal about the intended use of
the image. Only one hint is currently defined, the ``1`` bit which means
*transient*. The terminal is free to assume that an image with this hint
will be used for only a short time, and so may, for example, evict its
data before other images when the image is soft deleted, has no visible
placements and the terminal is under storage pressure, or skip writing
its data to disk. The terminal is also free to ignore the hint. If an
animation frame with the *transient* hint is composited onto another
frame, and any of the involved frames have the hint, the resulting
composited frame also has the hint.
**Keys for image display**
-----------------------------------------------------------

View file

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

View file

@ -768,10 +768,10 @@ handle_add_command(GraphicsManager *self, const GraphicsCommand *g, const uint8_
.is_opaque = self->currently_loading.is_opaque,
.is_4byte_aligned = self->currently_loading.is_4byte_aligned,
.width = img->width, .height = img->height,
.memory_only = !!g->no_disk_cache,
.transient = (g->usage_hints & GRAPHICS_USAGE_HINT_TRANSIENT) != 0,
};
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, img->root_frame.memory_only)) {
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.transient)) {
if (PyErr_Occurred()) PyErr_Print();
ABRT("ENOSPC", "Failed to store image data in cache");
}
@ -1355,7 +1355,7 @@ change_gap(Image *img, Frame *f, int32_t gap) {
typedef struct {
uint8_t *buf;
bool is_4byte_aligned, is_opaque;
bool is_4byte_aligned, is_opaque, transient;
} CoalescedFrameData;
static void
@ -1451,6 +1451,7 @@ compose(const ComposeData d, uint8_t *under_data, const uint8_t *over_data) {
static CoalescedFrameData
get_coalesced_frame_data_standalone(const Image *img, const Frame *f, uint8_t *frame_data) {
CoalescedFrameData ans = {0};
ans.transient = f->transient;
bool is_full_frame = f->width == img->width && f->height == img->height && !f->x && !f->y;
if (is_full_frame) {
ans.buf = frame_data;
@ -1514,6 +1515,7 @@ get_coalesced_frame_data_impl(GraphicsManager *self, Image *img, const Frame *f,
};
compose(d, base_data.buf, frame_data);
free(frame_data);
base_data.transient = base_data.transient || f->transient;
return base_data;
}
@ -1554,6 +1556,17 @@ reference_chain_too_large(Image *img, const Frame *frame) {
return num >= 5 || drawn_area >= limit;
}
static bool
frame_chain_is_transient(Image *img, const Frame *frame) {
// matches the recursion depth limit in get_coalesced_frame_data_impl
unsigned num = 0;
while (frame) {
if (frame->transient) return true;
if (!frame->base_frame_id || ++num > 32 || !(frame = frame_for_id(img, frame->base_frame_id))) break;
}
return false;
}
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;
@ -1596,7 +1609,7 @@ handle_animation_frame_load_command(GraphicsManager *self, GraphicsCommand *g, I
.alpha_blend = g->compose_mode != 1 && !load_data->is_opaque,
.gap = g->gap > 0 ? g->gap : (g->gap < 0) ? 0 : DEFAULT_GAP,
.bgcolor = g->bgcolor,
.memory_only = !!g->no_disk_cache,
.transient = (g->usage_hints & GRAPHICS_USAGE_HINT_TRANSIENT) != 0,
};
Frame *frame;
if (is_new_frame) {
@ -1632,12 +1645,14 @@ handle_animation_frame_load_command(GraphicsManager *self, GraphicsCommand *g, I
transmitted_frame.x = 0; transmitted_frame.y = 0;
transmitted_frame.is_4byte_aligned = cfd.is_4byte_aligned;
transmitted_frame.is_opaque = cfd.is_opaque;
transmitted_frame.transient = transmitted_frame.transient || cfd.transient;
} else {
transmitted_frame.base_frame_id = other_frame->id;
transmitted_frame.transient = transmitted_frame.transient || frame_chain_is_transient(img, other_frame);
}
}
*frame = transmitted_frame;
if (!add_to_cache(self, key, load_data->data, load_data->data_sz, frame->memory_only)) {
if (!add_to_cache(self, key, load_data->data, load_data->data_sz, frame->transient)) {
img->extra_framecnt--;
if (PyErr_Occurred()) PyErr_Print();
ABRT("ENOSPC", "Failed to cache data for image frame");
@ -1653,7 +1668,7 @@ handle_animation_frame_load_command(GraphicsManager *self, GraphicsCommand *g, I
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->memory_only = transmitted_frame.memory_only;
frame->transient = cfd.transient || transmitted_frame.transient;
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;
@ -1667,7 +1682,7 @@ handle_animation_frame_load_command(GraphicsManager *self, GraphicsCommand *g, I
};
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, frame->memory_only);
bool added = add_to_cache(self, key, cfd.buf, (size_t)bytes_per_pixel * frame->width * frame->height, frame->transient);
if (added && frame == current_frame(img)) {
update_current_frame(self, img, &cfd);
*is_dirty = true;
@ -1870,10 +1885,13 @@ handle_compose_command(GraphicsManager *self, bool *is_dirty, const GraphicsComm
.stride = img->width
};
compose_rectangles(d, dest_data.buf, src_data.buf);
bool transient = src_data.transient || dest_data.transient;
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, dest_frame->memory_only)) {
if (!add_to_cache(self, key, dest_data.buf, ((size_t)(dest_data.is_opaque ? 3 : 4)) * img->width * img->height, transient)) {
if (PyErr_Occurred()) PyErr_Print();
set_command_failed_response("ENOSPC", "Failed to store image data in cache");
} else {
dest_frame->transient = transient;
}
// 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;

View file

@ -8,9 +8,12 @@
#include "data-types.h"
#include "monotonic.h"
// Bitmask values for GraphicsCommand.usage_hints
#define GRAPHICS_USAGE_HINT_TRANSIENT 1u
typedef struct {
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, no_disk_cache;
uint32_t format, more, id, image_number, data_sz, data_offset, placement_id, quiet, parent_id, parent_placement_id, usage_hints;
uint32_t width, height, x_offset, y_offset;
union { uint32_t cursor_movement, compose_mode; };
union { uint32_t cell_x_offset; };
@ -71,7 +74,7 @@ typedef struct {
typedef struct {
uint32_t gap, id, width, height, x, y, base_frame_id, bgcolor;
bool is_opaque, is_4byte_aligned, alpha_blend, memory_only;
bool is_opaque, is_4byte_aligned, alpha_blend, transient;
} Frame;
typedef enum { ANIMATION_STOPPED = 0, ANIMATION_LOADING = 1, ANIMATION_RUNNING = 2} AnimationState;

View file

@ -46,7 +46,7 @@ static inline void parse_graphics_code(PS *self, uint8_t *parser_buf,
unicode_placement = 'U',
parent_id = 'P',
parent_placement_id = 'Q',
no_disk_cache = 'N',
usage_hints = 'N',
offset_from_parent_x = 'H',
offset_from_parent_y = 'V'
};
@ -142,7 +142,7 @@ static inline void parse_graphics_code(PS *self, uint8_t *parser_buf,
case parent_placement_id:
value_state = UINT;
break;
case no_disk_cache:
case usage_hints:
value_state = UINT;
break;
case offset_from_parent_x:
@ -303,7 +303,7 @@ static inline void parse_graphics_code(PS *self, uint8_t *parser_buf,
U(unicode_placement);
U(parent_id);
U(parent_placement_id);
U(no_disk_cache);
U(usage_hints);
default:
break;
}
@ -383,8 +383,8 @@ static inline void parse_graphics_code(PS *self, uint8_t *parser_buf,
"cell_y_offset", (unsigned int)g.cell_y_offset, "cursor_movement",
(unsigned int)g.cursor_movement, "unicode_placement",
(unsigned int)g.unicode_placement, "parent_id", (unsigned int)g.parent_id,
"parent_placement_id", (unsigned int)g.parent_placement_id,
"no_disk_cache", (unsigned int)g.no_disk_cache,
"parent_placement_id", (unsigned int)g.parent_placement_id, "usage_hints",
(unsigned int)g.usage_hints,
"z_index", (int)g.z_index, "offset_from_parent_x",
(int)g.offset_from_parent_x, "offset_from_parent_y",

View file

@ -386,7 +386,7 @@ class TestGraphics(BaseTest):
self.assertIsNone(li(payload='2' * 12, z=77, m=1, q=2))
self.assertIsNone(li(payload='2' * 12))
def test_no_disk_cache_graphics_image(self):
def test_transient_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')

View file

@ -920,7 +920,7 @@ class TestParser(BaseTest):
k.setdefault(f, b'\0')
for f in ('format more id data_sz data_offset width height x_offset y_offset data_height data_width cursor_movement'
' num_cells num_lines cell_x_offset cell_y_offset z_index placement_id image_number quiet unicode_placement'
' parent_id parent_placement_id offset_from_parent_x offset_from_parent_y'
' parent_id parent_placement_id usage_hints offset_from_parent_x offset_from_parent_y'
).split():
k.setdefault(f, 0)
p = k.pop('payload', '')
@ -943,6 +943,7 @@ class TestParser(BaseTest):
t('a=t,t=d,s=100,z=-9', payload='X', action='t', transmission_type='d', data_width=100, z_index=-9)
t('a=t,t=d,s=100,z=9', payload='payload', action='t', transmission_type='d', data_width=100, z_index=9)
t('a=t,t=d,s=100,z=9,q=2', action='t', transmission_type='d', data_width=100, z_index=9, quiet=2)
t('N=1', usage_hints=1)
e(',s=1', 'Malformed GraphicsCommand control block, invalid key character: 0x2c')
e('W=1', 'Malformed GraphicsCommand control block, invalid key character: 0x57')
e('1=1', 'Malformed GraphicsCommand control block, invalid key character: 0x31')

View file

@ -756,16 +756,12 @@ func (self *GraphicsCommand) SetFrameToMakeCurrent(c uint64) *GraphicsCommand {
return self
}
func (self *GraphicsCommand) NoDiskCache() bool {
return self.N != 0
func (self *GraphicsCommand) UsageHints() uint64 {
return self.N
}
func (self *GraphicsCommand) SetNoDiskCache(noDiskCache bool) *GraphicsCommand {
if noDiskCache {
self.N = 1
} else {
self.N = 0
}
func (self *GraphicsCommand) SetUsageHints(hints uint64) *GraphicsCommand {
self.N = hints
return self
}