Compare commits

...

2 Commits

Author SHA1 Message Date
Benjamin Otte
cd9d7b7da7 inspector: Add a button to save a recording as a node video
Take all the recorded nodes and put them into a container node, then
save that container node.

This "video" can then be played back with commands like
  gtk-rendernode-tool show --video
or similar.

Note that this actually achieves a pretty decent compression because
of the deduplication when saving rendernodes and the fact that
unchanging parts of the widget tree will reuse their nodes.
2024-10-07 22:14:23 +02:00
Benjamin Otte
cc333ce5cf tools: Add --video to rendernode-tool show
If given, the show cmmand treats the passed in container node as a
sequence of nodes and advances the currently shown frame in the tick
function.

This can be used for replaying animations or for benchmarking them.
2024-10-07 22:14:23 +02:00
3 changed files with 163 additions and 11 deletions

View File

@@ -1992,6 +1992,100 @@ render_node_list_selection_changed (GtkListBox *list,
g_object_unref (paintable);
}
static GskRenderNode *
gtk_inspector_recorder_get_node_video (GtkInspectorRecorder *self)
{
GskRenderNode *result;
GPtrArray *array;
gsize i;
array = g_ptr_array_new ();
for (i = 0; i < g_list_model_get_n_items (self->recordings); i++)
{
GtkInspectorRecording *recording = g_list_model_get_item (self->recordings, i);
if (GTK_INSPECTOR_IS_RENDER_RECORDING (recording))
g_ptr_array_add (array, gtk_inspector_render_recording_get_node (GTK_INSPECTOR_RENDER_RECORDING (recording)));
g_object_unref (recording);
}
if (array->len > 0)
result = gsk_container_node_new ((GskRenderNode **) array->pdata, array->len);
else
result = NULL;
g_ptr_array_free (array, TRUE);
return result;
}
static void
recording_save_video_response (GObject *source,
GAsyncResult *result,
gpointer node)
{
GtkFileDialog *dialog = GTK_FILE_DIALOG (source);
GFile *file;
GError *error = NULL;
file = gtk_file_dialog_save_finish (dialog, result, &error);
if (file)
{
GBytes *bytes = gsk_render_node_serialize (node);
if (!g_file_replace_contents (file,
g_bytes_get_data (bytes, NULL),
g_bytes_get_size (bytes),
NULL,
FALSE,
0,
NULL,
NULL,
&error))
{
GtkAlertDialog *alert;
alert = gtk_alert_dialog_new (_("Saving RenderNode failed"));
gtk_alert_dialog_set_detail (alert, error->message);
gtk_alert_dialog_show (alert, GTK_WINDOW (gtk_window_get_transient_for (GTK_WINDOW (dialog))));
g_object_unref (alert);
g_error_free (error);
}
g_bytes_unref (bytes);
g_object_unref (file);
}
else
{
g_print ("Error saving nodes: %s\n", error->message);
g_error_free (error);
}
gsk_render_node_unref (node);
}
static void
recording_save_video (GtkButton *button,
GtkInspectorRecorder *self)
{
GtkFileDialog *dialog;
GskRenderNode *video;
video = gtk_inspector_recorder_get_node_video (self);
if (video == NULL)
return;
dialog = gtk_file_dialog_new ();
gtk_file_dialog_set_initial_name (dialog, "recording.vnode");
gtk_file_dialog_save (dialog,
GTK_WINDOW (gtk_widget_get_root (GTK_WIDGET (self))),
NULL,
recording_save_video_response, video);
g_object_unref (dialog);
}
static void
render_node_save_response (GObject *source,
GAsyncResult *result,
@@ -2366,6 +2460,7 @@ gtk_inspector_recorder_class_init (GtkInspectorRecorderClass *klass)
gtk_widget_class_bind_template_callback (widget_class, recordings_clear_all);
gtk_widget_class_bind_template_callback (widget_class, recording_selected);
gtk_widget_class_bind_template_callback (widget_class, recording_save_video);
gtk_widget_class_bind_template_callback (widget_class, render_node_save);
gtk_widget_class_bind_template_callback (widget_class, render_node_clip);
//gtk_widget_class_bind_template_callback (widget_class, node_property_activated);

View File

@@ -41,6 +41,13 @@
<property name="icon-name">function-linear-symbolic</property>
<property name="tooltip-text" translatable="yes">Highlight event sequences</property>
<property name="active" bind-source="GtkInspectorRecorder" bind-property="highlight-sequences" bind-flags="bidirectional|sync-create"/>
</object>
</child>
<child>
<object class="GtkButton">
<property name="icon-name">document-save-as-symbolic</property>
<property name="tooltip-text" translatable="yes">Save nodes as video</property>
<signal name="clicked" handler="recording_save_video"/>
<property name="halign">start</property>
<property name="hexpand">1</property>
</object>

View File

@@ -52,29 +52,78 @@ quit_cb (GtkWidget *widget,
g_main_context_wakeup (NULL);
}
static void
show_file (const char *filename,
gboolean decorated)
typedef struct
{
GskRenderNode *node;
gsize next_tick;
} TickData;
static int
next_frame (GtkWidget *picture,
GdkFrameClock *frame_clock,
gpointer user_data)
{
TickData *tick = user_data;
GtkSnapshot *snapshot;
GskRenderNode *current_node;
graphene_rect_t node_bounds;
GdkPaintable *paintable;
gsk_render_node_get_bounds (tick->node, &node_bounds);
current_node = gsk_container_node_get_child (tick->node, tick->next_tick);
snapshot = gtk_snapshot_new ();
gtk_snapshot_translate (snapshot, &GRAPHENE_POINT_INIT (- node_bounds.origin.x, - node_bounds.origin.y));
gtk_snapshot_append_node (snapshot, current_node);
paintable = gtk_snapshot_free_to_paintable (snapshot, NULL);
gtk_picture_set_paintable (GTK_PICTURE (picture), paintable);
g_object_unref (paintable);
tick->next_tick++;
tick->next_tick %= gsk_container_node_get_n_children (tick->node);
return G_SOURCE_CONTINUE;
}
static void
show_file (const char *filename,
gboolean video,
gboolean decorated)
{
TickData tick_data;
GskRenderNode *node;
graphene_rect_t node_bounds;
GtkWidget *sw;
GtkWidget *handle;
GtkWidget *window;
gboolean done = FALSE;
GtkSnapshot *snapshot;
GtkWidget *picture;
node = load_node_file (filename);
gsk_render_node_get_bounds (node, &node_bounds);
snapshot = gtk_snapshot_new ();
gtk_snapshot_translate (snapshot, &GRAPHENE_POINT_INIT (- node_bounds.origin.x, - node_bounds.origin.y));
gtk_snapshot_append_node (snapshot, node);
paintable = gtk_snapshot_free_to_paintable (snapshot, NULL);
if (video && gsk_render_node_get_node_type (node) == GSK_CONTAINER_NODE)
{
tick_data.node = node;
tick_data.next_tick = 0;
picture = gtk_picture_new ();
gtk_widget_add_tick_callback (picture, next_frame, &tick_data, NULL);
}
else
{
GdkPaintable *paintable;
GtkSnapshot *snapshot;
snapshot = gtk_snapshot_new ();
gtk_snapshot_translate (snapshot, &GRAPHENE_POINT_INIT (- node_bounds.origin.x, - node_bounds.origin.y));
gtk_snapshot_append_node (snapshot, node);
paintable = gtk_snapshot_free_to_paintable (snapshot, NULL);
picture = gtk_picture_new_for_paintable (paintable);
g_object_unref (paintable);
}
picture = gtk_picture_new_for_paintable (paintable);
gtk_picture_set_can_shrink (GTK_PICTURE (picture), FALSE);
gtk_picture_set_content_fit (GTK_PICTURE (picture), GTK_CONTENT_FIT_SCALE_DOWN);
@@ -100,7 +149,6 @@ show_file (const char *filename,
while (!done)
g_main_context_iteration (NULL, TRUE);
g_clear_object (&paintable);
g_clear_pointer (&node, gsk_render_node_unref);
}
@@ -111,8 +159,10 @@ do_show (int *argc,
GOptionContext *context;
char **filenames = NULL;
gboolean decorated = TRUE;
gboolean video = FALSE;
const GOptionEntry entries[] = {
{ "undecorated", 0, G_OPTION_FLAG_REVERSE, G_OPTION_ARG_NONE, &decorated, N_("Don't add a titlebar"), NULL },
{ "video", 0, 0, G_OPTION_ARG_NONE, &video, N_("Treat file as video"), NULL },
{ G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &filenames, NULL, N_("FILE") },
{ NULL, }
};
@@ -151,7 +201,7 @@ do_show (int *argc,
exit (1);
}
show_file (filenames[0], decorated);
show_file (filenames[0], video, decorated);
g_strfreev (filenames);
}