diff --git a/12to11-test.xml b/12to11-test.xml
new file mode 100644
index 0000000..1b0946d
--- /dev/null
+++ b/12to11-test.xml
@@ -0,0 +1,94 @@
+
+
+
+ Copyright (C) 2022 various contributors.
+
+ This file is part of 12to11.
+
+ 12to11 is free software: you can redistribute it and/or modify it
+ under the terms of the GNU General Public License as published by the
+ Free Software Foundation, either version 3 of the License, or (at your
+ option) any later version.
+
+ 12to11 is distributed in the hope that it will be useful, but WITHOUT
+ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with 12to11. If not, see https://www.gnu.org/licenses/.
+
+
+
+
+ This protocol is used by the 12to11 protocol translator to
+ support various tests. The test_manager global allows creating
+ a surface whose bounds and contents can be inspected, and
+ connecting to the X server used by the compositor.
+
+ Upon binding to the test_manager, a display_string event is sent
+ containing the name of the X display.
+
+
+
+
+
+
+
+
+ Get a test_surface object for a particular surface. If a role
+ was already attached to this surface, or a role of a different
+ type was previously attached, a role_present error is issued.
+
+ The window is created immediately after get_test_surface is
+ called. It is mapped once a commit request with a non-nil
+ buffer is made.
+
+ Once the window associated with the test_surface object is
+ mapped, a mapped event is sent.
+
+
+
+
+
+
+
+ The display_string event sends the name of the X display to
+ clients. It is sent immediately after binding to the
+ test_manager object.
+
+
+
+
+
+
+
+ This role provides a test surface. Various buffers and
+ subsurfaces can be attached, and the resulting display contents
+ validated.
+
+
+
+
+ This request destroys the test_surface role. Subsequently,
+ get_test_surface can be called again with its surface.
+
+
+
+
+
+ The map event is sent once the window is mapped and its
+ contents can be retrieved. The two arguments are the XID of
+ the window and the name of the display it is on.
+
+ If the surface is mapped, then unmapped (by having a nil
+ buffer attached) and then mapped again, without waiting for
+ the first mapped event, the delivery of subsequent mapped
+ events becomes undefined.
+
+
+
+
+
+
diff --git a/buffer_release.c b/buffer_release.c
new file mode 100644
index 0000000..16927bc
--- /dev/null
+++ b/buffer_release.c
@@ -0,0 +1,146 @@
+/* Wayland compositor running on top of an X server.
+
+Copyright (C) 2022 to various contributors.
+
+This file is part of 12to11.
+
+12to11 is free software: you can redistribute it and/or modify it
+under the terms of the GNU General Public License as published by the
+Free Software Foundation, either version 3 of the License, or (at your
+option) any later version.
+
+12to11 is distributed in the hope that it will be useful, but WITHOUT
+ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with 12to11. If not, see . */
+
+#include "compositor.h"
+
+/* Simple helper code for managing buffer release in surfaces. */
+
+typedef struct _ReleaseLaterRecord ReleaseLaterRecord;
+
+struct _ReleaseLaterRecord
+{
+ /* A monotonically (overflow aside) increasing identifier. */
+ uint64_t id;
+
+ /* The buffer that should be released upon receiving this
+ message. */
+ ExtBuffer *buffer;
+
+ /* The idle callback, if any. */
+ IdleCallbackKey key;
+
+ /* The buffer release helper. */
+ BufferReleaseHelper *helper;
+
+ /* The next and last records. */
+ ReleaseLaterRecord *next, *last;
+};
+
+struct _BufferReleaseHelper
+{
+ /* Queue of buffers pending release. */
+ ReleaseLaterRecord records;
+
+ /* Callback run upon all buffers being released. */
+ AllReleasedCallback callback;
+
+ /* Data for that callback. */
+ void *callback_data;
+};
+
+BufferReleaseHelper *
+MakeBufferReleaseHelper (AllReleasedCallback callback,
+ void *callback_data)
+{
+ BufferReleaseHelper *helper;
+
+ helper = XLCalloc (1, sizeof *helper);
+ helper->records.next = &helper->records;
+ helper->records.last = &helper->records;
+ helper->callback = callback;
+ helper->callback_data = callback_data;
+
+ return helper;
+}
+
+void
+FreeBufferReleaseHelper (BufferReleaseHelper *helper)
+{
+ ReleaseLaterRecord *next, *last;
+
+ /* Do an XSync, and then release all the records. */
+ XSync (compositor.display, False);
+
+ next = helper->records.next;
+ while (next != &helper->records)
+ {
+ last = next;
+ next = next->next;
+
+ /* Cancel the idle callback if it already exists. */
+ if (last->key)
+ RenderCancelIdleCallback (last->key);
+
+ /* Release the buffer now. */
+ XLReleaseBuffer (last->buffer);
+
+ /* Before freeing the record itself. */
+ XLFree (last);
+ }
+
+ /* Free the helper. */
+ XLFree (helper);
+}
+
+static void
+BufferIdleCallback (RenderBuffer buffer, void *data)
+{
+ ReleaseLaterRecord *record;
+ BufferReleaseHelper *helper;
+
+ record = data;
+ helper = record->helper;
+
+ /* Release the buffer. */
+ XLReleaseBuffer (record->buffer);
+
+ /* Unlink and free the record. */
+ record->next->last = record->last;
+ record->last->next = record->next;
+ XLFree (record);
+
+ /* If there are no more records in the helper, run its
+ all-released-callback. */
+ if (helper->records.next == &helper->records)
+ helper->callback (helper->callback_data);
+}
+
+void
+ReleaseBufferWithHelper (BufferReleaseHelper *helper, ExtBuffer *buffer,
+ RenderTarget target)
+{
+ ReleaseLaterRecord *record;
+ RenderBuffer render_buffer;
+
+ render_buffer = XLRenderBufferFromBuffer (buffer);
+
+ record = XLCalloc (1, sizeof *record);
+ record->next = helper->records.next;
+ record->last = &helper->records;
+ helper->records.next->last = record;
+ helper->records.next = record;
+
+ /* Now, the record is linked into the list. Record the buffer and
+ add an idle callback. */
+ record->buffer = buffer;
+ record->key = RenderAddIdleCallback (render_buffer, target,
+ BufferIdleCallback,
+ record);
+ record->helper = helper;
+}
diff --git a/test.c b/test.c
new file mode 100644
index 0000000..4a99854
--- /dev/null
+++ b/test.c
@@ -0,0 +1,534 @@
+/* Wayland compositor running on top of an X server.
+
+Copyright (C) 2022 to various contributors.
+
+This file is part of 12to11.
+
+12to11 is free software: you can redistribute it and/or modify it
+under the terms of the GNU General Public License as published by the
+Free Software Foundation, either version 3 of the License, or (at your
+option) any later version.
+
+12to11 is distributed in the hope that it will be useful, but WITHOUT
+ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with 12to11. If not, see . */
+
+#include
+
+#include "compositor.h"
+#include "12to11-test.h"
+
+#define TestSurfaceFromRole(role) ((TestSurface *) (role))
+
+#define DefaultEventMask \
+ (ExposureMask | StructureNotifyMask | PropertyChangeMask)
+
+enum
+ {
+ IsSurfaceMapped = 1,
+ PendingBufferRelease = 1 << 1,
+ PendingFrameCallback = 1 << 2,
+ };
+
+typedef struct _TestSurface TestSurface;
+
+struct _TestSurface
+{
+ /* The associated role. */
+ Role role;
+
+ /* The associated subcompositor. */
+ Subcompositor *subcompositor;
+
+ /* The associated buffer release helper. */
+ BufferReleaseHelper *release_helper;
+
+ /* The associated window. */
+ Window window;
+
+ /* The associated rendering target. */
+ RenderTarget target;
+
+ /* The number of references to this test surface, and flags. */
+ int refcount, flags;
+
+ /* The last known width and height. */
+ int bounds_width, bounds_height;
+};
+
+/* The test surface manager global. */
+static struct wl_global *test_manager_global;
+
+/* Hash table of all surfaces. */
+static XLAssocTable *surfaces;
+
+static void
+DestroyBacking (TestSurface *test)
+{
+ if (--test->refcount)
+ return;
+
+ /* Release all allocated resources. */
+ RenderDestroyRenderTarget (test->target);
+ XDestroyWindow (compositor.display, test->window);
+
+ /* And the buffer release helper. */
+ FreeBufferReleaseHelper (test->release_helper);
+
+ /* Delete the association. */
+ XLDeleteAssoc (surfaces, test->window);
+
+ /* Free the subcompositor. */
+ SubcompositorFree (test->subcompositor);
+
+ /* And since there are no C level references to the role anymore, it
+ can be freed. */
+ XLFree (test);
+}
+
+static void
+RunFrameCallbacks (TestSurface *test)
+{
+ struct timespec time;
+
+ clock_gettime (CLOCK_MONOTONIC, &time);
+ XLSurfaceRunFrameCallbacks (test->role.surface, time);
+
+ test->flags &= ~PendingFrameCallback;
+}
+
+static void
+RunFrameCallbacksConditionally (TestSurface *test)
+{
+ if (!test->role.surface)
+ return;
+
+ if (test->flags & PendingBufferRelease)
+ /* Wait for all buffers to be released first. */
+ test->flags |= PendingFrameCallback;
+ else
+ RunFrameCallbacks (test);
+}
+
+static void
+AllBuffersReleased (void *data)
+{
+ TestSurface *test;
+
+ test = data;
+
+ if (!test->role.surface)
+ return;
+
+ test->flags &= ~PendingBufferRelease;
+
+ /* Run pending frame callbacks. */
+ if (test->flags & PendingFrameCallback)
+ RunFrameCallbacks (test);
+}
+
+static void
+NoteBounds (void *data, int min_x, int min_y, int max_x, int max_y)
+{
+ TestSurface *test;
+ int bounds_width, bounds_height;
+
+ test = data;
+
+ /* Avoid resizing the window should its actual size not have
+ changed. */
+
+ bounds_width = max_x - min_x + 1;
+ bounds_height = max_y - min_y + 1;
+
+ if (test->bounds_width != bounds_width
+ || test->bounds_height != bounds_height)
+ {
+ /* Resize the window to fit. */
+ XResizeWindow (compositor.display, test->window,
+ bounds_width, bounds_height);
+ test->bounds_width = bounds_width;
+ test->bounds_height = bounds_height;
+ }
+}
+
+static void
+NoteFrame (FrameMode mode, uint64_t id, void *data)
+{
+ if (mode != ModeComplete && mode != ModePresented)
+ return;
+
+ /* Run the frame callbacks. With the test surface, this also serves
+ to mean that painting has completed. */
+ RunFrameCallbacksConditionally (data);
+}
+
+static void
+MapTestSurface (TestSurface *test)
+{
+ /* Set the bounds width and height. */
+ test->bounds_width = SubcompositorWidth (test->subcompositor);
+ test->bounds_height = SubcompositorHeight (test->subcompositor);
+
+ /* First, resize the window to the current bounds. */
+ XResizeWindow (compositor.display, test->window,
+ test->bounds_width, test->bounds_height);
+
+ /* Next, map the window and raise it. Wait for a subsequent
+ MapNotify before sending the map event. */
+ XMapRaised (compositor.display, test->window);
+
+ /* And say that the window is now mapped. */
+ test->flags |= IsSurfaceMapped;
+}
+
+static void
+UnmapTestSurface (TestSurface *test)
+{
+ if (test->flags & IsSurfaceMapped)
+ /* Unmap the surface. */
+ XUnmapWindow (compositor.display, test->window);
+}
+
+static void
+Commit (Surface *surface, Role *role)
+{
+ TestSurface *test;
+
+ test = TestSurfaceFromRole (role);
+
+ if (surface->current_state.buffer
+ && !(test->flags & IsSurfaceMapped))
+ /* Map the surface now. */
+ MapTestSurface (test);
+ else if (!surface->current_state.buffer)
+ /* Unmap the surface now. */
+ UnmapTestSurface (test);
+
+ /* Finally, do a subcompositor update if the surface is now
+ mapped. */
+ if (test->flags & IsSurfaceMapped)
+ SubcompositorUpdate (test->subcompositor);
+}
+
+static Bool
+Setup (Surface *surface, Role *role)
+{
+ TestSurface *test;
+
+ test = TestSurfaceFromRole (role);
+
+ /* Set role->surface here, since this is where the refcounting is
+ done as well. */
+ role->surface = surface;
+
+ /* Prevent the surface from ever holding another kind of role. */
+ surface->role_type = TestSurfaceType;
+
+ /* Attach the views to the subcompositor. */
+ ViewSetSubcompositor (surface->view, test->subcompositor);
+ ViewSetSubcompositor (surface->under, test->subcompositor);
+
+ /* Make sure the under view ends up beneath surface->view. */
+ SubcompositorInsert (test->subcompositor, surface->under);
+ SubcompositorInsert (test->subcompositor, surface->view);
+
+ /* Retain the backing data. */
+ test->refcount++;
+ return True;
+}
+
+static void
+Teardown (Surface *surface, Role *role)
+{
+ TestSurface *test;
+
+ /* Clear role->surface here, since this is where the refcounting is
+ done as well. */
+ role->surface = NULL;
+
+ test = TestSurfaceFromRole (role);
+
+ /* Unparent the surface's views as well. */
+ ViewUnparent (surface->view);
+ ViewUnparent (surface->under);
+
+ /* Detach the surface's views from the subcompositor. */
+ ViewSetSubcompositor (surface->view, NULL);
+ ViewSetSubcompositor (surface->under, NULL);
+
+ /* Release the backing data. */
+ DestroyBacking (test);
+}
+
+static void
+Destroy (struct wl_client *client, struct wl_resource *resource)
+{
+ TestSurface *test;
+
+ test = wl_resource_get_user_data (resource);
+
+ /* Now detach the role from its surface, which can be reused in the
+ future. */
+ if (test->role.surface)
+ XLSurfaceReleaseRole (test->role.surface, &test->role);
+
+ /* And destroy the resource. */
+ wl_resource_destroy (resource);
+}
+
+static void
+ReleaseBuffer (Surface *surface, Role *role, ExtBuffer *buffer)
+{
+ TestSurface *test;
+ RenderBuffer render_buffer;
+
+ test = TestSurfaceFromRole (role);
+ render_buffer = XLRenderBufferFromBuffer (buffer);
+
+ if (RenderIsBufferIdle (render_buffer, test->target))
+ /* Release the buffer now -- it is already idle. */
+ XLReleaseBuffer (buffer);
+ else
+ {
+ /* Release the buffer once it becomes idle, or is destroyed. */
+ ReleaseBufferWithHelper (test->release_helper, buffer,
+ test->target);
+
+ /* Mark the surface as pending buffer release, so frame
+ callbacks can be deferred until all buffers are released. */
+ test->flags |= PendingBufferRelease;
+ }
+}
+
+static void
+SubsurfaceUpdate (Surface *surface, Role *role)
+{
+ TestSurface *test;
+
+ test = TestSurfaceFromRole (role);
+ SubcompositorUpdate (test->subcompositor);
+}
+
+static Window
+GetWindow (Surface *surface, Role *role)
+{
+ TestSurface *test;
+
+ test = TestSurfaceFromRole (role);
+ return test->window;
+}
+
+static const struct test_surface_interface test_surface_impl =
+ {
+ .destroy = Destroy,
+ };
+
+static void
+HandleResourceDestroy (struct wl_resource *resource)
+{
+ TestSurface *test;
+
+ test = wl_resource_get_user_data (resource);
+ test->role.resource = NULL;
+
+ /* Dereference the backing data. */
+ DestroyBacking (test);
+}
+
+static void
+GetTestSurface (struct wl_client *client, struct wl_resource *resource,
+ uint32_t id, struct wl_resource *surface_resource)
+{
+ Surface *surface;
+ TestSurface *test;
+ XSetWindowAttributes attrs;
+ unsigned long flags;
+
+ surface = wl_resource_get_user_data (surface_resource);
+
+ if (surface->role_type != AnythingType
+ && surface->role_type != TestSurfaceType)
+ {
+ /* The client is trying to create a test surface for a surface
+ that has or had some other role. */
+ wl_resource_post_error (resource, TEST_MANAGER_ERROR_ROLE_PRESENT,
+ "a role is/was already present on the given surface");
+ return;
+ }
+
+ test = XLSafeMalloc (sizeof *test);
+
+ if (!test)
+ {
+ wl_resource_post_no_memory (resource);
+ return;
+ }
+
+ memset (test, 0, sizeof *test);
+
+ /* Now create the associated resource. */
+ test->role.resource
+ = wl_resource_create (client, &test_surface_interface,
+ wl_resource_get_version (resource),
+ id);
+ if (!test->role.resource)
+ {
+ wl_resource_post_no_memory (resource);
+ XLFree (test);
+
+ return;
+ }
+
+ /* Create the window. */
+ attrs.colormap = compositor.colormap;
+ attrs.border_pixel = border_pixel;
+ attrs.event_mask = DefaultEventMask;
+ attrs.cursor = InitDefaultCursor ();
+ flags = CWColormap | CWBorderPixel | CWEventMask | CWCursor;
+
+ test->window = XCreateWindow (compositor.display,
+ DefaultRootWindow (compositor.display),
+ 0, 0, 20, 20, 0, compositor.n_planes,
+ InputOutput, compositor.visual, flags,
+ &attrs);
+
+ /* And the subcompositor and rendering target. */
+ test->subcompositor = MakeSubcompositor ();
+ test->target = RenderTargetFromWindow (test->window, DefaultEventMask);
+
+ /* And a buffer release helper. */
+ test->release_helper = MakeBufferReleaseHelper (AllBuffersReleased,
+ test);
+
+ /* Set the subcompositor target. */
+ SubcompositorSetTarget (test->subcompositor, &test->target);
+
+ /* Set some callbacks. The note frame callback is not useful as
+ test surfaces have no frame clock. */
+ SubcompositorSetBoundsCallback (test->subcompositor, NoteBounds, test);
+ SubcompositorSetNoteFrameCallback (test->subcompositor, NoteFrame,
+ test);
+
+ /* Create the hash table used to look up test surfaces if
+ necessary. */
+
+ if (!surfaces)
+ surfaces = XLCreateAssocTable (16);
+
+ /* Associate the window with the role. */
+ XLMakeAssoc (surfaces, test->window, test);
+
+ /* Set the role implementation. */
+ test->role.funcs.commit = Commit;
+ test->role.funcs.teardown = Teardown;
+ test->role.funcs.setup = Setup;
+ test->role.funcs.release_buffer = ReleaseBuffer;
+ test->role.funcs.subsurface_update = SubsurfaceUpdate;
+ test->role.funcs.get_window = GetWindow;
+
+ /* Add the resource implementation. */
+ wl_resource_set_implementation (test->role.resource, &test_surface_impl,
+ test, HandleResourceDestroy);
+ test->refcount++;
+
+ /* Attach the role. */
+ if (!XLSurfaceAttachRole (surface, &test->role))
+ abort ();
+}
+
+static const struct test_manager_interface test_manager_impl =
+ {
+ .get_test_surface = GetTestSurface,
+ };
+
+
+
+static void
+HandleBind (struct wl_client *client, void *data, uint32_t version,
+ uint32_t id)
+{
+ struct wl_resource *resource;
+ char *name;
+
+ resource = wl_resource_create (client, &test_manager_interface,
+ version, id);
+
+ if (!resource)
+ {
+ wl_client_post_no_memory (client);
+ return;
+ }
+
+ wl_resource_set_implementation (resource, &test_manager_impl,
+ NULL, NULL);
+
+ /* Send the display name to the client. */
+ name = DisplayString (compositor.display);
+ test_manager_send_display_string (resource, name);
+}
+
+void
+XLInitTest (void)
+{
+ test_manager_global
+ = wl_global_create (compositor.wl_display, &test_manager_interface,
+ 1, NULL, HandleBind);
+}
+
+static Bool
+DispatchMapNotify (XEvent *event)
+{
+ TestSurface *test;
+
+ /* Try to look up the surface. */
+ test = XLLookUpAssoc (surfaces, event->xmap.window);
+
+ if (!test)
+ return False;
+
+ /* The surface is now mapped. Dispatch the mapped event. */
+ if (test->flags & IsSurfaceMapped && test->role.resource)
+ test_surface_send_mapped (test->role.resource, test->window,
+ DisplayString (compositor.display));
+ return True;
+}
+
+static Bool
+DispatchExpose (XEvent *event)
+{
+ TestSurface *test;
+
+ /* Try to look up the surface. */
+ test = XLLookUpAssoc (surfaces, event->xexpose.window);
+
+ if (!test)
+ return False;
+
+ /* Expose the subcompositor. */
+ SubcompositorExpose (test->subcompositor, event);
+
+ return True;
+}
+
+Bool
+XLHandleOneXEventForTest (XEvent *event)
+{
+ if (!surfaces)
+ return False;
+
+ switch (event->type)
+ {
+ case MapNotify:
+ return DispatchMapNotify (event);
+
+ case Expose:
+ return DispatchExpose (event);
+ }
+
+ return False;
+}