Compare commits

...

9 Commits

Author SHA1 Message Date
Kovid Goyal
42d3cb2148 Merge branch 'copilot/review-image-usage-hints-functionality' of https://github.com/kovidgoyal/kitty 2026-06-19 12:36:08 +05:30
copilot-swe-agent[bot]
051a5b72e0 Preferentially evict transient images before non-transient ones under storage pressure 2026-06-19 06:48:39 +00:00
Kovid Goyal
d994a47fc8 Cleanup previous PR 2026-06-19 11:36:43 +05:30
Kovid Goyal
a2104d3755 Merge branch 'feat/10090-graphics-cache-dir' of https://github.com/Mekann2904/kitty 2026-06-19 10:16:10 +05:30
Kovid Goyal
9dfe10b682 Update changelog 2026-06-19 10:13:58 +05:30
Kovid Goyal
6202fb7847 Merge branch 'copilot/fix-issue-10146' of https://github.com/kovidgoyal/kitty
Fixes #10146
2026-06-19 10:13:08 +05:30
Matsumoto Kotaro
89946ebc07 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.
2026-06-19 13:22:01 +09:00
copilot-swe-agent[bot]
946867bf57 Fix first Wayland window wrong cell count with fractional scale (issue 10146) 2026-06-19 02:43:17 +00:00
Matsumoto Kotaro
cc2d7a1789 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.
2026-05-30 18:46:24 +09:00
12 changed files with 149 additions and 26 deletions

View File

@@ -176,8 +176,13 @@ Detailed list of changes
0.50.0 [future]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Graphics protocol: Add a new :ref:`transient usage hint <image_usage_hints>` that clients can send to terminals to indicate an image is meant for only short duration use (:pull:`10092`)
- kitten @ get-text: Add support for :code:`alternate` and :code:`alternate_scrollback` extents to fetch text from the alternate screen buffer (:iss:`10165`)
- Wayland: Fix first OS window being a few cells too small when ``initial_window_width/initial_window_height`` are set in cells and a fractional display scale is in use (:iss:`10146`)
0.47.4 [2026-06-15]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@@ -834,6 +834,27 @@ use the ``i`` key with the image id for all future communication.
The ability to use image numbers (see :doc:`kittens/query_terminal` to query kitty version)
.. _image_usage_hints:
Usage hints
-----------------------------
Clients can specify *usage hints* when creating images using the ``N`` key.
These hints allow the terminal to optimise resource consumption such as caching
strategies. The value of ``N`` is a bitmask.
Currently the only usage hint defined is ``transient (N == 1)``.
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. This hint must be specified when the
image or frame data is transmitted. It has no effect on placement commands.
.. _animation_protocol:
Animation
@@ -1051,7 +1072,8 @@ 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`` bitmask ``0`` Usage hints from the client to the terminal about the intended use of
the image.
**Keys for image display**
-----------------------------------------------------------
``x`` Positive integer ``0`` The left edge (in pixels) of the image area to display

View File

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

View File

@@ -33,7 +33,7 @@ typedef struct CacheKey {
typedef struct {
uint8_t *data;
size_t data_sz;
bool written_to_disk, uses_encryption;
bool written_to_disk, uses_encryption, memory_only;
off_t pos_in_cache_file;
uint8_t encryption_key[64];
} CacheValue;
@@ -593,7 +593,7 @@ create_cache_entry(void) {
}
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_;
if (!ensure_state(self)) 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);
}
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;
end:
mutex(unlock);
if (PyErr_Occurred()) return false;
wakeup_write_loop(self);
if (!memory_only) wakeup_write_loop(self);
return true;
}
@@ -740,7 +743,7 @@ disk_cache_clear_from_ram(PyObject *self_, bool(matches)(void*, void *key, unsig
mutex(lock);
cache_map_for_loop(i) {
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;
ans++;
}
@@ -848,7 +851,7 @@ add(PyObject *self, PyObject *args) {
const char *key, *data;
Py_ssize_t keylen, 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;
}

View File

@@ -9,7 +9,7 @@
#include "data-types.h"
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);
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);

View File

@@ -1850,8 +1850,21 @@ create_os_window(PyObject UNUSED *self, PyObject *args, PyObject *kw) {
// this can happen if the window is moved by the OS to a different monitor when shown or with fractional scales on Wayland
// it can also happen with layer shell windows if the callback is
// called before the window is fully created
xdpi = n_xdpi; ydpi = n_ydpi;
xdpi = n_xdpi; ydpi = n_ydpi; xscale = n_xscale; yscale = n_yscale;
fonts_data = load_fonts_data(OPT(font_size), xdpi, ydpi);
// Re-compute the window size with the updated scale/font metrics. This matters when
// the initial size is specified in cells: the first window on Wayland is created with
// scale=1 because fractional scale is only sent by the compositor after the surface
// exists, so the cell dimensions used for the initial size calculation were wrong.
PyObject *new_size = PyObject_CallFunction(get_window_size, "IIddff", fonts_data->fcm.cell_width, fonts_data->fcm.cell_height, fonts_data->logical_dpi_x, fonts_data->logical_dpi_y, xscale, yscale);
if (new_size != NULL) {
int new_width = PyLong_AsLong(PyTuple_GET_ITEM(new_size, 0)), new_height = PyLong_AsLong(PyTuple_GET_ITEM(new_size, 1));
Py_DECREF(new_size);
if (!PyErr_Occurred() && (new_width != width || new_height != height)) {
glfwSetWindowSize(glfw_window, new_width, new_height);
width = new_width; height = new_height;
} else PyErr_Clear();
} else PyErr_Clear();
}
}
if (is_first_window) {

View File

@@ -41,9 +41,9 @@ cache_key(const ImageAndFrame x, char *key) {
#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) {
add_to_cache(GraphicsManager *self, const ImageAndFrame x, const void *data, const size_t sz, bool memory_only) {
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
@@ -287,9 +287,12 @@ apply_storage_quota(GraphicsManager *self, size_t storage_limit, id_type current
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
#define transient_or_older_first(a, b) ( \
(*a)->root_frame.transient != (*b)->root_frame.transient ? \
(*a)->root_frame.transient > (*b)->root_frame.transient : \
(*a)->atime < (*b)->atime)
QSORT(Image*, sorted, num_images, transient_or_older_first);
#undef transient_or_older_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
@@ -768,11 +771,12 @@ 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,
.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)) {
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 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);
self->used_storage += required_sz;
@@ -1354,7 +1358,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
@@ -1450,6 +1454,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;
@@ -1513,6 +1518,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;
}
@@ -1553,6 +1559,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;
@@ -1595,6 +1612,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,
.transient = (g->usage_hints & GRAPHICS_USAGE_HINT_TRANSIENT) != 0,
};
Frame *frame;
if (is_new_frame) {
@@ -1630,12 +1648,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)) {
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");
@@ -1651,6 +1671,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->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;
@@ -1664,7 +1685,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);
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;
@@ -1867,10 +1888,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)) {
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 disk cache");
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;
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;
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,6 +46,7 @@ static inline void parse_graphics_code(PS *self, uint8_t *parser_buf,
unicode_placement = 'U',
parent_id = 'P',
parent_placement_id = 'Q',
usage_hints = 'N',
offset_from_parent_x = 'H',
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:
value_state = UINT;
break;
case usage_hints:
value_state = UINT;
break;
case offset_from_parent_x:
value_state = INT;
break;
@@ -299,6 +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(usage_hints);
default:
break;
}
@@ -359,7 +364,7 @@ static inline void parse_graphics_code(PS *self, uint8_t *parser_buf,
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 "
"sI sI sI sI si si si ss#}",
"sI sI sI sI sI si si si ss#}",
self->window_id, "graphics_command",
"action", g.action, "delete_action", g.delete_action, "transmission_type",
@@ -378,7 +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,
"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,6 +386,20 @@ class TestGraphics(BaseTest):
self.assertIsNone(li(payload='2' * 12, z=77, m=1, q=2))
self.assertIsNone(li(payload='2' * 12))
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')
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):
s, g, pl, sl = load_helpers(self)
self.assertEqual(g.disk_cache.total_size, 0)
@@ -1305,6 +1319,25 @@ class TestGraphics(BaseTest):
self.ae(g.image_count, 0)
self.assertEqual(g.disk_cache.total_size, 0)
def test_transient_image_preferential_eviction(self):
# Transient images should be evicted before non-transient ones when
# the storage quota is exceeded, regardless of insertion order.
s = self.create_screen()
g = s.grman
g.storage_limit = 36 * 2
li = make_send_command(s)
# Load a non-transient image first (older atime) and a transient image second.
self.assertEqual(li(a='T', i=1).code, 'OK')
self.assertEqual(li(a='T', i=2, N=1).code, 'OK')
self.assertEqual(g.image_count, 2)
# Adding a third image triggers the quota; the transient image (i=2) must be
# evicted first even though the non-transient image (i=1) is older.
self.assertEqual(li(a='T', i=3).code, 'OK')
self.assertEqual(g.image_count, 2)
self.assertIsNone(g.image_for_client_id(2), 'transient image should have been evicted')
self.assertIsNotNone(g.image_for_client_id(1), 'non-transient image should survive')
self.assertIsNotNone(g.image_for_client_id(3), 'newly added image should survive')
@unittest.skipIf(Image is None, 'PIL not available, skipping PNG tests')
def test_cached_rgba_conversion(self):
from kitty.render_cache import ImageRenderCacheForTesting

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

@@ -143,7 +143,7 @@ type GraphicsCommand struct {
d GRT_d
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
@@ -176,6 +176,7 @@ func (self *GraphicsCommand) serialize_non_default_fields() (ans []string) {
write_key('U', self.U, null.U)
write_key('d', self.d, null.d)
write_key('N', self.N, null.N)
write_key('s', self.s, null.s)
write_key('v', self.v, null.v)
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)
case 'd':
err = set_val(&self.d, GRT_d_from_string, value)
case 'N':
err = set_uval(&self.N, value)
case 's':
err = set_uval(&self.s, value)
case 'v':
@@ -753,6 +756,15 @@ func (self *GraphicsCommand) SetFrameToMakeCurrent(c uint64) *GraphicsCommand {
return self
}
func (self *GraphicsCommand) UsageHints() uint64 {
return self.N
}
func (self *GraphicsCommand) SetUsageHints(hints uint64) *GraphicsCommand {
self.N = hints
return self
}
func (self *GraphicsCommand) ImageId() uint32 {
return self.i
}