diff --git a/tooling/probes/step20c-corec-v2-followup/README.md b/tooling/probes/step20c-corec-v2-followup/README.md new file mode 100644 index 0000000..ce10f1f --- /dev/null +++ b/tooling/probes/step20c-corec-v2-followup/README.md @@ -0,0 +1,59 @@ +# CORE-C V2 native closure follow-up — experimental source-only kit + +This is the new, separately budgeted +`SWAYFX_MOSAIC_STEP20C_COREC_V2_FULLSCREEN_OOM_GLES_NATIVE_CLOSURE_BATCH_V1` +ticket. Its parent is project commit +`d46f854b3026e911659528fe94792aa13837a1d6`. The +[parent kit](../step20c-corec-v2/README.md) and its closed runtime receipts +remain unchanged. This directory does not add a production dependency. + +The follow-up derives prospective fullscreen, migration/refocus and root/nested +OOM fixtures in `graphics/`, and a distinct intrusive GLES diagnostic in `gpu/`. +Each implementation has its own offline tests. Independent acceptance of the +final source bytes and verified publication to both project remotes are +required before a runtime reservation. A source test or a successful build is +not native runtime evidence. No campaign is admitted by this README. + +The G2 Wayland client sets `RLIMIT_CORE` to zero, sets +`PR_SET_DUMPABLE=0`, and verifies `PR_GET_DUMPABLE=0` before any Wayland +operation. This protects the client itself; the compositor's `exec` does not +preserve a parent's `prctl` dumpable setting. The parent also applies +`RLIMIT_CORE=0` to owned children, while compositor dumpability after `exec` +remains an explicit limitation. + +Generated sources, clients, binaries, builds, receipts, logs, raw readbacks and +captures belong in a new mode-0700 LAB below `/var/tmp`. Redirect `TMPDIR`, +`TMP`, `TEMP`, `XDG_CACHE_HOME`, `XDG_DATA_HOME`, `PYTHONPYCACHEPREFIX` and +`CCACHE_DIR` there for every build/test. Runtime sockets must be private, +short, checked for quota, and explicitly selected. Never use an inherited +desktop socket or display. Keep heavy artifacts outside HOME and Git. + +The eight named reservations have fixed, nonfungible ceilings: + +| Run | Output captures maximum | Dependency | +| --- | ---: | --- | +| FS-P | 12 | Reviewed comparable pristine fullscreen fixture | +| FS-C | 16 | Same reviewed client and actual FS-P comparison | +| DY-C | 16 | Reviewed native-source refocus oracle | +| OM-ROOT | 10 | Reviewed root-only closure and availability oracle | +| OM-NESTED | 10 | Safe root result, or explicit independent dependency review | +| GPU-G3 | 12 | Reviewed capability/readback source and distinct built ELF | +| OM-EXTRA | 12 | New distinct discriminant and additional review | +| GPU-CTRL | 12 | New attribution question and additional review | + +Maximum eight runs, 150 seconds each, 18 captures per run, and 110 output +captures globally; the named ceilings sum to 100. A reservation is consumed +even on failure. No retry, reset, refund, historical token or scenario renaming +is permitted. Optional entries are not admitted by default. The previous +ticket's unused reservations are closed and cannot be transferred. + +CORE and OOM production mechanisms remain unchanged. Runtime Row 0 borrows +Sway's real tiling list. Pending IPC geometry, applied scene/input evidence, +native commits, presentation and buffer lifetime remain distinct. Stable +screenshots cannot establish atomic generation, complete damage or GPU +completion. OOM production policy and offscreen-popup UX remain undecided. + +STEP20A DONE; STEP20B EXPERIMENTAL; STEP20C NEXT; STEP20D–G PLANNED. +PRODUCTION_MULTI_OUTPUT=NOT_YET_SUPPORTED; PRODUCTION_AUTHORIZED=FALSE. +No installation, live session operation, production source/patch, public ABI, +package recipe or UX change is authorized by this kit. diff --git a/tooling/probes/step20c-corec-v2-followup/gpu/README.md b/tooling/probes/step20c-corec-v2-followup/gpu/README.md new file mode 100644 index 0000000..30871cd --- /dev/null +++ b/tooling/probes/step20c-corec-v2-followup/gpu/README.md @@ -0,0 +1,132 @@ +# GPU-G3 — private GLES admission follow-up + +EXPERIMENTAL_SOURCE_ONLY. This kit derives a new diagnostic from the published +[G2 sources](../../step20c-corec-v2/gpu/README.md). It carries no G2 binary or +runtime qualification. B4 is INCONCLUSIVE until an independent reviewer accepts +the exact final bytes and receipts. Compositor build, staging and native GPU +run are NOT_RUN. STEP20C remains NEXT; production multi-output is +NOT_YET_SUPPORTED and PRODUCTION_AUTHORIZED is FALSE. + +## Normative basis and bounded contract + +[EGL 1.5](https://registry.khronos.org/EGL/specs/eglspec.1.5.pdf), +§3.7.1.1, printed pp52–53 (PDF pages58–59), permits a later backwards compatible +context. Its footnote18 identifies CLIENT_VERSION as the major-version alias. +However §3.7.4, printed p63 (PDF page69), describes the context query as the +supported client version. Thus EGL2/GL3.2 remains an observed divergence; this +kit does **not** relabel the queried EGL value as merely a creation request or +assert universal conformance. The independent reviewer must assess the limited +diagnostic admission separately from that divergence. + +[OpenGL ES 3.2, May 5 2022](https://registry.khronos.org/OpenGL/specs/es/3.2/es_spec_3.2.pdf), +§20.2, printed pp441–442 (PDF pages458–459), specifies the VERSION string and +matching MAJOR_VERSION/MINOR_VERSION queries. The code first validates current +EGL context/display and ES API, then bounded GL_VERSION attesting ES3.0–3.2, +then these ES3 queries and GLSL string without errors. ES2, ES4, unqualified +ES3 minors, forged framing and contradictory integer results are refused. +Installed headers define EGL_CONTEXT_CLIENT_VERSION and MAJOR_VERSION as +0x3098; GLES3 MAJOR_VERSION/MINOR_VERSION are 0x821b/0x821c. Their hashes are +recorded in the offline receipt; headers alone prove no native capability. + +`g2_gl.h` keeps the exact `g2_read` symbol used by the unchanged G2 hooks. +`g2_native.h` adds observable lifecycle flags and G3 metadata. Queries consume +the GL error queue in separate pre/version/capability/state/attachment/read/ +restore windows. Error consumption cannot be restored. State is null when +unobserved; attempted restoration requires a clean error window and exact +state/context/display/API equality. Bindings restored are READ/DRAW FBO, RBO, +PBO and PACK alignment/row/skip. READ_BUFFER is observed on the target and is +never modified; returning the original FBO restores its original selection. +Other queried state is unchanged. Only complete, single-sample native RBO +color attachments with supported byte channels/dimensions are read. Texture +attachment dimensions remain NOT_PROVEN and are refused. Black RGBA with +alpha zero is valid. Functional blur shaders, sampler and rendering policy +are unchanged. GLSL string is evidence about the implementation; the linked +program's mediump/ESSL100 requirements remain tied to frozen shader sources, +actual program/type/location, precision capability and uniform bits. + +DMA-BUF FDs are borrowed and fstat identities are recorded without closing, +seeking or decoding. AMD modifier020000001046bb04 accepts image stride3584 +offset0 and auxiliary stride1024 offset2293760. Only the image plane requires +width×4; auxiliary allocation bounds/decoding remain NOT_PROVEN. Unknown +modifiers are refused. `halfpixel` records native bits and capability; there +is no fitted tolerance or mandatory CPU-float32 equality for mediump. + +## Source reconstruction and offline commands + +Start with the [CORE assembler](../../step20c-corec-v2/README.md) from a fresh +Git extraction and its verified 85-file SceneFX tree. Use a NEW LAB created by +mkdtemp under `/var/tmp/swayfx-mosaic-step20c-native-closure-*`, UID owner, +mode0700. All generated files and every temp/cache directory stay there. +Run commands with an allowlisted environment, for example: + +```sh +env -i PATH=/usr/bin:/bin LANG=C.UTF-8 \ + TMPDIR="$LAB/scratch" TMP="$LAB/scratch" TEMP="$LAB/scratch" \ + XDG_CACHE_HOME="$LAB/cache" XDG_DATA_HOME="$LAB/data" \ + PYTHONPYCACHEPREFIX="$LAB/pycache" CCACHE_DIR="$LAB/ccache" \ + python "$KIT/reproduce.py" --lab "$LAB" --core "$CORE/scene" \ + --destination "$LAB/replay-g3-scene" +``` + +Create the listed environment directories first, mode0700. `$KIT` is this +directory, `$CORE` the verified assembler destination. `reproduce.py` verifies +every CORE byte and mode against the versioned provenance. It applies the +relative parent kit's `gpu/g2-native.patch` with fixed SHA256 +77d7445ceaf643fd5cc708d31c3f2c77feaeb80f8130c419c55e5514ecc8a75d, then applies the G3 delta and verifies both diagnostic headers against +the supplied full new headers. The hook source SHA remains +c2886ef259fc91431dfe97841387082420ab80a0fe2f07e7dcdb71fc95731181. +`g3-admission.patch` is the review delta between the parent and new headers; +full new headers are the assembly authority and are explicitly frozen. + +With the same allowlisted environment, run: + +```sh +python "$KIT/test-targeted.py" --lab "$LAB" --scene "$LAB/replay-g3-scene" +python "$KIT/freeze.py" --lab "$LAB" --output "$LAB/g3-source-freeze-v2.json" +``` + +This performs C17 FakeGL execution of the exact header, controlled wrong +native variants, exact arm-consumer C extraction, actual native helper C, +Python parser/scenario negative tests, source syntax checking, Wayland header +generation and client syntax compilation. The client is not executed. It +does not require compile_commands from any historical LAB. C processes set +RLIMIT_CORE=0 and PR_SET_DUMPABLE=0. Generated tests, logs and receipts remain +under `LAB/g3-offline`; the relative source manifest is +`LAB/g3-source-freeze.json`. The freezer never grants its own review. + +## Prospective native wiring — requires parent admission + +Define `G3_ROOT` at compile time as the reviewed new LAB's exact +`gpu-work/acquisition` path. There is deliberately no default absolute LAB. +That definition is part of the new binary's reviewed identity. The C hook +requires MOSAIC_G3_DIR to equal both the macro and realpath, mode0700. Python +ROOT comes from MOSAIC_G3_DIR supplied **before import** and verifies the new +LAB prefix. No runtime directory is created by this kit. + +The parent must bind `scenario.run(session,binding)` to a **GPU-G3** Session +whose sockets, binary/maps, watchdog, capture limits and durable ledger have +been independently reviewed. `binding` requires `G3_ledger_reserved=True`, +gate_file, binary, client, client_sha256, nonce and source_manifest_sha256. +The Session binary category is `gpu` and its actual executable must be the +new reviewed G3 binary. Environment variables MOSAIC_G3_DIR/NONCE belong in +the Session's explicit compositor allowlist. The caller owns finally/cleanup. +The parent must provide a gate mode0600 containing source_review=ACCEPTED, +gl_observability=PASS, runtime_authorized=true, diagnostic=GPU-G3, +acquisition_root, and exact binary_sha256. This kit never produces that gate. + +Epochs A/B/A-return are single use, strictly ordered, capped at three. All +ten physical readbacks per epoch are full800×600 RGBA (the prospective ROI is +the complete field, without a post-hoc mask), total57,600,000 bytes, cap256MiB. +There are two H2 baselines then three H1/H2 pairs: eight output captures. +Every arm/raw/metadata publication is exclusive, nofollow and bounded. +Rejected/incomplete A forbids B and return. Input commands are G3-A/G3-B/ +G3-A-return; client content is the unchanged alternating 8-pixel RGB pattern. +Whole H1/H2 comparisons report zero-tolerance differences and row counts. +Synchronous reads may perturb timing; GPU completion, presented-buffer +identity, object lifetime, complete damage, atomic generation and whole-field +causality remain NOT_PROVEN. A future build/run requires B4 and C gates; +offline success does not consume or authorize a GPU-G3 run. The freezer's +default output name is `g3-source-freeze.json`; it creates a new mode-0600 file +exclusively, refuses any existing destination, and syncs the file and LAB +directory. Use a new explicit direct-child name for every later freeze; a prior +freeze is immutable evidence. diff --git a/tooling/probes/step20c-corec-v2-followup/gpu/clients-g2.c b/tooling/probes/step20c-corec-v2-followup/gpu/clients-g2.c new file mode 100644 index 0000000..6887c60 --- /dev/null +++ b/tooling/probes/step20c-corec-v2-followup/gpu/clients-g2.c @@ -0,0 +1,358 @@ +#define _GNU_SOURCE +#define _POSIX_C_SOURCE 200809L +#include "layer-shell-client.h" +#include "xdg-shell-client.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* WHY: one persistent background supplies real A/B commits below blur_tree. + * CONTRACT: fixed opaque background, premultiplied ARGB glass and static H2. + * INVARIANT: no animation, synthetic configure, popup or held transaction. */ +static struct wl_display *display; +static struct wl_compositor *compositor; +static struct wl_shm *shm; +static struct xdg_wm_base *wm; +static struct zwlr_layer_shell_v1 *shell; +static struct wl_output *h1; +static struct wl_surface *surface; +static struct xdg_surface *xdg; +static struct xdg_toplevel *top; +static struct zwlr_layer_surface_v1 *layer; +static const char *mode; +static int width = 420, height = 320; +static unsigned commits, live_buffers, released_buffers; +static char pattern = 'A'; +/* G3 new explicit source command protocol; first A bytes retained verbatim. */ +static unsigned char *source_a; +static size_t source_a_size; +static const char *source_command; +static uint32_t source_crc(const unsigned char *p,size_t n) { + uint32_t crc=~0u; + for(size_t i=0;i>1)^((0u-(crc&1u))&0xedb88320u);} + return ~crc; +} +static volatile sig_atomic_t running = 1; +struct pixels { + struct wl_buffer *buffer; + void *map; + size_t size; + struct pixels *next; +}; +static struct pixels *buffers; +static long long stamp(void) { + struct timespec t; + clock_gettime(CLOCK_MONOTONIC, &t); + return (long long)t.tv_sec * 1000000000LL + t.tv_nsec; +} +static void free_pixels(struct pixels *p) { + struct pixels **it = &buffers; + while (*it && *it != p) + it = &(*it)->next; + if (*it) + *it = p->next; + wl_buffer_destroy(p->buffer); + munmap(p->map, p->size); + free(p); + --live_buffers; +} +static void release(void *data, struct wl_buffer *b) { + ++released_buffers; + fprintf(stderr, "RELEASE ns=%lld buffer=%u\n", stamp(), + wl_proxy_get_id((struct wl_proxy *)b)); + free_pixels(data); +} +static const struct wl_buffer_listener buffer_listener = {.release = release}; +static void draw(void) { + /* CONTRACT: exactly one runner command per reserved A/B image. + * INVARIANT: configures cannot draw, and the sequence is A/B/A only. */ + if (!strcmp(mode, "background") && + (width != 800 || height != 600 || commits >= 3 || + pattern != (commits == 1 ? 'B' : 'A'))) + exit(2); + if (width <= 0 || height <= 0 || width > 4096 || height > 4096 || + live_buffers >= 8) + exit(2); + size_t size = (size_t)width * (size_t)height * 4; + /* CONTRACT: each mapping stays immutable until native wl_buffer.release. + * Bounds keep pool/stride integers valid, with at most eight outstanding. */ + char name[512]; + const char *runtime = getenv("TMPDIR"); + if (!runtime || strcmp(runtime,"/var/tmp/swayfx-mosaic-corec-v2-i516p6po/scratch") || snprintf(name, sizeof(name), "%s/g2-shm-XXXXXX", runtime) >= + (int)sizeof(name)) + exit(2); + int fd = mkstemp(name); + if (fd < 0) + exit(2); + unlink(name); + if (ftruncate(fd, (off_t)size) < 0) + exit(2); + struct pixels *p = calloc(1, sizeof(*p)); + if (!p) + exit(2); + p->map = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + p->size = size; + if (p->map == MAP_FAILED) + exit(2); + uint32_t *pixels = p->map; + for (int y = 0; y < height; ++y) + for (int x = 0; x < width; ++x) { + uint32_t color = 0x00303038; + if (!strcmp(mode, "background")) + color = pattern == 'A' ? (x / 8 % 2 ? 0x000f192d : 0x00f5f5f5) + : (x / 8 % 2 ? 0x00b02040 : 0x0020c0e0); + else if (!strcmp(mode, "glass")) + color = 0x88442010; + else if (!strcmp(mode, "filler")) + color = 0x00000000; + pixels[(size_t)y * (size_t)width + (size_t)x] = color; + } + if (!strcmp(mode,"background")) { + if (!commits) { + source_a=malloc(size);if(!source_a)exit(2); + memcpy(source_a,p->map,size);source_a_size=size; + } else if (commits==2) { + if(!source_a || source_a_size!=size)exit(2); + /* A-return is the preserved original payload, not a fitted redraw. */ + memcpy(p->map,source_a,size); + } else if(source_a_size==size && !memcmp(source_a,p->map,size)) exit(2); + fprintf(stderr,"G3_SOURCE_BYTES command=%s bytes=%zu crc32=%08x exact_original=%s\n", + source_command,size,source_crc(p->map,size),commits==2?"true":"not-return"); + } + struct wl_shm_pool *pool = wl_shm_create_pool(shm, fd, (int)size); + p->buffer = wl_shm_pool_create_buffer( + pool, 0, width, height, width * 4, + (!strcmp(mode, "glass") || !strcmp(mode, "filler")) + ? WL_SHM_FORMAT_ARGB8888 + : WL_SHM_FORMAT_XRGB8888); + p->next = buffers; + buffers = p; + ++live_buffers; + wl_buffer_add_listener(p->buffer, &buffer_listener, p); + wl_surface_attach(surface, p->buffer, 0, 0); + wl_surface_damage_buffer(surface, 0, 0, width, height); + wl_surface_commit(surface); + fprintf(stderr, + "CLIENT_COMMIT ns=%lld mode=%s surface=%u buffer=%u index=%u " + "pattern=%c size=%dx%d damage=0,0,%d,%d\n", + stamp(), mode, wl_proxy_get_id((struct wl_proxy *)surface), + wl_proxy_get_id((struct wl_proxy *)p->buffer), ++commits, pattern, + width, height, width, height); + wl_shm_pool_destroy(pool); + close(fd); +} +static void ping(void *d, struct xdg_wm_base *b, uint32_t s) { + xdg_wm_base_pong(b, s); +} +static const struct xdg_wm_base_listener wm_listener = {.ping = ping}; +static void top_configure(void *d, struct xdg_toplevel *t, int32_t w, int32_t h, + struct wl_array *a) { + if (w > 0) + width = w; + if (h > 0) + height = h; +} +static void top_close(void *d, struct xdg_toplevel *t) { running = 0; } +static const struct xdg_toplevel_listener top_listener = { + .configure = top_configure, .close = top_close}; +static void configure(void *d, struct xdg_surface *s, uint32_t serial) { + xdg_surface_ack_configure(s, serial); + draw(); +} +static const struct xdg_surface_listener xdg_listener = {.configure = + configure}; +/* CONTRACT: configures ACK geometry, never submit an unreserved image. + * WHY: duplicate layer configures must not spend autonomous mutations. + * INVARIANT: the runner reserves then sends every A/B draw command. */ +static void layer_configure(void *d, struct zwlr_layer_surface_v1 *s, + uint32_t serial, uint32_t w, uint32_t h) { + if (w == 0 || h == 0 || w > 4096 || h > 4096) + exit(2); + if (commits && ((int)w != width || (int)h != height)) + exit(2); + width = (int)w; + height = (int)h; + zwlr_layer_surface_v1_ack_configure(s, serial); + fprintf(stderr, "BACKGROUND_CONFIGURED ns=%lld serial=%u size=%dx%d\n", + stamp(), serial, width, height); +} +static void layer_close(void *d, struct zwlr_layer_surface_v1 *s) { + running = 0; +} +static const struct zwlr_layer_surface_v1_listener layer_listener = { + .configure = layer_configure, .closed = layer_close}; +static void output_name(void *d, struct wl_output *o, const char *name) { + if (!strcmp(name, "HEADLESS-1")) + h1 = o; +} +static void output_geometry(void *d, struct wl_output *o, int32_t x, int32_t y, + int32_t w, int32_t h, int32_t sp, const char *m, + const char *n, int32_t tr) {} +static void output_mode(void *d, struct wl_output *o, uint32_t flags, int32_t w, + int32_t h, int32_t refresh) {} +static void output_done(void *d, struct wl_output *o) {} +static void output_scale(void *d, struct wl_output *o, int32_t scale) {} +static void output_description(void *d, struct wl_output *o, const char *desc) { +} +static const struct wl_output_listener output_listener = { + .geometry = output_geometry, + .mode = output_mode, + .done = output_done, + .scale = output_scale, + .name = output_name, + .description = output_description}; +static void global(void *d, struct wl_registry *r, uint32_t id, + const char *name, uint32_t v) { + if (!strcmp(name, "wl_compositor")) + compositor = + wl_registry_bind(r, id, &wl_compositor_interface, v < 4 ? v : 4); + else if (!strcmp(name, "wl_shm")) + shm = wl_registry_bind(r, id, &wl_shm_interface, 1); + else if (!strcmp(name, "xdg_wm_base")) + wm = wl_registry_bind(r, id, &xdg_wm_base_interface, v < 3 ? v : 3); + else if (!strcmp(name, "zwlr_layer_shell_v1")) + shell = + wl_registry_bind(r, id, &zwlr_layer_shell_v1_interface, v < 2 ? v : 2); + else if (!strcmp(name, "wl_output") && v >= 4) { + struct wl_output *o = wl_registry_bind(r, id, &wl_output_interface, 4); + wl_output_add_listener(o, &output_listener, NULL); + } +} +static void removed(void *d, struct wl_registry *r, uint32_t id) {} +static const struct wl_registry_listener registry_listener = { + .global = global, .global_remove = removed}; +static void stop(int s) { running = 0; } +int main(int argc, char **argv) { + /* WHY: Linux may route core dumps through a pipe handler that execs another + * process, so a zero RLIMIT_CORE alone does not protect this client. + * CONTRACT/INVARIANT: establish both limits before any Wayland operation. */ + struct rlimit no_core = {0, 0}; + if (setrlimit(RLIMIT_CORE, &no_core) < 0) { + perror("setrlimit(RLIMIT_CORE)"); + return 1; + } + if (prctl(PR_SET_DUMPABLE, 0) < 0) { + perror("prctl(PR_SET_DUMPABLE, 0)"); + return 1; + } + int dumpable = prctl(PR_GET_DUMPABLE); + if (dumpable < 0) { + perror("prctl(PR_GET_DUMPABLE)"); + return 1; + } + if (dumpable != 0) { + fprintf(stderr, "PR_GET_DUMPABLE returned %d; expected 0\n", dumpable); + return 1; + } + if (argc != 2) + return 2; + mode = argv[1]; + if (strcmp(mode, "background") && strcmp(mode, "glass") && + strcmp(mode, "filler") && strcmp(mode, "reference")) + return 2; + signal(SIGTERM, stop); + display = wl_display_connect(NULL); + if (!display) + return 1; + struct wl_registry *r = wl_display_get_registry(display); + wl_registry_add_listener(r, ®istry_listener, NULL); + if (wl_display_roundtrip(display) < 0 || wl_display_roundtrip(display) < 0) + return 1; + if (!compositor || !shm || !wm) + return 2; + xdg_wm_base_add_listener(wm, &wm_listener, NULL); + surface = wl_compositor_create_surface(compositor); + if (!strcmp(mode, "background")) { + if (!shell || !h1) + return 2; + layer = zwlr_layer_shell_v1_get_layer_surface( + shell, surface, h1, ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND, + "mosaic-dynamic-background"); + zwlr_layer_surface_v1_add_listener(layer, &layer_listener, NULL); + zwlr_layer_surface_v1_set_size(layer, 0, 0); + zwlr_layer_surface_v1_set_anchor(layer, + ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP | + ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM | + ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT | + ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT); + zwlr_layer_surface_v1_set_exclusive_zone(layer, -1); + zwlr_layer_surface_v1_set_keyboard_interactivity(layer, 0); + } else { + xdg = xdg_wm_base_get_xdg_surface(wm, surface); + xdg_surface_add_listener(xdg, &xdg_listener, NULL); + top = xdg_surface_get_toplevel(xdg); + xdg_toplevel_add_listener(top, &top_listener, NULL); + const char *app = !strcmp(mode, "reference") ? "org.mosaic.StaticReference" + : !strcmp(mode, "glass") ? "org.mosaic.BlurGlass" + : "org.mosaic.BlurFiller"; + xdg_toplevel_set_app_id(top, app); + xdg_toplevel_set_title(top, app); + } + wl_surface_commit(surface); + while (running) { + if (wl_display_dispatch_pending(display) < 0) + break; + if (wl_display_flush(display) < 0 && errno != EAGAIN) + break; + struct pollfd fds[2] = { + {.fd = wl_display_get_fd(display), .events = POLLIN}, + {.fd = STDIN_FILENO, .events = POLLIN}}; + int n = poll(fds, 2, 100); + if (n < 0) { + if (errno == EINTR) + continue; + break; + } + if (fds[0].revents & (POLLIN | POLLERR | POLLHUP)) + if (wl_display_dispatch(display) < 0) + break; + if (fds[1].revents & POLLIN) { + char cmd[64]; + if (!fgets(cmd, sizeof(cmd), stdin)) + break; + if (!strcmp(mode, "background") && + ((!commits && !strcmp(cmd,"G3-A\n")) || + (commits==1 && !strcmp(cmd,"G3-B\n")) || + (commits==2 && !strcmp(cmd,"G3-A-return\n")))) { + pattern = commits==1?'B':'A'; + source_command=commits==0?"G3-A":commits==1?"G3-B":"G3-A-return"; + draw(); + fprintf(stderr, "COMMAND %c DONE ns=%lld\n", pattern, stamp()); + } else if (!strcmp(cmd, "close\n")) { + fprintf(stderr,"CLOSE_RECEIVED\n");running = 0; + } + else if(!strcmp(mode,"background"))return 2; + } + } + int error = wl_display_get_error(display); + fprintf(stderr, "EXIT ns=%lld mode=%s error=%d commits=%u\n", stamp(), mode, + error, commits); + if (layer) + zwlr_layer_surface_v1_destroy(layer); + if (top) + xdg_toplevel_destroy(top); + if (xdg) + xdg_surface_destroy(xdg); + wl_surface_destroy(surface); + wl_display_roundtrip(display); + fprintf(stderr,"SHM_CLOSURE created=%u released=%u\n",commits,released_buffers); + bool all_released=commits==released_buffers; + wl_display_flush(display); + while (buffers) + free_pixels(buffers); + free(source_a); + wl_registry_destroy(r); + wl_display_disconnect(display); + return error || !all_released ? 1 : 0; +} diff --git a/tooling/probes/step20c-corec-v2-followup/gpu/freeze.py b/tooling/probes/step20c-corec-v2-followup/gpu/freeze.py new file mode 100644 index 0000000..4562db4 --- /dev/null +++ b/tooling/probes/step20c-corec-v2-followup/gpu/freeze.py @@ -0,0 +1,43 @@ +"""Write a relative G3 source manifest outside Git; grants no review.""" +import argparse +import os +import json +from pathlib import Path +from reproduce import KIT, sha, tree + +if __name__ == '__main__': + p = argparse.ArgumentParser(description=__doc__) + p.add_argument('--lab', type=Path, required=True) + p.add_argument('--output', type=Path) + a = p.parse_args() + lab = a.lab.resolve(strict=True) + assert str(lab).startswith('/var/tmp/swayfx-mosaic-step20c-native-closure-') + files = tree(KIT) + result = {'schema': 'G3-relative-source-freeze-v1', 'status': 'EXPERIMENTAL_SOURCE_ONLY', + 'review': 'PENDING_INDEPENDENT', 'build': 'NOT_RUN', 'runtime': 'NOT_RUN', + 'no_inherited_G2_qualification': True, 'files': files} + out = a.output if a.output is not None else Path('g3-source-freeze.json') + if not out.is_absolute(): + out = lab / out + if out.parent.resolve(strict=True) != lab or out.name in ('', '.', '..'): + p.error('--output must name a direct file below --lab') + flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL + if hasattr(os, 'O_NOFOLLOW'): + flags |= os.O_NOFOLLOW + fd = os.open(out, flags, 0o600) + try: + payload = (json.dumps(result, indent=2) + '\n').encode() + with os.fdopen(fd, 'wb') as stream: + fd = -1 + stream.write(payload) + stream.flush() + os.fsync(stream.fileno()) + dirfd = os.open(lab, os.O_RDONLY | getattr(os, 'O_DIRECTORY', 0)) + try: + os.fsync(dirfd) + finally: + os.close(dirfd) + finally: + if fd >= 0: + os.close(fd) + print(sha(out)) diff --git a/tooling/probes/step20c-corec-v2-followup/gpu/g2_gl.h b/tooling/probes/step20c-corec-v2-followup/gpu/g2_gl.h new file mode 100644 index 0000000..adf736f --- /dev/null +++ b/tooling/probes/step20c-corec-v2-followup/gpu/g2_gl.h @@ -0,0 +1,152 @@ +/* G3 private admission delta; G2 hook symbols retained. No production API. */ +#ifndef G2_GL_H +#define G2_GL_H +#include +#include +#include +#include +#include +#include +#include +#define G2_CAP (256u * 1024u * 1024u) +#define G2_ERRORS 16 +struct g2_errors { GLenum values[G2_ERRORS]; unsigned count; bool exhausted; }; +struct g2_state { + GLint read_fbo, draw_fbo, rbo, pack, pbo, row, skip_rows, skip_pixels; + GLint viewport[4], scissor[4], program, active, texture; + GLboolean scissor_on; +}; +struct g2_sample { + EGLContext context; EGLint client_version; EGLenum api; + EGLDisplay display; char version[160], glsl[160]; GLint major, minor; + bool capability_queried, version_queried, state_saved, bound, read_attempted, restore_attempted; struct g2_state before, after; + struct g2_errors pre, version_errors, capability, state_errors, query, read, restore; + GLint object_type, object, level, width, height, format, channels[4]; + GLint read_buffer, samples; GLenum status; size_t bytes; unsigned char *pixels; + bool success; const char *reason; +}; +static struct g2_errors g2_drain(void) { + struct g2_errors e = {0}; + for (unsigned i=0;iG2_CAP || (size_t)w>SIZE_MAX/4/(size_t)h) return false; + *n=(size_t)w*(size_t)h*4; return *n<=G2_CAP-used; +} +static void g2_state_get(struct g2_state *s) { + memset(s,0,sizeof(*s)); + glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING,&s->read_fbo); + glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING,&s->draw_fbo); + glGetIntegerv(GL_RENDERBUFFER_BINDING,&s->rbo); + glGetIntegerv(GL_PACK_ALIGNMENT,&s->pack); + glGetIntegerv(GL_PIXEL_PACK_BUFFER_BINDING,&s->pbo); + glGetIntegerv(GL_PACK_ROW_LENGTH,&s->row); + glGetIntegerv(GL_PACK_SKIP_ROWS,&s->skip_rows); + glGetIntegerv(GL_PACK_SKIP_PIXELS,&s->skip_pixels); + glGetIntegerv(GL_VIEWPORT,s->viewport); glGetIntegerv(GL_SCISSOR_BOX,s->scissor); + s->scissor_on=glIsEnabled(GL_SCISSOR_TEST); + glGetIntegerv(GL_CURRENT_PROGRAM,&s->program); + glGetIntegerv(GL_ACTIVE_TEXTURE,&s->active); + glGetIntegerv(GL_TEXTURE_BINDING_2D,&s->texture); +} +static void g2_state_restore(const struct g2_state *s) { + glBindFramebuffer(GL_READ_FRAMEBUFFER,(GLuint)s->read_fbo); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER,(GLuint)s->draw_fbo); + glBindRenderbuffer(GL_RENDERBUFFER,(GLuint)s->rbo); + glBindBuffer(GL_PIXEL_PACK_BUFFER,(GLuint)s->pbo); + glPixelStorei(GL_PACK_ALIGNMENT,s->pack); + glPixelStorei(GL_PACK_ROW_LENGTH,s->row); + glPixelStorei(GL_PACK_SKIP_ROWS,s->skip_rows); + glPixelStorei(GL_PACK_SKIP_PIXELS,s->skip_pixels); +} +/* WHY: EGL creation allows a newer compatible ES context (§3.7.1), while + * EGL's context query describes supported version (§3.7.4). Preserve any + * EGL/GL divergence as evidence; never relabel EGL as a creation request. + * CONTRACT: attest bounded GL_VERSION first, then safely query ES3 enums. + * INVARIANT: only ES 3.0..3.2 native RBO reads; no shader or error-queue neutrality. */ +static bool g2_version(const char *v, GLint *minor) { + if(!v || strnlen(v,160)>=160 || strncmp(v,"OpenGL ES 3.",12))return false; + if(v[12]<'0' || v[12]>'2' || (v[13] && v[13]!=' '))return false; + for(size_t i=13;v[i];i++)if((unsigned char)v[i]<32 || (unsigned char)v[i]>126)return false; + *minor=v[12]-'0';return true; +} +static struct g2_sample g2_read(GLuint fbo,int width,int height,size_t used) { + struct g2_sample s={.reason="NOT_PROVEN"}; + s.context=eglGetCurrentContext(); s.api=eglQueryAPI(); s.display=eglGetCurrentDisplay(); + if(s.context==EGL_NO_CONTEXT || s.display==EGL_NO_DISPLAY || s.api!=EGL_OPENGL_ES_API) {s.reason="UNKNOWN_CONTEXT"; return s;} + if(!eglQueryContext(s.display,s.context,EGL_CONTEXT_CLIENT_VERSION,&s.client_version)) { + s.reason="EGL_QUERY_FAILED"; return s; + } + s.pre=g2_drain(); if(!g2_clean(s.pre)) {s.reason="PREEXISTING_GL_ERRORS"; return s;} + s.version_queried=true; + const char *v=(const char *)glGetString(GL_VERSION); + if(v && strnlen(v,sizeof(s.version))3 || !g2_version(v,&parsed_minor)) { + s.reason="GLES_STATE_NOT_SUPPORTED"; return s; + } + s.capability_queried=true; + glGetIntegerv(GL_MAJOR_VERSION,&s.major);glGetIntegerv(GL_MINOR_VERSION,&s.minor); + const char *glsl=(const char *)glGetString(GL_SHADING_LANGUAGE_VERSION); + if(glsl && strnlen(glsl,sizeof(s.glsl))0) { + glBindRenderbuffer(GL_RENDERBUFFER,(GLuint)s.object); + glGetRenderbufferParameteriv(GL_RENDERBUFFER,GL_RENDERBUFFER_WIDTH,&s.width); + glGetRenderbufferParameteriv(GL_RENDERBUFFER,GL_RENDERBUFFER_HEIGHT,&s.height); + glGetRenderbufferParameteriv(GL_RENDERBUFFER,GL_RENDERBUFFER_INTERNAL_FORMAT,&s.format); + glGetRenderbufferParameteriv(GL_RENDERBUFFER,GL_RENDERBUFFER_SAMPLES,&s.samples); + const GLenum c[4]={GL_RENDERBUFFER_RED_SIZE,GL_RENDERBUFFER_GREEN_SIZE,GL_RENDERBUFFER_BLUE_SIZE,GL_RENDERBUFFER_ALPHA_SIZE}; + for(int i=0;i<4;i++) glGetRenderbufferParameteriv(GL_RENDERBUFFER,c[i],&s.channels[i]); + } else if(s.object_type==GL_TEXTURE) { + glGetFramebufferAttachmentParameteriv(GL_READ_FRAMEBUFFER,GL_COLOR_ATTACHMENT0, + GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_LEVEL,&s.level); + } + glGetIntegerv(GL_READ_BUFFER,&s.read_buffer); + s.status=glCheckFramebufferStatus(GL_READ_FRAMEBUFFER); + s.query=g2_drain(); + if(!g2_clean(s.query)) {s.reason="ATTACHMENT_QUERY_ERROR"; goto done;} + if(s.object_type!=GL_RENDERBUFFER) {s.reason="TEXTURE_ATTACHMENT_DIMENSIONS_NOT_PROVEN"; goto done;} + if(s.read_buffer!=GL_COLOR_ATTACHMENT0 || s.samples!=0 || s.status!=GL_FRAMEBUFFER_COMPLETE || s.width!=width || s.height!=height || + s.channels[0]!=8 || s.channels[1]!=8 || s.channels[2]!=8 || (s.channels[3]!=0 && s.channels[3]!=8)) { + s.reason="ATTACHMENT_UNSUPPORTED"; goto done; + } + glBindBuffer(GL_PIXEL_PACK_BUFFER,0); glPixelStorei(GL_PACK_ALIGNMENT,1); + glPixelStorei(GL_PACK_ROW_LENGTH,0); glPixelStorei(GL_PACK_SKIP_ROWS,0); glPixelStorei(GL_PACK_SKIP_PIXELS,0); + s.query=g2_drain(); if(!g2_clean(s.query)) {s.reason="PACK_QUERY_ERROR"; goto done;} + s.pixels=malloc(s.bytes); if(!s.pixels) {s.reason="ALLOCATION_FAILED"; goto done;} + /* glReadPixels returns void. NO alpha sentinel or file hash success gate. */ + s.read_attempted=true; + glReadPixels(0,0,width,height,GL_RGBA,GL_UNSIGNED_BYTE,s.pixels); + s.read=g2_drain(); + if(!g2_clean(s.read)) {s.reason="READ_GL_ERROR"; goto done;} + s.success=true; s.reason="READ_SUCCESS"; + done: + if(s.state_saved && s.bound) { + s.restore_attempted=true; + g2_state_restore(&s.before); g2_state_get(&s.after); s.restore=g2_drain(); + if(!g2_clean(s.restore) || memcmp(&s.before,&s.after,sizeof(s.before)) || eglGetCurrentContext()!=s.context || eglGetCurrentDisplay()!=s.display || eglQueryAPI()!=s.api) { + s.success=false; s.reason="RESTORE_FAILED"; + } + } + return s; +} +#endif diff --git a/tooling/probes/step20c-corec-v2-followup/gpu/g2_native.h b/tooling/probes/step20c-corec-v2-followup/gpu/g2_native.h new file mode 100644 index 0000000..14a6612 --- /dev/null +++ b/tooling/probes/step20c-corec-v2-followup/gpu/g2_native.h @@ -0,0 +1,228 @@ +/* PRIVATE G3 admission; G2 symbols/hooks preserved. No public API. */ +#ifndef G2_NATIVE_H +#define G2_NATIVE_H +#include "g2_gl.h" +#include +#include +#include +#include +#include +#include +#include +#include +#ifndef G3_ROOT +#error "Define G3_ROOT as the reviewed new LAB acquisition path" +#endif +#define G2_ROOT G3_ROOT +static struct { + bool stopped, active; unsigned epoch, point; size_t used; + int dir; FILE *meta; const void *pass, *output; + uintptr_t pass_sequence; char nonce[65]; +} g2={.dir=-1}; +static const char *g2_names[]={"source","down0","down1","down2","up2","up1","up0","optimized-cache","no-blur-cache","composition-presubmit"}; +static void g2_fail(const char *reason) { + g2.stopped=true; + if(g2.meta) {fprintf(g2.meta,"{\"kind\":\"rejected\",\"reason\":\"%s\",\"point\":%u}\n",reason,g2.point); fflush(g2.meta); fsync(fileno(g2.meta)); fclose(g2.meta); g2.meta=NULL;} + g2.active=false; +} +static uint32_t g2_crc(const unsigned char *p,size_t n) { + uint32_t crc=~0u; + for(size_t i=0;i>1)^((0u-(crc&1u))&0xedb88320u);} + return ~crc; +} +static bool g2_write(int fd,const void *ptr,size_t n) { + const char *p=ptr; while(n) {ssize_t k=write(fd,p,n); if(k<0 && errno==EINTR) continue; if(k<=0)return false; p+=k;n-=(size_t)k;} return true; +} +static void g2_error_json(const char *name,struct g2_errors e) { + fprintf(g2.meta,",\"%s\":{\"exhausted\":%s,\"codes\":[",name,e.exhausted?"true":"false"); + for(unsigned i=0;iread_fbo,s->draw_fbo,s->rbo,s->pack,s->pbo,s->row,s->skip_rows,s->skip_pixels,s->viewport[0],s->viewport[1],s->viewport[2],s->viewport[3],s->scissor[0],s->scissor[1],s->scissor[2],s->scissor[3],s->scissor_on,s->program,s->active,s->texture); +} +static void g2_region(const pixman_region32_t *region) { + int n=0; pixman_box32_t *r=region?pixman_region32_rectangles(region,&n):NULL; + fputs(",\"fresh_rectangles\":[",g2.meta); + if(n>256) {fputs("],\"fresh_region_overflow\":true",g2.meta);return;} + for(int i=0;ibuffer,&a))return false; + bool format=a.format==DRM_FORMAT_ABGR8888 || a.format==DRM_FORMAT_XBGR8888 || a.format==DRM_FORMAT_ARGB8888 || a.format==DRM_FORMAT_XRGB8888; + bool amd=a.modifier==UINT64_C(0x020000001046bb04); + if(!format || (a.modifier!=0 && !amd) || a.n_planes!=(amd?2:1) || a.width!=b->buffer->width || a.height!=b->buffer->height || a.width<=0 || a.height<=0)return false; + if((uint64_t)a.stride[0]<(uint64_t)a.width*4)return false; + for(int i=0;ibuffer,b->fbo,b->rbo,b->tex,b->buffer->width,b->buffer->height,(uintptr_t)b->image); + struct wlr_dmabuf_attributes a={0}; + if(!wlr_buffer_get_dmabuf(b->buffer,&a)) {fputs(",\"dmabuf\":null",g2.meta);return;} + /* Borrowed descriptors: NEVER finish(), close(), duplicate or seek these FDs. + * fstat exposes descriptor identity, not a trustworthy DCC allocation length. */ + fprintf(g2.meta,",\"dmabuf\":{\"format\":%u,\"modifier\":\"%016"PRIx64"\",\"dimensions\":[%d,%d],\"borrowed\":true,\"allocation_length\":\"NOT_PROVEN\",\"planes\":[",a.format,a.modifier,a.width,a.height); + if(a.n_planes<1 || a.n_planes>4) {fputs("]}",g2.meta);return;} + for(int i=0;i=0)return true; + const char *d=getenv("MOSAIC_G3_DIR"),*nonce=getenv("MOSAIC_G3_NONCE"); char real[PATH_MAX]; + if(!d || !nonce)return false; + if(strlen(nonce)!=64 || !realpath(d,real) || strcmp(real,G2_ROOT) || strcmp(d,G2_ROOT)) {g2.stopped=true;return false;} + for(unsigned i=0;i<64;i++)if(!((nonce[i]>='0'&&nonce[i]<='9')||(nonce[i]>='a'&&nonce[i]<='f'))) {g2.stopped=true;return false;} + g2.dir=open(d,O_RDONLY|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC); + if(g2.dir<0 || !g2_owned(g2.dir,0700)) {if(g2.dir>=0)close(g2.dir);g2.dir=-1;g2.stopped=true;return false;} + memcpy(g2.nonce,nonce,65);return true; +} +static void g2_begin(struct fx_gles_render_pass *pass,struct fx_framebuffer *source,struct fx_render_blur_pass_options *options); +static bool g2_expected_source(const struct g2_sample *s,unsigned epoch) { + if(s->width!=800 || s->height!=600 || !s->pixels)return false; + const unsigned char a[2][3]={{245,245,245},{15,25,45}}; + const unsigned char b[2][3]={{32,192,224},{176,32,64}}; + for(int y=0;y<600;y++)for(int x=0;x<800;x++) { + const unsigned char *expected=(epoch==2?b:a)[(x/8)%2]; + if(memcmp(s->pixels+((size_t)y*800+(size_t)x)*4,expected,3))return false; + } + return true; /* RGB source admission only; alpha is recorded, never sentinel. */ +} +static void g2_sample_native(struct fx_gles_render_pass *pass,struct fx_framebuffer *b,unsigned point,const pixman_region32_t *region) { + if(!g2.active || g2.pass!=pass)return; + if(point!=g2.point || point>=10) {g2_fail("STAGE_ORDER_OR_DUPLICATE");return;} + if(region && pixman_region32_n_rects(region)>256) {g2_fail("REGION_LIMIT");return;} + if(!g2_dmabuf_supported(b)) {g2_fail("DMABUF_METADATA_UNSUPPORTED");return;} + struct g2_sample s=g2_read(b->fbo,b->buffer->width,b->buffer->height,g2.used); + fprintf(g2.meta,"{\"kind\":\"sample\",\"epoch\":%u,\"pass_sequence\":%"PRIuPTR",\"point\":%u,\"stage\":\"%s\",\"context\":\"%"PRIxPTR"\",\"api\":%u,\"client_version\":%d,\"result\":\"%s\",\"bytes\":%zu,\"attachment\":[%d,%d,%d,%d,%d,%d],\"channels\":[%d,%d,%d,%d],\"status\":%u",g2.epoch,g2.pass_sequence,point,g2_names[point],(uintptr_t)s.context,s.api,s.client_version,s.reason,s.bytes,s.object_type,s.object,s.level,s.width,s.height,s.format,s.channels[0],s.channels[1],s.channels[2],s.channels[3],s.status); + fprintf(g2.meta,",\"read_success\":%s,\"read_buffer\":%d,\"samples\":%d",s.success?"true":"false",s.read_buffer,s.samples); + fputs(",\"gl_version_hex\":\"",g2.meta);for(size_t i=0;i=0 && g2_write(fd,s.pixels,s.bytes) && fsync(fd)==0; + if(fd>=0 && close(fd))ok=false; + fprintf(g2.meta,",\"crc32\":\"%08x\",\"file\":\"%s\",\"published\":%s}\n",g2_crc(s.pixels,s.bytes),name,ok?"true":"false"); + g2.used+=s.bytes; + if(!ok || fflush(g2.meta) || fsync(fileno(g2.meta))) {free(s.pixels);g2_fail("PUBLICATION_FAILED");return;} + g2.point++; + if(point==0 && !g2_expected_source(&s,g2.epoch)) {free(s.pixels);g2_fail("SOURCE_PATTERN_NOT_CURRENT_EPOCH");return;} + free(s.pixels); + if(point==9) { + fprintf(g2.meta,"{\"kind\":\"end\",\"epoch\":%u,\"points\":10,\"raw_total\":%zu,\"accepted\":true}\n",g2.epoch,g2.used); + bool final_ok=fflush(g2.meta)==0 && fsync(fileno(g2.meta))==0; + if(fclose(g2.meta)) {final_ok=false;} + g2.meta=NULL;g2.active=false; + if(!final_ok)g2.stopped=true; + if(g2.epoch==3)g2.stopped=true; + } +} +/* Explicit intended-output mapping; bounded eight outputs, no public ABI. */ +static struct {const void *pass;bool intended;} g2_outputs[8]; +static void g2_forget_output(struct fx_gles_render_pass *pass) { + for(unsigned i=0;i<8;i++)if(g2_outputs[i].pass==pass) { + g2_outputs[i].pass=NULL;g2_outputs[i].intended=false; + } +} +static bool g2_intended_output(struct fx_gles_render_pass *pass) { + for(unsigned i=0;i<8;i++)if(g2_outputs[i].pass==pass)return g2_outputs[i].intended; + return false; +} +static void g2_track_output(struct fx_gles_render_pass *pass,struct wlr_output *output) { + /* A native pass owns its mapping only until submit. Address reuse overwrites + * prior identity. Overflow never silently associates another output. */ + g2_forget_output(pass); + for(unsigned i=0;i<8;i++)if(!g2_outputs[i].pass) { + g2_outputs[i].pass=pass; + g2_outputs[i].intended=strcmp(output->name,"HEADLESS-1")==0 && output->scale==1.0f && output->transform==WL_OUTPUT_TRANSFORM_NORMAL; + return; + } + g2_fail("TOO_MANY_SIMULTANEOUS_NATIVE_PASSES"); +} +static void g2_begin(struct fx_gles_render_pass *pass,struct fx_framebuffer *source,struct fx_render_blur_pass_options *options) { + if(g2.stopped)return; + if(g2.active) {if(g2.pass==pass)g2_fail("MULTIPLE_OPTIMIZED_CANDIDATES");return;} + if(!g2_intended_output(pass) || !g2_open())return; + unsigned e=g2.epoch+1;if(e>3)return; + char arm[40],consumed[40],meta[40],expected[80],data[81]={0}; + snprintf(arm,sizeof(arm),"epoch-%u.arm",e);snprintf(consumed,sizeof(consumed),"epoch-%u.consumed",e); + snprintf(expected,sizeof(expected),"%s %u\n",g2.nonce,e); + /* WHY: O_NONBLOCK makes non-regular substitutions (especially a FIFO with + * no writer) fail closed without stalling the compositor callback. + * CONTRACT: validate identity, permissions, link count and exact bounded + * framing size before reading or consuming the arm token. */ + int fd=openat(g2.dir,arm,O_RDONLY|O_NONBLOCK|O_NOFOLLOW|O_CLOEXEC);if(fd<0) {if(errno!=ENOENT)g2.stopped=true;return;} + struct stat st; + bool ok=fstat(fd,&st)==0 && S_ISREG(st.st_mode) && st.st_uid==geteuid() && + (st.st_mode&0777)==0600 && st.st_nlink==1 && st.st_size==(off_t)strlen(expected); + if(!ok) {close(fd);g2.stopped=true;return;} + ssize_t n=read(fd,data,sizeof(data)-1); + if(n!=(ssize_t)strlen(expected) || memcmp(data,expected,(size_t)n)) {close(fd);g2.stopped=true;return;} + /* Consume BEFORE any query, readback or publication. Existing consumed token + * is fatal, even if a prior process died or first A was rejected. */ + struct stat linked; + if(linkat(g2.dir,arm,g2.dir,consumed,0) || fstatat(g2.dir,consumed,&linked,AT_SYMLINK_NOFOLLOW) || + !S_ISREG(linked.st_mode) || linked.st_dev!=st.st_dev || linked.st_ino!=st.st_ino || linked.st_nlink!=2 || + unlinkat(g2.dir,arm,0) || fsync(g2.dir)) {close(fd);g2.stopped=true;return;} + close(fd); + g2.epoch=e;g2.point=0;g2.pass=pass;g2.pass_sequence++;g2.active=true; + snprintf(meta,sizeof(meta),"epoch-%u.jsonl",e);fd=openat(g2.dir,meta,O_WRONLY|O_CREAT|O_EXCL|O_NOFOLLOW|O_CLOEXEC,0600); + if(fd<0 || !(g2.meta=fdopen(fd,"w"))) {if(fd>=0)close(fd);g2.stopped=true;g2.active=false;return;} + fprintf(g2.meta,"{\"kind\":\"begin\",\"schema\":\"G3-followup-v1\",\"epoch\":%u,\"label\":\"%s\",\"nonce\":\"%s\",\"output\":\"HEADLESS-1\",\"pass\":\"%"PRIxPTR"\",\"offscreen_output_key\":\"%"PRIxPTR"\",\"intrusive_error_queue\":true,\"error_queue_restorable\":false,\"readback_may_synchronize\":true,\"rows\":\"GL_LOW_TO_HIGH\",\"present_buffer\":\"NOT_PROVEN\",\"gpu_completion\":\"NOT_PROVEN\",\"lifetime\":\"NOT_PROVEN\"}\n",e,e==1?"A":e==2?"B":"A-return",g2.nonce,(uintptr_t)pass,(uintptr_t)pass->fx_offscreen_buffers); + if(options->blur_data->num_passes!=3 || options->blur_strength!=1.0f || blur_data_should_parameters_blur_effects(options->blur_data)) {g2_fail("NON_PROSPECTIVE_BLUR_CONFIGURATION");return;} + g2_sample_native(pass,source,0,options->tex_options.base.clip); +} +/* Executed while the actual blur program and source sampler remain bound. */ +static void g2_uniform(struct fx_gles_render_pass *pass,struct blur_shader *shader,struct fx_texture *texture,const pixman_region32_t *region) { + if(!g2.active || g2.pass!=pass || g2.point<1 || g2.point>6)return; + if(region && pixman_region32_n_rects(region)>256) {g2_fail("REGION_LIMIT");return;} + struct g2_errors pre=g2_drain();if(!g2_clean(pre)) {fputs("{\"kind\":\"uniform-errors\"",g2.meta);g2_error_json("pre_errors",pre);fputs("}\n",g2.meta);g2_fail("PRE_UNIFORM_GL_ERROR");return;} + struct g2_state draw_state;GLboolean color_mask[4]={0}; + g2_state_get(&draw_state);glGetBooleanv(GL_COLOR_WRITEMASK,color_mask); + GLboolean blend=glIsEnabled(GL_BLEND),stencil=glIsEnabled(GL_STENCIL_TEST); + GLint program=0,min=0,mag=0,wraps=0,wrapt=0;GLfloat value[2]={0};GLint range[2]={0},precision=0; + glGetIntegerv(GL_CURRENT_PROGRAM,&program); + struct g2_errors initial=g2_drain(); + if(!g2_clean(initial)) {fputs("{\"kind\":\"uniform-errors\"",g2.meta);g2_error_json("query_errors",initial);fputs("}\n",g2.meta);g2_fail("UNIFORM_PROGRAM_QUERY_ERROR");return;} + if(program!=(GLint)shader->program || shader->halfpixel<0 || texture->target!=GL_TEXTURE_2D) {g2_fail("UNIFORM_PROGRAM_OR_TARGET");return;} + GLint actual=glGetUniformLocation((GLuint)program,"halfpixel"); + initial=g2_drain(); + if(!g2_clean(initial) || actual!=shader->halfpixel) {fputs("{\"kind\":\"uniform-errors\"",g2.meta);g2_error_json("query_errors",initial);fputs("}\n",g2.meta);g2_fail("UNIFORM_LOCATION");return;} + GLint count=0;glGetProgramiv((GLuint)program,GL_ACTIVE_UNIFORMS,&count);bool typed=false; + initial=g2_drain(); + if(!g2_clean(initial) || count<0 || count>128) {fputs("{\"kind\":\"uniform-errors\"",g2.meta);g2_error_json("query_errors",initial);fputs("}\n",g2.meta);g2_fail("UNIFORM_COUNT");return;} + for(GLuint i=0;i<(GLuint)count;i++) {char name[128];GLsizei len=0;GLint size=0;GLenum type=0;glGetActiveUniform((GLuint)program,i,sizeof(name),&len,&size,&type,name); + if(len==9 && !memcmp(name,"halfpixel",9) && size==1 && type==GL_FLOAT_VEC2)typed=true; + } + GLfloat radius=0,proj[9]={0},tex_proj[9]={0}; + glGetUniformfv((GLuint)program,shader->radius,&radius); + glGetUniformfv((GLuint)program,shader->proj,proj);glGetUniformfv((GLuint)program,shader->tex_proj,tex_proj); + glGetUniformfv((GLuint)program,actual,value); + glGetShaderPrecisionFormat(GL_FRAGMENT_SHADER,GL_MEDIUM_FLOAT,range,&precision); + glGetTexParameteriv(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,&min);glGetTexParameteriv(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,&mag); + glGetTexParameteriv(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,&wraps);glGetTexParameteriv(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,&wrapt); + struct g2_errors errors=g2_drain();if(!g2_clean(errors)||!typed) {fputs("{\"kind\":\"uniform-errors\"",g2.meta);g2_error_json("query_errors",errors);fputs("}\n",g2.meta);g2_fail("UNIFORM_QUERY_OR_TYPE");return;} + uint32_t bits[2];memcpy(bits,value,sizeof(bits)); + fprintf(g2.meta,"{\"kind\":\"uniform\",\"point\":%u,\"program\":%d,\"location\":%d,\"type\":%u,\"actual_bits\":[%u,%u],\"shader_default_float\":\"mediump\",\"varying\":\"mediump\",\"precision_capability\":[%d,%d,%d],\"operation_precision\":\"NOT_PROVEN\",\"sampler\":[%d,%d,%d,%d],\"source_texture\":%u,\"native_texture_dimensions\":[%u,%u],\"gl_level_dimensions\":\"NOT_PROVEN\"",g2.point,program,actual,GL_FLOAT_VEC2,bits[0],bits[1],range[0],range[1],precision,min,mag,wraps,wrapt,texture->tex,texture->wlr_texture.width,texture->wlr_texture.height); + fprintf(g2.meta,",\"qualifier_basis\":\"FROZEN_LINK_INPUT_SOURCE\",\"shader_source_sha256\":\"%s\"",g2.point<=3?"01664694e4201efb35ec698e89783e220ed930972b5396b86fa842814cd82d4d":"1db325237c9a6c70c2d97518e2f690d448cd006139e9f410cd16e097a1677ad1"); + uint32_t radius_bits,proj_bits[9],tex_proj_bits[9];memcpy(&radius_bits,&radius,4);memcpy(proj_bits,proj,sizeof(proj));memcpy(tex_proj_bits,tex_proj,sizeof(tex_proj)); + fprintf(g2.meta,",\"other_uniforms\":{\"radius_location\":%d,\"radius_bits\":%u,\"proj_location\":%d,\"proj_bits\":[",shader->radius,radius_bits,shader->proj); + for(unsigned i=0;i<9;i++)fprintf(g2.meta,"%s%u",i?",":"",proj_bits[i]); + fprintf(g2.meta,"],\"tex_proj_location\":%d,\"tex_proj_bits\":[",shader->tex_proj); + for(unsigned i=0;i<9;i++)fprintf(g2.meta,"%s%u",i?",":"",tex_proj_bits[i]); + fputs("]}",g2.meta);g2_state_json("draw_state",&draw_state); + fprintf(g2.meta,",\"blend_stencil\":[%u,%u],\"color_mask\":[%u,%u,%u,%u]",blend,stencil,color_mask[0],color_mask[1],color_mask[2],color_mask[3]); + g2_region(region);fputs("}\n",g2.meta); +} +#endif diff --git a/tooling/probes/step20c-corec-v2-followup/gpu/g3-admission.patch b/tooling/probes/step20c-corec-v2-followup/gpu/g3-admission.patch new file mode 100644 index 0000000..f6bc059 --- /dev/null +++ b/tooling/probes/step20c-corec-v2-followup/gpu/g3-admission.patch @@ -0,0 +1,155 @@ +--- a/render/fx_renderer/g2_gl.h ++++ b/render/fx_renderer/g2_gl.h +@@ -1,4 +1,4 @@ +-/* G2 new source-only intrusive diagnostic. No production API. */ ++/* G3 private admission delta; G2 hook symbols retained. No production API. */ + #ifndef G2_GL_H + #define G2_GL_H + #include +@@ -18,8 +18,9 @@ + }; + struct g2_sample { + EGLContext context; EGLint client_version; EGLenum api; +- char version[160]; struct g2_state before, after; +- struct g2_errors pre, query, read, restore; ++ EGLDisplay display; char version[160], glsl[160]; GLint major, minor; ++ bool capability_queried, version_queried, state_saved, bound, read_attempted, restore_attempted; struct g2_state before, after; ++ struct g2_errors pre, version_errors, capability, state_errors, query, read, restore; + GLint object_type, object, level, width, height, format, channels[4]; + GLint read_buffer, samples; GLenum status; size_t bytes; unsigned char *pixels; + bool success; const char *reason; +@@ -64,29 +65,44 @@ + glPixelStorei(GL_PACK_SKIP_ROWS,s->skip_rows); + glPixelStorei(GL_PACK_SKIP_PIXELS,s->skip_pixels); + } +-/* Called with the actual current native target. ES2 is explicitly unsupported: +- * no ES3-only enum is ever queried before both EGL and GL attest ES3. +- * Only native RBO color attachments are accepted. Texture attachments retain +- * type/name/level, but safe dimensions on this pinned API are NOT_PROVEN. +- * Error queue consumption and synchronous readback cannot be restored. */ ++/* WHY: EGL creation allows a newer compatible ES context (§3.7.1), while ++ * EGL's context query describes supported version (§3.7.4). Preserve any ++ * EGL/GL divergence as evidence; never relabel EGL as a creation request. ++ * CONTRACT: attest bounded GL_VERSION first, then safely query ES3 enums. ++ * INVARIANT: only ES 3.0..3.2 native RBO reads; no shader or error-queue neutrality. */ ++static bool g2_version(const char *v, GLint *minor) { ++ if(!v || strnlen(v,160)>=160 || strncmp(v,"OpenGL ES 3.",12))return false; ++ if(v[12]<'0' || v[12]>'2' || (v[13] && v[13]!=' '))return false; ++ for(size_t i=13;v[i];i++)if((unsigned char)v[i]<32 || (unsigned char)v[i]>126)return false; ++ *minor=v[12]-'0';return true; ++} + static struct g2_sample g2_read(GLuint fbo,int width,int height,size_t used) { +- struct g2_sample s={.reason="NOT_PROVEN"}; bool saved=false, changed=false; +- s.context=eglGetCurrentContext(); s.api=eglQueryAPI(); +- if(s.context==EGL_NO_CONTEXT || s.api!=EGL_OPENGL_ES_API) {s.reason="UNKNOWN_CONTEXT"; return s;} +- if(!eglQueryContext(eglGetCurrentDisplay(),s.context,EGL_CONTEXT_CLIENT_VERSION,&s.client_version)) { ++ struct g2_sample s={.reason="NOT_PROVEN"}; ++ s.context=eglGetCurrentContext(); s.api=eglQueryAPI(); s.display=eglGetCurrentDisplay(); ++ if(s.context==EGL_NO_CONTEXT || s.display==EGL_NO_DISPLAY || s.api!=EGL_OPENGL_ES_API) {s.reason="UNKNOWN_CONTEXT"; return s;} ++ if(!eglQueryContext(s.display,s.context,EGL_CONTEXT_CLIENT_VERSION,&s.client_version)) { + s.reason="EGL_QUERY_FAILED"; return s; + } + s.pre=g2_drain(); if(!g2_clean(s.pre)) {s.reason="PREEXISTING_GL_ERRORS"; return s;} ++ s.version_queried=true; + const char *v=(const char *)glGetString(GL_VERSION); +- if(v) snprintf(s.version,sizeof(s.version),"%s",v); +- s.query=g2_drain(); if(!g2_clean(s.query)) {s.reason="VERSION_QUERY_ERROR"; return s;} +- if(s.client_version<3 || !v || strncmp(v,"OpenGL ES 3.",12)!=0) { ++ if(v && strnlen(v,sizeof(s.version))3 || !g2_version(v,&parsed_minor)) { + s.reason="GLES_STATE_NOT_SUPPORTED"; return s; + } ++ s.capability_queried=true; ++ glGetIntegerv(GL_MAJOR_VERSION,&s.major);glGetIntegerv(GL_MINOR_VERSION,&s.minor); ++ const char *glsl=(const char *)glGetString(GL_SHADING_LANGUAGE_VERSION); ++ if(glsl && strnlen(glsl,sizeof(s.glsl)) + #include + #include +-#define G2_ROOT "/var/tmp/swayfx-mosaic-corec-v2-i516p6po/gpu-work/acquisition" ++#ifndef G3_ROOT ++#error "Define G3_ROOT as the reviewed new LAB acquisition path" ++#endif ++#define G2_ROOT G3_ROOT + static struct { + bool stopped, active; unsigned epoch, point; size_t used; + int dir; FILE *meta; const void *pass, *output; +@@ -70,7 +73,7 @@ + static bool g2_owned(int fd,mode_t mode) {struct stat s;return fstat(fd,&s)==0 && s.st_uid==geteuid() && (s.st_mode&0777)==mode;} + static bool g2_open(void) { + if(g2.dir>=0)return true; +- const char *d=getenv("MOSAIC_G2_DIR"),*nonce=getenv("MOSAIC_G2_NONCE"); char real[PATH_MAX]; ++ const char *d=getenv("MOSAIC_G3_DIR"),*nonce=getenv("MOSAIC_G3_NONCE"); char real[PATH_MAX]; + if(!d || !nonce)return false; + if(strlen(nonce)!=64 || !realpath(d,real) || strcmp(real,G2_ROOT) || strcmp(d,G2_ROOT)) {g2.stopped=true;return false;} + for(unsigned i=0;i<64;i++)if(!((nonce[i]>='0'&&nonce[i]<='9')||(nonce[i]>='a'&&nonce[i]<='f'))) {g2.stopped=true;return false;} +@@ -96,10 +99,15 @@ + if(!g2_dmabuf_supported(b)) {g2_fail("DMABUF_METADATA_UNSUPPORTED");return;} + struct g2_sample s=g2_read(b->fbo,b->buffer->width,b->buffer->height,g2.used); + fprintf(g2.meta,"{\"kind\":\"sample\",\"epoch\":%u,\"pass_sequence\":%"PRIuPTR",\"point\":%u,\"stage\":\"%s\",\"context\":\"%"PRIxPTR"\",\"api\":%u,\"client_version\":%d,\"result\":\"%s\",\"bytes\":%zu,\"attachment\":[%d,%d,%d,%d,%d,%d],\"channels\":[%d,%d,%d,%d],\"status\":%u",g2.epoch,g2.pass_sequence,point,g2_names[point],(uintptr_t)s.context,s.api,s.client_version,s.reason,s.bytes,s.object_type,s.object,s.level,s.width,s.height,s.format,s.channels[0],s.channels[1],s.channels[2],s.channels[3],s.status); +- fprintf(g2.meta,",\"read_buffer\":%d,\"samples\":%d",s.read_buffer,s.samples); ++ fprintf(g2.meta,",\"read_success\":%s,\"read_buffer\":%d,\"samples\":%d",s.success?"true":"false",s.read_buffer,s.samples); + fputs(",\"gl_version_hex\":\"",g2.meta);for(size_t i=0;i=0)close(fd);g2.stopped=true;g2.active=false;return;} +- fprintf(g2.meta,"{\"kind\":\"begin\",\"schema\":\"G2-new-v1\",\"epoch\":%u,\"label\":\"%s\",\"nonce\":\"%s\",\"output\":\"HEADLESS-1\",\"pass\":\"%"PRIxPTR"\",\"offscreen_output_key\":\"%"PRIxPTR"\",\"intrusive_error_queue\":true,\"error_queue_restorable\":false,\"readback_may_synchronize\":true,\"rows\":\"GL_LOW_TO_HIGH\",\"present_buffer\":\"NOT_PROVEN\",\"gpu_completion\":\"NOT_PROVEN\",\"lifetime\":\"NOT_PROVEN\"}\n",e,e==1?"A":e==2?"B":"A-return",g2.nonce,(uintptr_t)pass,(uintptr_t)pass->fx_offscreen_buffers); ++ fprintf(g2.meta,"{\"kind\":\"begin\",\"schema\":\"G3-followup-v1\",\"epoch\":%u,\"label\":\"%s\",\"nonce\":\"%s\",\"output\":\"HEADLESS-1\",\"pass\":\"%"PRIxPTR"\",\"offscreen_output_key\":\"%"PRIxPTR"\",\"intrusive_error_queue\":true,\"error_queue_restorable\":false,\"readback_may_synchronize\":true,\"rows\":\"GL_LOW_TO_HIGH\",\"present_buffer\":\"NOT_PROVEN\",\"gpu_completion\":\"NOT_PROVEN\",\"lifetime\":\"NOT_PROVEN\"}\n",e,e==1?"A":e==2?"B":"A-return",g2.nonce,(uintptr_t)pass,(uintptr_t)pass->fx_offscreen_buffers); + if(options->blur_data->num_passes!=3 || options->blur_strength!=1.0f || blur_data_should_parameters_blur_effects(options->blur_data)) {g2_fail("NON_PROSPECTIVE_BLUR_CONFIGURATION");return;} + g2_sample_native(pass,source,0,options->tex_options.base.clip); + } diff --git a/tooling/probes/step20c-corec-v2-followup/gpu/protocol.py b/tooling/probes/step20c-corec-v2-followup/gpu/protocol.py new file mode 100644 index 0000000..488b29a --- /dev/null +++ b/tooling/probes/step20c-corec-v2-followup/gpu/protocol.py @@ -0,0 +1,98 @@ +"""G3 protocol tooling; never spawns compositor/client/GPU or grants review. +prepare-plan prints prospective framing only. arm requires an external accepted +review and runtime authorization. No retry is implemented. All files exclusive. +""" +import argparse,hashlib,json,os,stat,zlib,re +from pathlib import Path +from validate_source import need,metadata,halfpixel +CAP=256*1024*1024 +FRAME=(800,600) +STAGES=['source','down0','down1','down2','up2','up1','up0','optimized-cache','no-blur-cache','composition-presubmit'] +ROOT=Path(os.environ.get('MOSAIC_G3_DIR','/G3_ROOT_NOT_BOUND')) +def owned(p,mode): + s=p.lstat();need(not stat.S_ISLNK(s.st_mode) and s.st_uid==os.geteuid() and stat.S_IMODE(s.st_mode)==mode,'owned path/mode') + return s +def bounded(p,cap): + s=owned(p,0o600);need(stat.S_ISREG(s.st_mode) and s.st_size<=cap and s.st_nlink==1,'bounded regular exclusive file') + fd=os.open(p,os.O_RDONLY|os.O_NOFOLLOW|os.O_CLOEXEC) + try: + after=os.fstat(fd);need((s.st_dev,s.st_ino)==(after.st_dev,after.st_ino),'path identity') + data=os.read(fd,cap+1);need(len(data)==s.st_size and len(data)<=cap,'bounded stable framing');return data + finally:os.close(fd) +def exclusive(p,b): + fd=os.open(p,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW|os.O_CLOEXEC,0o600) + try: + with os.fdopen(fd,'wb',closefd=False) as f:f.write(b);f.flush();os.fsync(fd) + finally:os.close(fd) + d=os.open(p.parent,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW) + try:os.fsync(d) + finally:os.close(d) +def epoch(directory,e,nonce,prior=0): + need(e in (1,2,3),'epoch') + raw=bounded(directory/f'epoch-{e}.jsonl',1024*1024);need(raw.endswith(b'\n'),'metadata line framing') + rows=[json.loads(x) for x in raw.splitlines()];need(len(rows)==18,'bounded exact event count') + begin,end=rows[0],rows[-1] + need(begin['kind']=='begin' and begin['schema']=='G3-followup-v1' and begin['epoch']==e and begin['nonce']==nonce and begin['label']==('A','B','A-return')[e-1],'begin identity') + need(begin['output']=='HEADLESS-1' and begin['intrusive_error_queue'] is True and begin['error_queue_restorable'] is False,'intrusion/output') + need(end['kind']=='end' and end['epoch']==e and end['points']==10 and end['accepted'] is True,'complete accepted epoch; rejection or incomplete A stops all') + # Consumed files intentionally had one hard link during atomic consumption; + # publication is accepted only after arm unlink gives a single link again. + need(bounded(directory/f'epoch-{e}.consumed',80)==f'{nonce} {e}\n'.encode(),'consumed token') + samples=[x for x in rows if x['kind']=='sample'];uniforms=[x for x in rows if x['kind']=='uniform'] + need(len(samples)==10 and len(uniforms)==6,'stage/uniform count') + need([x['point'] for x in samples]==list(range(10)) and [x['point'] for x in uniforms]==list(range(1,7)),'strict point order') + need([x['kind'] for x in rows[1:-1]]==['sample']+['uniform','sample']*6+['sample']*3,'query/draw/read sequence') + total=prior;context=samples[0]['context'];source_analysis=[] + for p,s in enumerate(samples): + need(s['point']==p and s['stage']==STAGES[p] and s['epoch']==e and s['pass_sequence']==e and s['context']==context and context!='0','pass/context identity') + need(s['result']=='READ_SUCCESS' and s['read_success'] is True and s['published'] is True and s['before']==s['after'],'GL/read/state acceptance') + for key in ('pre_errors','query_errors','read_errors','restore_errors'):need(s[key]=={'exhausted':False,'codes':[]},'error queue ambiguity') + need(s['client_version'] in (2,3) and re.fullmatch(rb'OpenGL ES 3\.([0-2])(?: [ -~]*)?',bytes.fromhex(s['gl_version_hex'])) is not None,'bounded actual GLES3 version') + need(s['gl_major']==3 and s['gl_minor']==int(bytes.fromhex(s['gl_version_hex'])[12:13]) and bool(bytes.fromhex(s['glsl_hex'])),'consistent native GL version/GLSL') + need(all(s.get(k) is True for k in ('version_queried','state_saved','bound','read_attempted','restore_attempted')),'observable exact read lifecycle') + for key in ('version_errors','capability_errors','state_errors'):need(s[key]=={'exhausted':False,'codes':[]},'capability/state error attribution') + a=s['attachment'];need(a[0]==0x8d41 and a[1]>0 and a[2]==0 and a[3:5]==s['native']['dimensions'] and s['status']==0x8cd5 and s['read_buffer']==0x8ce0 and s['samples']==0,'supported RBO attachment') + need(s['channels'][:3]==[8,8,8] and s['channels'][3] in (0,8),'actual alpha storage') + native=s['native'];need(native['fbo_rbo_tex'][1]==a[1] and all(int(native[key],16)>0 for key in ('fx','wlr','image')),'native attachment identity') + d=s['dmabuf'];need(d is not None and d['borrowed'] is True and d['dimensions']==native['dimensions'],'actual borrowed dmabuf') + metadata(dict(d,width=d['dimensions'][0],height=d['dimensions'][1])) + need(all(p.get('fstat_ok') is True and all(type(p.get(k)) is int and p[k]>=0 for k in ('fd','dev','ino')) for p in d['planes']),'borrowed FD fstat identity observed') + w,h=native['dimensions'];need((w,h)==FRAME,'prospective physical full-frame dimensions');n=w*h*4;need(00 and u['location']>=0 and u['shader_default_float']=='mediump' and u['varying']=='mediump','uniform native program/type/qualifier') + need(u['sampler']==[9729,9729,33071,33071] and len(u['precision_capability'])==3,'actual sampler/capability') + source_analysis.append(halfpixel(*u['native_texture_dimensions'],u['point']<=3,u['actual_bits'])) + need(end['raw_total']==total,'cumulative raw budget') + return {'epoch':e,'raw_total':total,'context':context,'observed_versions':[dict(egl_client_version=s['client_version'],gl_version=bytes.fromhex(s['gl_version_hex']).decode('ascii'),gl_major=s['gl_major'],gl_minor=s['gl_minor'],egl_gl_divergence=s['client_version']!=s['gl_major']) for s in samples],'uniform_comparison':source_analysis,'whole_field_causality':'NOT_PROVEN','native_present_buffer':'NOT_PROVEN','gpu_completion':'NOT_PROVEN'} +def gate(directory,gatefile,binary): + need(directory==ROOT and directory.resolve(strict=True)==ROOT,'fixed real acquisition path') + need(str(ROOT).startswith('/var/tmp/swayfx-mosaic-step20c-native-closure-') and ROOT.name=='acquisition','new LAB acquisition only') + owned(directory,0o700) + g=json.loads(bounded(gatefile,65536));need(g['source_review']=='ACCEPTED' and g['gl_observability']=='PASS' and g['runtime_authorized'] is True and g['diagnostic']=='GPU-G3' and g['acquisition_root']==str(ROOT),'independent external review/authorization gate') + need(hashlib.sha256(binary.read_bytes()).hexdigest()==g['binary_sha256'],'distinct reviewed binary identity') + return g + +def arm(directory,e,nonce,gatefile,binary): + gate(directory,gatefile,binary);need(len(nonce)==64 and all(x in '0123456789abcdef' for x in nonce),'nonce') + # Prior complete success is mandatory. An existing consumed/current epoch, + # failed A, or any unexpected future epoch prevents every subsequent arm. + total=0 + for prior in range(1,e):total=epoch(directory,prior,nonce,total)['raw_total'] + need(e in (1,2,3),'one-shot three epochs') + for future in range(e,5):need(not any(directory.glob(f'epoch-{future}.*')),'no retry/future token') + exclusive(directory/f'epoch-{e}.arm',f'{nonce} {e}\n'.encode()) + +def plan(): + return {'schema':'G3-followup-v1','status':'EXPERIMENTAL_SOURCE_ONLY_REVIEW_REQUIRED','diagnostic':'GPU-G3','max_raw_bytes':CAP,'epochs':['A','B','A-return'],'points':STAGES,'full_field':[0,0,800,600],'ROI':[0,0,800,600],'ROI_policy':'FULL_FIELD_NO_POSTHOC_MASK','requirements':['Independent R-GPU exact-source acceptance','GL observability acceptance','Explicit new pair and distinct binary identity','External one-shot ledger reservation before compositor spawn','Explicit source A/B/A client commands with original A byte identity','If A incomplete/rejected STOP; preserve bytes, no B, no retry'],'intrusive':['glGetError consumes nonrestorable queue','glReadPixels may synchronize timing'],'unobserved':['native presented physical buffer','GPU completion','GPU object lifetime','atomic generation'],'launches_gpu':False} +if __name__=='__main__': + p=argparse.ArgumentParser();p.add_argument('operation',choices=['prepare-plan','verify-epoch','arm']);p.add_argument('--epoch',type=int);p.add_argument('--nonce');p.add_argument('--gate',type=Path);p.add_argument('--binary',type=Path);a=p.parse_args() + if a.operation=='prepare-plan':print(json.dumps(plan(),indent=2)) + elif a.operation=='verify-epoch':print(json.dumps(epoch(ROOT,a.epoch,a.nonce),indent=2)) + else:arm(ROOT,a.epoch,a.nonce,a.gate,a.binary) diff --git a/tooling/probes/step20c-corec-v2-followup/gpu/protocols/wlr-layer-shell-unstable-v1.xml b/tooling/probes/step20c-corec-v2-followup/gpu/protocols/wlr-layer-shell-unstable-v1.xml new file mode 100644 index 0000000..d62fd51 --- /dev/null +++ b/tooling/probes/step20c-corec-v2-followup/gpu/protocols/wlr-layer-shell-unstable-v1.xml @@ -0,0 +1,390 @@ + + + + Copyright © 2017 Drew DeVault + + Permission to use, copy, modify, distribute, and sell this + software and its documentation for any purpose is hereby granted + without fee, provided that the above copyright notice appear in + all copies and that both that copyright notice and this permission + notice appear in supporting documentation, and that the name of + the copyright holders not be used in advertising or publicity + pertaining to distribution of the software without specific, + written prior permission. The copyright holders make no + representations about the suitability of this software for any + purpose. It is provided "as is" without express or implied + warranty. + + THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + THIS SOFTWARE. + + + + + Clients can use this interface to assign the surface_layer role to + wl_surfaces. Such surfaces are assigned to a "layer" of the output and + rendered with a defined z-depth respective to each other. They may also be + anchored to the edges and corners of a screen and specify input handling + semantics. This interface should be suitable for the implementation of + many desktop shell components, and a broad number of other applications + that interact with the desktop. + + + + + Create a layer surface for an existing surface. This assigns the role of + layer_surface, or raises a protocol error if another role is already + assigned. + + Creating a layer surface from a wl_surface which has a buffer attached + or committed is a client error, and any attempts by a client to attach + or manipulate a buffer prior to the first layer_surface.configure call + must also be treated as errors. + + After creating a layer_surface object and setting it up, the client + must perform an initial commit without any buffer attached. + The compositor will reply with a layer_surface.configure event. + The client must acknowledge it and is then allowed to attach a buffer + to map the surface. + + You may pass NULL for output to allow the compositor to decide which + output to use. Generally this will be the one that the user most + recently interacted with. + + Clients can specify a namespace that defines the purpose of the layer + surface. + + + + + + + + + + + + + + + + + These values indicate which layers a surface can be rendered in. They + are ordered by z depth, bottom-most first. Traditional shell surfaces + will typically be rendered between the bottom and top layers. + Fullscreen shell surfaces are typically rendered at the top layer. + Multiple surfaces can share a single layer, and ordering within a + single layer is undefined. + + + + + + + + + + + + + This request indicates that the client will not use the layer_shell + object any more. Objects that have been created through this instance + are not affected. + + + + + + + An interface that may be implemented by a wl_surface, for surfaces that + are designed to be rendered as a layer of a stacked desktop-like + environment. + + Layer surface state (layer, size, anchor, exclusive zone, + margin, interactivity) is double-buffered, and will be applied at the + time wl_surface.commit of the corresponding wl_surface is called. + + Attaching a null buffer to a layer surface unmaps it. + + Unmapping a layer_surface means that the surface cannot be shown by the + compositor until it is explicitly mapped again. The layer_surface + returns to the state it had right after layer_shell.get_layer_surface. + The client can re-map the surface by performing a commit without any + buffer attached, waiting for a configure event and handling it as usual. + + + + + Sets the size of the surface in surface-local coordinates. The + compositor will display the surface centered with respect to its + anchors. + + If you pass 0 for either value, the compositor will assign it and + inform you of the assignment in the configure event. You must set your + anchor to opposite edges in the dimensions you omit; not doing so is a + protocol error. Both values are 0 by default. + + Size is double-buffered, see wl_surface.commit. + + + + + + + + Requests that the compositor anchor the surface to the specified edges + and corners. If two orthogonal edges are specified (e.g. 'top' and + 'left'), then the anchor point will be the intersection of the edges + (e.g. the top left corner of the output); otherwise the anchor point + will be centered on that edge, or in the center if none is specified. + + Anchor is double-buffered, see wl_surface.commit. + + + + + + + Requests that the compositor avoids occluding an area with other + surfaces. The compositor's use of this information is + implementation-dependent - do not assume that this region will not + actually be occluded. + + A positive value is only meaningful if the surface is anchored to one + edge or an edge and both perpendicular edges. If the surface is not + anchored, anchored to only two perpendicular edges (a corner), anchored + to only two parallel edges or anchored to all edges, a positive value + will be treated the same as zero. + + A positive zone is the distance from the edge in surface-local + coordinates to consider exclusive. + + Surfaces that do not wish to have an exclusive zone may instead specify + how they should interact with surfaces that do. If set to zero, the + surface indicates that it would like to be moved to avoid occluding + surfaces with a positive exclusive zone. If set to -1, the surface + indicates that it would not like to be moved to accommodate for other + surfaces, and the compositor should extend it all the way to the edges + it is anchored to. + + For example, a panel might set its exclusive zone to 10, so that + maximized shell surfaces are not shown on top of it. A notification + might set its exclusive zone to 0, so that it is moved to avoid + occluding the panel, but shell surfaces are shown underneath it. A + wallpaper or lock screen might set their exclusive zone to -1, so that + they stretch below or over the panel. + + The default value is 0. + + Exclusive zone is double-buffered, see wl_surface.commit. + + + + + + + Requests that the surface be placed some distance away from the anchor + point on the output, in surface-local coordinates. Setting this value + for edges you are not anchored to has no effect. + + The exclusive zone includes the margin. + + Margin is double-buffered, see wl_surface.commit. + + + + + + + + + + Types of keyboard interaction possible for layer shell surfaces. The + rationale for this is twofold: (1) some applications are not interested + in keyboard events and not allowing them to be focused can improve the + desktop experience; (2) some applications will want to take exclusive + keyboard focus. + + + + + This value indicates that this surface is not interested in keyboard + events and the compositor should never assign it the keyboard focus. + + This is the default value, set for newly created layer shell surfaces. + + This is useful for e.g. desktop widgets that display information or + only have interaction with non-keyboard input devices. + + + + + Request exclusive keyboard focus if this surface is above the shell surface layer. + + For the top and overlay layers, the seat will always give + exclusive keyboard focus to the top-most layer which has keyboard + interactivity set to exclusive. If this layer contains multiple + surfaces with keyboard interactivity set to exclusive, the compositor + determines the one receiving keyboard events in an implementation- + defined manner. In this case, no guarantee is made when this surface + will receive keyboard focus (if ever). + + For the bottom and background layers, the compositor is allowed to use + normal focus semantics. + + This setting is mainly intended for applications that need to ensure + they receive all keyboard events, such as a lock screen or a password + prompt. + + + + + This requests the compositor to allow this surface to be focused and + unfocused by the user in an implementation-defined manner. The user + should be able to unfocus this surface even regardless of the layer + it is on. + + Typically, the compositor will want to use its normal mechanism to + manage keyboard focus between layer shell surfaces with this setting + and regular toplevels on the desktop layer (e.g. click to focus). + Nevertheless, it is possible for a compositor to require a special + interaction to focus or unfocus layer shell surfaces (e.g. requiring + a click even if focus follows the mouse normally, or providing a + keybinding to switch focus between layers). + + This setting is mainly intended for desktop shell components (e.g. + panels) that allow keyboard interaction. Using this option can allow + implementing a desktop shell that can be fully usable without the + mouse. + + + + + + + Set how keyboard events are delivered to this surface. By default, + layer shell surfaces do not receive keyboard events; this request can + be used to change this. + + This setting is inherited by child surfaces set by the get_popup + request. + + Layer surfaces receive pointer, touch, and tablet events normally. If + you do not want to receive them, set the input region on your surface + to an empty region. + + Keyboard interactivity is double-buffered, see wl_surface.commit. + + + + + + + This assigns an xdg_popup's parent to this layer_surface. This popup + should have been created via xdg_surface::get_popup with the parent set + to NULL, and this request must be invoked before committing the popup's + initial state. + + See the documentation of xdg_popup for more details about what an + xdg_popup is and how it is used. + + + + + + + When a configure event is received, if a client commits the + surface in response to the configure event, then the client + must make an ack_configure request sometime before the commit + request, passing along the serial of the configure event. + + If the client receives multiple configure events before it + can respond to one, it only has to ack the last configure event. + + A client is not required to commit immediately after sending + an ack_configure request - it may even ack_configure several times + before its next surface commit. + + A client may send multiple ack_configure requests before committing, but + only the last request sent before a commit indicates which configure + event the client really is responding to. + + + + + + + This request destroys the layer surface. + + + + + + The configure event asks the client to resize its surface. + + Clients should arrange their surface for the new states, and then send + an ack_configure request with the serial sent in this configure event at + some point before committing the new surface. + + The client is free to dismiss all but the last configure event it + received. + + The width and height arguments specify the size of the window in + surface-local coordinates. + + The size is a hint, in the sense that the client is free to ignore it if + it doesn't resize, pick a smaller size (to satisfy aspect ratio or + resize in steps of NxM pixels). If the client picks a smaller size and + is anchored to two opposite anchors (e.g. 'top' and 'bottom'), the + surface will be centered on this axis. + + If the width or height arguments are zero, it means the client should + decide its own window dimension. + + + + + + + + + The closed event is sent by the compositor when the surface will no + longer be shown. The output may have been destroyed or the user may + have asked for it to be removed. Further changes to the surface will be + ignored. The client should destroy the resource after receiving this + event, and create a new surface if they so choose. + + + + + + + + + + + + + + + + + + + + + + Change the layer that the surface is rendered on. + + Layer is double-buffered, see wl_surface.commit. + + + + + diff --git a/tooling/probes/step20c-corec-v2-followup/gpu/protocols/xdg-shell.xml b/tooling/probes/step20c-corec-v2-followup/gpu/protocols/xdg-shell.xml new file mode 100644 index 0000000..f8b78ee --- /dev/null +++ b/tooling/probes/step20c-corec-v2-followup/gpu/protocols/xdg-shell.xml @@ -0,0 +1,1420 @@ + + + + + Copyright © 2008-2013 Kristian Høgsberg + Copyright © 2013 Rafael Antognolli + Copyright © 2013 Jasper St. Pierre + Copyright © 2010-2013 Intel Corporation + Copyright © 2015-2017 Samsung Electronics Co., Ltd + Copyright © 2015-2017 Red Hat Inc. + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice (including the next + paragraph) shall be included in all copies or substantial portions of the + Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + + + + + The xdg_wm_base interface is exposed as a global object enabling clients + to turn their wl_surfaces into windows in a desktop environment. It + defines the basic functionality needed for clients and the compositor to + create windows that can be dragged, resized, maximized, etc, as well as + creating transient windows such as popup menus. + + + + + + + + + + + + + + + Destroy this xdg_wm_base object. + + Destroying a bound xdg_wm_base object while there are surfaces + still alive created by this xdg_wm_base object instance is illegal + and will result in a defunct_surfaces error. + + + + + + Create a positioner object. A positioner object is used to position + surfaces relative to some parent surface. See the interface description + and xdg_surface.get_popup for details. + + + + + + + This creates an xdg_surface for the given surface. An xdg_surface is + used as basis to define a role to a given surface, such as xdg_toplevel + or xdg_popup. It also manages functionality shared between xdg_surface + based surface roles. + + While xdg_surface itself is not a role, the corresponding surface may + only be assigned a role extending xdg_surface, such as xdg_toplevel or + xdg_popup. It is illegal to create an xdg_surface for a wl_surface which + already has anassigned role and this will result in a role error. + + See the documentation of xdg_surface for more details about what an + xdg_surface is and how it is used. + + + + + + + + A client must respond to a ping event with a pong request or + the client may be deemed unresponsive. See xdg_wm_base.ping + and xdg_wm_base.error.unresponsive. + + + + + + + The ping event asks the client if it's still alive. Pass the + serial specified in the event back to the compositor by sending + a "pong" request back with the specified serial. See xdg_wm_base.pong. + + Compositors can use this to determine if the client is still + alive. It's unspecified what will happen if the client doesn't + respond to the ping request, or in what timeframe. Clients should + try to respond in a reasonable amount of time. The “unresponsive” + error is provided for compositors that wish to disconnect unresponsive + clients. + + A compositor is free to ping in any way it wants, but a client must + always respond to any xdg_wm_base object it created. + + + + + + + + The xdg_positioner provides a collection of rules for the placement of a + child surface relative to a parent surface. Rules can be defined to ensure + the child surface remains within the visible area's borders, and to + specify how the child surface changes its position, such as sliding along + an axis, or flipping around a rectangle. These positioner-created rules are + constrained by the requirement that a child surface must intersect with or + be at least partially adjacent to its parent surface. + + See the various requests for details about possible rules. + + At the time of the request, the compositor makes a copy of the rules + specified by the xdg_positioner. Thus, after the request is complete the + xdg_positioner object can be destroyed or reused; further changes to the + object will have no effect on previous usages. + + For an xdg_positioner object to be considered complete, it must have a + non-zero size set by set_size, and a non-zero anchor rectangle set by + set_anchor_rect. Passing an incomplete xdg_positioner object when + positioning a surface raises an invalid_positioner error. + + + + + + + + + Notify the compositor that the xdg_positioner will no longer be used. + + + + + + Set the size of the surface that is to be positioned with the positioner + object. The size is in surface-local coordinates and corresponds to the + window geometry. See xdg_surface.set_window_geometry. + + If a zero or negative size is set the invalid_input error is raised. + + + + + + + + Specify the anchor rectangle within the parent surface that the child + surface will be placed relative to. The rectangle is relative to the + window geometry as defined by xdg_surface.set_window_geometry of the + parent surface. + + When the xdg_positioner object is used to position a child surface, the + anchor rectangle may not extend outside the window geometry of the + positioned child's parent surface. + + If a negative size is set the invalid_input error is raised. + + + + + + + + + + + + + + + + + + + + + + Defines the anchor point for the anchor rectangle. The specified anchor + is used to derive an anchor point that the child surface will be + positioned relative to. If a corner anchor is set (e.g. 'top_left' or + 'bottom_right'), the anchor point will be at the specified corner; + otherwise, the derived anchor point will be centered on the specified + edge, or in the center of the anchor rectangle if no edge is specified. + + + + + + + + + + + + + + + + + + + Defines in what direction a surface should be positioned, relative to + the anchor point of the parent surface. If a corner gravity is + specified (e.g. 'bottom_right' or 'top_left'), then the child surface + will be placed towards the specified gravity; otherwise, the child + surface will be centered over the anchor point on any axis that had no + gravity specified. If the gravity is not in the ‘gravity’ enum, an + invalid_input error is raised. + + + + + + + The constraint adjustment value define ways the compositor will adjust + the position of the surface, if the unadjusted position would result + in the surface being partly constrained. + + Whether a surface is considered 'constrained' is left to the compositor + to determine. For example, the surface may be partly outside the + compositor's defined 'work area', thus necessitating the child surface's + position be adjusted until it is entirely inside the work area. + + The adjustments can be combined, according to a defined precedence: 1) + Flip, 2) Slide, 3) Resize. + + + + Don't alter the surface position even if it is constrained on some + axis, for example partially outside the edge of an output. + + + + + Slide the surface along the x axis until it is no longer constrained. + + First try to slide towards the direction of the gravity on the x axis + until either the edge in the opposite direction of the gravity is + unconstrained or the edge in the direction of the gravity is + constrained. + + Then try to slide towards the opposite direction of the gravity on the + x axis until either the edge in the direction of the gravity is + unconstrained or the edge in the opposite direction of the gravity is + constrained. + + + + + Slide the surface along the y axis until it is no longer constrained. + + First try to slide towards the direction of the gravity on the y axis + until either the edge in the opposite direction of the gravity is + unconstrained or the edge in the direction of the gravity is + constrained. + + Then try to slide towards the opposite direction of the gravity on the + y axis until either the edge in the direction of the gravity is + unconstrained or the edge in the opposite direction of the gravity is + constrained. + + + + + Invert the anchor and gravity on the x axis if the surface is + constrained on the x axis. For example, if the left edge of the + surface is constrained, the gravity is 'left' and the anchor is + 'left', change the gravity to 'right' and the anchor to 'right'. + + If the adjusted position also ends up being constrained, the resulting + position of the flip_x adjustment will be the one before the + adjustment. + + + + + Invert the anchor and gravity on the y axis if the surface is + constrained on the y axis. For example, if the bottom edge of the + surface is constrained, the gravity is 'bottom' and the anchor is + 'bottom', change the gravity to 'top' and the anchor to 'top'. + + The adjusted position is calculated given the original anchor + rectangle and offset, but with the new flipped anchor and gravity + values. + + If the adjusted position also ends up being constrained, the resulting + position of the flip_y adjustment will be the one before the + adjustment. + + + + + Resize the surface horizontally so that it is completely + unconstrained. + + + + + Resize the surface vertically so that it is completely unconstrained. + + + + + + + Specify how the window should be positioned if the originally intended + position caused the surface to be constrained, meaning at least + partially outside positioning boundaries set by the compositor. The + adjustment is set by constructing a bitmask describing the adjustment to + be made when the surface is constrained on that axis. + + If no bit for one axis is set, the compositor will assume that the child + surface should not change its position on that axis when constrained. + + If more than one bit for one axis is set, the order of how adjustments + are applied is specified in the corresponding adjustment descriptions. + + The default adjustment is none. + + + + + + + Specify the surface position offset relative to the position of the + anchor on the anchor rectangle and the anchor on the surface. For + example if the anchor of the anchor rectangle is at (x, y), the surface + has the gravity bottom|right, and the offset is (ox, oy), the calculated + surface position will be (x + ox, y + oy). The offset position of the + surface is the one used for constraint testing. See + set_constraint_adjustment. + + An example use case is placing a popup menu on top of a user interface + element, while aligning the user interface element of the parent surface + with some user interface element placed somewhere in the popup surface. + + + + + + + + + + When set reactive, the surface is reconstrained if the conditions used + for constraining changed, e.g. the parent window moved. + + If the conditions changed and the popup was reconstrained, an + xdg_popup.configure event is sent with updated geometry, followed by an + xdg_surface.configure event. + + + + + + Set the parent window geometry the compositor should use when + positioning the popup. The compositor may use this information to + determine the future state the popup should be constrained using. If + this doesn't match the dimension of the parent the popup is eventually + positioned against, the behavior is undefined. + + The arguments are given in the surface-local coordinate space. + + + + + + + + Set the serial of an xdg_surface.configure event this positioner will be + used in response to. The compositor may use this information together + with set_parent_size to determine what future state the popup should be + constrained using. + + + + + + + + An interface that may be implemented by a wl_surface, for + implementations that provide a desktop-style user interface. + + It provides a base set of functionality required to construct user + interface elements requiring management by the compositor, such as + toplevel windows, menus, etc. The types of functionality are split into + xdg_surface roles. + + Creating an xdg_surface does not set the role for a wl_surface. In order + to map an xdg_surface, the client must create a role-specific object + using, e.g., get_toplevel, get_popup. The wl_surface for any given + xdg_surface can have at most one role, and may not be assigned any role + not based on xdg_surface. + + A role must be assigned before any other requests are made to the + xdg_surface object. + + The client must call wl_surface.commit on the corresponding wl_surface + for the xdg_surface state to take effect. + + Creating an xdg_surface from a wl_surface which has a buffer attached or + committed is a client error, and any attempts by a client to attach or + manipulate a buffer prior to the first xdg_surface.configure call must + also be treated as errors. + + After creating a role-specific object and setting it up (e.g. by sending + the title, app ID, size constraints, parent, etc), the client must + perform an initial commit without any buffer attached. The compositor + will reply with initial wl_surface state such as + wl_surface.preferred_buffer_scale followed by an xdg_surface.configure + event. The client must acknowledge it and is then allowed to attach a + buffer to map the surface. + + Mapping an xdg_surface-based role surface is defined as making it + possible for the surface to be shown by the compositor. Note that + a mapped surface is not guaranteed to be visible once it is mapped. + + For an xdg_surface to be mapped by the compositor, the following + conditions must be met: + (1) the client has assigned an xdg_surface-based role to the surface + (2) the client has set and committed the xdg_surface state and the + role-dependent state to the surface + (3) the client has committed a buffer to the surface + + A newly-unmapped surface is considered to have met condition (1) out + of the 3 required conditions for mapping a surface if its role surface + has not been destroyed, i.e. the client must perform the initial commit + again before attaching a buffer. + + + + + + + + + + + + + + Destroy the xdg_surface object. An xdg_surface must only be destroyed + after its role object has been destroyed, otherwise + a defunct_role_object error is raised. + + + + + + This creates an xdg_toplevel object for the given xdg_surface and gives + the associated wl_surface the xdg_toplevel role. + + See the documentation of xdg_toplevel for more details about what an + xdg_toplevel is and how it is used. + + + + + + + This creates an xdg_popup object for the given xdg_surface and gives + the associated wl_surface the xdg_popup role. + + If null is passed as a parent, a parent surface must be specified using + some other protocol, before committing the initial state. + + See the documentation of xdg_popup for more details about what an + xdg_popup is and how it is used. + + + + + + + + + The window geometry of a surface is its "visible bounds" from the + user's perspective. Client-side decorations often have invisible + portions like drop-shadows which should be ignored for the + purposes of aligning, placing and constraining windows. Note that + in some situations, compositors may clip rendering to the window + geometry, so the client should avoid putting functional elements + outside of it. + + The window geometry is double-buffered state, see wl_surface.commit. + + When maintaining a position, the compositor should treat the (x, y) + coordinate of the window geometry as the top left corner of the window. + A client changing the (x, y) window geometry coordinate should in + general not alter the position of the window. + + Once the window geometry of the surface is set, it is not possible to + unset it, and it will remain the same until set_window_geometry is + called again, even if a new subsurface or buffer is attached. + + If never set, the value is the full bounds of the surface, + including any subsurfaces. This updates dynamically on every + commit. This unset is meant for extremely simple clients. + + The arguments are given in the surface-local coordinate space of + the wl_surface associated with this xdg_surface, and may extend outside + of the wl_surface itself to mark parts of the subsurface tree as part of + the window geometry. + + When applied, the effective window geometry will be the set window + geometry clamped to the bounding rectangle of the combined + geometry of the surface of the xdg_surface and the associated + subsurfaces. + + The effective geometry will not be recalculated unless a new call to + set_window_geometry is done and the new pending surface state is + subsequently applied. + + The width and height of the effective window geometry must be + greater than zero. Setting an invalid size will raise an + invalid_size error. + + + + + + + + + + When a configure event is received, if a client commits the + surface in response to the configure event, then the client + must make an ack_configure request sometime before the commit + request, passing along the serial of the configure event. + + For instance, for toplevel surfaces the compositor might use this + information to move a surface to the top left only when the client has + drawn itself for the maximized or fullscreen state. + + If the client receives multiple configure events before it + can respond to one, it only has to ack the last configure event. + Acking a configure event that was never sent raises an invalid_serial + error. + + A client is not required to commit immediately after sending + an ack_configure request - it may even ack_configure several times + before its next surface commit. + + A client may send multiple ack_configure requests before committing, but + only the last request sent before a commit indicates which configure + event the client really is responding to. + + Sending an ack_configure request consumes the serial number sent with + the request, as well as serial numbers sent by all configure events + sent on this xdg_surface prior to the configure event referenced by + the committed serial. + + It is an error to issue multiple ack_configure requests referencing a + serial from the same configure event, or to issue an ack_configure + request referencing a serial from a configure event issued before the + event identified by the last ack_configure request for the same + xdg_surface. Doing so will raise an invalid_serial error. + + + + + + + The configure event marks the end of a configure sequence. A configure + sequence is a set of one or more events configuring the state of the + xdg_surface, including the final xdg_surface.configure event. + + Where applicable, xdg_surface surface roles will during a configure + sequence extend this event as a latched state sent as events before the + xdg_surface.configure event. Such events should be considered to make up + a set of atomically applied configuration states, where the + xdg_surface.configure commits the accumulated state. + + Clients should arrange their surface for the new states, and then send + an ack_configure request with the serial sent in this configure event at + some point before committing the new surface. + + If the client receives multiple configure events before it can respond + to one, it is free to discard all but the last event it received. + + + + + + + + + This interface defines an xdg_surface role which allows a surface to, + among other things, set window-like properties such as maximize, + fullscreen, and minimize, set application-specific metadata like title and + id, and well as trigger user interactive operations such as interactive + resize and move. + + An xdg_toplevel by default is responsible for providing the full intended + visual representation of the toplevel, which depending on the window + state, may mean things like a title bar, window controls and drop shadow. + + Unmapping an xdg_toplevel means that the surface cannot be shown + by the compositor until it is explicitly mapped again. + All active operations (e.g., move, resize) are canceled and all + attributes (e.g. title, state, stacking, ...) are discarded for + an xdg_toplevel surface when it is unmapped. The xdg_toplevel returns to + the state it had right after xdg_surface.get_toplevel. The client + can re-map the toplevel by performing a commit without any buffer + attached, waiting for a configure event and handling it as usual (see + xdg_surface description). + + Attaching a null buffer to a toplevel unmaps the surface. + + + + + This request destroys the role surface and unmaps the surface; + see "Unmapping" behavior in interface section for details. + + + + + + + + + + + + Set the "parent" of this surface. This surface should be stacked + above the parent surface and all other ancestor surfaces. + + Parent surfaces should be set on dialogs, toolboxes, or other + "auxiliary" surfaces, so that the parent is raised when the dialog + is raised. + + Setting a null parent for a child surface unsets its parent. Setting + a null parent for a surface which currently has no parent is a no-op. + + Only mapped surfaces can have child surfaces. Setting a parent which + is not mapped is equivalent to setting a null parent. If a surface + becomes unmapped, its children's parent is set to the parent of + the now-unmapped surface. If the now-unmapped surface has no parent, + its children's parent is unset. If the now-unmapped surface becomes + mapped again, its parent-child relationship is not restored. + + The parent toplevel must not be one of the child toplevel's + descendants, and the parent must be different from the child toplevel, + otherwise the invalid_parent protocol error is raised. + + + + + + + Set a short title for the surface. + + This string may be used to identify the surface in a task bar, + window list, or other user interface elements provided by the + compositor. + + The string must be encoded in UTF-8. + + + + + + + Set an application identifier for the surface. + + The app ID identifies the general class of applications to which + the surface belongs. The compositor can use this to group multiple + surfaces together, or to determine how to launch a new application. + + For D-Bus activatable applications, the app ID is used as the D-Bus + service name. + + The compositor shell will try to group application surfaces together + by their app ID. As a best practice, it is suggested to select app + ID's that match the basename of the application's .desktop file. + For example, "org.freedesktop.FooViewer" where the .desktop file is + "org.freedesktop.FooViewer.desktop". + + Like other properties, a set_app_id request can be sent after the + xdg_toplevel has been mapped to update the property. + + See the desktop-entry specification [0] for more details on + application identifiers and how they relate to well-known D-Bus + names and .desktop files. + + [0] https://standards.freedesktop.org/desktop-entry-spec/ + + + + + + + Clients implementing client-side decorations might want to show + a context menu when right-clicking on the decorations, giving the + user a menu that they can use to maximize or minimize the window. + + This request asks the compositor to pop up such a window menu at + the given position, relative to the local surface coordinates of + the parent surface. There are no guarantees as to what menu items + the window menu contains, or even if a window menu will be drawn + at all. + + This request must be used in response to some sort of user action + like a button press, key press, or touch down event. + + + + + + + + + + Start an interactive, user-driven move of the surface. + + This request must be used in response to some sort of user action + like a button press, key press, or touch down event. The passed + serial is used to determine the type of interactive move (touch, + pointer, etc). + + The server may ignore move requests depending on the state of + the surface (e.g. fullscreen or maximized), or if the passed serial + is no longer valid. + + If triggered, the surface will lose the focus of the device + (wl_pointer, wl_touch, etc) used for the move. It is up to the + compositor to visually indicate that the move is taking place, such as + updating a pointer cursor, during the move. There is no guarantee + that the device focus will return when the move is completed. + + + + + + + + These values are used to indicate which edge of a surface + is being dragged in a resize operation. + + + + + + + + + + + + + + + Start a user-driven, interactive resize of the surface. + + This request must be used in response to some sort of user action + like a button press, key press, or touch down event. The passed + serial is used to determine the type of interactive resize (touch, + pointer, etc). + + The server may ignore resize requests depending on the state of + the surface (e.g. fullscreen or maximized). + + If triggered, the client will receive configure events with the + "resize" state enum value and the expected sizes. See the "resize" + enum value for more details about what is required. The client + must also acknowledge configure events using "ack_configure". After + the resize is completed, the client will receive another "configure" + event without the resize state. + + If triggered, the surface also will lose the focus of the device + (wl_pointer, wl_touch, etc) used for the resize. It is up to the + compositor to visually indicate that the resize is taking place, + such as updating a pointer cursor, during the resize. There is no + guarantee that the device focus will return when the resize is + completed. + + The edges parameter specifies how the surface should be resized, and + is one of the values of the resize_edge enum. Values not matching + a variant of the enum will cause the invalid_resize_edge protocol error. + The compositor may use this information to update the surface position + for example when dragging the top left corner. The compositor may also + use this information to adapt its behavior, e.g. choose an appropriate + cursor image. + + + + + + + + + The different state values used on the surface. This is designed for + state values like maximized, fullscreen. It is paired with the + configure event to ensure that both the client and the compositor + setting the state can be synchronized. + + States set in this way are double-buffered, see wl_surface.commit. + + + + The surface is maximized. The window geometry specified in the configure + event must be obeyed by the client, or the xdg_wm_base.invalid_surface_state + error is raised. + + The client should draw without shadow or other + decoration outside of the window geometry. + + + + + The surface is fullscreen. The window geometry specified in the + configure event is a maximum; the client cannot resize beyond it. For + a surface to cover the whole fullscreened area, the geometry + dimensions must be obeyed by the client. For more details, see + xdg_toplevel.set_fullscreen. + + + + + The surface is being resized. The window geometry specified in the + configure event is a maximum; the client cannot resize beyond it. + Clients that have aspect ratio or cell sizing configuration can use + a smaller size, however. + + + + + Client window decorations should be painted as if the window is + active. Do not assume this means that the window actually has + keyboard or pointer focus. + + + + + The window is currently in a tiled layout and the left edge is + considered to be adjacent to another part of the tiling grid. + + The client should draw without shadow or other decoration outside of + the window geometry on the left edge. + + + + + The window is currently in a tiled layout and the right edge is + considered to be adjacent to another part of the tiling grid. + + The client should draw without shadow or other decoration outside of + the window geometry on the right edge. + + + + + The window is currently in a tiled layout and the top edge is + considered to be adjacent to another part of the tiling grid. + + The client should draw without shadow or other decoration outside of + the window geometry on the top edge. + + + + + The window is currently in a tiled layout and the bottom edge is + considered to be adjacent to another part of the tiling grid. + + The client should draw without shadow or other decoration outside of + the window geometry on the bottom edge. + + + + + The surface is currently not ordinarily being repainted; for + example because its content is occluded by another window, or its + outputs are switched off due to screen locking. + + + + + The left edge of the window is currently constrained, meaning it + shouldn't attempt to resize from that edge. It can for example mean + it's tiled next to a monitor edge on the constrained side of the + window. + + + + + The right edge of the window is currently constrained, meaning it + shouldn't attempt to resize from that edge. It can for example mean + it's tiled next to a monitor edge on the constrained side of the + window. + + + + + The top edge of the window is currently constrained, meaning it + shouldn't attempt to resize from that edge. It can for example mean + it's tiled next to a monitor edge on the constrained side of the + window. + + + + + The bottom edge of the window is currently constrained, meaning it + shouldn't attempt to resize from that edge. It can for example mean + it's tiled next to a monitor edge on the constrained side of the + window. + + + + + + + Set a maximum size for the window. + + The client can specify a maximum size so that the compositor does + not try to configure the window beyond this size. + + The width and height arguments are in window geometry coordinates. + See xdg_surface.set_window_geometry. + + Values set in this way are double-buffered, see wl_surface.commit. + + The compositor can use this information to allow or disallow + different states like maximize or fullscreen and draw accurate + animations. + + Similarly, a tiling window manager may use this information to + place and resize client windows in a more effective way. + + The client should not rely on the compositor to obey the maximum + size. The compositor may decide to ignore the values set by the + client and request a larger size. + + If never set, or a value of zero in the request, means that the + client has no expected maximum size in the given dimension. + As a result, a client wishing to reset the maximum size + to an unspecified state can use zero for width and height in the + request. + + Requesting a maximum size to be smaller than the minimum size of + a surface is illegal and will result in an invalid_size error. + + The width and height must be greater than or equal to zero. Using + strictly negative values for width or height will result in an + invalid_size error. + + + + + + + + Set a minimum size for the window. + + The client can specify a minimum size so that the compositor does + not try to configure the window below this size. + + The width and height arguments are in window geometry coordinates. + See xdg_surface.set_window_geometry. + + Values set in this way are double-buffered, see wl_surface.commit. + + The compositor can use this information to allow or disallow + different states like maximize or fullscreen and draw accurate + animations. + + Similarly, a tiling window manager may use this information to + place and resize client windows in a more effective way. + + The client should not rely on the compositor to obey the minimum + size. The compositor may decide to ignore the values set by the + client and request a smaller size. + + If never set, or a value of zero in the request, means that the + client has no expected minimum size in the given dimension. + As a result, a client wishing to reset the minimum size + to an unspecified state can use zero for width and height in the + request. + + Requesting a minimum size to be larger than the maximum size of + a surface is illegal and will result in an invalid_size error. + + The width and height must be greater than or equal to zero. Using + strictly negative values for width and height will result in an + invalid_size error. + + + + + + + + Maximize the surface. + + After requesting that the surface should be maximized, the compositor + will respond by emitting a configure event. Whether this configure + actually sets the window maximized is subject to compositor policies. + The client must then update its content, drawing in the configured + state. The client must also acknowledge the configure when committing + the new content (see ack_configure). + + It is up to the compositor to decide how and where to maximize the + surface, for example which output and what region of the screen should + be used. + + If the surface was already maximized, the compositor will still emit + a configure event with the "maximized" state. + + If the surface is in a fullscreen state, this request has no direct + effect. It may alter the state the surface is returned to when + unmaximized unless overridden by the compositor. + + + + + + Unmaximize the surface. + + After requesting that the surface should be unmaximized, the compositor + will respond by emitting a configure event. Whether this actually + un-maximizes the window is subject to compositor policies. + If available and applicable, the compositor will include the window + geometry dimensions the window had prior to being maximized in the + configure event. The client must then update its content, drawing it in + the configured state. The client must also acknowledge the configure + when committing the new content (see ack_configure). + + It is up to the compositor to position the surface after it was + unmaximized; usually the position the surface had before maximizing, if + applicable. + + If the surface was already not maximized, the compositor will still + emit a configure event without the "maximized" state. + + If the surface is in a fullscreen state, this request has no direct + effect. It may alter the state the surface is returned to when + unmaximized unless overridden by the compositor. + + + + + + Make the surface fullscreen. + + After requesting that the surface should be fullscreened, the + compositor will respond by emitting a configure event. Whether the + client is actually put into a fullscreen state is subject to compositor + policies. The client must also acknowledge the configure when + committing the new content (see ack_configure). + + The output passed by the request indicates the client's preference as + to which display it should be set fullscreen on. If this value is NULL, + it's up to the compositor to choose which display will be used to map + this surface. + + If the surface doesn't cover the whole output, the compositor will + position the surface in the center of the output and compensate with + border fill covering the rest of the output. The content of the + border fill is undefined, but should be assumed to be in some way that + attempts to blend into the surrounding area (e.g. solid black). + + If the fullscreened surface is not opaque, the compositor must make + sure that other screen content not part of the same surface tree (made + up of subsurfaces, popups or similarly coupled surfaces) are not + visible below the fullscreened surface. + + + + + + + Make the surface no longer fullscreen. + + After requesting that the surface should be unfullscreened, the + compositor will respond by emitting a configure event. + Whether this actually removes the fullscreen state of the client is + subject to compositor policies. + + Making a surface unfullscreen sets states for the surface based on the following: + * the state(s) it may have had before becoming fullscreen + * any state(s) decided by the compositor + * any state(s) requested by the client while the surface was fullscreen + + The compositor may include the previous window geometry dimensions in + the configure event, if applicable. + + The client must also acknowledge the configure when committing the new + content (see ack_configure). + + + + + + Request that the compositor minimize your surface. There is no + way to know if the surface is currently minimized, nor is there + any way to unset minimization on this surface. + + If you are looking to throttle redrawing when minimized, please + instead use the wl_surface.frame event for this, as this will + also work with live previews on windows in Alt-Tab, Expose or + similar compositor features. + + + + + + This configure event asks the client to resize its toplevel surface or + to change its state. The configured state should not be applied + immediately. See xdg_surface.configure for details. + + The width and height arguments specify a hint to the window + about how its surface should be resized in window geometry + coordinates. See set_window_geometry. + + If the width or height arguments are zero, it means the client + should decide its own window dimension. This may happen when the + compositor needs to configure the state of the surface but doesn't + have any information about any previous or expected dimension. + + The states listed in the event specify how the width/height + arguments should be interpreted, and possibly how it should be + drawn. + + The states are sent as an array of 32-bit unsigned integers in + native endianness. State values are defined in the state enum. + + Clients must send an ack_configure in response to this event. See + xdg_surface.configure and xdg_surface.ack_configure for details. + + + + + + + + + The close event is sent by the compositor when the user + wants the surface to be closed. This should be equivalent to + the user clicking the close button in client-side decorations, + if your application has any. + + This is only a request that the user intends to close the + window. The client may choose to ignore this request, or show + a dialog to ask the user to save their data, etc. + + + + + + + + The configure_bounds event may be sent prior to a xdg_toplevel.configure + event to communicate the bounds a window geometry size is recommended + to constrain to. + + The passed width and height are in surface coordinate space. If width + and height are 0, it means bounds is unknown and equivalent to as if no + configure_bounds event was ever sent for this surface. + + The bounds can for example correspond to the size of a monitor excluding + any panels or other shell components, so that a surface isn't created in + a way that it cannot fit. + + The bounds may change at any point, and in such a case, a new + xdg_toplevel.configure_bounds will be sent, followed by + xdg_toplevel.configure and xdg_surface.configure. + + + + + + + + + + + + + + + + + This event advertises the capabilities supported by the compositor. If + a capability isn't supported, clients should hide or disable the UI + elements that expose this functionality. For instance, if the + compositor doesn't advertise support for minimized toplevels, a button + triggering the set_minimized request should not be displayed. + + The compositor will ignore requests it doesn't support. For instance, + a compositor which doesn't advertise support for minimized will ignore + set_minimized requests. + + Compositors must send this event once before the first + xdg_surface.configure event. When the capabilities change, compositors + must send this event again and then send an xdg_surface.configure + event. + + The configured state should not be applied immediately. See + xdg_surface.configure for details. + + The capabilities are sent as an array of 32-bit unsigned integers in + native endianness. Capability values are defined in the wm_capabilities enum. + + + + + + + + A popup surface is a short-lived, temporary surface. It can be used to + implement for example menus, popovers, tooltips and other similar user + interface concepts. + + A popup can be made to take an explicit grab. See xdg_popup.grab for + details. + + When the popup is dismissed, a popup_done event will be sent out, and at + the same time the surface will be unmapped. See the xdg_popup.popup_done + event for details. + + Explicitly destroying the xdg_popup object will also dismiss the popup and + unmap the surface. Clients that want to dismiss the popup when another + surface of their own is clicked should dismiss the popup using the destroy + request. + + A newly created xdg_popup will be stacked on top of all previously created + xdg_popup surfaces associated with the same xdg_toplevel. + + The parent of an xdg_popup must be mapped (see the xdg_surface + description) before the xdg_popup itself. + + The client must call wl_surface.commit on the corresponding wl_surface + for the xdg_popup state to take effect. + + + + + + + + + This destroys the popup. Explicitly destroying the xdg_popup + object will also dismiss the popup, and unmap the surface. + + If this xdg_popup is not the "topmost" popup, the + xdg_wm_base.not_the_topmost_popup protocol error will be sent. + + + + + + This request makes the created popup take an explicit grab. An explicit + grab will be dismissed when the user dismisses the popup, or when the + client destroys the xdg_popup. This can be done by the user clicking + outside the surface, using the keyboard, or even locking the screen + through closing the lid or a timeout. + + If the compositor denies the grab, the popup will be immediately + dismissed. + + This request must be used in response to some sort of user action like a + button press, key press, or touch down event. The serial number of the + event should be passed as 'serial'. + + The parent of a grabbing popup must either be an xdg_toplevel surface or + another xdg_popup with an explicit grab. If the parent is another + xdg_popup it means that the popups are nested, with this popup now being + the topmost popup. + + Nested popups must be destroyed in the reverse order they were created + in, e.g. the only popup you are allowed to destroy at all times is the + topmost one. + + When compositors choose to dismiss a popup, they may dismiss every + nested grabbing popup as well. When a compositor dismisses popups, it + will follow the same dismissing order as required from the client. + + If the topmost grabbing popup is destroyed, the grab will be returned to + the parent of the popup, if that parent previously had an explicit grab. + + If the parent is a grabbing popup which has already been dismissed, this + popup will be immediately dismissed. If the parent is a popup that did + not take an explicit grab, an error will be raised. + + During a popup grab, the client owning the grab will receive pointer + and touch events for all their surfaces as normal (similar to an + "owner-events" grab in X11 parlance), while the top most grabbing popup + will always have keyboard focus. + + + + + + + + This event asks the popup surface to configure itself given the + configuration. The configured state should not be applied immediately. + See xdg_surface.configure for details. + + The x and y arguments represent the position the popup was placed at + given the xdg_positioner rule, relative to the upper left corner of the + window geometry of the parent surface. + + For version 2 or older, the configure event for an xdg_popup is only + ever sent once for the initial configuration. Starting with version 3, + it may be sent again if the popup is setup with an xdg_positioner with + set_reactive requested, or in response to xdg_popup.reposition requests. + + + + + + + + + + The popup_done event is sent out when a popup is dismissed by the + compositor. The client should destroy the xdg_popup object at this + point. + + + + + + + + Reposition an already-mapped popup. The popup will be placed given the + details in the passed xdg_positioner object, and a + xdg_popup.repositioned followed by xdg_popup.configure and + xdg_surface.configure will be emitted in response. Any parameters set + by the previous positioner will be discarded. + + The passed token will be sent in the corresponding + xdg_popup.repositioned event. The new popup position will not take + effect until the corresponding configure event is acknowledged by the + client. See xdg_popup.repositioned for details. The token itself is + opaque, and has no other special meaning. + + If multiple reposition requests are sent, the compositor may skip all + but the last one. + + If the popup is repositioned in response to a configure event for its + parent, the client should send an xdg_positioner.set_parent_configure + and possibly an xdg_positioner.set_parent_size request to allow the + compositor to properly constrain the popup. + + If the popup is repositioned together with a parent that is being + resized, but not in response to a configure event, the client should + send an xdg_positioner.set_parent_size request. + + + + + + + + The repositioned event is sent as part of a popup configuration + sequence, together with xdg_popup.configure and lastly + xdg_surface.configure to notify the completion of a reposition request. + + The repositioned event is to notify about the completion of a + xdg_popup.reposition request. The token argument is the token passed + in the xdg_popup.reposition request. + + Immediately after this event is emitted, xdg_popup.configure and + xdg_surface.configure will be sent with the updated size and position, + as well as a new configure serial. + + The client should optionally update the content of the popup, but must + acknowledge the new popup configuration for the new position to take + effect. See xdg_surface.ack_configure for details. + + + + + + diff --git a/tooling/probes/step20c-corec-v2-followup/gpu/reproduce.py b/tooling/probes/step20c-corec-v2-followup/gpu/reproduce.py new file mode 100644 index 0000000..6bfad88 --- /dev/null +++ b/tooling/probes/step20c-corec-v2-followup/gpu/reproduce.py @@ -0,0 +1,73 @@ +"""Assemble private G3 sources, never build or launch a compositor.""" +import argparse +import ctypes +import hashlib +import json +import os +from pathlib import Path +import resource +import shutil +import stat +import subprocess + +KIT = Path(__file__).resolve().parent +PARENT = KIT.parents[1] / 'step20c-corec-v2' +PATCH_SHA = '77d7445ceaf643fd5cc708d31c3f2c77feaeb80f8130c419c55e5514ecc8a75d' + + +def sha(p): + return hashlib.sha256(p.read_bytes()).hexdigest() + + +def tree(p): + return {str(f.relative_to(p)): {'sha256': sha(f), 'mode': oct(stat.S_IMODE(f.stat().st_mode))} + for f in sorted(p.rglob('*')) if f.is_file() and '.git' not in f.relative_to(p).parts} + + +def main(): + resource.setrlimit(resource.RLIMIT_CORE, (0, 0)) + assert ctypes.CDLL(None).prctl(4, 0, 0, 0, 0) == 0 + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--lab', type=Path, required=True) + parser.add_argument('--core', type=Path, required=True) + parser.add_argument('--destination', type=Path, required=True) + a = parser.parse_args() + lab, core, dest = a.lab.resolve(strict=True), a.core.resolve(strict=True), a.destination.resolve() + assert str(lab).startswith('/var/tmp/swayfx-mosaic-step20c-native-closure-') + assert lab.stat().st_uid == os.geteuid() and stat.S_IMODE(lab.stat().st_mode) == 0o700 + assert dest.is_relative_to(lab) and not dest.exists() and not dest.is_relative_to(core) + before = tree(core) + expected = json.loads((PARENT / 'provenance.json').read_text())['components']['scene']['files'] + assert before == expected, 'complete CORE bytes/modes differ from versioned provenance' + patch = PARENT / 'gpu/g2-native.patch' + assert sha(patch) == PATCH_SHA + shutil.copytree(core, dest, ignore=shutil.ignore_patterns('.git')) + result = subprocess.run(['patch', '--batch', '--forward', '-p1', '-i', str(patch)], + cwd=dest, capture_output=True, text=True) + assert result.returncode == 0, result.stdout + result.stderr + # WHY: hook placement is frozen G2 instrumentation; only admission and + # observability headers differ. CONTRACT: no shader or CORE source mutation. + native = dest / 'render/fx_renderer' + delta = KIT / 'g3-admission.patch' + result = subprocess.run(['patch', '--batch', '--forward', '-p1', '-i', str(delta)], + cwd=dest, capture_output=True, text=True) + assert result.returncode == 0, result.stdout + result.stderr + for name in ('g2_gl.h', 'g2_native.h'): + assert sha(KIT / name) == sha(native / name), 'delta/header mismatch' + assert sha(native / 'fx_pass.c') == 'c2886ef259fc91431dfe97841387082420ab80a0fe2f07e7dcdb71fc95731181' + after = tree(dest) + changed = sorted(n for n in before.keys() | after.keys() if before.get(n) != after.get(n)) + assert changed == ['render/fx_renderer/fx_pass.c', 'render/fx_renderer/g2_gl.h', 'render/fx_renderer/g2_native.h'] + assert tree(core) == before + receipt = {'schema': 'G3-source-assembly-v1', 'status': 'EXPERIMENTAL_SOURCE_ONLY', + 'review': 'PENDING_INDEPENDENT', 'runtime': 'NOT_RUN', 'build': 'NOT_RUN', + 'core_manifest_sha256': sha(PARENT / 'provenance.json'), + 'g2_patch_sha256': sha(patch), 'g3_delta_sha256': sha(delta), 'changed': {n: after[n] for n in changed}, + 'core_unchanged': True, 'acquisition_root': str(lab / 'gpu-work/acquisition'), + 'G3_ROOT_compile_definition_required': True} + (dest.parent / (dest.name + '-receipt.json')).write_text(json.dumps(receipt, indent=2) + '\n') + print(json.dumps(receipt, indent=2)) + + +if __name__ == '__main__': + main() diff --git a/tooling/probes/step20c-corec-v2-followup/gpu/scenario.py b/tooling/probes/step20c-corec-v2-followup/gpu/scenario.py new file mode 100644 index 0000000..1fed80e --- /dev/null +++ b/tooling/probes/step20c-corec-v2-followup/gpu/scenario.py @@ -0,0 +1,68 @@ +"""Executable G3 scenario adapter. Import has NO run/arm/spawn side effect. +The parent binds an accepted new diagnostic pair Session and reserves its own G3 ledger. +The common Session owns its private sockets, process groups and timeout cleanup. +""" +import hashlib,json,time +from pathlib import Path +from PIL import ImageChops +from protocol import ROOT,arm,epoch,gate,need + +def exact_rgb(a,b): + need(a.mode==b.mode=='RGB' and a.size==b.size,'whole-output homologous RGB') + d=ImageChops.difference(a,b) + # Exact all-output zero tolerance. This is a result, not an admission mask. + return {'equal':d.getbbox() is None,'bbox':list(d.getbbox()) if d.getbbox() else None,'extrema':[list(x) for x in d.getextrema()],'pixels':a.width*a.height,'difference_count':sum(any(p) for p in d.getdata()),'differences_per_row':[sum(any(d.getpixel((x,y))) for x in range(a.width)) for y in range(a.height)],'tolerance':0} +def run(s,binding): + """Must be called once AFTER external review+ledger binding, BEFORE s.start. + binding: gate_file, binary, client, nonce, source_manifest_sha256, + G3_ledger_reserved=True, reviewed_scene_sha256. All operations debit s.ledger. + No retry path; an exception before epoch A completes bypasses B/A-return. + """ + need(binding.get('G3_ledger_reserved') is True,'external closed-ledger reservation absent') + need(s.spec['id']=='GPU-G3' and s.spec['binary']=='gpu','distinct G3 Session identity/static pair') + need(s.auth['binary']==binding['binary'],'accepted exact third executable') + need(s.auth['manifest_sha256']==binding['source_manifest_sha256'],'frozen G3 manifest identity') + gate(ROOT,Path(binding['gate_file']),Path(binding['binary'])) + need(not any(ROOT.glob('epoch-*')),'fresh one-shot publication directory') + nonce=binding['nonce'];s.env.update(MOSAIC_G3_DIR=str(ROOT),MOSAIC_G3_NONCE=nonce,WLR_RENDERER='gles2') + s.start() + # Session captures actual executable hash, mapped libraries, renderer and + # explicit private sockets. Its caller owns finally/cleanup and verdict. + client=Path(binding['client']);need(client.is_file() and hashlib.sha256(client.read_bytes()).hexdigest()==binding['client_sha256'],'new client identity') + s.cmd('workspace 2');s.spawn('g2-reference',[client,'reference']);s.wait(lambda:s.find('g2-reference'));rid=s.find('g2-reference')['id'];s.cmd(f'[con_id={rid}] border none') + s.cmd('workspace 1');s.cmd('layout mosaic');s.cmd('gaps inner 0') + s.spawn('g2-background',[client,'background']);s.wait(lambda:'BACKGROUND_CONFIGURED ' in s.log('g2-background')) + s.spawn('g2-glass',[client,'glass']);s.wait(lambda:s.find('g2-glass'));gid=s.find('g2-glass')['id'];s.cmd(f'[con_id={gid}] border none') + s.cmd('blur enable');s.cmd('blur_xray disable');s.cmd('blur_passes 3');s.cmd('blur_radius 5');s.cmd('blur_brightness 1');s.cmd('blur_contrast 1');s.cmd('blur_noise 0');s.cmd('blur_saturation 1') + # Settle setup before the first arm. This sleep carries no proof of GPU + # completion; exact native source admission rejects any wrong candidate. + time.sleep(.35) + reference=s.capture('G3-H2-baseline1','HEADLESS-2');time.sleep(.15) + reference2=s.capture('G3-H2-baseline2','HEADLESS-2') + baseline=exact_rgb(reference,reference2);s.save('G3-H2-baseline',baseline) + need(baseline['equal'],'static whole H2 baseline changed before A') + captures=[];validated=[];total=0 + for e,command in enumerate(('G3-A','G3-B','G3-A-return'),1): + s.debit('mutation');s.record('G3-epoch-reservation',epoch=e,raw_cap=256*1024*1024) + arm(ROOT,e,nonce,Path(binding['gate_file']),Path(binding['binary'])) + s.proc['g2-background'].stdin.write(command+'\n');s.proc['g2-background'].stdin.flush() + s.wait(lambda:f'G3_SOURCE_BYTES command={command} ' in s.log('g2-background')) + path=ROOT/f'epoch-{e}.jsonl' + def complete_or_rejected(): + if not path.exists():return False + data=path.read_bytes();need(len(data)<=1024*1024,'bounded metadata while waiting') + if b'"kind":"rejected"' in data:raise RuntimeError(f'epoch {e} rejected; STOP, no next command/retry') + return data.endswith(b'\n') and b'"kind":"end"' in data + s.wait(complete_or_rejected) + # Failure here exits before subsequent epoch token or source command. + result=epoch(ROOT,e,nonce,total);total=result['raw_total'];validated.append(result) + pictures=s.pair_capture(f'G3-{command}');captures.append(pictures);s.snapshot(f'G3-{command}') + h2=exact_rgb(reference,pictures[1]);s.save(f'G3-{command}-wholeH2',h2) + need(h2['equal'],'whole H2 changed: rejected epoch, STOP without retry') + s.record('G3-epoch-accepted',epoch=e,command=command,native_raw_total=total) + whole_h1=exact_rgb(captures[0][0],captures[2][0]);whole_h2=[exact_rgb(captures[0][1],x[1]) for x in captures[1:]] + sa=(ROOT/'epoch-1-point-0.rgba').read_bytes();sr=(ROOT/'epoch-3-point-0.rgba').read_bytes() + result={'schema':'G3-followup-v1','epochs':validated,'native_source_A_Areturn_exact':sa==sr,'whole_H1_A_Areturn':whole_h1,'whole_H2_baseline':baseline,'whole_H2_A_B_Areturn':whole_h2,'whole_field_causality':'NOT_PROVEN','physical_present_buffer':'NOT_PROVEN','gpu_completion':'NOT_PROVEN','atomic_generation':'NOT_PROVEN','instrumentation':'INTRUSIVE_ERROR_QUEUE_AND_READBACK_SYNC','source_commands':['G3-A','G3-B','G3-A-return']} + s.save('G3-result',result) + for name in ('g2-glass','g2-background','g2-reference'):s.close_client(name,'close') + return result diff --git a/tooling/probes/step20c-corec-v2-followup/gpu/test-targeted.py b/tooling/probes/step20c-corec-v2-followup/gpu/test-targeted.py new file mode 100644 index 0000000..5dc544c --- /dev/null +++ b/tooling/probes/step20c-corec-v2-followup/gpu/test-targeted.py @@ -0,0 +1,71 @@ +"""G3 offline C17/FakeGL/protocol/client checks; no compositor build/run.""" +import argparse +import ctypes +import json +import hashlib +import os +from pathlib import Path +import resource +import shlex +import subprocess +import sys + +KIT = Path(__file__).resolve().parent + + +def main(): + resource.setrlimit(resource.RLIMIT_CORE, (0, 0)) + assert ctypes.CDLL(None).prctl(4, 0, 0, 0, 0) == 0 + p = argparse.ArgumentParser(description=__doc__) + p.add_argument('--lab', required=True, type=Path) + p.add_argument('--scene', required=True, type=Path) + a = p.parse_args() + lab, scene = a.lab.resolve(strict=True), a.scene.resolve(strict=True) + assert str(lab).startswith('/var/tmp/swayfx-mosaic-step20c-native-closure-') + assert scene.is_relative_to(lab) + work = lab / 'g3-offline' + work.mkdir(mode=0o700, exist_ok=True) + env = {'PATH': '/usr/bin:/bin', 'LANG': 'C.UTF-8'} + for key, name in [('TMPDIR','scratch'),('TMP','scratch'),('TEMP','scratch'), + ('XDG_CACHE_HOME','cache'),('XDG_DATA_HOME','data'), + ('PYTHONPYCACHEPREFIX','pycache'),('CCACHE_DIR','ccache')]: + path = work / name + path.mkdir(mode=0o700, exist_ok=True) + env[key] = str(path) + flags = shlex.split(subprocess.check_output(['pkg-config','--cflags','wlroots-0.20','pixman-1','egl','glesv2','libdrm','wayland-server'],env=env,text=True)) + base = ['cc','-std=c17','-D_GNU_SOURCE','-DWLR_USE_UNSTABLE','-DWLR_PRIVATE=', + '-DWLR_LITTLE_ENDIAN=1','-DWLR_BIG_ENDIAN=0','-Wall','-Wextra','-Werror', + '-Wno-unused-parameter','-Wno-missing-braces','-I'+str(scene/'include')] + root = '-DG3_ROOT="'+str(lab/'gpu-work/acquisition')+'"' + commands = [ + ['cc','-std=c17','-Wall','-Wextra','-Werror','-I'+str(KIT),str(KIT/'test_gl.c'),'-o',str(work/'test_gl')], + [str(work/'test_gl')], + [sys.executable,str(KIT/'test_mutations.py')], + [sys.executable,str(KIT/'validate_source.py')], + [sys.executable,str(KIT/'test_protocol.py')], + [sys.executable,str(KIT/'test_begin.py')], + base + flags + [root,'-I'+str(KIT),'-Wno-unused-function','-ffunction-sections','-fdata-sections',str(KIT/'test_native.c'),'-Wl,--gc-sections','-o',str(work/'test_native')], + [str(work/'test_native')], + base + flags + [root,'-fsyntax-only',str(scene/'render/fx_renderer/fx_pass.c')], + ] + for xml, header in [('wlr-layer-shell-unstable-v1.xml','layer-shell-client.h'),('xdg-shell.xml','xdg-shell-client.h')]: + commands.append(['wayland-scanner','client-header',str(KIT/'protocols'/xml),str(work/header)]) + commands.append(['cc','-std=c17','-Wall','-Wextra','-Werror','-Wno-unused-parameter','-Wno-missing-braces','-I'+str(work),'-fsyntax-only',str(KIT/'clients-g2.c')]) + rows=[] + for i, command in enumerate(commands): + r = subprocess.run(command, cwd=work, env=env, text=True, capture_output=True, timeout=60) + log = work / f'test-{i}.log' + log.write_text(r.stdout+r.stderr) + rows.append({'command':command,'exit':r.returncode,'log':log.name}) + print(f'{i}: exit={r.returncode}\n{r.stdout}{r.stderr}',flush=True) + if r.returncode: + break + passed=len(rows)==len(commands) and all(r['exit']==0 for r in rows) + (work/'receipt.json').write_text(json.dumps({'schema':'G3-offline-v1','pass':passed, + 'review':'PENDING_INDEPENDENT','runtime':'NOT_RUN','compositor_build':'NOT_RUN', + 'environment':env,'scene':str(scene),'native_sha256':{n:hashlib.sha256((scene/'render/fx_renderer'/n).read_bytes()).hexdigest() for n in ('fx_pass.c','g2_gl.h','g2_native.h')},'headers':{n:hashlib.sha256(Path(n).read_bytes()).hexdigest() for n in ('/usr/include/EGL/egl.h','/usr/include/GLES3/gl3.h','/usr/include/libdrm/drm_fourcc.h')},'commands':rows},indent=2)+'\n') + raise SystemExit(0 if passed else 1) + + +if __name__=='__main__': + main() diff --git a/tooling/probes/step20c-corec-v2-followup/gpu/test_begin.py b/tooling/probes/step20c-corec-v2-followup/gpu/test_begin.py new file mode 100644 index 0000000..df5981e --- /dev/null +++ b/tooling/probes/step20c-corec-v2-followup/gpu/test_begin.py @@ -0,0 +1,73 @@ +"""Compile the exact native arm consumer against bounded filesystem fixtures.""" +import ctypes +import os +from pathlib import Path +import resource +import subprocess +import tempfile + +resource.setrlimit(resource.RLIMIT_CORE, (0, 0)) +assert ctypes.CDLL(None).prctl(4, 0, 0, 0, 0) == 0 +KIT = Path(__file__).resolve().parent +header = (KIT/'g2_native.h').read_text() +marker = 'static void g2_begin(struct fx_gles_render_pass *pass,struct fx_framebuffer *source,struct fx_render_blur_pass_options *options) {' +start = header.index(marker) +end = header.index('\n}\n',start)+3 +body = header[start:end] +prefix = r''' +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +struct fx_gles_render_pass {void *fx_offscreen_buffers;}; +struct fx_framebuffer {int unused;}; +struct bd {int num_passes;}; +struct fx_render_blur_pass_options {struct bd *blur_data;float blur_strength;struct {struct {void *clip;}base;}tex_options;}; +static struct {bool stopped,active;unsigned epoch,point;size_t used;int dir;FILE *meta;const void *pass;uintptr_t pass_sequence;char nonce[65];}g2; +static bool g2_intended_output(void *p){(void)p;return true;} +static bool g2_open(void){return true;} +static void g2_fail(const char *s){(void)s;g2.stopped=true;} +static bool blur_data_should_parameters_blur_effects(void *p){(void)p;return false;} +static void g2_sample_native(void *p,void *b,unsigned x,void *r){(void)p;(void)b;(void)x;(void)r;} +''' +suffix = r''' +int main(int argc,char **argv) { + (void)argc;struct rlimit r={0,0};assert(!setrlimit(RLIMIT_CORE,&r));assert(!prctl(PR_SET_DUMPABLE,0)); + g2.dir=open(argv[1],O_RDONLY|O_DIRECTORY|O_NOFOLLOW);assert(g2.dir>=0);memset(g2.nonce,'a',64); + struct fx_gles_render_pass pass={0};struct bd bd={.num_passes=3}; + struct fx_render_blur_pass_options options={.blur_data=&bd,.blur_strength=1}; + g2_begin(&pass,NULL,&options); + if(!strcmp(argv[2],"accept")){assert(g2.active&&!g2.stopped&&g2.epoch==1);assert(!fclose(g2.meta));} + else assert(g2.stopped&&!g2.active&&g2.epoch==0); + assert(!close(g2.dir));return 0; +} +''' +with tempfile.TemporaryDirectory(prefix='g3-begin-',dir=os.environ['TMPDIR']) as name: + work=Path(name);work.chmod(0o700) + source=work/'begin.c';source.write_text(prefix+body+suffix) + binary=work/'begin' + subprocess.run(['cc','-std=c17','-Wall','-Wextra','-Werror',str(source),'-o',str(binary)],check=True) + for case in ('fifo','symlink','wrong-mode','hardlink','oversize','consumed','accept'): + d=work/case;d.mkdir(mode=0o700);arm=d/'epoch-1.arm' + if case=='fifo':os.mkfifo(arm,0o600) + elif case=='symlink':arm.symlink_to('/dev/null') + else: + arm.write_bytes(b'a'*64+b' 1\n');arm.chmod(0o600) + if case=='wrong-mode':arm.chmod(0o644) + if case=='hardlink':os.link(arm,d/'alias') + if case=='oversize':arm.write_bytes(b'a'*1000) + if case=='consumed':(d/'epoch-1.consumed').write_bytes(b'old') + subprocess.run([str(binary),str(d),case],check=True,timeout=3) + if case=='accept':assert not arm.exists() and (d/'epoch-1.consumed').exists() + else:assert arm.exists() or arm.is_symlink() + print('PASS exact native g2_begin',case) diff --git a/tooling/probes/step20c-corec-v2-followup/gpu/test_gl.c b/tooling/probes/step20c-corec-v2-followup/gpu/test_gl.c new file mode 100644 index 0000000..53f2297 --- /dev/null +++ b/tooling/probes/step20c-corec-v2-followup/gpu/test_gl.c @@ -0,0 +1,87 @@ +#define _GNU_SOURCE +#include "g2_gl.h" +#include +#include +#include +#include +static struct g2_state state; +static int mode, read_calls, unsupported, pending, query_count; +static bool read_done; +static const char *version="OpenGL ES 3.2 FakeGL"; +static void reset(int m) { + memset(&state,0,sizeof(state));state.read_fbo=11;state.draw_fbo=12;state.rbo=13; + state.pack=8;state.pbo=15;state.row=32;state.skip_rows=2;state.skip_pixels=3; + state.viewport[2]=2;state.viewport[3]=3;state.scissor[2]=2;state.scissor[3]=3; + state.program=21;state.active=GL_TEXTURE0;state.texture=22; + mode=m;read_calls=unsupported=pending=query_count=0;read_done=false; + version=(m==6||m==14)?"OpenGL ES 2.0 FakeGL":m==20?"OpenGL ES 3.20 FakeGL":m==21?"OpenGL ES 3.3 FakeGL":m==22?"OpenGL 3.2":m==23?"OpenGL ES 4.0":"OpenGL ES 3.2 FakeGL"; +} +EGLContext EGLAPIENTRY eglGetCurrentContext(void) {return mode==5?EGL_NO_CONTEXT:(EGLContext)(uintptr_t)(mode==17&&read_done?7:5);} +EGLDisplay EGLAPIENTRY eglGetCurrentDisplay(void) {return mode==24?EGL_NO_DISPLAY:(EGLDisplay)(uintptr_t)6;} +EGLenum EGLAPIENTRY eglQueryAPI(void) {return mode==12?EGL_OPENGL_API:EGL_OPENGL_ES_API;} +EGLBoolean EGLAPIENTRY eglQueryContext(EGLDisplay d,EGLContext c,EGLint a,EGLint *v) {(void)d;(void)c;(void)a;*v=(mode==6||mode==18)?2:3;return mode==13?EGL_FALSE:EGL_TRUE;} +GLenum GL_APIENTRY glGetError(void) { + if(mode==1 && query_count==0) {query_count++;return GL_INVALID_OPERATION;} + if(mode==8)return GL_INVALID_OPERATION; + if(pending){GLenum e=(GLenum)pending;pending=0;return e;}return GL_NO_ERROR; +} +const GLubyte *GL_APIENTRY glGetString(GLenum e) {if(mode==25)pending=GL_INVALID_ENUM;return (const GLubyte *)(e==GL_SHADING_LANGUAGE_VERSION?"OpenGL ES GLSL ES 3.20 FakeGL":version);} +void GL_APIENTRY glGetIntegerv(GLenum e,GLint *v) { + if(mode==6)unsupported++; + if(mode==2 && e==GL_READ_FRAMEBUFFER_BINDING)pending=GL_INVALID_ENUM; + switch(e) { + case GL_MAJOR_VERSION:*v=mode==19?2:3;if(mode==26)pending=GL_INVALID_ENUM;break; + case GL_MINOR_VERSION:*v=mode==27?1:2;break; + #define FIELD(e,f) case e:*v=state.f;break + case GL_READ_BUFFER:*v=mode==16?GL_NONE:GL_COLOR_ATTACHMENT0;break; + FIELD(GL_READ_FRAMEBUFFER_BINDING,read_fbo);FIELD(GL_DRAW_FRAMEBUFFER_BINDING,draw_fbo); + FIELD(GL_RENDERBUFFER_BINDING,rbo);FIELD(GL_PACK_ALIGNMENT,pack);FIELD(GL_PIXEL_PACK_BUFFER_BINDING,pbo); + FIELD(GL_PACK_ROW_LENGTH,row);FIELD(GL_PACK_SKIP_ROWS,skip_rows);FIELD(GL_PACK_SKIP_PIXELS,skip_pixels); + FIELD(GL_CURRENT_PROGRAM,program);FIELD(GL_ACTIVE_TEXTURE,active);FIELD(GL_TEXTURE_BINDING_2D,texture); + case GL_VIEWPORT:memcpy(v,state.viewport,sizeof(state.viewport));break; + case GL_SCISSOR_BOX:memcpy(v,state.scissor,sizeof(state.scissor));break; + default:unsupported++;pending=GL_INVALID_ENUM;break; + } +} +GLboolean GL_APIENTRY glIsEnabled(GLenum e) {(void)e;return state.scissor_on;} +void GL_APIENTRY glBindFramebuffer(GLenum e,GLuint v) {if(e==GL_READ_FRAMEBUFFER)state.read_fbo=(GLint)v;else state.draw_fbo=(GLint)v;} +void GL_APIENTRY glBindRenderbuffer(GLenum e,GLuint v) {(void)e;state.rbo=(GLint)v;} +void GL_APIENTRY glBindBuffer(GLenum e,GLuint v) {(void)e;if(mode==11&&read_done&&v==15)pending=GL_INVALID_OPERATION;if(!(mode==4&&read_done))state.pbo=(GLint)v;} +void GL_APIENTRY glPixelStorei(GLenum e,GLint v) { + if(mode==9&&e==GL_PACK_ALIGNMENT&&v==1)pending=GL_INVALID_OPERATION; + switch(e) {case GL_PACK_ALIGNMENT:state.pack=v;break;case GL_PACK_ROW_LENGTH:state.row=v;break; + case GL_PACK_SKIP_ROWS:state.skip_rows=v;break;case GL_PACK_SKIP_PIXELS:state.skip_pixels=v;break;default:unsupported++;} +} +void GL_APIENTRY glGetFramebufferAttachmentParameteriv(GLenum t,GLenum a,GLenum e,GLint *v) { + (void)t;(void)a;if(mode==10)pending=GL_INVALID_ENUM;*v=e==GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE?(mode==7?GL_TEXTURE:GL_RENDERBUFFER):e==GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME?44:0; +} +void GL_APIENTRY glGetRenderbufferParameteriv(GLenum t,GLenum e,GLint *v) { + (void)t;*v=e==GL_RENDERBUFFER_WIDTH?2:e==GL_RENDERBUFFER_HEIGHT?3:e==GL_RENDERBUFFER_INTERNAL_FORMAT?GL_RGBA8:e==GL_RENDERBUFFER_SAMPLES?(mode==15?4:0):8; +} +GLenum GL_APIENTRY glCheckFramebufferStatus(GLenum e) {(void)e;return mode==28?GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT:GL_FRAMEBUFFER_COMPLETE;} +void GL_APIENTRY glReadPixels(GLint x,GLint y,GLsizei w,GLsizei h,GLenum f,GLenum t,void *p) { + (void)x;(void)y;assert(f==GL_RGBA&&t==GL_UNSIGNED_BYTE);assert(state.pbo==0&&state.pack==1&&state.row==0); + read_calls++;read_done=true;if(mode==3)pending=GL_INVALID_OPERATION;else memset(p,0,(size_t)w*(size_t)h*4); +} +int main(void) { + struct rlimit r={0,0};assert(!setrlimit(RLIMIT_CORE,&r));assert(!prctl(PR_SET_DUMPABLE,0)); + const char *reasons[]={"READ_SUCCESS","PREEXISTING_GL_ERRORS","STATE_QUERY_ERROR","READ_GL_ERROR","RESTORE_FAILED","UNKNOWN_CONTEXT","GLES_STATE_NOT_SUPPORTED","TEXTURE_ATTACHMENT_DIMENSIONS_NOT_PROVEN","PREEXISTING_GL_ERRORS","PACK_QUERY_ERROR","ATTACHMENT_QUERY_ERROR","RESTORE_FAILED","UNKNOWN_CONTEXT","EGL_QUERY_FAILED","GLES_STATE_NOT_SUPPORTED","ATTACHMENT_UNSUPPORTED","ATTACHMENT_UNSUPPORTED","RESTORE_FAILED","READ_SUCCESS","VERSION_INCONSISTENT","GLES_STATE_NOT_SUPPORTED","GLES_STATE_NOT_SUPPORTED","GLES_STATE_NOT_SUPPORTED","GLES_STATE_NOT_SUPPORTED","UNKNOWN_CONTEXT","VERSION_QUERY_ERROR","CAPABILITY_QUERY_ERROR","VERSION_INCONSISTENT","ATTACHMENT_UNSUPPORTED"}; + for(int i=0;i<29;i++) { + reset(i);struct g2_state before=state;struct g2_sample s=g2_read(99,2,3,0); + assert(s.success==(i==0||i==18));assert(!strcmp(s.reason,reasons[i]));assert(!unsupported); + if(i==0||i==3||i==7)assert(!memcmp(&state,&before,sizeof(state))); + if(i==0||i==18){assert(s.version_queried&&s.state_saved&&s.bound&&s.read_attempted&&s.restore_attempted);assert(read_calls==1&&s.bytes==24);for(size_t n=0;n=19)assert(!read_calls); + printf("PASS exact g2_read mode=%d reason=%s\n",i,s.reason);free(s.pixels); + } + GLint minor=0;char too_long[170];memset(too_long,'x',sizeof(too_long));memcpy(too_long,"OpenGL ES 3.2 ",14);too_long[169]=0; + assert(!g2_version(NULL,&minor));assert(!g2_version(too_long,&minor));assert(!g2_version("OpenGL ES 3.2\nforged",&minor)); + assert(g2_version("OpenGL ES 3.0",&minor)&&minor==0);assert(g2_version("OpenGL ES 3.1 vendor",&minor)&&minor==1); + size_t n=0;assert(!g2_size(INT_MAX,INT_MAX,0,&n));assert(!g2_size(2,3,G2_CAP-23,&n));assert(!g2_size(-1,3,0,&n)); + assert(g2_size(2,3,G2_CAP-24,&n)&&n==24); + reset(0);struct g2_sample s=g2_read(99,INT_MAX,INT_MAX,0);assert(!s.success&&!read_calls);free(s.pixels); + puts("PASS size/overflow/exact-cap; black RGBA alpha zero is valid, no sentinel gate");return 0; +} diff --git a/tooling/probes/step20c-corec-v2-followup/gpu/test_mutations.py b/tooling/probes/step20c-corec-v2-followup/gpu/test_mutations.py new file mode 100644 index 0000000..57f6245 --- /dev/null +++ b/tooling/probes/step20c-corec-v2-followup/gpu/test_mutations.py @@ -0,0 +1,30 @@ +"""Wrong native variants must fail the unchanged exact-body FakeGL tests.""" +import ctypes +import os +from pathlib import Path +import resource +import shutil +import subprocess +import tempfile + +resource.setrlimit(resource.RLIMIT_CORE, (0, 0)) +assert ctypes.CDLL(None).prctl(4, 0, 0, 0, 0) == 0 +KIT=Path(__file__).resolve().parent +header=(KIT/'g2_gl.h').read_text() +variants={ + 'major-contradiction':('s.major!=3 || s.minor!=parsed_minor','s.minor!=parsed_minor'), + 'minor-contradiction':('s.major!=3 || s.minor!=parsed_minor','s.major!=3'), + 'false-read-lifecycle':('s.read_attempted=true;','s.read_attempted=false;'), + 'missing-pack-restore':('glPixelStorei(GL_PACK_ALIGNMENT,s->pack);','/* deliberately broken restore */'), +} +with tempfile.TemporaryDirectory(prefix='g3-mutants-',dir=os.environ['TMPDIR']) as name: + work=Path(name) + shutil.copyfile(KIT/'test_gl.c',work/'test_gl.c') + for label,(old,new) in variants.items(): + assert header.count(old)==1 + (work/'g2_gl.h').write_text(header.replace(old,new)) + binary=work/label + subprocess.run(['cc','-std=c17','-Wall','-Wextra','-Werror','-I'+str(work),str(work/'test_gl.c'),'-o',str(binary)],check=True) + result=subprocess.run([str(binary)],capture_output=True,text=True,timeout=5) + assert result.returncode != 0, label + print(f'PASS wrong native variant rejected {label}: raw_exit={result.returncode}') diff --git a/tooling/probes/step20c-corec-v2-followup/gpu/test_native.c b/tooling/probes/step20c-corec-v2-followup/gpu/test_native.c new file mode 100644 index 0000000..b367b17 --- /dev/null +++ b/tooling/probes/step20c-corec-v2-followup/gpu/test_native.c @@ -0,0 +1,86 @@ +#include /* private G2 state observability */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "render/egl.h" +#include "render/fx_renderer/fx_renderer.h" +#include "render/fx_renderer/shaders.h" +#include "render/pass.h" +#include "render/tracy.h" +#include "scenefx/render/fx_renderer/fx_offscreen_buffers.h" +#include "scenefx/render/fx_renderer/fx_renderer.h" +#include "scenefx/types/fx/blur_data.h" +#include "util/matrix.h" + +#include "g2_native.h" + + +static struct wlr_dmabuf_attributes controlled_dmabuf; +bool wlr_buffer_get_dmabuf(struct wlr_buffer *buffer, struct wlr_dmabuf_attributes *a) { + (void)buffer; *a=controlled_dmabuf;return true; +} +#include +#include +int main(void) { + struct rlimit r={0,0};assert(!setrlimit(RLIMIT_CORE,&r));assert(!prctl(PR_SET_DUMPABLE,0)); + /* WHY: DCC auxiliary pitch is not an RGBA image pitch. CONTRACT: + * exercise the exact native predicate with borrowed real descriptors. */ + FILE *plane=tmpfile();assert(plane);int fd=fileno(plane); + controlled_dmabuf=(struct wlr_dmabuf_attributes){.width=800,.height=600, + .format=DRM_FORMAT_ABGR8888,.modifier=UINT64_C(0x020000001046bb04),.n_planes=2, + .stride={3584,1024},.offset={0,2293760},.fd={fd,fd}}; + struct wlr_buffer buffer={.width=800,.height=600};struct fx_framebuffer fb={.buffer=&buffer}; + assert(g2_dmabuf_supported(&fb));assert(fcntl(fd,F_GETFD)>=0); + controlled_dmabuf.stride[1]=0;assert(!g2_dmabuf_supported(&fb));controlled_dmabuf.stride[1]=1024; + controlled_dmabuf.modifier=UINT64_MAX;assert(!g2_dmabuf_supported(&fb)); + controlled_dmabuf.modifier=0;controlled_dmabuf.n_planes=1;assert(g2_dmabuf_supported(&fb)); + controlled_dmabuf.stride[0]=3199;assert(!g2_dmabuf_supported(&fb));controlled_dmabuf.stride[0]=3584; + controlled_dmabuf.fd[0]=-1;assert(!g2_dmabuf_supported(&fb)); + assert(!fclose(plane));puts("PASS exact C DMA-BUF DCC/linear metadata and borrowed FD; allocation bounds NOT_PROVEN"); + struct fx_gles_render_pass passes[9]={0}; + struct wlr_output h1={.name="HEADLESS-1",.scale=1,.transform=WL_OUTPUT_TRANSFORM_NORMAL}; + struct wlr_output h2={.name="HEADLESS-2",.scale=1,.transform=WL_OUTPUT_TRANSFORM_NORMAL}; + for(unsigned i=0;i<9;i++) { + g2_track_output(&passes[i],i%2?&h2:&h1); + assert(g2_intended_output(&passes[i])==(i%2==0)); + g2_forget_output(&passes[i]);assert(!g2_intended_output(&passes[i])); + } + assert(!g2.stopped); + g2_track_output(&passes[0],&h1);assert(g2_intended_output(&passes[0])); + g2_track_output(&passes[0],&h2);assert(!g2_intended_output(&passes[0])); + g2_forget_output(&passes[0]);g2_track_output(&passes[0],&h1);assert(g2_intended_output(&passes[0]));g2_forget_output(&passes[0]); + for(unsigned i=0;i<9;i++)g2_track_output(&passes[i],i%2?&h2:&h1); + assert(g2.stopped && !g2_intended_output(&passes[8])); + for(unsigned i=0;i<9;i++) {g2_forget_output(&passes[i]);} + g2.stopped=false; + puts("PASS nine sequential passes/output forget, pointer reuse H1/H2 and nine-simultaneous fail-closed overflow"); + struct g2_sample s={.width=800,.height=600,.bytes=800*600*4};s.pixels=calloc(1,s.bytes);assert(s.pixels); + for(int y=0;y<600;y++)for(int x=0;x<800;x++) { + const unsigned char a[2][3]={{245,245,245},{15,25,45}}; + memcpy(s.pixels+((size_t)y*800+(size_t)x)*4,a[(x/8)%2],3); + } + assert(g2_expected_source(&s,1));assert(g2_expected_source(&s,3));assert(!g2_expected_source(&s,2)); + s.pixels[4]^=1;assert(!g2_expected_source(&s,1));s.pixels[4]^=1; + for(int y=0;y<600;y++)for(int x=0;x<800;x++) { + const unsigned char b[2][3]={{32,192,224},{176,32,64}}; + memcpy(s.pixels+((size_t)y*800+(size_t)x)*4,b[(x/8)%2],3); + } + assert(g2_expected_source(&s,2));assert(!g2_expected_source(&s,3));free(s.pixels); + assert(g2_crc((const unsigned char *)"123456789",9)==UINT32_C(0xcbf43926)); + FILE *f=tmpfile();assert(f && g2_owned(fileno(f),0600));assert(g2_write(fileno(f),"frame",5));assert(!fseek(f,0,SEEK_SET));char b[6]={0};assert(fread(b,1,5,f)==5 && !strcmp(b,"frame"));assert(!fclose(f)); + assert(!setenv("MOSAIC_G3_DIR","/tmp",1));assert(!setenv("MOSAIC_G3_NONCE","aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",1)); + assert(!g2_open() && g2.stopped && g2.dir==-1); + puts("PASS exact native source epoch RGB admission A/B/A-return and negative substitution"); + puts("PASS exact native CRC framing/writer/owned FD cleanup and wrong realpath refusal"); + return 0; +} diff --git a/tooling/probes/step20c-corec-v2-followup/gpu/test_protocol.py b/tooling/probes/step20c-corec-v2-followup/gpu/test_protocol.py new file mode 100644 index 0000000..deca2e6 --- /dev/null +++ b/tooling/probes/step20c-corec-v2-followup/gpu/test_protocol.py @@ -0,0 +1,110 @@ +"""Controlled synthetic framing and scenario tests; NO GPU/native evidence.""" +import resource,ctypes +resource.setrlimit(resource.RLIMIT_CORE,(0,0)) +assert ctypes.CDLL(None).prctl(4,0,0,0,0)==0 + +import json,os,shutil,tempfile,zlib,copy +from pathlib import Path +from unittest.mock import patch +from PIL import Image +import protocol,scenario +from validate_source import Rejected +NONCE='a'*64 +BASE=Path(os.environ['TMPDIR']) +def write_epoch(d,e,total): + raw=bytes(([32,192,224,255] if e==2 else [245,245,245,255])*6);total+=len(raw)*10 + rows=[dict(kind='begin',schema='G3-followup-v1',epoch=e,nonce=NONCE,label=('A','B','A-return')[e-1],output='HEADLESS-1',intrusive_error_queue=True,error_queue_restorable=False)] + for p,stage in enumerate(protocol.STAGES): + if 1<=p<=6:rows.append(dict(kind='uniform',point=p,type=0x8b50,program=5,location=3,shader_default_float='mediump',varying='mediump',sampler=[9729,9729,33071,33071],precision_capability=[15,15,10],native_texture_dimensions=[2,3],actual_bits=[1,2])) + filename=f'epoch-{e}-point-{p}.rgba';protocol.exclusive(d/filename,raw) + rows.append(dict(kind='sample',epoch=e,point=p,stage=stage,pass_sequence=e,context='5',result='READ_SUCCESS',read_success=True,published=True,before={'pack':8},after={'pack':8},pre_errors={'exhausted':False,'codes':[]},query_errors={'exhausted':False,'codes':[]},read_errors={'exhausted':False,'codes':[]},restore_errors={'exhausted':False,'codes':[]},client_version=2,gl_major=3,gl_minor=2,glsl_hex=b'OpenGL ES GLSL ES 3.20'.hex(),version_queried=True,state_saved=True,bound=True,read_attempted=True,restore_attempted=True,version_errors={'exhausted':False,'codes':[]},capability_errors={'exhausted':False,'codes':[]},state_errors={'exhausted':False,'codes':[]},gl_version_hex=b'OpenGL ES 3.2 ControlledFake'.hex(),attachment=[0x8d41,44,0,2,3,0x8058],channels=[8,8,8,8],status=0x8cd5,read_buffer=0x8ce0,samples=0,native={'dimensions':[2,3],'fbo_rbo_tex':[11,44,22],'fx':'1','wlr':'2','image':'3'},dmabuf={'borrowed':True,'dimensions':[2,3],'format':int.from_bytes(b'AB24','little'),'modifier':'0000000000000000','planes':[{'stride':8,'offset':0,'fd':4,'fstat_ok':True,'dev':1,'ino':2}]},bytes=len(raw),file=filename,crc32=f'{zlib.crc32(raw):08x}',fresh_rectangles=[[0,0,2,3]])) + rows.append(dict(kind='end',epoch=e,points=10,accepted=True,raw_total=total)) + protocol.exclusive(d/f'epoch-{e}.jsonl',b''.join((json.dumps(row,separators=(',',':'))+'\n').encode() for row in rows));(None if (d/f'epoch-{e}.consumed').exists() else protocol.exclusive(d/f'epoch-{e}.consumed',f'{NONCE} {e}\n'.encode())) + return rows,total + +def framing(d): + rows,total=write_epoch(d,1,0);assert protocol.epoch(d,1,NONCE)['raw_total']==240 + tests=[('early-return',lambda x:x[1].update(read_attempted=False)),('fake-version',lambda x:x[1].update(gl_minor=1)),('unqualified-es',lambda x:x[1].update(gl_version_hex=b'OpenGL ES 3.3'.hex())),('capability-error',lambda x:x[1]['capability_errors']['codes'].append(0x500)),('read-error',lambda x:x[1]['read_errors']['codes'].append(0x502)),('restore',lambda x:x[1]['after'].update(pack=1)),('context',lambda x:x[1].update(context='0')),('path',lambda x:x[1].update(file='../escape')),('budget',lambda x:x[1].update(bytes=protocol.CAP+1)),('incomplete',lambda x:x.pop()),('stage',lambda x:x[1].update(stage='presented-buffer'))] + p=d/'epoch-1.jsonl' + for label,mutate in tests: + changed=copy.deepcopy(rows);mutate(changed);p.write_text(''.join(json.dumps(x)+'\n' for x in changed));p.chmod(0o600) + try:protocol.epoch(d,1,NONCE) + except (Rejected,KeyError,AssertionError):print('PASS controlled framing rejection',label) + else:raise AssertionError(label) + p.write_text(''.join(json.dumps(x)+'\n' for x in rows));p.chmod(0o600) + q=d/'epoch-1-point-0.rgba';q.write_bytes(b'bad') + try:protocol.epoch(d,1,NONCE) + except Rejected:print('PASS raw truncation/CRC rejection') + else:raise AssertionError('truncation') + q.unlink();q.symlink_to('/dev/null') + try:protocol.bounded(q,10) + except Rejected:print('PASS symlink refusal') + else:raise AssertionError('symlink') + try:protocol.exclusive(p,b'overwrite') + except FileExistsError:print('PASS exclusive publication refusal') + else:raise AssertionError('exclusive') + assert scenario.exact_rgb(Image.new('RGB',(2,3)),Image.new('RGB',(2,3)))['equal'] + a=Image.new('RGB',(2,3));b=a.copy();b.putpixel((1,2),(1,0,0));assert not scenario.exact_rgb(a,b)['equal'] + print('PASS whole H1/H2 all-pixel zero RGB tolerance') + +def token_test(d): + with patch.object(protocol,'gate',lambda *args:None): + protocol.arm(d,1,NONCE,d/'gate',d/'binary') + assert protocol.bounded(d/'epoch-1.arm',80)==f'{NONCE} 1\n'.encode() + for e in (1,2): + try:protocol.arm(d,e,NONCE,d/'gate',d/'binary') + except (Rejected,FileNotFoundError):pass + else:raise AssertionError('retry or B before complete A') + os.link(d/'epoch-1.arm',d/'epoch-1.consumed');(d/'epoch-1.arm').unlink() + write_epoch(d,1,0) + protocol.arm(d,2,NONCE,d/'gate',d/'binary') + try:protocol.arm(d,3,NONCE,d/'gate',d/'binary') + except (Rejected,FileNotFoundError):pass + else:raise AssertionError('return before complete B') + print('PASS exact arm token nonce/exclusive/replay and first A completion gate, controlled directory') + +class Session: + def __init__(self,d,fail=False): + self.d=d;self.fail=fail;self.commands=[];self.captures=0;self.total=0;self.env={};self.spec={'id':'GPU-G3','binary':'gpu'};self.auth={'binary':str(d/'fake-binary'),'manifest_sha256':'controlled'};self.proc={};self.data={};self.saved={} + def start(self):self.data['started']=True + def cmd(self,*a,**kw):pass + def spawn(self,name,argv): + parent=self + class Input: + def write(self,text): + parent.commands.append(text.strip());e=len(parent.commands) + if parent.fail:protocol.exclusive(parent.d/f'epoch-{e}.jsonl',b'{"kind":"rejected"}\n') + else:_,parent.total=write_epoch(parent.d,e,parent.total) + def flush(self):pass + self.proc[name]=type('Process',(),{'stdin':Input()})() + def wait(self,fn):assert fn() + def find(self,name):return {'id':1} + def log(self,name):return 'BACKGROUND_CONFIGURED '+' '.join('G3_SOURCE_BYTES command='+x+' ' for x in self.commands) + def debit(self,*a):pass + def record(self,*a,**kw):pass + def capture(self,*a):self.captures+=1;return Image.new('RGB',(2,3)) + def pair_capture(self,*a):self.captures+=2;return [Image.new('RGB',(2,3)),Image.new('RGB',(2,3))] + def snapshot(self,*a):pass + def save(self,name,result):self.saved[name]=result + def close_client(self,*a):pass + +def scenario_test(d,fail): + s=Session(d,fail);binary=d/'fake-binary';binary.write_bytes(b'controlled') + binding=dict(G3_ledger_reserved=True,binary=str(binary),source_manifest_sha256='controlled',gate_file=str(d/'gate'),client=str(binary),client_sha256=__import__('hashlib').sha256(binary.read_bytes()).hexdigest(),nonce=NONCE) + armed=[] + with patch.object(scenario,'ROOT',d),patch.object(scenario,'gate',lambda *args:None),patch.object(scenario,'arm',lambda root,e,*args:armed.append(e)),patch.object(scenario.time,'sleep',lambda seconds:None): + if fail: + try:scenario.run(s,binding) + except RuntimeError:pass + else:raise AssertionError('rejected A continued') + assert s.commands==['G3-A'] and armed==[1] and s.captures==2 + print('PASS rejected first A stops B/A-return: exact executable adapter, fake Session only') + else: + r=scenario.run(s,binding);assert s.commands==['G3-A','G3-B','G3-A-return'] and armed==[1,2,3] and s.captures==8 and r['whole_H1_A_Areturn']['equal'] + print('PASS one-shot A/B/A adapter flow, eight captures with whole H1/H2 and H2 baseline, controlled framing') +if __name__=='__main__': + with tempfile.TemporaryDirectory(prefix='protocol-controlled-',dir=BASE) as temp: + p=Path(temp);p.chmod(0o700) + for name in ('framing','accepted','rejected','token'):(p/name).mkdir(mode=0o700) + with patch.object(protocol,'FRAME',(2,3)): + framing(p/'framing');token_test(p/'token');scenario_test(p/'accepted',False);scenario_test(p/'rejected',True) diff --git a/tooling/probes/step20c-corec-v2-followup/gpu/validate_source.py b/tooling/probes/step20c-corec-v2-followup/gpu/validate_source.py new file mode 100644 index 0000000..9829e94 --- /dev/null +++ b/tooling/probes/step20c-corec-v2-followup/gpu/validate_source.py @@ -0,0 +1,53 @@ +"""New G2 offline controls. Historical G1 stays HISTORICAL_REPORTED_ONLY.""" +import resource,ctypes +resource.setrlimit(resource.RLIMIT_CORE,(0,0)) +assert ctypes.CDLL(None).prctl(4,0,0,0,0)==0 + +import hashlib,json,struct +from pathlib import Path +HEADER_SHA='992f33804acda60b0b46f1a272942c3592484ea3217d469239f1cb12249de2eb' +BPP={int.from_bytes(s,'little'):4 for s in (b'AB24',b'XB24',b'AR24',b'XR24')} +AMD='020000001046bb04' +class Rejected(ValueError):pass +def need(x,msg): + if not x:raise Rejected(msg) +def metadata(o): + need(type(o['width']) is int and type(o['height']) is int and 00,'plane uint32') + need(p['offset']+p['stride']<=2**64-1,'overflow') + p=planes[0];need(p['stride']>=o['width']*BPP[o['format']],'image stride') + end=p['offset']+p['stride']*(o['height']-1)+o['width']*BPP[o['format']] + need(end<=2**64-1,'image overflow') + # No fabricated DCC extent or same-allocation restriction. fstat sizes are + # recorded as descriptor metadata, not interpreted as allocation length. + return {'image_stride':'PASS','auxiliary_stride':'NOT_AN_IMAGE_STRIDE' if len(planes)>1 else 'N/A','allocation_bounds':'NOT_PROVEN','fd_ownership':'BORROWED' if o.get('borrowed') is True else 'NOT_PROVEN','decode_support':'NOT_PROVEN'} +def f32(x):return struct.unpack('0 and height>0 and len(observed)==2 and all(type(x)is int and 0<=x<=2**32-1 for x in observed),'uniform framing') + divisor=f32(2.0) + ideal=[f32(f32(.5)/f32(f32(n)/divisor if down else f32(n)*divisor)) for n in (width,height)] + return {'observed_bits':observed,'cpu_float32_ideal_bits':[bits(x) for x in ideal],'comparison_gate':'NONE','effective_arithmetic':'NOT_PROVEN','capability_is_per_operation_precision':False} +def test(): + from copy import deepcopy + need(hashlib.sha256(Path('/usr/include/libdrm/drm_fourcc.h').read_bytes()).hexdigest()==HEADER_SHA,'header pin') + o={'width':800,'height':600,'format':int.from_bytes(b'AB24','little'),'modifier':AMD,'planes':[{'stride':3584,'offset':0},{'stride':1024,'offset':2293760}],'borrowed':True} + r=metadata(o);need(r['auxiliary_stride']=='NOT_AN_IMAGE_STRIDE','AMD aux') + # Controlled fixtures, never actual allocation evidence or historical PASS. + for label,fn in [('unknown-format',lambda x:x.update(format=1)),('short-image',lambda x:x['planes'][0].update(stride=3199)),('plane-count',lambda x:x['planes'].pop()),('unknown-modifier',lambda x:x.update(modifier='ffffffffffffffff')),('negative-offset',lambda x:x['planes'][1].update(offset=-1)),('zero-aux',lambda x:x['planes'][1].update(stride=0))]: + x=deepcopy(o);fn(x) + try:metadata(x) + except Rejected:print('PASS controlled rejection',label) + else:raise AssertionError(label) + u=halfpixel(800,600,True,[bits(1/800),bits(1/600)]) + v=halfpixel(800,600,True,[bits(.0012493133544921875),bits(.0016660690307617188)]) + need(u['cpu_float32_ideal_bits']==v['cpu_float32_ideal_bits'] and u['observed_bits']!=v['observed_bits'] and v['comparison_gate']=='NONE','no mediump equality or fit') + try:halfpixel(800,600,False,[2**32,0]) + except Rejected:pass + else:raise AssertionError('uniform framing') + print('PASS controlled AMD/linear policy and separate halfpixel bits; HISTORICAL_REPORTED_ONLY') +if __name__=='__main__':test() diff --git a/tooling/probes/step20c-corec-v2-followup/graphics/README.md b/tooling/probes/step20c-corec-v2-followup/graphics/README.md new file mode 100644 index 0000000..a577a1e --- /dev/null +++ b/tooling/probes/step20c-corec-v2-followup/graphics/README.md @@ -0,0 +1,172 @@ +# Focused native closure graphics fixture + +EXPERIMENTAL SOURCE ONLY. This is a new derivation of the source-only graphics +fixture at Git `d46f854b3026e911659528fe94792aa13837a1d6`, not a modification or +re-admission of P1/C7/C8/O1/G2. No production code, package or UX contract changes. +The closed historical LAB is read-only. All native results remain NOT_RUN until +independent final-byte review, source publication and individual admission. + +The eight nonfungible IDs are FS-P(12 captures), FS-C(16), DY-C(16), OM-ROOT(10), +OM-NESTED(10), GPU-G3(12), OM-EXTRA(12), GPU-CTRL(12): sum100, global110, at most +8 runs /150s. No reset/refund/retry API. Graphics source leaves GPU-CTRL and OM-EXTRA NOT_ADMITTED. GPU-G3 is +NOT_ADMITTED by default and has a distinct executable branch requiring B4 and +independent exact-byte/source/build receipts; no gate is inferred from a plan. + +WHY: the former fullscreen insertion placed peer outside owner group; migration +ignored focus reveal; root OOM closure required nonexistent B. CONTRACT: exact +owner+peer direct children and measured400/400, current-data clamp then focus +reveal, scenario-created child-first closure. INVARIANT: whole-field expectations +are calculated before acquisition, every action is debited first, raw failure +and mechanical rescue remain visible and cannot establish scientific cleanup. + +`fullscreen.prepare_group` uses an existing filler (container_split does not wrap +a singleton H/V child), wraps owner, marks the real group, creates peer on ws3, +then explicitly moves it to that mark. The source route is pinned +`commands/split.c:do_split`, `tree/container.c:container_split`, +`commands/move.c:container_move_to_container` (nonleaf destination adds child). +No empirical runtime adjustment or retry exists. A failed topology/crossing is +NOT_EXERCISED. Both binaries use the same fullscreen-client/protocol XML, explicit +positioner adjustment0 and actual configured rectangles. A/B input, explicit +close B with A surviving, fullscreen exit/splith return and final H2 are sampled. +The complete H2 field composes A then B before each capture; A-only is predicted +after B closes. FS-P uses12 captures; FS-C uses the same12 within16. + +IPC `fullscreen_mode` is explicitly **pending** (`ipc-json.c` serializes +`c->pending.fullscreen_mode`); workspace marker1 is not fullscreen proof. A +post-command toplevel configure/ACK/MAP plus measured group geometry, paints and +fresh input corroborates sampled applied fullscreen. Direct observation of the +ancestor's `current.fullscreen_mode` is NOT_OBSERVABLE with these unchanged ELFs; +atomic current-state generation is NOT_PROVEN. The validator rejects a +pending-only or mismatched-owner/workspace certificate. Do not report a native +current-bit measurement that these binaries do not expose. + +`cases.dynamic_geometry` derives row extent and uniform gap from current actual +child rectangles and pinned default640 sizing, validates row identity/order and +focused owner, then clamps old scroll to current bounds and performs native +left/right reveal for migration. The audited route is +`commands/move.c:cmd_move_workspace` → arrange old/new output → seat_set_focus → +`input/seat.c` row focus/reveal → `tree/arrange.c:apply_mosaic_layout` and +`tree/mosaic.c:mosaic_row_clamp_scroll`. Neither800 nor1120 is a universal expected +camera. Every resize/migration invalidates previous input/camera/origin; new +painted interior and pointer-local delivery are required, or actual dismissal +records input NOT_EXERCISED. Opening6 + four mutation pairs8 + final pair2=16. + +OOM root closes A→TOP, nested failure closes B→A→TOP after mapped rootA; no_memory +requires actual wl_display.error code1 and client ENOMEM/EPROTO, separately from +requested error0 close. Every created object gets one wire destroy in reverse +hierarchy; release accounting and requested close are required. Failed popup +must never MAP. Native role/PID/nonce/counters and the same-XDG NULL return bind +the failure. The diagnostic's `children` is **existing parent children**, not +children owned by the failed tree (`wl_list_length(&parent->children)`); retain +its raw value. Initialized/listeners/published0 plus the audited prepublication +NULL path establish the scoped failed node, without inventing a new child-count +counter or demanding nested parent_children0. RootA native success, real wire +grab/configure/ACK/SHM/map and clipped pre-injection paint qualify nested start. + +Availability identity (actual /proc exe hash, BuildID, mappings, exact wlroots, +static SceneFX), tree/outputs and fresh exclusive neighbor press/release are +saved **before cleanup**. A surviving painted rootA receives fresh right-button +input before any neighbor click, checked against its actual configure, updated +owner origin, exclusive wire pair and pointer-local coordinates. Actual root +dismissal and ENOMEM disconnect remain distinct NOT_EXERCISED input outcomes. +H2 pre/post/final must equal the whole reference. Failed-popup color is absent +on both outputs before/post injection; final owner close rejects both A and B +on both outputs. Surviving A is allowed after nested B failure. +Fail-closed and availability are separate observations; full damage, GPU +retirement, presented-buffer identity and atomic generation remain NOT_PROVEN. +OM-NESTED needs qualified root outcome and scientific cleanup, or a new explicit +independent dependency review with rationale and no common native fault. + +Preparation never executes clients or compositors. All generated protocols, +ELFs, pycache, caches, tests and receipts belong to a fresh canonical owned0700 +LAB directly below /var/tmp. Run every command using an allowlist environment +with TMPDIR/TMP/TEMP=LAB/scratch, XDG_CACHE_HOME=LAB/cache, XDG_DATA_HOME=LAB/data, +PYTHONPYCACHEPREFIX=LAB/pycache, CCACHE_DIR=LAB/cache/ccache, SMX_LAB=LAB. Source +may reside in a fresh Git extraction; no historical absolute path is required. + +```sh +python graphics/prepare.py --lab "$LAB" +python graphics/validate_offline.py --lab "$LAB" +python graphics/freeze.py --lab "$LAB" --author author-identity +``` + +The optional `validate_offline.py --historical-c7 ` +replays preserved resized-back and migrated-H2 snapshots through the same live +validator and rejects a stale-camera mutation. It does not upgrade old C7. +The manifest goes to LAB/followup-work/source-manifest.json and contains only +relative hashes and modes for the **whole followup source kit**. Review/test/publication +and runtime identities stay outside source. Review is never generated by these +commands. `freeze` refuses replacement; changed source requires a new explicitly +reviewed freeze path/version using `--output "$LAB/followup-work/source-v2.json"`, +not an automatic rewrite at launch. + +Independent review JSON binds ticket, manifest_sha256, independent:true, +reviewer different from manifest.author, decision:ACCEPTED, runs[ID] with +ACCEPTED/client_build_review and client_build_receipt path/SHA. `gates[NAME]` +binds decision:ACCEPTED, manifest_sha256 and receipt path/SHA: the receipt requires +status PASS, tests_run>0, failures0 and source_digest of the manifest files. +Required names: FULLSCREEN_COMPARABILITY_OFFLINE, MIGRATION_REFOCUS_OFFLINE, +OOM_ROOT_NESTED_ORACLE_OFFLINE, separately by scenario. Runtime identity file +is external via review.runtime_manifest / runtime_manifest_sha256, with ticket +and identities[pristine/core/oom]: binary path/SHA/BuildID, wlroots path/SHA, +build_review and source_qualification ACCEPTED; pristine also binds its shared +SceneFX path/SHA. OOM runs additionally require oom_source_review ACCEPTED, +variant_identity COREC_V2_NATIVE_OOM_DIAGNOSTIC and source_manifest_sha256 in +runs[ID]. Clients are newly built in LAB/followup-work/client-build. Source, +protocol and ELF hashes are reconciled before admission and actions. + +Publication JSON binds ticket, decision VERIFIED, source_commit, manifest SHA, +source_digest, origin_sha/github_sha equal to that commit, offline_test_only:false +and direct remote_observations origin/github {argv:[git,ls-remote,remote, +refs/heads/main],exit:0,stdout:}. The real preflight also hashes +`git show :` bytes for every source entry before reservation. +The same gate reads `git ls-tree -rz --full-tree -- ` +without a shell, parses NUL-delimited paths and requires the exact inventory, +blob type and matching 100644/100755 modes. Local freeze/refreeze rejects +nonportable modes: source files use 0644 (or explicitly reviewed 0755); receipt +0600 permissions are never represented as portable source permissions. +Matching remote strings alone do not bind a source kit to Git. Ledger init is +an explicit parent operation `Ledger(LAB).initialize()` after provenance gate; +no child/test creates a real ledger. Example single-case CLI after publication: + +```sh +python graphics/run.py FS-P --lab "$LAB" \ + --manifest "$LAB/followup-work/source-manifest.json" \ + --review "$LAB/reviews/independent-graphics.json" \ + --publication "$LAB/reviews/pre-run-publication.json" +``` + +Ledger state/lock enforce nofollow, regular file, owner, mode0600, single link, +stable flock inode and atomic fsync replacement. Offline tests use only +OFFLINE_TEST_ONLY ledgers and can never launch Session. Missing, stale or +superficial input rejects before debit. Logs/actions are bounded; an independent +watchdog TERM143/KILL≤146 seconds owns only registered process groups. Runtime +creation probes quota with a write in a new /tmp/smx-*0700 directory and falls +back on EDQUOT/ENOSPC to /var/tmp/smx-* if UNIX path fits. Runtime is socket-only. +All IPC uses explicit private swaymsg -s. Daily display/socket/loader variables +are excluded by allowlist. Prlimit sets core0 for every owned child; fixture +clients and Python explicitly set PR_SET_DUMPABLE0. Linux may reset dumpable on +exec of the unchanged compositor ELF: this is a stated limitation, never an +asserted native dumpable0 measurement. No system coredump cleanup is attempted. + +The GPU-G3 adapter imports only the frozen sibling gpu/ modules and validates +every module origin and the exact acquisition ROOT. Its Session identity stays +GPU-G3/gpu. The adapter redirects MOSAIC_G3_DIR/NONCE and WLR_RENDERER exclusively +to compositor_environment; ordinary helpers and clients receive no hook vars. +Each epoch debits mutation+hook before arm. Rejected/incomplete A stops before +B and A-return. Native protocol validation and GL queries are owned by gpu/, +not reimplemented here. GPU background closure checks its real layer-shell role; +reference/glass check XDG, all with actual error0 exit/SHM release/wire destroys. + +Additional GPU review fields: gates.B4 with the ordinary test semantic receipt; +runs.GPU-G3 gpu_source_review/gl_observability_preflight/build_review ACCEPTED, +each with *_receipt and *_receipt_sha256 whose content is independent:true, +decision ACCEPTED and exact manifest_sha256; native_gl_status NOT_RUN; +gpu_source_manifest(+SHA) schema G3-relative-source-freeze-v1; gpu_client_build_receipt +(+SHA) {status:PASS,source_sha256:,client_sha256:}; +gate_file(+SHA); reviewed_scene_sha256; binding {binary,client,client_sha256, +source_manifest_sha256,gate_file,reviewed_scene_sha256}. The native gate separately +binds source_review ACCEPTED, gl_observability PASS, runtime_authorized true, +diagnostic GPU-G3, binary_sha256 and exact acquisition_root. Every receipt must +be canonical owned0600 below the new LAB. Runtime identity gpu binary must differ +from every other identity. These are input contracts, never auto-written accepts. diff --git a/tooling/probes/step20c-corec-v2-followup/graphics/cases.py b/tooling/probes/step20c-corec-v2-followup/graphics/cases.py new file mode 100644 index 0000000..453ffc8 --- /dev/null +++ b/tooling/probes/step20c-corec-v2-followup/graphics/cases.py @@ -0,0 +1,175 @@ +"""Small campaign scenarios over the same reviewed Session; no historical runner copy.""" +import re,time +from PIL import ImageChops +from engine import * +from geometry import * + +def init_reference(s,prefix_fillers=1): + s.cmd('workspace 2');s.launch_client('reference','static-reference');rid=s.find('reference')['id'];s.cmd(f'[con_id={rid}] border none') + s.cmd('workspace 1') + for i in range(prefix_fillers):s.launch_client('filler'+str(i),'static-reference') + s.cmd('layout mosaic' if s.spec['mode']=='mosaic' else 'layout splith') + return rid + +def stable_reference(s): + s.inject('move',[s.outputs[0]['logical'][0]+10,s.outputs[0]['logical'][1]+10]);time.sleep(.15) + a=s.capture('H2-reference1','HEADLESS-2');time.sleep(.6);b=s.capture('H2-reference2','HEADLESS-2');exact(a,b) + need(all(v==(48,48,56) for v in a.get_flattened_data()),'static uniform H2');s.save('reference-stability',dict(status='PASS',exact=exact(a,b)));return a + +def root_state(s,state,origin,window_start,expect_hidden=False,previous=None,expected_tree=None): + # WHY/CONTRACT/INVARIANT: no PI/native scene identity exists in CORE. + # Pixel/input evidence qualifies sampled clipping only, never generation. + return dict(status='NOT OBSERVABLE',native_tree_identity='NOT OBSERVABLE', + applied_generation='NOT OBSERVABLE',owner_id=state['owner']['id'], + workspace_id=state['workspace']['id'],window=[window_start,time.monotonic_ns()]) + +def open_chain(s,tail_fillers=0): + + s.cmd('workspace 1');s.cmd('layout mosaic' if s.spec['mode']=='mosaic' else 'layout splith');s.expected_layout='mosaic' if s.spec['mode']=='mosaic' else 'splith' + pre=s.snapshot('before-chain-'+str(sum(e['kind']=='spawn' and e['name']=='owner' for e in s.events))) + validators.selected_workspace(pre,'1','HEADLESS-1',s.expected_layout) + s.debit('chain');s.launch_client('owner',s.spec['client'],s.spec['placement']) + s.owner('owner') # Reject wrong map before any click; never move mapped popup chains. + for i in range(tail_fillers):s.launch_client('tail'+str(i),'static-reference') + s.cmd(f'[con_id={s.find("owner")["id"]}] focus');state=s.calibrate('owner');baseline=s.reference if hasattr(s,'reference') else stable_reference(s);s.reference=baseline + im=s.capture('owner-top-'+str(sum(e['kind']=='spawn' and e['name']=='owner' for e in s.events)),'HEADLESS-1');rect=[*state['origin'],state['owner']['window_rect']['width'],state['owner']['window_rect']['height']] + xy=solid_point(im,COLORS['TOPLEVEL'],s.outputs[0],s.frames['HEADLESS-1'],rect) + need(contains([*state['origin'],420,575 if s.spec['axis']==1 else 320],xy),'opening native bounds') + ta,ba=s.click('owner',xy,'left',True);s.wait(lambda:'POPUP_A MAP ' in s.log('owner'));time.sleep(.2) + aim=s.capture('popup-A-'+str(sum(e['kind']=='spawn' and e['name']=='owner' for e in s.events)),'HEADLESS-1');prov=Provenance(state['owner']['id'],state['workspace']['id'],'NOT OBSERVABLE',state['evidence']) + xyA=solid_point(aim,COLORS['POPUP_A'],s.outputs[0],s.frames['HEADLESS-1']) + tb,bb=s.click('owner',xyA,'left',True);s.wait(lambda:'POPUP_B MAP ' in s.log('owner'));time.sleep(.2) + window=time.monotonic_ns();time.sleep(.15);p=validators.grab(s.log('owner'));g=geometry(s.log('owner'),state['origin'],prov,s.spec['axis'],s.spec['boundary']) + s.delivery('owner',rect,xy,ta,ba,'TOPLEVEL',p['grabs'][0]['serial']);s.delivery('owner',g['A'],xyA,tb,bb,'POPUP_A',p['grabs'][1]['serial']) + [validators.crosses(g[k],s.spec['axis'],s.spec['boundary']) for k in ('A','B')] + left,right=s.pair_capture('mapped-'+str(sum(e['kind']=='spawn' and e['name']=='owner' for e in s.events))) + for name in ('POPUP_A','POPUP_B'): + need(count(left,COLORS[name])>0,'permitted popup pixels missing') + if s.spec['mode']=='pristine':need(count(right,COLORS[name])>0,'standard popup continuation missing') + if s.spec['mode']=='mosaic':validators.rgb(baseline,right);root=root_state(s,state,state['origin'],window) + else:root=dict(status='NOT OBSERVABLE',meaning='unmodified control has no private clip instrumentation') + s.save('chain',dict(protocol=p,geometry=g,state=state,root=root,H2_exact=s.spec['mode']=='mosaic',color_counts={n:[count(left,c),count(right,c)] for n,c in COLORS.items()})) + return state,baseline,p,g,left,right + +def popup_campaign(s): + init_reference(s);state,baseline,p,g,left,right=open_chain(s) + xy=solid_point(left,COLORS['POPUP_B'],s.outputs[0],s.frames['HEADLESS-1'],g['B']);tails,buttons=s.click('owner',xy);s.delivery('owner',g['B'],xy,tails,buttons,'POPUP_B') + forbidden=intersection(g['B'],s.outputs[1]['logical']);need(forbidden[2]>6 and forbidden[3]>6,'H2 crossing interior missing') + xy=[forbidden[0]+forbidden[2]//2,forbidden[1]+forbidden[3]//2] + if s.spec['mode']=='pristine': + # The point must be real standard paint, not just a requested extent. + xy=solid_point(right,COLORS['POPUP_B'],s.outputs[1],s.frames['HEADLESS-2'],g['B']);tails,buttons=s.click('owner',xy);s.delivery('owner',g['B'],xy,tails,buttons,'POPUP_B') + else: + tails,buttons=s.click('owner',xy);ds=dismissal(tails['owner'],buttons['owner']);s.save('dismissal',dict(proof=ds,reference_buttons=buttons.get('reference'),interpretation='outside grab dismissal; no universal click-through claim')) + need(not any('target=POPUP_A ' in l or 'target=POPUP_B ' in l for v in buttons.values() for l in v),'forbidden popup delivery') + s.close_client('owner');time.sleep(.2);final=s.pair_capture('final');exact(baseline,final[1]);need(not any(count(im,COLORS[n]) for im in final for n in ('POPUP_A','POPUP_B')),'popup residual') + return 'PARTIALLY_FEASIBLE' + +def hidden_campaign(s): + init_reference(s,2);state,baseline,p,g,left,right=open_chain(s,2) + xy=solid_point(left,COLORS['POPUP_B'],s.outputs[0],'logical',g['B']);tails,buttons=s.click('owner',xy);s.delivery('owner',g['B'],xy,tails,buttons,'POPUP_B') + delta=state['scene'][0]+state['scene'][2]-s.outputs[0]['logical'][0]+16;start=time.monotonic_ns();s.scroll('owner',delta);hidden=s.owner('owner') + need(intersection(hidden['scene'],s.outputs[0]['logical'])[2]==0,'owner not completely hidden') + images=s.pair_capture('fully-hidden');need(not any(count(im,COLORS[n]) for im in images for n in ('POPUP_A','POPUP_B')),'hidden popup residual');exact(baseline,images[1]) + root=root_state(s,hidden,hidden['origin'],start,True,previous=state);cut=len(s.log('owner'));s.inject('move',[-790,590]);need('target=POPUP_A ' not in s.log('owner')[cut:] and 'target=POPUP_B ' not in s.log('owner')[cut:],'hidden popup pointer enter') + validators.hidden_survival(s.log('owner'),p,[]) + return_start=time.monotonic_ns();s.scroll('owner',-delta);restored=s.owner('owner');need(restored['origin']==state['origin'],'same owner origin restored') + images=s.pair_capture('returned');validators.hidden_survival(s.log('owner'),p,[e for e in s.events if e['kind']=='injection' and start<=e['ns']<=return_start]);exact(baseline,images[1]);returned_root=root_state(s,restored,restored['origin'],return_start,previous=hidden,expected_tree=root.get('tree'));fresh=validators.grab(s.log('owner'));need(fresh['ids']==p['ids'] and fresh['grabs']==p['grabs'],'unchanged live chain') + g=geometry(s.log('owner'),restored['origin'],Provenance(restored['owner']['id'],restored['workspace']['id'],'NOT OBSERVABLE','fresh return geometry'));xy=solid_point(images[0],COLORS['POPUP_B'],s.outputs[0],'logical',g['B']);tails,buttons=s.click('owner',xy);s.delivery('owner',g['B'],xy,tails,buttons,'POPUP_B') + s.save('hidden-return',dict(status='PASS',hidden=hidden,empty_root=root,returned=restored,returned_root=returned_root,ids=p['ids'])) + s.close_client('owner');final=s.pair_capture('final');exact(baseline,final[1]);return 'PARTIALLY_FEASIBLE' + +def sample_surviving(s,images,tag): + if 'POPUP_DONE ' in s.log('owner'): + s.save(tag+'-input',dict(status='NOT_EXERCISED',reason='legitimate popup dismissal, no new chain reserved'));return + state=s.owner('owner');prot=validators.grab(s.log('owner'));geo=geometry(s.log('owner'),state['origin'],Provenance(state['owner']['id'],state['workspace']['id'],'NOT OBSERVABLE','mutation paint+pointer')) + index=next(i for i,o in enumerate(s.outputs) if o['name']==state['workspace']['output']) + try:point=solid_point(images[index],COLORS['POPUP_B'],s.outputs[index],s.frames[s.outputs[index]['name']],geo['B']) + except ValueError: + s.save(tag+'-input',dict(status='NOT_EXERCISED',reason='B has no measured visible interior'));return + tails,buttons=s.click('owner',point);s.delivery('owner',geo['B'],point,tails,buttons,'POPUP_B');s.save(tag+'-input',dict(status='PASS',point=point,protocol=prot)) + +def dynamic_geometry(snap,previous=None,mutation='resize',expected_focus=None): + # WHY: migration move.c arranges both outputs, then seat_set_focus reveals + # the current selected cell. CONTRACT: derive clamp/reveal from current row + # extents, gaps, viewport and focus; no universal800/1120 expected value. + # INVARIANT: IPC is a candidate; paint/input after each mutation qualify only + # sampled applied behavior, never a native atomic generation. + need(snap.get('state_source')!='pending-as-current','pending mixed with current') + ws=workspace(snap['tree'],'1');need(ws.get('layout','mosaic')=='mosaic','dynamic layout') + rows=ws['mosaic']['rows'];need(len(rows)==1,'runtime Row0 only');row=rows[0] + children=ws['nodes'];ids=[c['id'] for c in children] + need(len(children)>=2 and row['children']==ids and len(set(ids))==len(ids),'dynamic row order/identity') + viewport=[ws['rect'][k] for k in ('x','y','width','height')];width=viewport[2] + need(width>0,'dynamic viewport width') + widths=[c['rect']['width'] for c in children] + gaps=[b['rect']['x']-a['rect']['x']-a['rect']['width'] for a,b in zip(children,children[1:])] + need(len(set(widths))==1 and widths[0]>=640 and len(set(gaps))==1 and gaps[0]>=0,'pinned equal cell / gap geometry') + gap=gaps[0];fitting=(width-(len(children)-1)*gap)//len(children) + need(widths[0]==max(640,fitting),'native default640 sizing mismatch') + need(children[0]['rect']['x']==viewport[0],'row start differs from viewport') + content=children[-1]['rect']['x']+widths[-1]-viewport[0];maximum=max(0,content-width) + scroll=row['scroll_x'];need(0<=scroll<=maximum,'camera outside native clamp') + focused=[c for c in children if c.get('focused')] + need(len(focused)==1 and row.get('focused')==focused[0]['id'],'missing/ambiguous row refocus') + selected=focused[0] + if expected_focus is not None:need(selected['id']==expected_focus,'refocus selected another owner') + result=dict(workspace_id=ws['id'],output=ws['output'],children=ids,viewport=viewport,minimum_width=640,cell_width=widths[0],gap=gap,content_width=content,min_scroll=0,max_scroll=maximum,scroll=scroll,focused=selected['id'],geometry_status='IPC_PENDING_CANDIDATE; native generation NOT_OBSERVABLE') + if previous is not None: + need(ws['id']==previous['workspace_id'] and ids==previous['children'],'workspace/order changed') + need(mutation in ('resize','migration'),'unknown camera mutation') + candidate=max(0,min(previous['scroll'],maximum));expected=candidate + if mutation=='migration': + lo=selected['rect']['x']-viewport[0];hi=lo+selected['rect']['width'] + if loexpected+width:expected=hi-width + expected=max(0,min(expected,maximum)) + need(scroll==expected,'native reconstructed '+mutation+' camera mismatch') + result.update(expected_scroll=expected,resize_candidate=candidate,mutation=mutation,clamp='PASS') + return result + + +def dynamic_pixels(images,outputs,frames,viewport,reference=None): + # WHY/CONTRACT: each mutation must reject old-output and outside-clip paint. + # INVARIANT: sampled RGB exclusion cannot prove complete native damage. + for image,out in zip(images,outputs): + for y in range(image.height): + for x in range(image.width): + if image.getpixel((x,y)) in (COLORS['POPUP_A'],COLORS['POPUP_B']): + need(contains(viewport,png_global((x,y),out,frames[out['name']])),'dynamic popup pixel outside current viewport') + if reference is not None:exact(reference,images[1]) + return dict(exclusion='PASS',whole_H2='PASS' if reference is not None else 'NOT_APPLICABLE: current owner workspace on H2',complete_damage='NOT OBSERVABLE') + +def dynamic_campaign(s): + init_reference(s,2);s.cmd('gaps inner 0');state,baseline,p,g,left,right=open_chain(s) + owner_id=state['owner']['id'];previous=dynamic_geometry(state['snapshot'],expected_focus=owner_id);stages=[] + mutations=[('output HEADLESS-1 resolution 960x600 position -960 0','resized960','HEADLESS-1'), + ('output HEADLESS-1 resolution 800x600 position -800 0','resized-back','HEADLESS-1'), + ('move workspace to output HEADLESS-2','migrated-H2','HEADLESS-2'), + ('move workspace to output HEADLESS-1','migrated-back-H1','HEADLESS-1')] + for command,tag,out in mutations: + s.cmd('workspace 1');s.record('invalidate-mutation-coordinates',tag=tag,old_viewport=previous['viewport'],old_camera=previous['scroll'],old_focus=previous['focused']);s.cmd(command,True);s.expected_output=out + if tag=='resized960':s.outputs[0]={**s.outputs[0],'logical':[-960,0,960,600],'mode':[960,600]} + elif tag=='resized-back':s.outputs[0]=dict(s.spec['outputs'][0]) + time.sleep(.35);snap=s.snapshot(tag);current=dynamic_geometry(snap,previous,"migration" if tag.startswith("migrated") else "resize",owner_id) + need(current['output']==out and s.find('owner')['id']==owner_id,'dynamic exact owner/output') + expected_viewport=next(o['logical'] for o in s.outputs if o['name']==out) + need(current['viewport']==expected_viewport,'dynamic viewport/output reconstruction') + pictures=s.pair_capture(tag) + pixels=dynamic_pixels(pictures,s.outputs,s.frames,current['viewport'],baseline if out=='HEADLESS-1' else None) + # An alive chain must supply fresh paint and local pointer input; an + # actual XDG dismissal is recorded without injecting on destroyed popups. + if 'POPUP_DONE ' not in s.log('owner'): + fresh=s.owner('owner');geo=geometry(s.log('owner'),fresh['origin'],Provenance(owner_id,current['workspace_id'],'NOT OBSERVABLE','fresh mutation IPC + paint + pointer')) + index=next(i for i,o in enumerate(s.outputs) if o['name']==out) + point=solid_point(pictures[index],COLORS['POPUP_B'],s.outputs[index],s.frames[out],geo['B']) + tails,buttons=s.click('owner',point);s.delivery('owner',geo['B'],point,tails,buttons,'POPUP_B') + need(validators.grab(s.log('owner'))['ids']==p['ids'],'dynamic popup IDs changed') + input_proof=dict(status='PASS',point=point) + else:input_proof=dict(status='NOT_EXERCISED',reason='XDG dismissal; no injection on destroyed popup') + stages.append(dict(tag=tag,geometry=current,pixels=pixels,input=input_proof));previous=current + s.save('dynamic-ownership',dict(status='PASS',stages=stages,ids_before=p['ids'],atomic_generation='NOT PROVEN',complete_damage='NOT OBSERVABLE')) + s.close_client('owner') + for name in list(s.proc): + if name.startswith('filler'):s.close_client(name,'close') + final=s.pair_capture('dynamic-final');exact(baseline,final[1]);need(not any(count(im,COLORS[n]) for im in final for n in ('POPUP_A','POPUP_B')),'dynamic final popup residual');s.save('dynamic-final-restoration',dict(status='PARTIALLY_FEASIBLE',H2='EXACT',captured_after_close=True));return 'PARTIALLY_FEASIBLE' diff --git a/tooling/probes/step20c-corec-v2-followup/graphics/clients/fullscreen-client.c b/tooling/probes/step20c-corec-v2-followup/graphics/clients/fullscreen-client.c new file mode 100644 index 0000000..edf37b1 --- /dev/null +++ b/tooling/probes/step20c-corec-v2-followup/graphics/clients/fullscreen-client.c @@ -0,0 +1,310 @@ +#define _POSIX_C_SOURCE 200809L +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "xdg-shell-client.h" + +/* EXPERIMENTAL CONTRACT: every popup owns distinct XDG objects. B's actual + * parent is A's xdg_surface; logs name object IDs as well as colors. */ +struct surface { + const char *name; + struct wl_surface *wl; + struct xdg_surface *xdg; + struct xdg_popup *popup; + bool configured, mapped, done; + int width, height, anchor_x, anchor_y; + uint32_t color; +}; +static struct wl_display *display; +static struct wl_compositor *compositor; +static struct wl_shm *shm; +static struct xdg_wm_base *wm; +static struct wl_seat *seat; +static struct wl_pointer *pointer; +static struct xdg_toplevel *top_role; +static struct surface top = {.name="TOPLEVEL", .width=420, .height=320, .color=0x002344cc}; +static struct surface a = {.name="POPUP_A", .width=300, .height=170, .color=0x00e0c020}; +static struct surface b = {.name="POPUP_B", .width=300, .height=130, .color=0x0020d0a0}; +static volatile sig_atomic_t running = 1; +static struct surface *entered; + +/* Observe client-side SHM release separately from native GPU retirement. + * Keep protocol buffer alive until release; no backing memory is reused. */ +static unsigned shm_created, shm_released; +static void buffer_release(void *data, struct wl_buffer *buffer) { + (void)data; + ++shm_released; + fprintf(stderr,"SHM_RELEASE buffer=%u released=%u created=%u\n", + wl_proxy_get_id((struct wl_proxy *)buffer),shm_released,shm_created); + wl_buffer_destroy(buffer); +} +static const struct wl_buffer_listener buffer_listener = {.release=buffer_release}; +static void draw(struct surface *s) { + size_t bytes = (size_t)s->width * (size_t)s->height * 4; + char path[4096]; + const char *scratch = getenv("TMPDIR"); + const char *lab = getenv("SMX_LAB"); + /* WHY: SHM belongs to LAB, never the short socket-only runtime. + * CONTRACT: launcher supplies canonical LAB/scratch; fail before mkstemp. */ + char expected[4096]; + int expected_len = lab ? snprintf(expected, sizeof(expected), "%s/scratch", lab) : -1; + int path_len = scratch ? snprintf(path, sizeof(path), "%s/smx-shm-XXXXXX", scratch) : -1; + if (!lab || strncmp(lab, "/var/tmp/", 9) || strstr(lab, "/../") || + expected_len < 0 || expected_len >= (int)sizeof(expected) || + !scratch || strcmp(scratch, expected) || path_len < 0 || + path_len >= (int)sizeof(path)) { + fprintf(stderr, "invalid LAB scratch for SHM\n"); + exit(1); + } + int fd = mkstemp(path); + if (fd < 0 || ftruncate(fd, (off_t)bytes) < 0) { perror("buffer"); exit(1); } + unlink(path); + uint32_t *pixels = mmap(NULL, bytes, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + if (pixels == MAP_FAILED) { perror("mmap"); exit(1); } + for (size_t i=0; icolor; + struct wl_shm_pool *pool = wl_shm_create_pool(shm, fd, (int)bytes); + struct wl_buffer *buffer = wl_shm_pool_create_buffer(pool, 0, s->width, s->height, + s->width*4, WL_SHM_FORMAT_XRGB8888); + ++shm_created; + wl_buffer_add_listener(buffer,&buffer_listener,NULL); + wl_surface_attach(s->wl, buffer, 0, 0); + wl_surface_damage_buffer(s->wl, 0, 0, s->width, s->height); + wl_surface_commit(s->wl); + wl_shm_pool_destroy(pool); + munmap(pixels, bytes); + close(fd); + s->mapped = true; + fprintf(stderr, "%s MAP wl=%u xdg=%u\n", s->name, + wl_proxy_get_id((struct wl_proxy *)s->wl), wl_proxy_get_id((struct wl_proxy *)s->xdg)); +} +static void on_ping(void *data, struct xdg_wm_base *base, uint32_t serial) { + xdg_wm_base_pong(base, serial); +} +static const struct xdg_wm_base_listener wm_listener = {.ping=on_ping}; +static void on_xdg_configure(void *data, struct xdg_surface *xdg, uint32_t serial) { + struct surface *s=data; + fprintf(stderr, "%s XDG_CONFIGURE serial=%u ack\n", s->name, serial); + xdg_surface_ack_configure(xdg, serial); + s->configured=true; + if (!s->done) draw(s); /* fresh native commit after every configure */ +} +static const struct xdg_surface_listener xdg_listener = {.configure=on_xdg_configure}; +static void on_top_configure(void *data, struct xdg_toplevel *role, int32_t w, + int32_t h, struct wl_array *states) { + fprintf(stderr, "TOPLEVEL ROLE_CONFIGURE w=%d h=%d\n", w,h); +} +static void on_top_close(void *data, struct xdg_toplevel *role) { + fprintf(stderr, "TOPLEVEL CLOSE_REQUEST\n"); running=0; +} +static const struct xdg_toplevel_listener top_listener = { + .configure=on_top_configure, .close=on_top_close, +}; +static void on_popup_configure(void *data, struct xdg_popup *role, int32_t x, + int32_t y, int32_t w, int32_t h) { + struct surface *s=data; + fprintf(stderr, "%s ROLE_CONFIGURE x=%d y=%d w=%d h=%d\n",s->name,x,y,w,h); +} +static void on_popup_done(void *data, struct xdg_popup *role) { + struct surface *s=data; + s->done=true; + s->mapped=false; + fprintf(stderr, "%s POPUP_DONE role=%u\n",s->name,wl_proxy_get_id((struct wl_proxy *)role)); +} +static const struct xdg_popup_listener popup_listener = { + .configure=on_popup_configure, .popup_done=on_popup_done, +}; +/* WHY: XDG requires child-first destruction even if the compositor sends + * dismissals for multiple levels in one dispatch batch. */ +static void destroy_popup(struct surface *s) { + if (!s->popup) return; + fprintf(stderr,"%s DESTROY role=%u xdg=%u wl=%u\n",s->name, + wl_proxy_get_id((struct wl_proxy *)s->popup), + wl_proxy_get_id((struct wl_proxy *)s->xdg), + wl_proxy_get_id((struct wl_proxy *)s->wl)); + xdg_popup_destroy(s->popup); + xdg_surface_destroy(s->xdg); + wl_surface_destroy(s->wl); + s->popup=NULL; s->xdg=NULL; s->wl=NULL; s->mapped=false; + if (entered==s) entered=NULL; +} +static void collect_dismissals(void) { + if (b.done) destroy_popup(&b); + if (a.done) { + destroy_popup(&b); + destroy_popup(&a); + } +} +static void open_popup(struct surface *s, struct surface *parent, uint32_t serial) { + if (s->popup || !parent->mapped || !serial) return; + s->wl=wl_compositor_create_surface(compositor); + s->xdg=xdg_wm_base_get_xdg_surface(wm,s->wl); + xdg_surface_add_listener(s->xdg,&xdg_listener,s); + struct xdg_positioner *pos=xdg_wm_base_create_positioner(wm); + /* WHY/CONTRACT/INVARIANT: explicit identical adjustment in both controls. */ + xdg_positioner_set_constraint_adjustment(pos, 0); + xdg_positioner_set_size(pos,s->width,s->height); + xdg_positioner_set_anchor_rect(pos,s->anchor_x,s->anchor_y,1,1); + xdg_positioner_set_anchor(pos,XDG_POSITIONER_ANCHOR_TOP_LEFT); + xdg_positioner_set_gravity(pos,XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT); + s->popup=xdg_surface_get_popup(s->xdg,parent->xdg,pos); + xdg_popup_add_listener(s->popup,&popup_listener,s); + fprintf(stderr,"%s CREATE wl=%u xdg=%u role=%u parent=%s parent_xdg=%u\n", + s->name,wl_proxy_get_id((struct wl_proxy *)s->wl), + wl_proxy_get_id((struct wl_proxy *)s->xdg), + wl_proxy_get_id((struct wl_proxy *)s->popup),parent->name, + wl_proxy_get_id((struct wl_proxy *)parent->xdg)); + fprintf(stderr,"%s GRAB_REQUEST origin=wl_pointer.button target=%s serial=%u used=%u\n", + s->name,parent->name,serial,serial); + xdg_popup_grab(s->popup,seat,serial); + wl_surface_commit(s->wl); /* Initial bufferless commit requests configure. */ + xdg_positioner_destroy(pos); +} +static struct surface *which(struct wl_surface *wl) { + if (wl==top.wl) return ⊤ + if (wl==a.wl && wl) return &a; + if (wl==b.wl && wl) return &b; + return NULL; +} +static void on_enter(void *data,struct wl_pointer *ptr,uint32_t serial, + struct wl_surface *wl,wl_fixed_t x,wl_fixed_t y) { + entered=which(wl); + fprintf(stderr,"POINTER_ENTER target=%s wl=%u serial=%u x=%.2f y=%.2f\n", + entered?entered->name:"OTHER",wl_proxy_get_id((struct wl_proxy *)wl),serial, + wl_fixed_to_double(x),wl_fixed_to_double(y)); +} +static void on_leave(void *data,struct wl_pointer *ptr,uint32_t serial, + struct wl_surface *wl) { + struct surface *s=which(wl); + fprintf(stderr,"POINTER_LEAVE target=%s serial=%u\n",s?s->name:"OTHER",serial); + if (entered==s) entered=NULL; +} +static void on_motion(void *data,struct wl_pointer *ptr,uint32_t time, + wl_fixed_t x,wl_fixed_t y) { + fprintf(stderr,"POINTER_MOTION target=%s x=%.2f y=%.2f\n", + entered?entered->name:"OTHER",wl_fixed_to_double(x),wl_fixed_to_double(y)); +} +static void on_button(void *data,struct wl_pointer *ptr,uint32_t serial, + uint32_t time,uint32_t button,uint32_t state) { + fprintf(stderr,"POINTER_BUTTON target=%s serial=%u button=%u state=%u\n", + entered?entered->name:"OTHER",serial,button,state); + if (state!=WL_POINTER_BUTTON_STATE_PRESSED || button!=272) return; + if (entered==&top && !a.popup) open_popup(&a,&top,serial); + else if (entered==&a && a.mapped && !b.popup) open_popup(&b,&a,serial); +} +static void on_axis(void *data,struct wl_pointer *ptr,uint32_t t,uint32_t axis,wl_fixed_t v) {} +static void on_frame(void *data,struct wl_pointer *ptr) {} +static const struct wl_pointer_listener pointer_listener={ + .enter=on_enter,.leave=on_leave,.motion=on_motion,.button=on_button, + .axis=on_axis,.frame=on_frame, +}; +static void on_caps(void *data,struct wl_seat *s,uint32_t caps) { + if ((caps & WL_SEAT_CAPABILITY_POINTER) && !pointer) { + pointer=wl_seat_get_pointer(s); + wl_pointer_add_listener(pointer,&pointer_listener,NULL); + } +} +static void on_seat_name(void *data,struct wl_seat *s,const char *name) {} +static const struct wl_seat_listener seat_listener={.capabilities=on_caps,.name=on_seat_name}; +static void on_global(void *data,struct wl_registry *reg,uint32_t name, + const char *interface,uint32_t version) { + if (!strcmp(interface,wl_compositor_interface.name)) + compositor=wl_registry_bind(reg,name,&wl_compositor_interface,version<4?version:4); + else if (!strcmp(interface,wl_shm_interface.name)) + shm=wl_registry_bind(reg,name,&wl_shm_interface,1); + else if (!strcmp(interface,xdg_wm_base_interface.name)) + wm=wl_registry_bind(reg,name,&xdg_wm_base_interface,version<3?version:3); + else if (!strcmp(interface,wl_seat_interface.name)) { + seat=wl_registry_bind(reg,name,&wl_seat_interface,version<5?version:5); + wl_seat_add_listener(seat,&seat_listener,NULL); + } +} +static void on_removed(void *data,struct wl_registry *reg,uint32_t name) {} +static const struct wl_registry_listener registry_listener={.global=on_global,.global_remove=on_removed}; +static void stop(int sig) { running=0; } +int main(int argc,char **argv) { + /* WHY/CONTRACT/INVARIANT: no core dump or dumpable client in this LAB. */ + struct rlimit no_core = {0, 0}; + if (setrlimit(RLIMIT_CORE, &no_core) || prctl(PR_SET_DUMPABLE, 0)) return 1; + signal(SIGTERM,stop); + const char *placement=argc>1?argv[1]:"b_cross"; + /* EXPERIMENTAL: only placement changes; serial/grab/lifecycle stay historical. + * Both 1x1 anchors lie inside their parent SHM window geometry. */ + /* WHY/CONTRACT/INVARIANT: fullscreen group geometry is measured; legal + * anchors and wide buffers request crossing without forcing native placement. */ + if (!strcmp(placement,"fullscreen_cross")) { + a.anchor_x=350; a.anchor_y=80; a.width=900; + b.anchor_x=100; b.anchor_y=60; b.width=900; + } else if (!strcmp(placement,"static_cross")) { + a.anchor_x=100; a.anchor_y=80; a.width=350; + b.anchor_x=100; b.anchor_y=60; b.width=300; + } else if (!strcmp(placement,"visible")) { + a.anchor_x=50; a.anchor_y=80; a.width=250; + b.anchor_x=20; b.anchor_y=60; b.width=170; + } else if (!strcmp(placement,"both_cross")) { + a.anchor_x=350; a.anchor_y=80; a.width=350; + b.anchor_x=160; b.anchor_y=60; b.width=300; + } else if (!strcmp(placement,"b_cross")) { + a.anchor_x=200; a.anchor_y=80; a.width=300; + b.anchor_x=220; b.anchor_y=60; b.width=300; + } else { fprintf(stderr,"invalid placement\n"); return 2; } + display=wl_display_connect(NULL); + if (!display) { fprintf(stderr,"cannot connect Wayland\n"); return 1; } + struct wl_registry *registry=wl_display_get_registry(display); + wl_registry_add_listener(registry,®istry_listener,NULL); + wl_display_roundtrip(display); wl_display_roundtrip(display); + if (!compositor || !shm || !wm || !seat || !pointer) { + fprintf(stderr,"missing Wayland globals/pointer\n"); return 1; + } + xdg_wm_base_add_listener(wm,&wm_listener,NULL); + top.wl=wl_compositor_create_surface(compositor); + top.xdg=xdg_wm_base_get_xdg_surface(wm,top.wl); + xdg_surface_add_listener(top.xdg,&xdg_listener,&top); + top_role=xdg_surface_get_toplevel(top.xdg); + xdg_toplevel_add_listener(top_role,&top_listener,NULL); + xdg_toplevel_set_title(top_role,"Mosaic Nested Grab Probe"); + xdg_toplevel_set_app_id(top_role,"org.mosaic.NestedGrabProbe"); + fprintf(stderr,"TOPLEVEL CREATE wl=%u xdg=%u role=%u placement=%s\n", + wl_proxy_get_id((struct wl_proxy *)top.wl), + wl_proxy_get_id((struct wl_proxy *)top.xdg), + wl_proxy_get_id((struct wl_proxy *)top_role),placement); + wl_surface_commit(top.wl); + while (running) { + wl_display_flush(display); + struct pollfd fds[2]={{.fd=wl_display_get_fd(display),.events=POLLIN}, + {.fd=STDIN_FILENO,.events=POLLIN}}; + int ready=poll(fds,2,500); + if (ready<0) { if (errno==EINTR) continue; perror("poll"); break; } + if (fds[0].revents & (POLLIN|POLLERR|POLLHUP)) { + if (wl_display_dispatch(display)<0) break; + collect_dismissals(); + } + if (fds[1].revents & POLLIN) { + char cmd[64]; + if (fgets(cmd,sizeof(cmd),stdin)) { + if (!strncmp(cmd,"close_b",7)) { destroy_popup(&b); fprintf(stderr,"COMMAND close_b DONE\n"); } + else if (!strncmp(cmd,"close_owner",11)) { fprintf(stderr,"CLOSE_RECEIVED\nCOMMAND close_owner\n"); running=0; } + } + } + } + destroy_popup(&b); destroy_popup(&a); + fprintf(stderr,"TOPLEVEL DESTROY\n"); + xdg_toplevel_destroy(top_role); xdg_surface_destroy(top.xdg); wl_surface_destroy(top.wl); + wl_display_roundtrip(display); + fprintf(stderr,"SHM_CLOSURE created=%u released=%u\n",shm_created,shm_released); + wl_display_flush(display); + /* WHY/CONTRACT/INVARIANT: exit0 does not prove protocol error0. */ + fprintf(stderr,"EXIT error=%d\n",wl_display_get_error(display)); + wl_display_disconnect(display); + return 0; +} diff --git a/tooling/probes/step20c-corec-v2-followup/graphics/clients/nested-client.c b/tooling/probes/step20c-corec-v2-followup/graphics/clients/nested-client.c new file mode 100644 index 0000000..4f75692 --- /dev/null +++ b/tooling/probes/step20c-corec-v2-followup/graphics/clients/nested-client.c @@ -0,0 +1,303 @@ +#define _POSIX_C_SOURCE 200809L +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "xdg-shell-client.h" + +/* EXPERIMENTAL CONTRACT: every popup owns distinct XDG objects. B's actual + * parent is A's xdg_surface; logs name object IDs as well as colors. */ +struct surface { + const char *name; + struct wl_surface *wl; + struct xdg_surface *xdg; + struct xdg_popup *popup; + bool configured, mapped, done; + int width, height, anchor_x, anchor_y; + uint32_t color; +}; +static struct wl_display *display; +static struct wl_compositor *compositor; +static struct wl_shm *shm; +static struct xdg_wm_base *wm; +static struct wl_seat *seat; +static struct wl_pointer *pointer; +static struct xdg_toplevel *top_role; +static struct surface top = {.name="TOPLEVEL", .width=420, .height=320, .color=0x002344cc}; +static struct surface a = {.name="POPUP_A", .width=300, .height=170, .color=0x00e0c020}; +static struct surface b = {.name="POPUP_B", .width=300, .height=130, .color=0x0020d0a0}; +static volatile sig_atomic_t running = 1; +static struct surface *entered; + +/* Observe client-side SHM release separately from native GPU retirement. + * Keep protocol buffer alive until release; no backing memory is reused. */ +static unsigned shm_created, shm_released; +static void buffer_release(void *data, struct wl_buffer *buffer) { + (void)data; + ++shm_released; + fprintf(stderr,"SHM_RELEASE buffer=%u released=%u created=%u\n", + wl_proxy_get_id((struct wl_proxy *)buffer),shm_released,shm_created); + wl_buffer_destroy(buffer); +} +static const struct wl_buffer_listener buffer_listener = {.release=buffer_release}; +static void draw(struct surface *s) { + size_t bytes = (size_t)s->width * (size_t)s->height * 4; + char path[4096]; + const char *scratch = getenv("TMPDIR"); + const char *lab = getenv("SMX_LAB"); + /* WHY: SHM belongs to LAB, never the short socket-only runtime. + * CONTRACT: launcher supplies canonical LAB/scratch; fail before mkstemp. */ + char expected[4096]; + int expected_len = lab ? snprintf(expected, sizeof(expected), "%s/scratch", lab) : -1; + int path_len = scratch ? snprintf(path, sizeof(path), "%s/smx-shm-XXXXXX", scratch) : -1; + if (!lab || strncmp(lab, "/var/tmp/", 9) || strstr(lab, "/../") || + expected_len < 0 || expected_len >= (int)sizeof(expected) || + !scratch || strcmp(scratch, expected) || path_len < 0 || + path_len >= (int)sizeof(path)) { + fprintf(stderr, "invalid LAB scratch for SHM\n"); + exit(1); + } + int fd = mkstemp(path); + if (fd < 0 || ftruncate(fd, (off_t)bytes) < 0) { perror("buffer"); exit(1); } + unlink(path); + uint32_t *pixels = mmap(NULL, bytes, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + if (pixels == MAP_FAILED) { perror("mmap"); exit(1); } + for (size_t i=0; icolor; + struct wl_shm_pool *pool = wl_shm_create_pool(shm, fd, (int)bytes); + struct wl_buffer *buffer = wl_shm_pool_create_buffer(pool, 0, s->width, s->height, + s->width*4, WL_SHM_FORMAT_XRGB8888); + ++shm_created; + wl_buffer_add_listener(buffer,&buffer_listener,NULL); + wl_surface_attach(s->wl, buffer, 0, 0); + wl_surface_damage_buffer(s->wl, 0, 0, s->width, s->height); + wl_surface_commit(s->wl); + wl_shm_pool_destroy(pool); + munmap(pixels, bytes); + close(fd); + s->mapped = true; + fprintf(stderr, "%s MAP wl=%u xdg=%u\n", s->name, + wl_proxy_get_id((struct wl_proxy *)s->wl), wl_proxy_get_id((struct wl_proxy *)s->xdg)); +} +static void on_ping(void *data, struct xdg_wm_base *base, uint32_t serial) { + xdg_wm_base_pong(base, serial); +} +static const struct xdg_wm_base_listener wm_listener = {.ping=on_ping}; +static void on_xdg_configure(void *data, struct xdg_surface *xdg, uint32_t serial) { + struct surface *s=data; + fprintf(stderr, "%s XDG_CONFIGURE serial=%u ack\n", s->name, serial); + xdg_surface_ack_configure(xdg, serial); + s->configured=true; + if (!s->mapped && !s->done) draw(s); +} +static const struct xdg_surface_listener xdg_listener = {.configure=on_xdg_configure}; +static void on_top_configure(void *data, struct xdg_toplevel *role, int32_t w, + int32_t h, struct wl_array *states) { + fprintf(stderr, "TOPLEVEL ROLE_CONFIGURE w=%d h=%d\n", w,h); +} +static void on_top_close(void *data, struct xdg_toplevel *role) { + fprintf(stderr, "TOPLEVEL CLOSE_REQUEST\n"); running=0; +} +static const struct xdg_toplevel_listener top_listener = { + .configure=on_top_configure, .close=on_top_close, +}; +static void on_popup_configure(void *data, struct xdg_popup *role, int32_t x, + int32_t y, int32_t w, int32_t h) { + struct surface *s=data; + fprintf(stderr, "%s ROLE_CONFIGURE x=%d y=%d w=%d h=%d\n",s->name,x,y,w,h); +} +static void on_popup_done(void *data, struct xdg_popup *role) { + struct surface *s=data; + s->done=true; + s->mapped=false; + fprintf(stderr, "%s POPUP_DONE role=%u\n",s->name,wl_proxy_get_id((struct wl_proxy *)role)); +} +static const struct xdg_popup_listener popup_listener = { + .configure=on_popup_configure, .popup_done=on_popup_done, +}; +/* WHY: XDG requires child-first destruction even if the compositor sends + * dismissals for multiple levels in one dispatch batch. */ +static void destroy_popup(struct surface *s) { + if (!s->popup) return; + fprintf(stderr,"%s DESTROY role=%u xdg=%u wl=%u\n",s->name, + wl_proxy_get_id((struct wl_proxy *)s->popup), + wl_proxy_get_id((struct wl_proxy *)s->xdg), + wl_proxy_get_id((struct wl_proxy *)s->wl)); + xdg_popup_destroy(s->popup); + xdg_surface_destroy(s->xdg); + wl_surface_destroy(s->wl); + s->popup=NULL; s->xdg=NULL; s->wl=NULL; s->mapped=false; + if (entered==s) entered=NULL; +} +static void collect_dismissals(void) { + if (b.done) destroy_popup(&b); + if (a.done) { + destroy_popup(&b); + destroy_popup(&a); + } +} +static void open_popup(struct surface *s, struct surface *parent, uint32_t serial) { + if (s->popup || !parent->mapped || !serial) return; + s->wl=wl_compositor_create_surface(compositor); + s->xdg=xdg_wm_base_get_xdg_surface(wm,s->wl); + xdg_surface_add_listener(s->xdg,&xdg_listener,s); + struct xdg_positioner *pos=xdg_wm_base_create_positioner(wm); + xdg_positioner_set_size(pos,s->width,s->height); + xdg_positioner_set_anchor_rect(pos,s->anchor_x,s->anchor_y,1,1); + xdg_positioner_set_anchor(pos,XDG_POSITIONER_ANCHOR_TOP_LEFT); + xdg_positioner_set_gravity(pos,XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT); + s->popup=xdg_surface_get_popup(s->xdg,parent->xdg,pos); + xdg_popup_add_listener(s->popup,&popup_listener,s); + fprintf(stderr,"%s CREATE wl=%u xdg=%u role=%u parent=%s parent_xdg=%u\n", + s->name,wl_proxy_get_id((struct wl_proxy *)s->wl), + wl_proxy_get_id((struct wl_proxy *)s->xdg), + wl_proxy_get_id((struct wl_proxy *)s->popup),parent->name, + wl_proxy_get_id((struct wl_proxy *)parent->xdg)); + fprintf(stderr,"%s GRAB_REQUEST origin=wl_pointer.button target=%s serial=%u used=%u\n", + s->name,parent->name,serial,serial); + xdg_popup_grab(s->popup,seat,serial); + wl_surface_commit(s->wl); /* Initial bufferless commit requests configure. */ + xdg_positioner_destroy(pos); +} +static struct surface *which(struct wl_surface *wl) { + if (wl==top.wl) return ⊤ + if (wl==a.wl && wl) return &a; + if (wl==b.wl && wl) return &b; + return NULL; +} +static void on_enter(void *data,struct wl_pointer *ptr,uint32_t serial, + struct wl_surface *wl,wl_fixed_t x,wl_fixed_t y) { + entered=which(wl); + fprintf(stderr,"POINTER_ENTER target=%s wl=%u serial=%u x=%.2f y=%.2f\n", + entered?entered->name:"OTHER",wl_proxy_get_id((struct wl_proxy *)wl),serial, + wl_fixed_to_double(x),wl_fixed_to_double(y)); +} +static void on_leave(void *data,struct wl_pointer *ptr,uint32_t serial, + struct wl_surface *wl) { + struct surface *s=which(wl); + fprintf(stderr,"POINTER_LEAVE target=%s serial=%u\n",s?s->name:"OTHER",serial); + if (entered==s) entered=NULL; +} +static void on_motion(void *data,struct wl_pointer *ptr,uint32_t time, + wl_fixed_t x,wl_fixed_t y) { + fprintf(stderr,"POINTER_MOTION target=%s x=%.2f y=%.2f\n", + entered?entered->name:"OTHER",wl_fixed_to_double(x),wl_fixed_to_double(y)); +} +static void on_button(void *data,struct wl_pointer *ptr,uint32_t serial, + uint32_t time,uint32_t button,uint32_t state) { + fprintf(stderr,"POINTER_BUTTON target=%s serial=%u button=%u state=%u\n", + entered?entered->name:"OTHER",serial,button,state); + if (state!=WL_POINTER_BUTTON_STATE_PRESSED || button!=272) return; + if (entered==&top && !a.popup) open_popup(&a,&top,serial); + else if (entered==&a && a.mapped && !b.popup) open_popup(&b,&a,serial); +} +static void on_axis(void *data,struct wl_pointer *ptr,uint32_t t,uint32_t axis,wl_fixed_t v) {} +static void on_frame(void *data,struct wl_pointer *ptr) {} +static const struct wl_pointer_listener pointer_listener={ + .enter=on_enter,.leave=on_leave,.motion=on_motion,.button=on_button, + .axis=on_axis,.frame=on_frame, +}; +static void on_caps(void *data,struct wl_seat *s,uint32_t caps) { + if ((caps & WL_SEAT_CAPABILITY_POINTER) && !pointer) { + pointer=wl_seat_get_pointer(s); + wl_pointer_add_listener(pointer,&pointer_listener,NULL); + } +} +static void on_seat_name(void *data,struct wl_seat *s,const char *name) {} +static const struct wl_seat_listener seat_listener={.capabilities=on_caps,.name=on_seat_name}; +static void on_global(void *data,struct wl_registry *reg,uint32_t name, + const char *interface,uint32_t version) { + if (!strcmp(interface,wl_compositor_interface.name)) + compositor=wl_registry_bind(reg,name,&wl_compositor_interface,version<4?version:4); + else if (!strcmp(interface,wl_shm_interface.name)) + shm=wl_registry_bind(reg,name,&wl_shm_interface,1); + else if (!strcmp(interface,xdg_wm_base_interface.name)) + wm=wl_registry_bind(reg,name,&xdg_wm_base_interface,version<3?version:3); + else if (!strcmp(interface,wl_seat_interface.name)) { + seat=wl_registry_bind(reg,name,&wl_seat_interface,version<5?version:5); + wl_seat_add_listener(seat,&seat_listener,NULL); + } +} +static void on_removed(void *data,struct wl_registry *reg,uint32_t name) {} +static const struct wl_registry_listener registry_listener={.global=on_global,.global_remove=on_removed}; +static void stop(int sig) { running=0; } +int main(int argc,char **argv) { + /* WHY/CONTRACT/INVARIANT: no core dump or dumpable client in this LAB. */ + struct rlimit no_core = {0, 0}; + if (setrlimit(RLIMIT_CORE, &no_core) || prctl(PR_SET_DUMPABLE, 0)) return 1; + signal(SIGTERM,stop); + const char *placement=argc>1?argv[1]:"b_cross"; + /* EXPERIMENTAL: only placement changes; serial/grab/lifecycle stay historical. + * Both 1x1 anchors lie inside their parent SHM window geometry. */ + if (!strcmp(placement,"static_cross")) { + a.anchor_x=100; a.anchor_y=80; a.width=350; + b.anchor_x=100; b.anchor_y=60; b.width=300; + } else if (!strcmp(placement,"visible")) { + a.anchor_x=50; a.anchor_y=80; a.width=250; + b.anchor_x=20; b.anchor_y=60; b.width=170; + } else if (!strcmp(placement,"both_cross")) { + a.anchor_x=350; a.anchor_y=80; a.width=350; + b.anchor_x=160; b.anchor_y=60; b.width=300; + } else if (!strcmp(placement,"b_cross")) { + a.anchor_x=200; a.anchor_y=80; a.width=300; + b.anchor_x=220; b.anchor_y=60; b.width=300; + } else { fprintf(stderr,"invalid placement\n"); return 2; } + display=wl_display_connect(NULL); + if (!display) { fprintf(stderr,"cannot connect Wayland\n"); return 1; } + struct wl_registry *registry=wl_display_get_registry(display); + wl_registry_add_listener(registry,®istry_listener,NULL); + wl_display_roundtrip(display); wl_display_roundtrip(display); + if (!compositor || !shm || !wm || !seat || !pointer) { + fprintf(stderr,"missing Wayland globals/pointer\n"); return 1; + } + xdg_wm_base_add_listener(wm,&wm_listener,NULL); + top.wl=wl_compositor_create_surface(compositor); + top.xdg=xdg_wm_base_get_xdg_surface(wm,top.wl); + xdg_surface_add_listener(top.xdg,&xdg_listener,&top); + top_role=xdg_surface_get_toplevel(top.xdg); + xdg_toplevel_add_listener(top_role,&top_listener,NULL); + xdg_toplevel_set_title(top_role,"Mosaic Nested Grab Probe"); + xdg_toplevel_set_app_id(top_role,"org.mosaic.NestedGrabProbe"); + fprintf(stderr,"TOPLEVEL CREATE wl=%u xdg=%u role=%u placement=%s\n", + wl_proxy_get_id((struct wl_proxy *)top.wl), + wl_proxy_get_id((struct wl_proxy *)top.xdg), + wl_proxy_get_id((struct wl_proxy *)top_role),placement); + wl_surface_commit(top.wl); + while (running) { + wl_display_flush(display); + struct pollfd fds[2]={{.fd=wl_display_get_fd(display),.events=POLLIN}, + {.fd=STDIN_FILENO,.events=POLLIN}}; + int ready=poll(fds,2,500); + if (ready<0) { if (errno==EINTR) continue; perror("poll"); break; } + if (fds[0].revents & (POLLIN|POLLERR|POLLHUP)) { + if (wl_display_dispatch(display)<0) break; + collect_dismissals(); + } + if (fds[1].revents & POLLIN) { + char cmd[64]; + if (fgets(cmd,sizeof(cmd),stdin)) { + if (!strncmp(cmd,"close_b",7)) { destroy_popup(&b); fprintf(stderr,"COMMAND close_b DONE\n"); } + else if (!strncmp(cmd,"close_owner",11)) { fprintf(stderr,"CLOSE_RECEIVED\nCOMMAND close_owner\n"); running=0; } + } + } + } + destroy_popup(&b); destroy_popup(&a); + fprintf(stderr,"TOPLEVEL DESTROY\n"); + xdg_toplevel_destroy(top_role); xdg_surface_destroy(top.xdg); wl_surface_destroy(top.wl); + wl_display_roundtrip(display); + fprintf(stderr,"SHM_CLOSURE created=%u released=%u\n",shm_created,shm_released); + wl_display_flush(display); + /* WHY/CONTRACT/INVARIANT: exit0 does not prove protocol error0. */ + fprintf(stderr,"EXIT error=%d\n",wl_display_get_error(display)); + wl_display_disconnect(display); + return 0; +} diff --git a/tooling/probes/step20c-corec-v2-followup/graphics/clients/pointer-daemon.c b/tooling/probes/step20c-corec-v2-followup/graphics/clients/pointer-daemon.c new file mode 100644 index 0000000..6fab741 --- /dev/null +++ b/tooling/probes/step20c-corec-v2-followup/graphics/clients/pointer-daemon.c @@ -0,0 +1,62 @@ +#define _DEFAULT_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "virtual-pointer-client.h" +static struct zwlr_virtual_pointer_manager_v1 *manager; +static void global(void *data, struct wl_registry *registry, uint32_t name, + const char *interface, uint32_t version) { + if (!strcmp(interface, zwlr_virtual_pointer_manager_v1_interface.name)) + manager=wl_registry_bind(registry,name,&zwlr_virtual_pointer_manager_v1_interface, + version < 2 ? version : 2); +} +static void removed(void *data, struct wl_registry *registry, uint32_t name) {} +int main(void) { + /* WHY/CONTRACT/INVARIANT: no core dump or dumpable client in this LAB. */ + struct rlimit no_core = {0, 0}; + if (setrlimit(RLIMIT_CORE, &no_core) || prctl(PR_SET_DUMPABLE, 0)) return 1; + struct wl_display *display=wl_display_connect(NULL); + if (!display) return 1; + struct wl_registry *registry=wl_display_get_registry(display); + struct wl_registry_listener listener={global,removed}; + wl_registry_add_listener(registry,&listener,NULL); + wl_display_roundtrip(display); + if (!manager) return 2; + struct zwlr_virtual_pointer_v1 *pointer= + zwlr_virtual_pointer_manager_v1_create_virtual_pointer(manager,NULL); + wl_display_roundtrip(display); + fprintf(stderr,"POINTER_READY\n");fflush(stderr); + char line[128]; + uint32_t stamp=1000; + while (fgets(line,sizeof(line),stdin)) { + char action[16];int x,y; + unsigned width,height; + if (sscanf(line,"%15s %d %d %u %u",action,&x,&y,&width,&height)!=5 || !width || !height || x<0 || y<0 || (unsigned)x>=width || (unsigned)y>=height || (strcmp(action,"move") && strcmp(action,"left") && strcmp(action,"right"))) return 3; + fprintf(stderr,"INJECT %s %d,%d\n",action,x,y);fflush(stderr); + zwlr_virtual_pointer_v1_motion_absolute(pointer,stamp++,x,y,width,height); + zwlr_virtual_pointer_v1_frame(pointer); + wl_display_flush(display);usleep(250000); + if (!strcmp(action,"left") || !strcmp(action,"right")) { + uint32_t button=!strcmp(action,"left") ? BTN_LEFT : BTN_RIGHT; + zwlr_virtual_pointer_v1_button(pointer,stamp++,button,WL_POINTER_BUTTON_STATE_PRESSED); + zwlr_virtual_pointer_v1_frame(pointer); + wl_display_flush(display);usleep(80000); + zwlr_virtual_pointer_v1_button(pointer,stamp++,button,WL_POINTER_BUTTON_STATE_RELEASED); + zwlr_virtual_pointer_v1_frame(pointer); + wl_display_flush(display); + } + wl_display_roundtrip(display); + fprintf(stderr,"INJECT_DONE %s %d,%d\n",action,x,y);fflush(stderr); + } + zwlr_virtual_pointer_v1_destroy(pointer); + zwlr_virtual_pointer_manager_v1_destroy(manager); + wl_registry_destroy(registry); + wl_display_disconnect(display); + return 0; +} diff --git a/tooling/probes/step20c-corec-v2-followup/graphics/clients/saved-client.c b/tooling/probes/step20c-corec-v2-followup/graphics/clients/saved-client.c new file mode 100644 index 0000000..ccc2e31 --- /dev/null +++ b/tooling/probes/step20c-corec-v2-followup/graphics/clients/saved-client.c @@ -0,0 +1,258 @@ +#define _POSIX_C_SOURCE 200809L +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "xdg-shell-client.h" + +/* WHY: the compositor must wait on a real, responsive XDG client. + * CONTRACT: hold only a received configure; ACK the latest received serial. + * INVARIANT: Wayland dispatch and ping replies continue while ACK is held. */ +static struct wl_display *display; +static struct wl_compositor *compositor; +static struct wl_shm *shm; +static struct xdg_wm_base *wm; +static struct wl_surface *surface; +static struct xdg_surface *xdg; +static struct xdg_toplevel *toplevel; +static struct wl_seat *seat; +static struct wl_pointer *pointer; +static volatile sig_atomic_t running = 1; +static bool mapped, hold_next, held; +static uint32_t latest_serial; +static int32_t width = 420, height = 320, configured_width = 420, configured_height = 320; +static unsigned generation; + +static long long stamp(void) { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (long long)ts.tv_sec * 1000000000LL + ts.tv_nsec; +} + +/* Observe client-side SHM release separately from native GPU retirement. + * Keep protocol buffer alive until release; no backing memory is reused. */ +static unsigned shm_created, shm_released; +static void buffer_release(void *data, struct wl_buffer *buffer) { + (void)data; + ++shm_released; + fprintf(stderr,"SHM_RELEASE buffer=%u released=%u created=%u\n", + wl_proxy_get_id((struct wl_proxy *)buffer),shm_released,shm_created); + wl_buffer_destroy(buffer); +} +static const struct wl_buffer_listener buffer_listener = {.release=buffer_release}; +static void draw(void) { + if (configured_width > 0) width = configured_width; + if (configured_height > 0) height = configured_height; + if (width <= 0 || height <= 0 || width > 4096 || height > 4096) { + fprintf(stderr, "INVALID_SIZE %d %d\n", width, height); + exit(2); + } + size_t bytes = (size_t)width * (size_t)height * 4; + char name[4096]; + const char *scratch = getenv("TMPDIR"); + const char *lab = getenv("SMX_LAB"); + /* WHY: SHM belongs to LAB, never the short socket-only runtime. + * CONTRACT: launcher supplies canonical LAB/scratch; fail before mkstemp. */ + char expected[4096]; + int expected_len = lab ? snprintf(expected, sizeof(expected), "%s/scratch", lab) : -1; + int path_len = scratch ? snprintf(name, sizeof(name), "%s/smx-shm-XXXXXX", scratch) : -1; + if (!lab || strncmp(lab, "/var/tmp/", 9) || strstr(lab, "/../") || + expected_len < 0 || expected_len >= (int)sizeof(expected) || + !scratch || strcmp(scratch, expected) || path_len < 0 || + path_len >= (int)sizeof(name)) { + fprintf(stderr, "invalid LAB scratch for SHM\n"); + exit(1); + } + int fd = mkstemp(name); + if (fd < 0 || ftruncate(fd, (off_t)bytes) < 0) { perror("shm"); exit(2); } + unlink(name); + uint32_t *pixels = mmap(NULL, bytes, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + if (pixels == MAP_FAILED) { perror("mmap"); exit(2); } + uint32_t color = (generation++ % 2) ? 0x0020d0a0 : 0x002344cc; +#ifdef STATIC_REFERENCE + /* CONTRACT: H2 uses one opaque SHM color across every real configure. */ + color = 0x00303038; +#endif + for (size_t i = 0; i < bytes / 4; ++i) pixels[i] = color; + struct wl_shm_pool *pool = wl_shm_create_pool(shm, fd, (int)bytes); + struct wl_buffer *buffer = wl_shm_pool_create_buffer(pool, 0, width, height, + width * 4, WL_SHM_FORMAT_XRGB8888); + ++shm_created; + wl_buffer_add_listener(buffer,&buffer_listener,NULL); + wl_surface_attach(surface, buffer, 0, 0); + wl_surface_damage_buffer(surface, 0, 0, width, height); + wl_surface_commit(surface); + fprintf(stderr, "COMMIT ns=%lld serial=%u size=%dx%d color=%08x\n", + stamp(), latest_serial, width, height, color); + wl_shm_pool_destroy(pool); + munmap(pixels, bytes); + close(fd); + mapped = true; +} + +static void ack_and_commit(void) { + if (!latest_serial) return; + xdg_surface_ack_configure(xdg, latest_serial); + fprintf(stderr, "ACK ns=%lld serial=%u size=%dx%d\n", + stamp(), latest_serial, configured_width, configured_height); + draw(); + held = false; +} + +static void ping(void *data, struct xdg_wm_base *base, uint32_t serial) { + xdg_wm_base_pong(base, serial); + fprintf(stderr, "PONG ns=%lld serial=%u\n", stamp(), serial); +} +static const struct xdg_wm_base_listener wm_listener = {.ping = ping}; + +static void role_configure(void *data, struct xdg_toplevel *role, + int32_t w, int32_t h, struct wl_array *states) { + if (w > 0) configured_width = w; + if (h > 0) configured_height = h; + fprintf(stderr, "ROLE_CONFIGURE ns=%lld size=%dx%d\n", stamp(), w, h); +} +static void role_close(void *data, struct xdg_toplevel *role) { running = 0; } +static const struct xdg_toplevel_listener role_listener = { + .configure = role_configure, .close = role_close, +}; +static void configure(void *data, struct xdg_surface *s, uint32_t serial) { + latest_serial = serial; + fprintf(stderr, "XDG_CONFIGURE ns=%lld serial=%u size=%dx%d hold=%d previous=%d\n", + stamp(), serial, configured_width, configured_height, hold_next || held, held); + if (hold_next || held) { + held = true; + hold_next = false; + fprintf(stderr, "HELD ns=%lld serial=%u\n", stamp(), serial); + } else { + ack_and_commit(); + } +} +static const struct xdg_surface_listener surface_listener = {.configure = configure}; +static void pointer_enter(void *data, struct wl_pointer *p, uint32_t serial, + struct wl_surface *s, wl_fixed_t x, wl_fixed_t y) { + fprintf(stderr, "ENTER ns=%lld x=%.2f y=%.2f\n", stamp(), + wl_fixed_to_double(x), wl_fixed_to_double(y)); +} +static void pointer_leave(void *data, struct wl_pointer *p, uint32_t serial, + struct wl_surface *s) { fprintf(stderr, "LEAVE ns=%lld\n", stamp()); } +static void pointer_motion(void *data, struct wl_pointer *p, uint32_t time, + wl_fixed_t x, wl_fixed_t y) { + fprintf(stderr, "MOTION ns=%lld x=%.2f y=%.2f\n", stamp(), + wl_fixed_to_double(x), wl_fixed_to_double(y)); +} +static void pointer_button(void *data, struct wl_pointer *p, uint32_t serial, + uint32_t time, uint32_t button, uint32_t state) { + fprintf(stderr, "BUTTON ns=%lld serial=%u button=%u state=%u\n", + stamp(), serial, button, state); +} +static void pointer_axis(void *data, struct wl_pointer *p, uint32_t t, + uint32_t axis, wl_fixed_t value) {} +static void pointer_frame(void *data, struct wl_pointer *p) {} +static const struct wl_pointer_listener pointer_listener = { + .enter = pointer_enter, .leave = pointer_leave, .motion = pointer_motion, + .button = pointer_button, .axis = pointer_axis, .frame = pointer_frame, +}; +static void capabilities(void *data, struct wl_seat *s, uint32_t caps) { + if ((caps & WL_SEAT_CAPABILITY_POINTER) && !pointer) { + pointer = wl_seat_get_pointer(s); + wl_pointer_add_listener(pointer, &pointer_listener, NULL); + } +} +static void seat_name(void *data, struct wl_seat *s, const char *name) {} +static const struct wl_seat_listener seat_listener = { + .capabilities = capabilities, .name = seat_name, +}; +static void global(void *data, struct wl_registry *registry, uint32_t name, + const char *interface, uint32_t version) { + if (!strcmp(interface, wl_compositor_interface.name)) + compositor = wl_registry_bind(registry, name, &wl_compositor_interface, version < 4 ? version : 4); + else if (!strcmp(interface, wl_shm_interface.name)) + shm = wl_registry_bind(registry, name, &wl_shm_interface, 1); + else if (!strcmp(interface, xdg_wm_base_interface.name)) + wm = wl_registry_bind(registry, name, &xdg_wm_base_interface, version < 3 ? version : 3); + else if (!strcmp(interface, wl_seat_interface.name)) { + seat = wl_registry_bind(registry, name, &wl_seat_interface, version < 5 ? version : 5); + wl_seat_add_listener(seat, &seat_listener, NULL); + } +} +static void global_remove(void *data, struct wl_registry *registry, uint32_t name) {} +static const struct wl_registry_listener registry_listener = { + .global = global, .global_remove = global_remove, +}; +static void stop(int signum) { running = 0; } + +int main(void) { + /* WHY/CONTRACT/INVARIANT: no core dump or dumpable client in this LAB. */ + struct rlimit no_core = {0, 0}; + if (setrlimit(RLIMIT_CORE, &no_core) || prctl(PR_SET_DUMPABLE, 0)) return 1; + signal(SIGTERM, stop); + display = wl_display_connect(NULL); + if (!display) { fprintf(stderr, "CONNECT_FAILED\n"); return 1; } + struct wl_registry *registry = wl_display_get_registry(display); + wl_registry_add_listener(registry, ®istry_listener, NULL); + wl_display_roundtrip(display); + wl_display_roundtrip(display); + if (!compositor || !shm || !wm) { fprintf(stderr, "MISSING_GLOBAL\n"); return 2; } + xdg_wm_base_add_listener(wm, &wm_listener, NULL); + surface = wl_compositor_create_surface(compositor); + xdg = xdg_wm_base_get_xdg_surface(wm, surface); + xdg_surface_add_listener(xdg, &surface_listener, NULL); + toplevel = xdg_surface_get_toplevel(xdg); + xdg_toplevel_add_listener(toplevel, &role_listener, NULL); +#ifdef STATIC_REFERENCE + xdg_toplevel_set_title(toplevel, "Mosaic Static Reference"); + xdg_toplevel_set_app_id(toplevel, "org.mosaic.StaticReference"); +#else + xdg_toplevel_set_title(toplevel, "Mosaic Saved Buffer Probe"); + xdg_toplevel_set_app_id(toplevel, "org.mosaic.SavedBufferProbe"); +#endif + wl_surface_commit(surface); + fprintf(stderr, "READY ns=%lld\n", stamp()); + while (running) { + wl_display_flush(display); + struct pollfd fds[2] = { + {.fd = wl_display_get_fd(display), .events = POLLIN}, + {.fd = STDIN_FILENO, .events = POLLIN}, + }; + int ready = poll(fds, 2, 100); + if (ready < 0) { if (errno == EINTR) continue; perror("poll"); break; } + if (fds[0].revents & (POLLIN | POLLERR | POLLHUP)) { + if (wl_display_dispatch(display) < 0) break; + } + if (fds[1].revents & POLLIN) { + char cmd[64]; + if (!fgets(cmd, sizeof(cmd), stdin)) break; + if (!strncmp(cmd, "hold_next_configure", 19)) { + hold_next = true; + fprintf(stderr, "COMMAND hold_next_configure DONE ns=%lld\n", stamp()); + } else if (!strncmp(cmd, "release_configure", 17)) { + if (held) ack_and_commit(); + fprintf(stderr, "COMMAND release_configure DONE ns=%lld\n", stamp()); + } else if (!strncmp(cmd, "close", 5)) { + fprintf(stderr,"CLOSE_RECEIVED\n"); + running = 0; + fprintf(stderr, "COMMAND close DONE ns=%lld\n", stamp()); + } + } + } + fprintf(stderr, "EXIT ns=%lld mapped=%d held=%d error=%d\n", + stamp(), mapped, held, wl_display_get_error(display)); + xdg_toplevel_destroy(toplevel); + xdg_surface_destroy(xdg); + wl_surface_destroy(surface); + wl_display_roundtrip(display); + fprintf(stderr,"SHM_CLOSURE created=%u released=%u\n",shm_created,shm_released); + wl_display_flush(display); + wl_display_disconnect(display); + return 0; +} diff --git a/tooling/probes/step20c-corec-v2-followup/graphics/clients/static-reference.c b/tooling/probes/step20c-corec-v2-followup/graphics/clients/static-reference.c new file mode 100644 index 0000000..c6818ba --- /dev/null +++ b/tooling/probes/step20c-corec-v2-followup/graphics/clients/static-reference.c @@ -0,0 +1,5 @@ +/* WHY: a deterministic H2 reference cannot contain Kitty's blinking cursor. + * CONTRACT: use the same real XDG configure/ACK loop with fixed opaque SHM. + * INVARIANT: once configured and stable, this client changes no pixels. */ +#define STATIC_REFERENCE 1 +#include "saved-client.c" diff --git a/tooling/probes/step20c-corec-v2-followup/graphics/clients/vertical-client.c b/tooling/probes/step20c-corec-v2-followup/graphics/clients/vertical-client.c new file mode 100644 index 0000000..154ad05 --- /dev/null +++ b/tooling/probes/step20c-corec-v2-followup/graphics/clients/vertical-client.c @@ -0,0 +1,313 @@ +#define _POSIX_C_SOURCE 200809L +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "xdg-shell-client.h" + +/* EXPERIMENTAL CONTRACT: every popup owns distinct XDG objects. B's actual + * parent is A's xdg_surface; logs name object IDs as well as colors. */ +struct surface { + const char *name; + struct wl_surface *wl; + struct xdg_surface *xdg; + struct xdg_popup *popup; + bool configured, mapped, done; + int width, height, anchor_x, anchor_y; + uint32_t color; +}; +static struct wl_display *display; +static struct wl_compositor *compositor; +static struct wl_shm *shm; +static struct xdg_wm_base *wm; +static struct wl_seat *seat; +static struct wl_pointer *pointer; +static struct xdg_toplevel *top_role; +static struct surface top = {.name="TOPLEVEL", .width=420, .height=320, .color=0x002344cc}; +static struct surface a = {.name="POPUP_A", .width=300, .height=170, .color=0x00e0c020}; +static struct surface b = {.name="POPUP_B", .width=300, .height=130, .color=0x0020d0a0}; +static volatile sig_atomic_t running = 1; +static struct surface *entered; + +/* Observe client-side SHM release separately from native GPU retirement. + * Keep protocol buffer alive until release; no backing memory is reused. */ +static unsigned shm_created, shm_released; +static void buffer_release(void *data, struct wl_buffer *buffer) { + (void)data; + ++shm_released; + fprintf(stderr,"SHM_RELEASE buffer=%u released=%u created=%u\n", + wl_proxy_get_id((struct wl_proxy *)buffer),shm_released,shm_created); + wl_buffer_destroy(buffer); +} +static const struct wl_buffer_listener buffer_listener = {.release=buffer_release}; +static void draw(struct surface *s) { + size_t bytes = (size_t)s->width * (size_t)s->height * 4; + char path[4096]; + const char *scratch = getenv("TMPDIR"); + const char *lab = getenv("SMX_LAB"); + /* WHY: SHM belongs to LAB, never the short socket-only runtime. + * CONTRACT: launcher supplies canonical LAB/scratch; fail before mkstemp. */ + char expected[4096]; + int expected_len = lab ? snprintf(expected, sizeof(expected), "%s/scratch", lab) : -1; + int path_len = scratch ? snprintf(path, sizeof(path), "%s/smx-shm-XXXXXX", scratch) : -1; + if (!lab || strncmp(lab, "/var/tmp/", 9) || strstr(lab, "/../") || + expected_len < 0 || expected_len >= (int)sizeof(expected) || + !scratch || strcmp(scratch, expected) || path_len < 0 || + path_len >= (int)sizeof(path)) { + fprintf(stderr, "invalid LAB scratch for SHM\n"); + exit(1); + } + int fd = mkstemp(path); + if (fd < 0 || ftruncate(fd, (off_t)bytes) < 0) { perror("buffer"); exit(1); } + unlink(path); + uint32_t *pixels = mmap(NULL, bytes, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + if (pixels == MAP_FAILED) { perror("mmap"); exit(1); } + for (size_t i=0; icolor; + struct wl_shm_pool *pool = wl_shm_create_pool(shm, fd, (int)bytes); + struct wl_buffer *buffer = wl_shm_pool_create_buffer(pool, 0, s->width, s->height, + s->width*4, WL_SHM_FORMAT_XRGB8888); + ++shm_created; + wl_buffer_add_listener(buffer,&buffer_listener,NULL); + wl_surface_attach(s->wl, buffer, 0, 0); + wl_surface_damage_buffer(s->wl, 0, 0, s->width, s->height); + wl_surface_commit(s->wl); + wl_shm_pool_destroy(pool); + munmap(pixels, bytes); + close(fd); + s->mapped = true; + fprintf(stderr, "%s MAP wl=%u xdg=%u\n", s->name, + wl_proxy_get_id((struct wl_proxy *)s->wl), wl_proxy_get_id((struct wl_proxy *)s->xdg)); +} +static void on_ping(void *data, struct xdg_wm_base *base, uint32_t serial) { + xdg_wm_base_pong(base, serial); +} +static const struct xdg_wm_base_listener wm_listener = {.ping=on_ping}; +static void on_xdg_configure(void *data, struct xdg_surface *xdg, uint32_t serial) { + struct surface *s=data; + fprintf(stderr, "%s XDG_CONFIGURE serial=%u ack\n", s->name, serial); + xdg_surface_ack_configure(xdg, serial); + s->configured=true; + if (!s->mapped && !s->done) draw(s); +} +static const struct xdg_surface_listener xdg_listener = {.configure=on_xdg_configure}; +static void on_top_configure(void *data, struct xdg_toplevel *role, int32_t w, + int32_t h, struct wl_array *states) { + fprintf(stderr, "TOPLEVEL ROLE_CONFIGURE w=%d h=%d\n", w,h); +} +static void on_top_close(void *data, struct xdg_toplevel *role) { + fprintf(stderr, "TOPLEVEL CLOSE_REQUEST\n"); running=0; +} +static const struct xdg_toplevel_listener top_listener = { + .configure=on_top_configure, .close=on_top_close, +}; +static void on_popup_configure(void *data, struct xdg_popup *role, int32_t x, + int32_t y, int32_t w, int32_t h) { + struct surface *s=data; + fprintf(stderr, "%s ROLE_CONFIGURE x=%d y=%d w=%d h=%d\n",s->name,x,y,w,h); +} +static void on_popup_done(void *data, struct xdg_popup *role) { + struct surface *s=data; + s->done=true; + s->mapped=false; + fprintf(stderr, "%s POPUP_DONE role=%u\n",s->name,wl_proxy_get_id((struct wl_proxy *)role)); +} +static const struct xdg_popup_listener popup_listener = { + .configure=on_popup_configure, .popup_done=on_popup_done, +}; +/* WHY: XDG requires child-first destruction even if the compositor sends + * dismissals for multiple levels in one dispatch batch. */ +static void destroy_popup(struct surface *s) { + if (!s->popup) return; + fprintf(stderr,"%s DESTROY role=%u xdg=%u wl=%u\n",s->name, + wl_proxy_get_id((struct wl_proxy *)s->popup), + wl_proxy_get_id((struct wl_proxy *)s->xdg), + wl_proxy_get_id((struct wl_proxy *)s->wl)); + xdg_popup_destroy(s->popup); + xdg_surface_destroy(s->xdg); + wl_surface_destroy(s->wl); + s->popup=NULL; s->xdg=NULL; s->wl=NULL; s->mapped=false; + if (entered==s) entered=NULL; +} +static void collect_dismissals(void) { + if (b.done) destroy_popup(&b); + if (a.done) { + destroy_popup(&b); + destroy_popup(&a); + } +} +static void open_popup(struct surface *s, struct surface *parent, uint32_t serial) { + if (s->popup || !parent->mapped || !serial) return; + s->wl=wl_compositor_create_surface(compositor); + s->xdg=xdg_wm_base_get_xdg_surface(wm,s->wl); + xdg_surface_add_listener(s->xdg,&xdg_listener,s); + struct xdg_positioner *pos=xdg_wm_base_create_positioner(wm); + xdg_positioner_set_size(pos,s->width,s->height); + xdg_positioner_set_anchor_rect(pos,s->anchor_x,s->anchor_y,1,1); + xdg_positioner_set_anchor(pos,XDG_POSITIONER_ANCHOR_TOP_LEFT); + xdg_positioner_set_gravity(pos,XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT); + s->popup=xdg_surface_get_popup(s->xdg,parent->xdg,pos); + xdg_popup_add_listener(s->popup,&popup_listener,s); + fprintf(stderr,"%s CREATE wl=%u xdg=%u role=%u parent=%s parent_xdg=%u\n", + s->name,wl_proxy_get_id((struct wl_proxy *)s->wl), + wl_proxy_get_id((struct wl_proxy *)s->xdg), + wl_proxy_get_id((struct wl_proxy *)s->popup),parent->name, + wl_proxy_get_id((struct wl_proxy *)parent->xdg)); + fprintf(stderr,"%s GRAB_REQUEST origin=wl_pointer.button target=%s serial=%u used=%u\n", + s->name,parent->name,serial,serial); + xdg_popup_grab(s->popup,seat,serial); + wl_surface_commit(s->wl); /* Initial bufferless commit requests configure. */ + xdg_positioner_destroy(pos); +} +static struct surface *which(struct wl_surface *wl) { + if (wl==top.wl) return ⊤ + if (wl==a.wl && wl) return &a; + if (wl==b.wl && wl) return &b; + return NULL; +} +static void on_enter(void *data,struct wl_pointer *ptr,uint32_t serial, + struct wl_surface *wl,wl_fixed_t x,wl_fixed_t y) { + entered=which(wl); + fprintf(stderr,"POINTER_ENTER target=%s wl=%u serial=%u x=%.2f y=%.2f\n", + entered?entered->name:"OTHER",wl_proxy_get_id((struct wl_proxy *)wl),serial, + wl_fixed_to_double(x),wl_fixed_to_double(y)); +} +static void on_leave(void *data,struct wl_pointer *ptr,uint32_t serial, + struct wl_surface *wl) { + struct surface *s=which(wl); + fprintf(stderr,"POINTER_LEAVE target=%s serial=%u\n",s?s->name:"OTHER",serial); + if (entered==s) entered=NULL; +} +static void on_motion(void *data,struct wl_pointer *ptr,uint32_t time, + wl_fixed_t x,wl_fixed_t y) { + fprintf(stderr,"POINTER_MOTION target=%s x=%.2f y=%.2f\n", + entered?entered->name:"OTHER",wl_fixed_to_double(x),wl_fixed_to_double(y)); +} +static void on_button(void *data,struct wl_pointer *ptr,uint32_t serial, + uint32_t time,uint32_t button,uint32_t state) { + fprintf(stderr,"POINTER_BUTTON target=%s serial=%u button=%u state=%u\n", + entered?entered->name:"OTHER",serial,button,state); + if (state!=WL_POINTER_BUTTON_STATE_PRESSED || button!=272) return; + if (entered==&top && !a.popup) open_popup(&a,&top,serial); + else if (entered==&a && a.mapped && !b.popup) open_popup(&b,&a,serial); +} +static void on_axis(void *data,struct wl_pointer *ptr,uint32_t t,uint32_t axis,wl_fixed_t v) {} +static void on_frame(void *data,struct wl_pointer *ptr) {} +static const struct wl_pointer_listener pointer_listener={ + .enter=on_enter,.leave=on_leave,.motion=on_motion,.button=on_button, + .axis=on_axis,.frame=on_frame, +}; +static void on_caps(void *data,struct wl_seat *s,uint32_t caps) { + if ((caps & WL_SEAT_CAPABILITY_POINTER) && !pointer) { + pointer=wl_seat_get_pointer(s); + wl_pointer_add_listener(pointer,&pointer_listener,NULL); + } +} +static void on_seat_name(void *data,struct wl_seat *s,const char *name) {} +static const struct wl_seat_listener seat_listener={.capabilities=on_caps,.name=on_seat_name}; +static void on_global(void *data,struct wl_registry *reg,uint32_t name, + const char *interface,uint32_t version) { + if (!strcmp(interface,wl_compositor_interface.name)) + compositor=wl_registry_bind(reg,name,&wl_compositor_interface,version<4?version:4); + else if (!strcmp(interface,wl_shm_interface.name)) + shm=wl_registry_bind(reg,name,&wl_shm_interface,1); + else if (!strcmp(interface,xdg_wm_base_interface.name)) + wm=wl_registry_bind(reg,name,&xdg_wm_base_interface,version<3?version:3); + else if (!strcmp(interface,wl_seat_interface.name)) { + seat=wl_registry_bind(reg,name,&wl_seat_interface,version<5?version:5); + wl_seat_add_listener(seat,&seat_listener,NULL); + } +} +static void on_removed(void *data,struct wl_registry *reg,uint32_t name) {} +static const struct wl_registry_listener registry_listener={.global=on_global,.global_remove=on_removed}; +static void stop(int sig) { running=0; } +int main(int argc,char **argv) { + /* WHY/CONTRACT/INVARIANT: no core dump or dumpable client in this LAB. */ + struct rlimit no_core = {0, 0}; + if (setrlimit(RLIMIT_CORE, &no_core) || prctl(PR_SET_DUMPABLE, 0)) return 1; + signal(SIGTERM,stop); + const char *placement=argc>1?argv[1]:"b_cross"; + /* CONTRACT: vertical positioner anchors remain inside the toplevel's + * effective window geometry, which defaults to its buffer bounds. */ + if (!strncmp(placement,"v_",2)) top.height=575; + if (!strcmp(placement,"visible")) { + a.anchor_x=50; a.anchor_y=80; a.width=250; + b.anchor_x=20; b.anchor_y=60; b.width=170; + } else if (!strcmp(placement,"v_visible")) { + a.anchor_x=50; a.anchor_y=80; a.width=250; + b.anchor_x=20; b.anchor_y=60; b.width=170; + } else if (!strcmp(placement,"v_b_cross")) { + /* WHY: only B's requested rectangle reaches below y=600 when the + * tiled toplevel content begins near y=25. Configure and pixels + * remain the authority for the actual crossing. */ + a.anchor_x=50; a.anchor_y=350; a.width=250; + b.anchor_x=20; b.anchor_y=155; b.width=170; + } else if (!strcmp(placement,"v_both_cross")) { + a.anchor_x=50; a.anchor_y=480; a.width=250; + b.anchor_x=20; b.anchor_y=80; b.width=170; + } else if (!strcmp(placement,"both_cross")) { + a.anchor_x=350; a.anchor_y=80; a.width=350; + b.anchor_x=160; b.anchor_y=60; b.width=300; + } else if (!strcmp(placement,"b_cross")) { + a.anchor_x=200; a.anchor_y=80; a.width=300; + b.anchor_x=220; b.anchor_y=60; b.width=300; + } else { fprintf(stderr,"invalid placement\n"); return 2; } + display=wl_display_connect(NULL); + if (!display) { fprintf(stderr,"cannot connect Wayland\n"); return 1; } + struct wl_registry *registry=wl_display_get_registry(display); + wl_registry_add_listener(registry,®istry_listener,NULL); + wl_display_roundtrip(display); wl_display_roundtrip(display); + if (!compositor || !shm || !wm || !seat || !pointer) { + fprintf(stderr,"missing Wayland globals/pointer\n"); return 1; + } + xdg_wm_base_add_listener(wm,&wm_listener,NULL); + top.wl=wl_compositor_create_surface(compositor); + top.xdg=xdg_wm_base_get_xdg_surface(wm,top.wl); + xdg_surface_add_listener(top.xdg,&xdg_listener,&top); + top_role=xdg_surface_get_toplevel(top.xdg); + xdg_toplevel_add_listener(top_role,&top_listener,NULL); + xdg_toplevel_set_title(top_role,"Mosaic Nested Grab Probe"); + xdg_toplevel_set_app_id(top_role,"org.mosaic.NestedGrabProbe"); + fprintf(stderr,"TOPLEVEL CREATE wl=%u xdg=%u role=%u placement=%s\n", + wl_proxy_get_id((struct wl_proxy *)top.wl), + wl_proxy_get_id((struct wl_proxy *)top.xdg), + wl_proxy_get_id((struct wl_proxy *)top_role),placement); + wl_surface_commit(top.wl); + while (running) { + wl_display_flush(display); + struct pollfd fds[2]={{.fd=wl_display_get_fd(display),.events=POLLIN}, + {.fd=STDIN_FILENO,.events=POLLIN}}; + int ready=poll(fds,2,500); + if (ready<0) { if (errno==EINTR) continue; perror("poll"); break; } + if (fds[0].revents & (POLLIN|POLLERR|POLLHUP)) { + if (wl_display_dispatch(display)<0) break; + collect_dismissals(); + } + if (fds[1].revents & POLLIN) { + char cmd[64]; + if (fgets(cmd,sizeof(cmd),stdin)) { + if (!strncmp(cmd,"close_b",7)) { destroy_popup(&b); fprintf(stderr,"COMMAND close_b DONE\n"); } + else if (!strncmp(cmd,"close_owner",11)) { fprintf(stderr,"CLOSE_RECEIVED\nCOMMAND close_owner\n"); running=0; } + } + } + } + destroy_popup(&b); destroy_popup(&a); + fprintf(stderr,"TOPLEVEL DESTROY\n"); + xdg_toplevel_destroy(top_role); xdg_surface_destroy(top.xdg); wl_surface_destroy(top.wl); + wl_display_roundtrip(display); + fprintf(stderr,"SHM_CLOSURE created=%u released=%u\n",shm_created,shm_released); + wl_display_flush(display); + /* WHY/CONTRACT/INVARIANT: exit0 does not prove protocol error0. */ + fprintf(stderr,"EXIT error=%d\n",wl_display_get_error(display)); + wl_display_disconnect(display); + return 0; +} diff --git a/tooling/probes/step20c-corec-v2-followup/graphics/engine.py b/tooling/probes/step20c-corec-v2-followup/graphics/engine.py new file mode 100644 index 0000000..de5c908 --- /dev/null +++ b/tooling/probes/step20c-corec-v2-followup/graphics/engine.py @@ -0,0 +1,345 @@ +"""Bounded owned headless session, usable only after the separate matrix review gate.""" +import errno,json,os,re,shutil,signal,subprocess,tempfile,threading,time +from pathlib import Path +from PIL import Image,ImageDraw +from geometry import * +from gate import sha,clean_environment,Ledger,canonical_lab,disable_dump,durable + +import sys + +from ipc_predicates import nodes,workspace +from predicates import protocol,geometry,pair,dismissal +import validators +from lifecycle import opening_pair +from model import Provenance +COLORS={'TOPLEVEL':(35,68,204),'POPUP_A':(224,192,32),'POPUP_B':(32,208,160)} + +def storage_guard(lab): + # WHY/CONTRACT/INVARIANT: stop before new action at 9GiB or <20GiB free; + # never purge old evidence to manufacture space. + v=os.statvfs(lab);need(v.f_bavail*v.f_frsize>=20*1024**3,'SSD free space below20GiB') + total=sum(p.lstat().st_size for p in lab.rglob('*') if p.is_file() and not p.is_symlink()) + need(total<=9*1024**3,'new LAB exceeds9GiB prudent stop') + +def private_runtime(): + # WHY: tmpfs may have user quota exhaustion despite statvfs free space. + # CONTRACT: probe an owned 0700 directory by writing, then fall back only + # on EDQUOT/ENOSPC. INVARIANT: socket-only path stays UNIX-length-safe. + for base in ('/tmp','/var/tmp'): + runtime=None + try: + v=os.statvfs(base) + if v.f_bavail*v.f_frsize<1024*1024:raise OSError(errno.ENOSPC,'runtime free space') + runtime=Path(tempfile.mkdtemp(prefix='smx-',dir=base));runtime.chmod(0o700) + need(len(os.fsencode(runtime/'sway-ipc.4294967295.4294967295.sock'))<108,'UNIX socket path too long') + fd=os.open(runtime/'quota-probe',os.O_WRONLY|os.O_CREAT|os.O_EXCL,0o600) + try:os.write(fd,b'X'*4096);os.fsync(fd) + finally:os.close(fd) + (runtime/'quota-probe').unlink();return runtime + except OSError as exc: + if runtime is not None: + p=runtime/'quota-probe' + if p.exists():p.unlink() + runtime.rmdir() + if exc.errno not in (errno.EDQUOT,errno.ENOSPC):raise + if base=='/var/tmp':raise + raise ValueError('no safe private runtime') + +class Session: + def __init__(self,spec,auth): + need(not auth.get('offline'), 'OFFLINE_TEST_ONLY cannot launch');self.spec=spec;self.auth=auth;self.lab=canonical_lab(auth['lab']);self.ledger=Ledger(self.lab);self.folder=self.lab/'graphics-runs'/spec['id'];self.run=self.folder/'run';self.run.mkdir(parents=True) + disable_dump();self.runtime=private_runtime() + self.env=clean_environment(self.lab);self.env.update(XDG_RUNTIME_DIR=str(self.runtime),WLR_BACKENDS='headless',WLR_HEADLESS_OUTPUTS='2') + self.outputs=[dict(o) for o in spec['outputs']];self.frames={o['name']:'logical' for o in self.outputs} + self.proc_lock=threading.RLock();self.proc={};self.transient={};self.tool_serial=0;self.closed=[];self.helper_exits=[];self.handles=[];self.started=time.monotonic();self.finished=threading.Event();self.expired=False;self.events=[];self.pointer=None + def start(self): + spec=self.spec;auth=self.auth + cfg='\n'.join(f"output {o['name']} resolution {o['mode'][0]}x{o['mode'][1]} scale {o['scale']} transform {o['transform']} position {o['logical'][0]} {o['logical'][1]}" for o in self.outputs) + cfg+='\nworkspace 1 output HEADLESS-1\nworkspace 2 output HEADLESS-2\nfocus_follows_mouse no\nseat seat0 hide_cursor 100\nshadows disable\nblur disable\n' + if spec['id'] in ('C3','C4'): + marker=Image.new('RGB',(800,600),(30,30,46));draw=ImageDraw.Draw(marker) + for k,box in [('TL',(15,15,44,44)),('TR',(755,15,784,44)),('BL',(15,555,44,584)),('BR',(755,555,784,584)),('C',(385,285,414,314))]:draw.rectangle(box,fill=MARKERS[k]) + marker.save(self.run/'marker-source.png');self.marker_source=self.run/'marker-source.png';self.record('generated-content-PNG',path=str(self.marker_source),counts_as_capture=False) + need(cfg==self.spec['config'],'sealed config bytes');(self.run/'config').write_text(cfg);self.watch=threading.Thread(target=self.watchdog,daemon=True);self.watch.start() + if spec['id'] in ('OM-ROOT','OM-NESTED'): + from oom import process_environment + self.oom_env=process_environment(self.lab,spec['id'],auth['token']) + self.comp=self.spawn('compositor',[auth['binary'],'-d',*(['-Dtxn-timeout=5000'] if spec['id']=='C6' else []),'-c',self.run/'config']) + self.sock=self.wait(lambda:next(iter(self.runtime.glob('sway-ipc.*.sock')),None));display=self.wait(lambda:next((p for p in self.runtime.glob('wayland-*') if p.is_socket()),None)) + self.env.update(WAYLAND_DISPLAY=display.name,SWAYSOCK=str(self.sock));need(Path(f'/proc/{self.comp.pid}/exe').resolve()==Path(auth['binary']).resolve() and sha(Path(f'/proc/{self.comp.pid}/exe'))==auth['binary_sha256'],'actual executable') + maps=Path(f'/proc/{self.comp.pid}/maps').read_text();(self.run/'proc-maps.txt').write_text(maps) + paths={s.split()[-1] for s in maps.splitlines() if len(s.split())>=6 and s.split()[-1].startswith('/')} + wl=[Path(p).resolve() for p in paths if 'libwlroots' in p];need(wl==[Path(auth['wlroots'])] and sha(wl[0])==auth['wlroots_sha256'],'mapped wlroots') + scene=[Path(p).resolve() for p in paths if 'libscenefx' in p] + if spec['binary'] in ('core','oom','gpu'):need(not scene,'static SceneFX') + else:need(scene==[Path(auth['pristine_scene'])] and sha(scene[0])==auth['pristine_scene_sha256'],'mapped pristine SceneFX') + render=[s for s in self.log('compositor').splitlines() if any(x in s for x in ('Creating scenefx FX renderer','GL renderer:','GL vendor:','Using OpenGL ES'))] + need(any('Creating scenefx FX renderer' in s for s in render) and any('GL renderer:' in s for s in render),'actual renderer') + self.save('runtime-identity',dict(pid=self.comp.pid,executable=str(Path(f'/proc/{self.comp.pid}/exe').resolve()),binary_sha256=sha(Path(f'/proc/{self.comp.pid}/exe')),wlroots=[dict(path=str(p),sha256=sha(p)) for p in wl],renderer=render,egl=[line for line in self.log('compositor').splitlines() if 'EGL' in line],egl_source='compositor startup log; NOT_OBSERVABLE if empty',source_manifest=self.auth['manifest_sha256']));self.record('actual-renderer',lines=render);self.snapshot('startup') + if spec['id'] in ('C3','C4'): + self.spawn('marker-background',['swaybg','-o','HEADLESS-1','-i',self.marker_source,'-m','fill']);time.sleep(.35) + im=self.capture('marker-H1','HEADLESS-1',orientation_pending=True);m=marker_frame(im,self.outputs[0]['scale'],self.outputs[0]['transform']=='90');self.frames['HEADLESS-1']=m['frame'];self.record('marker-frame',**m);self.close_helper('marker-background') + self.pointer=self.spawn('pointer',[self.lab/'followup-work/client-build/pointer-daemon']);self.wait(lambda:'POINTER_READY' in self.log('pointer')) + def close_helper(self,name): + # WHY: swaybg is a calibration helper, not an XDG SHM test client. + # CONTRACT: retire its owned group after the marker screenshot; retain + # raw status separately. Never reinterpret SIGTERM as scientific close. + need(name=='marker-background','unknown helper role');p=self.proc[name] + if p.poll() is None: + try:os.killpg(p.pid,signal.SIGTERM) + except ProcessLookupError:pass + code=p.wait(timeout=2) + proof=dict(name=name,pid=p.pid,exit=code,expected_termination='owned helper SIGTERM after calibration',scientific_client=False) + self.helper_exits.append(proof);self.record('helper-exit',**proof) + need(code in (0,-signal.SIGTERM),'unexpected calibration helper exit') + need(not Path('/proc',str(p.pid)).exists(),'calibration helper residual') + if p.stdin and not p.stdin.closed:p.stdin.close() + del self.proc[name] + + def record(self,kind,**fields): + e=dict(kind=kind,ns=time.monotonic_ns(),**fields);self.events.append(e) + with (self.run/'events.jsonl').open('a') as f:f.write(json.dumps(e)+'\n') + def save(self,name,value): + durable(self.run/(name+'.json'),value) + def debit(self,*kinds): + storage_guard(self.lab);self.ledger.action_debit(self.auth,*kinds) + def spawn(self,name,argv,debug=False): + self.debit('spawn');need(not self.expired,'expired before spawn');f=(self.run/(name+'.log')).open('w');self.handles.append(f) + requested_argv=list(map(str,argv));prlimit=shutil.which('prlimit') + need(prlimit is not None,'util-linux prlimit unavailable; refusing unbounded child') + # WHY: a child core dump can exhaust the bounded graphics LAB. + # CONTRACT: every owned child starts with a zero soft and hard core limit. + # INVARIANT: exec keeps the child PID stable for /proc identity checks. + actual_argv=[prlimit,'--core=0:0','--',*requested_argv] + # WHY/CONTRACT/INVARIANT: registration and expiry are atomic: no child + # can escape the watchdog's owned-group snapshot by racing its deadline. + with getattr(self,'proc_lock',threading.RLock()): + need(not self.expired,'expired before process creation') + # WHY/CONTRACT/INVARIANT: diagnostics are compositor-private and + # never enter the environment shared with capture, IPC or clients. + child_env={**self.env,**({'WAYLAND_DEBUG':'1'} if debug else {})} + need(not any(k.startswith(('SMX_OOM_','MOSAIC_G2_','MOSAIC_G3_')) for k in child_env),'diagnostic environment leaked into common environment') + if name=='compositor': + child_env.update(getattr(self,'oom_env',{}));child_env.update(getattr(self,'compositor_environment',{})) + p=subprocess.Popen(actual_argv,stdin=subprocess.PIPE,stdout=f,stderr=subprocess.STDOUT,text=True,start_new_session=True,env=child_env) + self.proc[name]=p + self.record('spawn',name=name,pid=p.pid,requested_argv=requested_argv,actual_argv=actual_argv);return p + def log(self,name):return (self.run/(name+'.log')).read_text(errors='replace') + def wait(self,fn,seconds=5): + end=min(self.started+143,time.monotonic()+seconds) + while time.monotonic()=self.started+143 or any(p.stat().st_size>32*1024*1024 for p in self.run.glob('*.log')): + with getattr(self,'proc_lock',threading.RLock()):self.expired=True + self.signal_owned(signal.SIGTERM) + grace_end=min(self.started+146,time.monotonic()+2) + if not self.finished.wait(max(0,grace_end-time.monotonic())): + self.signal_owned(signal.SIGKILL) + return + def command_process(self,argv,timeout=4): + # WHY: capture/IPC helpers also belong to the autonomous watchdog. + # CONTRACT: bounded file output and owned session groups for every tool. + # INVARIANT: debit precedes spawn; raw tool status remains independent. + self.debit('tool');self.tool_serial+=1;tag='tool-'+str(self.tool_serial) + outpath=self.run/(tag+'.stdout.log');errpath=self.run/(tag+'.stderr.log') + with outpath.open('w') as stdout,errpath.open('w') as stderr: + with self.proc_lock: + need(not self.expired,'expired before helper spawn') + argv=list(map(str,argv));actual=['prlimit','--core=0:0','--',*argv] + p=subprocess.Popen(actual,env=self.env,stdout=stdout,stderr=stderr,start_new_session=True) + self.transient[p.pid]=p + try: + try:p.wait(timeout=min(timeout,self.remaining())) + except subprocess.TimeoutExpired: + os.killpg(p.pid,signal.SIGKILL);p.wait(timeout=1);raise + finally: + with self.proc_lock:self.transient.pop(p.pid,None) + need(outpath.stat().st_size<=32*1024*1024 and errpath.stat().st_size<=32*1024*1024,'helper log bound') + result=subprocess.CompletedProcess(argv,p.returncode,outpath.read_text(errors='replace'),errpath.read_text(errors='replace')) + self.record('tool-exit',argv=argv,pid=p.pid,exit=p.returncode,stdout_path=str(outpath),stderr_path=str(errpath)) + return result + + def ipc(self,*args,strict=True): + need(self.sock.is_relative_to(self.runtime) and self.sock.is_socket(),'private socket') + self.record('ipc-request',args=args) + p=self.command_process(['swaymsg','-s',str(self.sock),*args]) + self.record('ipc-reply',args=args,exit=p.returncode,stdout=p.stdout,stderr=p.stderr) + if strict:need(p.returncode==0,'IPC command failure') + return json.loads(p.stdout) if p.returncode==0 else None + def cmd(self,command,business=False): + self.debit('mutation' if business else 'configuration');reply=self.ipc(command);need(all(r['success'] for r in reply),'command rejected') + def snapshot(self,tag,validate=True): + snap=dict(tree=self.ipc('-t','get_tree'),outputs=self.ipc('-t','get_outputs'),workspaces=self.ipc('-t','get_workspaces'),ns=time.monotonic_ns()) + if validate:expected_outputs(snap['outputs'],self.outputs) + self.save(tag+'-snapshot',snap);return snap + def find(self,name): + pid=self.proc[name].pid;rows=[n for n in nodes(self.ipc('-t','get_tree')) if n.get('pid')==pid];need(len(rows)<=1,'client owner ambiguity');return rows[0] if rows else None + def launch_client(self,name,client,*args): + build=self.lab/'followup-work/client-build' + p=self.spawn(name,[build/client,*args],True);self.wait(lambda:self.find(name));time.sleep(.25) + if client=='fullscreen-client': + exe=Path(f'/proc/{p.pid}/exe');need(exe.resolve()==build/client and sha(exe)==self.auth['client_binary_sha256'],'actual fullscreen client executable') + self.record('fullscreen-client-identity',pid=p.pid,binary=str(exe.resolve()),sha256=sha(exe)) + return p + def capture(self,tag,out,orientation_pending=False): + self.debit('capture');spec=next(o for o in self.outputs if o['name']==out);p=self.run/(tag+'.png');need(not p.exists(),'capture unique') + t=time.monotonic_ns();r=self.command_process(['grim','-s',str(spec['scale']),'-o',out,str(p)]) + self.record('capture',tag=tag,output=out,start_ns=t,end_ns=time.monotonic_ns(),exit=r.returncode,path=str(p),sha256=sha(p) if p.exists() else None) + need(r.returncode==0,'capture failure');im=Image.open(p).convert('RGB') + logical=tuple(round(v*spec['scale']) for v in spec['logical'][2:]);physical=tuple(spec['mode']) + need(im.size==logical or (orientation_pending and im.size==physical) or (self.frames[out]=='physical90' and im.size==physical),'PNG size') + return im + def pair_capture(self,tag):return [self.capture(tag+'-H1','HEADLESS-1'),self.capture(tag+'-H2','HEADLESS-2')] + def inject(self,action,point,grab=False): + need(action in ('move','left','right'),'injection action');outs=self.ipc('-t','get_outputs');active=[o for o in outs if o['active']] + rects=[[o['rect'][k] for k in ('x','y','width','height')] for o in active];need(any(contains(r,point) for r in rects),'logical point outside output') + minx=min(r[0] for r in rects);miny=min(r[1] for r in rects);w=max(r[0]+r[2] for r in rects)-minx;h=max(r[1]+r[3] for r in rects)-miny + self.debit('move',*(['click'] if action!='move' else []),*(['grab'] if grab else [])) + sent=[point[0]-minx,point[1]-miny];marker=f'INJECT_DONE {action} {sent[0]},{sent[1]}';before=self.log('pointer').count(marker) + self.pointer.stdin.write(f'{action} {sent[0]} {sent[1]} {w} {h}\n');self.pointer.stdin.flush();self.wait(lambda:self.log('pointer').count(marker)>before) + self.record('injection',action=action,global_logical=point,sent=sent,logical_extent=[w,h],grab=grab);time.sleep(.12) + def click(self,name,point,action='right',grab=False): + before={n:len(self.log(n)) for n in self.proc if n not in ('compositor','pointer','background')};t=time.monotonic_ns();self.inject(action,point,grab) + validators.window(t,time.monotonic_ns(),[e['ns'] for e in self.events if e['kind']=='injection' and e['ns']>=t]);tails={n:self.log(n)[i:] for n,i in before.items()};lines={n:[s for s in v.splitlines() if s.startswith(('POINTER_BUTTON ','BUTTON '))] for n,v in tails.items()} + self.record('click-observation',name=name,point=point,start_ns=t,tails=tails,buttons=lines) + return tails,lines + def delivery(self,name,rect,point,tails,lines,target,opening_serial=None): + need(not any(v for n,v in lines.items() if n!=name),'wrong client received popup button') + proof=opening_pair(lines[name],target,opening_serial) if opening_serial else pair(lines[name],target) + rows=re.findall(rf'POINTER_(?:ENTER|MOTION) target={target} .*?x=([\d.-]+) y=([\d.-]+)',tails[name]);need(rows,'fresh pointer local coordinates') + local=list(map(float,rows[-1]));validators.applied(rect[:2],rect[:2],[point[0]-local[0],point[1]-local[1]],'applied') + for e in proof:need(re.search(rf'wl_pointer#\d+\.button\({e[1]}, \d+, {e[2]}, {e[3]}\)',tails[name]) is not None,'button actual wire') + return proof + def owner(self,name): + snap=self.snapshot(name+'-owner') + rows=[n for n in nodes(snap['tree']) if n.get('pid')==self.proc[name].pid] + need(len(rows)==1,'owner identity/ambiguity');owner=rows[0] + ws=workspace(snap['tree'],'1') + path=__import__('ipc_predicates').ancestry(snap['tree'],owner['id']) + validators.owner_context(owner,path,self.proc[name].pid,'1',getattr(self,'expected_output','HEADLESS-1'),getattr(self,'expected_layout','mosaic' if self.spec['mode']=='mosaic' else 'splith')) + # WHY: standard_return changes layout after the initial campaign mode. + # CONTRACT: camera applies only to current Mosaic tiled descendants; + # floating branches and fullscreen group ancestry remain exempt. + # WHY/CONTRACT: root/output/workspace IPC descriptors may have floating=null. + # INVARIANT: null means no floating flag; branch membership still counts, + # and this geometry remains a pending candidate, never native-applied. + eligible=validators.camera_eligible(path,ws) + origin=assigned_origin(owner);scene=exterior_ipc(owner) + if eligible: + scroll=int(ws['mosaic']['rows'][0]['scroll_x']);origin[0]-=scroll;scene[0]-=scroll + # INVARIANT: IPC never becomes native-applied without fresh paint and pointer corroboration. + raw='current-layout IPC candidate; native paint and fresh pointer-local corroboration required, generation NOT OBSERVABLE' + data=dict(geometry_status='IPC_PENDING_CANDIDATE; requires fresh paint/pointer after every mutation',owner=owner,workspace=ws,origin=origin,scene=scene,evidence=raw,snapshot=snap);self.save(name+'-geometry',data);return data + def calibrate(self,name): + state=self.owner(name) + if self.spec['mode']=='mosaic' and self.spec['axis']==0: + delta=int(state['origin'][0]+state['owner']['window_rect']['width']//2) + if delta:self.scroll(name,delta) + state=self.owner(name);need(crossing([*state['origin'],state['owner']['window_rect']['width'],state['owner']['window_rect']['height']],0,0),'owner real x0 crossing') + return state + def scroll(self,name,delta): + self.debit('camera');self.cmd(f'[con_id={self.find(name)["id"]}] layout mosaic scroll {int(delta)}');time.sleep(.35) + self.record('camera',owner=self.find(name)['id'],delta=int(delta)) + def client_cmd(self,name,cmd,marker=None): + self.debit('configuration' if cmd.startswith('close') else 'mutation');before=len(self.log(name));self.record('client-request',name=name,command=cmd) + self.proc[name].stdin.write(cmd+'\n');self.proc[name].stdin.flush() + if marker:self.wait(lambda:marker in self.log(name)[before:]) + def close_client(self,name,command='close_owner',scenario=None): + # WHY: exit0 alone cannot prove SHM release or an explicit normal close. + # CONTRACT: retain each PID's validation, including retired same-name owners. + # INVARIANT: a failed closure is durable evidence, never laundered by cleanup. + p=self.proc[name];proof=dict(name=name,pid=p.pid,status='INCONCLUSIVE',failure=None) + try: + if p.poll() is None:self.client_cmd(name,command) + code=p.wait(timeout=min(3,self.remaining()));raw=self.log(name) + archive=self.run/f'{name}-closed-{p.pid}.log';archive.write_text(raw) + proof.update(exit=code,log=str(archive),sha256=sha(archive),close_received='CLOSE_RECEIVED' in raw,shm_closure=re.findall(r'SHM_CLOSURE created=(\d+) released=(\d+)',raw),wayland_errors=re.findall(r'EXIT[^\n]*error=(\d+)',raw)) + validators.client_exit(raw,code,scenario or getattr(self,'closure_scenarios',{}).get(name,'observed'));self.wait(lambda:self.find(name) is None) + proof['status']='PASS' + except Exception as error: + proof['failure']=f'{type(error).__name__}: {error}' + proof.setdefault('exit',p.poll()) + raise + finally: + self.closed.append(proof);self.record('client-closed',**proof) + + def cleanup(self,measurement_status,failure=None): + terminal=None;rescue=False;closures=[] + # Close test clients while their Wayland server still exists. Mechanical + # rescue and terminal IPC can never substitute for scientific closure. + for name in reversed(list(self.proc)): + if name in ('compositor','pointer') or any(c['pid']==self.proc[name].pid for c in self.closed):continue + if self.expired or time.monotonic()>=self.started+143:break + try: + self.close_client(name,'close_owner' if name=='owner' and self.spec['client']!='saved-client' and self.spec['id']!='C6' else 'close') + except Exception:pass # close_client retains the explicit validation failure. + # WHY/CONTRACT/INVARIANT: archived and already-exited clients count equally; + # an empty list of newly closed processes must not erase prior failures. + closures=[dict(c) for c in self.closed] + tracked=[p.pid for n,p in self.proc.items() if n not in ('compositor','pointer')] + closure_coverage=all(any(c['pid']==pid for c in closures) for pid in tracked) + + if hasattr(self,'comp') and self.comp.poll() is None and hasattr(self,'sock') and self.sock.is_socket() and not self.expired and time.monotonic()0 and cfg[3]>0 and rect==[parent[0]+cfg[0],parent[1]+cfg[1],cfg[2],cfg[3]],'actual configure/global rectangle differs') + if require_crossing:need(crossing(rect,0,0),'actual popup noncrossing') + l,t,r,b=max(rect[0],ox),max(rect[1],oy),min(rect[0]+rect[2],ox+ow),min(rect[1]+rect[3],oy+oh) + if name in active_roles and l1; peer is created on ws3, then explicitly inserted + # in the marked nonleaf group. No post-failure empirical repair is allowed. + s.debit('chain');s.launch_client('owner','fullscreen-client','fullscreen_cross') + oid=s.find('owner')['id'];s.cmd(f'[con_id={oid}] focus');s.cmd(f'[con_id={oid}] split h') + path=ancestry(s.snapshot('group-wrapper')['tree'],oid);group=path[-2] + need(group.get('type')=='con' and [n['id'] for n in group.get('nodes',[])]==[oid],'split did not create owner wrapper') + gid=group['id'];s.cmd(f'[con_id={gid}] mark --add smx-followup-group') + s.cmd('workspace 3');s.launch_client('fullscreen-peer','static-reference');peer=s.find('fullscreen-peer')['id'] + s.cmd(f'[con_id={peer}] move container to mark smx-followup-group');s.cmd('workspace 1');s.cmd(f'[con_id={oid}] focus') + snap=s.snapshot('fullscreen-before');path=ancestry(snap['tree'],oid) + group=next(n for n in path if n.get('id')==gid) + need({n['id'] for n in group.get('nodes',[])}=={oid,peer},'peer outside exact owner group') + return oid,peer,gid + + +def run(s): + cases.init_reference(s,1) + try:oid,peer,gid=prepare_group(s) + except ValueError as exc: + s.save('fullscreen-not-exercised',dict(status='NOT_EXERCISED',reason=str(exc)));return 'NOT_EXERCISED' + baseline=cases.stable_reference(s);cut=len(s.log('owner')) + s.cmd(f'[con_id={gid}] fullscreen enable');s.cmd(f'[con_id={oid}] focus') + s.wait(lambda:'TOPLEVEL XDG_CONFIGURE ' in s.log('owner')[cut:] and 'TOPLEVEL MAP ' in s.log('owner')[cut:]);time.sleep(.2) + state=s.owner('owner');applied=applied_configure(s.log('owner'),cut,oid,state['workspace']['id'],gid) + group=group_candidate(state,peer,gid,applied=applied) + top=s.capture('fullscreen-owner','HEADLESS-1');xy=solid_point(top,COLORS['TOPLEVEL'],s.outputs[0],'logical',[*state['origin'],420,320]) + ta,ba=s.click('owner',xy,'left',True);s.wait(lambda:'POPUP_A MAP ' in s.log('owner')) + root=s.capture('fullscreen-A','HEADLESS-1');xyA=solid_point(root,COLORS['POPUP_A'],s.outputs[0],'logical') + tb,bb=s.click('owner',xyA,'left',True);s.wait(lambda:'POPUP_B MAP ' in s.log('owner'));time.sleep(.2) + p=validators.grab(s.log('owner'));adjust=positioner_adjust(s.log('owner'));g=fresh_geometry(s,state) + s.delivery('owner',[*state['origin'],420,320],xy,ta,ba,'TOPLEVEL',p['grabs'][0]['serial']);s.delivery('owner',g['A'],xyA,tb,bb,'POPUP_A',p['grabs'][1]['serial']) + if not all(crossing(g[k],0,0) for k in ('A','B')): + s.save('fullscreen-not-exercised',dict(status='NOT_EXERCISED',geometry=g));return 'NOT_EXERCISED' + images=measured_pair(s,'fullscreen-mapped',baseline,g,cross=True);input_target(s,images,g,'POPUP_B','B') + m=__import__('json').loads(__import__('pathlib').Path(s.auth['manifest']).read_text()) + record=dict(client_sha256=s.auth['client_binary_sha256'],client_source_sha256=m['files']['graphics/clients/fullscreen-client.c'],protocol_sha256=m['files']['graphics/protocols/xdg-shell.xml'],positioner_adjust=adjust,fresh=True,applied=applied,roles=p['hierarchy'],top_configure=re.findall(r'TOPLEVEL ROLE_CONFIGURE w=(\d+) h=(\d+)',s.log('owner'))[-1],child_rects=[c['rect'] for c in group['nodes']],child_widths=[c['rect']['width'] for c in group['nodes']],group_rect=group['rect'],popup_A=g['A'],popup_B=g['B'],configured_A=g['configured_A'],configured_B=g['configured_B'],origin=g['origin'],outputs=s.outputs,owner_id=oid,group_id=gid,peer_id=peer,protocol=p) + s.save('fullscreen-comparability',record) + before=s.log('owner');start=time.monotonic_ns();s.client_cmd('owner','close_b','COMMAND close_b DONE') + from lifecycle import explicit_child_close + explicit_child_close(before,s.log('owner'),p['ids'],start,time.monotonic_ns());time.sleep(.15) + state=s.owner('owner');group_candidate(state,peer,gid,applied=applied);g=fresh_geometry(s,state) + images=measured_pair(s,'fullscreen-B-closed',baseline,g,('POPUP_A',));need(not any(count(im,COLORS['POPUP_B']) for im in images),'B residual');input_target(s,images,g,'POPUP_A','A') + s.cmd(f'[con_id={gid}] fullscreen disable',True);s.cmd(f'[con_id={state["workspace"]["id"]}] layout splith',True);s.expected_layout='splith';time.sleep(.35) + state=s.owner('owner');need(state['owner']['id']==oid,'owner changed on standard return');group_candidate(state,peer,gid,fullscreen=False) + g=fresh_geometry(s,state);images=measured_pair(s,'fullscreen-standard-A',baseline,g,('POPUP_A',));input_target(s,images,g,'POPUP_A','A') + s.close_client('owner',scenario='nested');final=s.pair_capture('fullscreen-final');exact(baseline,final[1]) + return 'PARTIALLY_FEASIBLE' diff --git a/tooling/probes/step20c-corec-v2-followup/graphics/gate.py b/tooling/probes/step20c-corec-v2-followup/graphics/gate.py new file mode 100644 index 0000000..b08f9f9 --- /dev/null +++ b/tooling/probes/step20c-corec-v2-followup/graphics/gate.py @@ -0,0 +1,212 @@ +"""WHY: a new ticket cannot borrow historical reservations or source reviews. +CONTRACT: freeze + independent review + publication precede every reservation. +INVARIANT: no reset/refund; failed actions remain debited, bindings never repaired. +""" +import ctypes,fcntl,hashlib,json,os,re,resource,stat,subprocess,time,uuid +from pathlib import Path +from plans import RUNS,LIMITS +TICKET='SWAYFX_MOSAIC_STEP20C_COREC_V2_FULLSCREEN_OOM_GLES_NATIVE_CLOSURE_BATCH_V1' +WL_SHA='7edca9a7d65855addda68e29ba5db94d57c803eb5fa7d0f86db3136b90ca9837' +SOURCE_ROOT=Path(__file__).resolve().parent.parent + +def need(v,m): + if not v:raise ValueError(m) +def sha(p):return hashlib.sha256(Path(p).read_bytes()).hexdigest() +def encoded(v):return (json.dumps(v,sort_keys=True,indent=2)+'\n').encode() +def digest(v):return hashlib.sha256(encoded(v)).hexdigest() +def canonical_lab(lab): + p=Path(lab);need(p.is_absolute() and p.resolve()==p and p.parent==Path('/var/tmp'),'canonical direct /var/tmp LAB required') + s=p.lstat();need(stat.S_ISDIR(s.st_mode) and s.st_uid==os.getuid() and stat.S_IMODE(s.st_mode)==0o700,'LAB mode/owner/type');return p + +def clean_environment(lab): + lab=canonical_lab(lab) + for name in ('scratch','cache','data','pycache'): + p=lab/name;need(p.resolve()==p and p.is_dir() and p.stat().st_uid==os.getuid(),'owned scratch/cache/data required') + e={k:os.environ[k] for k in ('PATH','LANG','LC_ALL','TZ') if k in os.environ} + e.update(TMPDIR=str(lab/'scratch'),TMP=str(lab/'scratch'),TEMP=str(lab/'scratch'),SMX_LAB=str(lab),XDG_CACHE_HOME=str(lab/'cache'),XDG_DATA_HOME=str(lab/'data'),PYTHONPYCACHEPREFIX=str(lab/'pycache'),CCACHE_DIR=str(lab/'cache/ccache')) + return e + +def disable_dump(): + resource.setrlimit(resource.RLIMIT_CORE,(0,0)) + need(ctypes.CDLL(None).prctl(4,0,0,0,0)==0,'PR_SET_DUMPABLE failed') + +def secure_read(path): + fd=os.open(path,os.O_RDONLY|os.O_NOFOLLOW|os.O_NONBLOCK) + try: + s=os.fstat(fd);need(stat.S_ISREG(s.st_mode) and s.st_uid==os.getuid() and s.st_nlink==1 and not s.st_mode&0o077,'receipt/ledger owner/type/link/mode') + need(s.st_size<=16*1024*1024,'receipt size bound') + with os.fdopen(fd,'r',closefd=False) as f:return json.load(f) + finally:os.close(fd) + +def durable(path,value,exclusive=False): + p=Path(path);need(p.parent.resolve()==p.parent and p.parent.is_dir(),'canonical output parent') + if p.exists() or p.is_symlink(): + s=p.lstat();need(stat.S_ISREG(s.st_mode) and s.st_uid==os.getuid() and s.st_nlink==1 and not s.st_mode&0o077,'unsafe existing output') + tmp=p.with_name(p.name+'.tmp-'+uuid.uuid4().hex) + fd=os.open(tmp,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600) + try: + with os.fdopen(fd,'wb') as f:f.write(encoded(value));f.flush();os.fsync(f.fileno()) + if exclusive:os.link(tmp,p);tmp.unlink() + else:os.replace(tmp,p) + d=os.open(p.parent,os.O_RDONLY|os.O_DIRECTORY) + try:os.fsync(d) + finally:os.close(d) + finally: + if tmp.exists():tmp.unlink() + +def source_files(root=None): + root=root or SOURCE_ROOT;result={} + for p in sorted(root.rglob('*')): + need(not p.is_symlink(),'source symlink forbidden') + if p.is_file(): + rel=p.relative_to(root).as_posix();need('__pycache__' not in p.parts and p.suffix not in ('.pyc','.png','.o','.so','.a'),'generated source artifact') + result[rel]=sha(p) + need(result,'empty source kit');return result + +def source_modes(root=None): + root=root or SOURCE_ROOT + files=source_files(root);result={} + for rel in files: + mode=stat.S_IMODE((root/rel).stat().st_mode) + need(mode in (0o644,0o755),'source mode not portable Git 0644/0755') + result[rel]=oct(mode) + return result + +def git_inventory(raw,prefix,files,modes): + # WHY: identical blob bytes do not prove file type, mode, or full inventory. + # CONTRACT/INVARIANT: exact Git subtree, canonical portable modes, blobs only. + need(set(files)==set(modes),'manifest mode inventory') + need(all(v in ('0o644','0o755') for v in modes.values()),'invalid manifest modes') + prefix=prefix.encode()+b'/';seen={} + need(not raw or raw.endswith(b'\0'),'truncated Git tree') + for entry in raw.split(b'\0')[:-1]: + meta,path=entry.split(b'\t',1);mode,kind,oid=meta.split(b' ') + need(path.startswith(prefix),'Git tree wrong prefix') + rel=path[len(prefix):].decode('utf-8',errors='strict') + need(rel in files and rel not in seen,'Git tree added/duplicate path') + need(kind==b'blob' and mode in (b'100644',b'100755'),'Git tree type/mode') + need(mode=={'0o644':b'100644','0o755':b'100755'}[modes[rel]],'Git mode differs') + need(re.fullmatch(b'[0-9a-f]{40}',oid),'Git object identity') + seen[rel]=oid.decode() + need(set(seen)==set(files),'Git tree missing path') + return seen + +def published_sources(top,commit,prefix,files,modes,env): + tree=subprocess.run(['git','-C',str(top),'ls-tree','-rz','--full-tree',commit,'--',str(prefix)],capture_output=True,env=env,timeout=4) + need(tree.returncode==0,'published Git tree unavailable') + git_inventory(tree.stdout,str(prefix),files,modes) + for rel,h in files.items(): + blob=subprocess.run(['git','-C',str(top),'show',f'{commit}:{prefix/rel}'],capture_output=True,env=env,timeout=4) + need(blob.returncode==0 and hashlib.sha256(blob.stdout).hexdigest()==h,'commit does not contain frozen bytes') + + +def _receipt(lab,path,expected): + p=Path(path);need(p.resolve()==p and p.is_relative_to(lab),'runtime receipt outside new LAB') + value=secure_read(p);need(sha(p)==expected,'receipt SHA mismatch');return value + +def validate_review(lab,manifest,review,publication,rid,offline=False,check_git=True): + lab=canonical_lab(lab);need(rid in RUNS,'historical/unknown run ID');need(RUNS[rid]['admission']!='NOT_ADMITTED' or rid=='GPU-G3','scenario NOT_ADMITTED') + manifest,review,publication=map(Path,(manifest,review,publication)) + need(all(p.resolve()==p and p.is_relative_to(lab) for p in (manifest,review,publication)),'external admission receipt outside new LAB') + m=secure_read(manifest);r=secure_read(review);p=secure_read(publication) + need(m.get('ticket')==TICKET and m.get('schema')=='native-closure-source-v1' and m.get('plans')==RUNS and m.get('limits')==LIMITS,'manifest ticket/plans/schema/limits') + need(m.get('files')==source_files() and m.get('modes')==source_modes(),'source changed/added/removed/mode after freeze') + need(r.get('decision')=='ACCEPTED' and r.get('independent') is True and r.get('reviewer') and r['reviewer']!=m.get('author'),'independent review absent') + need(r.get('manifest_sha256')==sha(manifest) and r.get('ticket')==TICKET,'review stale/ticket') + case=r.get('runs',{}).get(rid,{}) + need(case.get('decision')=='ACCEPTED' and case.get('client_build_review')=='ACCEPTED','per-run client/source review absent') + for gate in {'FS-P':('FULLSCREEN_COMPARABILITY_OFFLINE',),'FS-C':('FULLSCREEN_COMPARABILITY_OFFLINE',),'DY-C':('MIGRATION_REFOCUS_OFFLINE',),'OM-ROOT':('OOM_ROOT_NESTED_ORACLE_OFFLINE',),'OM-NESTED':('OOM_ROOT_NESTED_ORACLE_OFFLINE',),'GPU-G3':('B4',)}[rid]: + proof=r.get('gates',{}).get(gate,{}) + need(proof.get('decision')=='ACCEPTED' and proof.get('manifest_sha256')==sha(manifest),'required gate not independently accepted') + receipt=_receipt(lab,proof.get('receipt',''),proof.get('receipt_sha256')) + need(receipt.get('status')=='PASS' and receipt.get('tests_run',0)>0 and receipt.get('failures')==0 and receipt.get('source_digest')==digest(m['files']),'superficial/stale offline gate') + need(p.get('ticket')==TICKET and p.get('decision')=='VERIFIED' and p.get('offline_test_only') is offline,'publication namespace/ticket') + commit=p.get('source_commit','');need(re.fullmatch('[0-9a-f]{40}',commit),'publication commit') + need(p.get('manifest_sha256')==sha(manifest) and p.get('source_digest')==digest(m['files']),'publication manifest binding') + need(p.get('origin_sha')==p.get('github_sha')==commit,'source kit unpublished') + for name in ('origin','github'): + obs=p.get('remote_observations',{}).get(name,{}) + need(obs.get('exit')==0 and obs.get('argv')==['git','ls-remote',name,'refs/heads/main'] and obs.get('stdout','').split()==[commit,'refs/heads/main'],'direct remote observation absent') + if not offline and check_git: + # Git bytes independently bind the remote commit to this portable kit. + repo=SOURCE_ROOT + root=subprocess.run(['git','-C',str(repo),'rev-parse','--show-toplevel'],capture_output=True,text=True,env=clean_environment(lab),timeout=4) + need(root.returncode==0,'published Git repository unavailable');top=Path(root.stdout.strip());prefix=SOURCE_ROOT.relative_to(top) + published_sources(top,commit,prefix,m['files'],m['modes'],clean_environment(lab)) + runtime=_receipt(lab,r.get('runtime_manifest',''),r.get('runtime_manifest_sha256')) + need(runtime.get('ticket')==TICKET,'runtime identity ticket') + identity=runtime['identities'][RUNS[rid]['binary']] + need(sha(identity['binary'])==identity['binary_sha256'],'binary changed') + if not offline and check_git: + elf=subprocess.run(['readelf','-n',identity['binary']],capture_output=True,text=True,env=clean_environment(lab),timeout=4) + need(elf.returncode==0 and re.findall(r'Build ID: ([0-9a-f]+)',elf.stdout)==[identity.get('build_id')],'binary BuildID mismatch') + need(sha(identity['wlroots'])==identity['wlroots_sha256']==WL_SHA,'wlroots changed') + need(identity.get('build_review')=='ACCEPTED' and identity.get('source_qualification')=='ACCEPTED','identity/source build unreviewed') + build=_receipt(lab,case.get('client_build_receipt',''),case.get('client_build_receipt_sha256')) + need(build.get('status')=='PASS' and build.get('runtime')=='NOT_RUN','client compile receipt') + for name in ('pointer-daemon','static-reference',RUNS[rid]['client']): + binary=lab/'followup-work/client-build'/name + need(sha(binary)==build['generated'][name] and build['inputs']['clients/'+name+'.c']==m['files']['graphics/clients/'+name+'.c'],'client generated/source mismatch') + need(build['inputs']['protocols/xdg-shell.xml']==m['files']['graphics/protocols/xdg-shell.xml'] and build['inputs']['protocols/wlr-virtual-pointer-unstable-v1.xml']==m['files']['graphics/protocols/wlr-virtual-pointer-unstable-v1.xml'],'protocol source binding') + if rid=='GPU-G3': + need(all(identity['binary_sha256']!=other.get('binary_sha256') for name,other in runtime['identities'].items() if name!='gpu'),'GPU new distinct ELF required') + from gpu_handler import validate_binding + validate_binding(lab,case,identity,sha(manifest)) + if rid in ('OM-ROOT','OM-NESTED'): + need(case.get('oom_source_review')=='ACCEPTED' and case.get('variant_identity')=='COREC_V2_NATIVE_OOM_DIAGNOSTIC' and case.get('source_manifest_sha256')==sha(manifest),'OOM native/source variant review absent') + if rid=='OM-NESTED' and case.get('root_dependency_override'): + v=case['root_dependency_override'];need(v.get('independent') is True and v.get('reviewer')!=m['author'] and v.get('decision')=='ACCEPTED' and v.get('manifest_sha256')==sha(manifest) and v.get('rationale') and v.get('no_common_native_fault') is True,'invalid independent nested dependency review') + return dict(identity,prospective=case,manifest=str(manifest),review=str(review),publication=str(publication),manifest_sha256=sha(manifest),review_sha256=sha(review),publication_sha256=sha(publication),runtime_manifest_sha256=r['runtime_manifest_sha256'],source_digest=digest(m['files']),client_binary_sha256=build['generated'][RUNS[rid]['client']]) + +class Ledger: + def __init__(self,lab,path=None,offline=False): + self.lab=canonical_lab(lab);self.offline=offline;self.path=Path(path) if path else self.lab/'native-closure-budget.json' + need(self.path.parent.resolve()==self.path.parent and self.path.parent.is_relative_to(self.lab),'ledger outside new LAB/canonical parent') + need(not self.path.is_symlink(),'ledger symlink') + if offline:need('OFFLINE_TEST_ONLY' in self.path.parts,'offline namespace') + else:need(self.path==self.lab/'native-closure-budget.json','real ledger fixed new path') + self.lock=self.path.with_name(self.path.name+'.lock') + def transaction(self,fn,initialize=False): + fd=os.open(self.lock,os.O_RDWR|os.O_CREAT|os.O_NOFOLLOW,0o600) + try: + s=os.fstat(fd);need(stat.S_ISREG(s.st_mode) and s.st_uid==os.getuid() and s.st_nlink==1 and not s.st_mode&0o077,'lock owner/type/link/mode') + fcntl.flock(fd,fcntl.LOCK_EX);need(self.lock.lstat().st_ino==s.st_ino,'lock inode changed') + if initialize:need(not self.path.exists(),'ledger already initialized');d=None + else: + d=secure_read(self.path);need(d.get('ticket')==TICKET and d.get('lab')==str(self.lab) and d.get('uid')==os.getuid(),'old ticket/LAB/UID') + need(d.get('namespace')==('OFFLINE_TEST_ONLY' if self.offline else 'REAL') and d.get('limits')==LIMITS,'namespace/limits') + result,new=fn(d);durable(self.path,new,exclusive=initialize);return result + finally:os.close(fd) + def initialize(self): + return self.transaction(lambda _:(None,dict(ticket=TICKET,lab=str(self.lab),uid=os.getuid(),namespace='OFFLINE_TEST_ONLY' if self.offline else 'REAL',limits=LIMITS,runs=[])),True) + def admit(self,rid,manifest,review,publication): + auth=validate_review(self.lab,manifest,review,publication,rid,offline=self.offline) + def reserve(d): + spec=RUNS[rid];need(not any(x['id']==rid for x in d['runs']),'consumed run; no retry') + if rid=='OM-NESTED': + roots=[x for x in d['runs'] if x['id']=='OM-ROOT'];good=len(roots)==1 and roots[0]['status']=='COMPLETED' and roots[0].get('outcome',{}).get('status')=='PARTIALLY_FEASIBLE' and roots[0].get('outcome',{}).get('scientific_cleanup') is True + need(good or auth['prospective'].get('root_dependency_override'),'root unresolved; independent dependency review required') + if rid=='FS-C': + controls=[x for x in d['runs'] if x['id']=='FS-P'];need(len(controls)==1 and controls[0]['status']=='COMPLETED' and controls[0].get('outcome',{}).get('status')=='PARTIALLY_FEASIBLE','fullscreen control incomplete') + need(len(d['runs'])=100 for v in positions.values()),'marker color missing') + c={k:[sum(p[i] for p in v)/len(v) for i in (0,1)] for k,v in positions.items()} + logical=image.size==(round(800*scale),round(600*scale)) and c['TL'][0]c['BL'][0] and c['TL'][1]0,'child-zero-residual-A-painted') + require(before.startswith('') and after.startswith(before),'A-input-protocol-appended') + require('POPUP_A DESTROY ' not in after and 'POPUP_A POPUP_DONE ' not in after and + after.count(f'POPUP_A MAP wl={a[0]} xdg={a[1]}')==1,'same-A-live-after-input') + require(rect[0]<=global_point[0]= 0 and self.height >= 0, 'nonnegative-size') + require(bool(self.provenance.evidence), 'evidence-required') + + @property + def end(self): + return (self.x + self.width, self.y + self.height) + + def contains(self, xy): + return self.x <= xy[0] < self.end[0] and self.y <= xy[1] < self.end[1] + + def values(self): + return [self.x, self.y, self.width, self.height] + +@dataclass(frozen=True) +class Native: + rectangle: Rect + buffer_id: int + stride: int + format: int + commit_serial: int + +@dataclass(frozen=True) +class Applied: + scene: Rect + window: Rect + configured: Rect + native: Native + border_left: int + border_top: int + titlebar: int + configure_ack: int + view_offset: tuple = (0, 0) + +@dataclass(frozen=True) +class Output: + name: str + rectangle: Rect + scale: float + transform: str + + +def same(a, b): + require(a.owner == b.owner, 'owner-identity') + require(a.workspace == b.workspace, 'workspace-identity') + require(a.generation == b.generation, 'applied-generation') + require(a.state == b.state == 'applied', 'pending-not-applied') + + +def content(applied): + s, w, c, n = applied.scene, applied.window, applied.configured, applied.native + require(s.space is Space.GLOBAL and w.space is Space.CONTENT and c.space is Space.CONFIGURE, + 'assignment-spaces') + require(n.rectangle.space is Space.NATIVE, 'native-space') + for r in (w, c, n.rectangle): + same(s.provenance, r.provenance) + require(n.buffer_id > 0 and n.stride == n.rectangle.width * 4 and n.format in (0, 1), 'native-buffer-identity') + require(0 < n.commit_serial <= 0xffffffff, 'commit-serial') + require(0 < applied.configure_ack <= 0xffffffff, 'assigned-configure-ack') + require((w.width, w.height) == (c.width, c.height), 'configure-assignment') + require(applied.border_left == w.x and applied.border_top == applied.titlebar + w.y, + 'titlebar-content-offset') + require(w.width + 2 * applied.border_left == s.width and + w.height + applied.border_top + applied.border_left == s.height, 'assigned-destination') + return Rect(Space.GLOBAL, s.x + applied.border_left + applied.view_offset[0], + s.y + applied.border_top + applied.view_offset[1], w.width, w.height, s.provenance) + + +def intersect(rect, boundary): + require(rect.space is Space.GLOBAL and boundary.space in (Space.GLOBAL, Space.VIEWPORT), 'global-intersection') + same(rect.provenance, boundary.provenance) + x, y = max(rect.x, boundary.x), max(rect.y, boundary.y) + return Rect(Space.GLOBAL, x, y, max(0, min(rect.end[0], boundary.end[0]) - x), + max(0, min(rect.end[1], boundary.end[1]) - y), rect.provenance) + + +def to_png(rect, output): + require(rect.space is Space.GLOBAL and output.rectangle.space is Space.GLOBAL, 'global-to-png-spaces') + require(output.scale == 1.0, 'unmeasured-output-scale') + require(output.transform == 'normal', 'unmeasured-output-transform') + same(rect.provenance, output.rectangle.provenance) + visible = intersect(rect, output.rectangle) + return Rect(Space.PNG, min(max(visible.x - output.rectangle.x,0),output.rectangle.width), min(max(visible.y - output.rectangle.y,0),output.rectangle.height), + visible.width, visible.height, rect.provenance) + + +def from_png(rect, output): + require(rect.space is Space.PNG and output.rectangle.space is Space.GLOBAL, 'png-to-global-spaces') + require(output.scale == 1.0 and output.transform == 'normal', 'unmeasured-png-transform') + same(rect.provenance, output.rectangle.provenance) + require(0 <= rect.x <= rect.end[0] <= output.rectangle.width and + 0 <= rect.y <= rect.end[1] <= output.rectangle.height, 'png-bounds') + return Rect(Space.GLOBAL, rect.x + output.rectangle.x, rect.y + output.rectangle.y, + rect.width, rect.height, rect.provenance) + + +def verify_paint(rect, output, bbox, count, clip=None): + expected = to_png(intersect(rect, clip) if clip is not None else rect, output) + actual = [] if bbox is None else list(bbox) + box=[expected.x, expected.y, *expected.end] if expected.width and expected.height else [] + require(actual == box, 'paint-origin-intersection') + require(count == expected.width * expected.height, 'paint-area') + return expected + + +def popup(parent, configured, parent_xdg, expected_parent_xdg, ack, mapped): + require(parent.space is Space.GLOBAL and configured.space is Space.POPUP, 'popup-spaces') + same(parent.provenance, configured.provenance) + require(parent_xdg == expected_parent_xdg and expected_parent_xdg > 0, 'xdg-parent') + require(0 < ack <= 0xffffffff, 'popup-ack') + require(mapped is True, 'popup-map') + return Rect(Space.GLOBAL, parent.x + configured.x, parent.y + configured.y, + configured.width, configured.height, parent.provenance) + + +def crossing(rect, boundary_x): + require(rect.space is Space.GLOBAL, 'crossing-space') + return 'EXERCISED' if rect.x < boundary_x < rect.end[0] else 'NOT EXERCISED' + + +def local_clip_to_global(local, origin, provenance): + require(local.space is Space.CONTENT, 'clip-local-space') + same(local.provenance, provenance) + return Rect(Space.VIEWPORT, local.x + origin[0], local.y + origin[1], + local.width, local.height, provenance) + + +def pointer_local(xy, rect, actual): + require(rect.space is Space.GLOBAL and rect.contains(xy), 'pointer-global-bounds') + require(list(actual) == [xy[0] - rect.x, xy[1] - rect.y], 'pointer-local-correlation') + return list(actual) + + +def fresh_chain(old, new): + require(old['owner'] != new['owner'] and old['pid'] != new['pid'], 'fresh-E2-identity') + require(len(set(old['serials'] + new['serials'])) == 4 and + max(old['serials']) < min(new['serials']), 'fresh-E2-serials') + + +def fresh_standard(mosaic, standard): + require(mosaic.provenance.owner == standard.provenance.owner and + mosaic.provenance.workspace == standard.provenance.workspace, 'standard-owner') + require(mosaic.provenance.generation != standard.provenance.generation, 'fresh-standard-generation') + return standard + + +def constraints(flags, requested, configured): + require(type(flags) is int and 0 <= flags <= 63, 'positioner-flags') + require(flags != 0 or requested == configured, 'zero-flags-no-adjustment') + return {'flags': flags, 'request': requested, 'actual': configured, 'prediction_only': True} diff --git a/tooling/probes/step20c-corec-v2-followup/graphics/offline/test_followup.py b/tooling/probes/step20c-corec-v2-followup/graphics/offline/test_followup.py new file mode 100644 index 0000000..18bc6e1 --- /dev/null +++ b/tooling/probes/step20c-corec-v2-followup/graphics/offline/test_followup.py @@ -0,0 +1,332 @@ +"""OFFLINE_TEST_ONLY: real final predicates; no headless spawn or real budget.""" +import copy,json,os,sys,tempfile,time,unittest +from pathlib import Path +from unittest.mock import patch +sys.path.insert(0,str(Path(__file__).resolve().parents[1])) +import cases,engine,fullscreen,gate,oom,oom_handler,plans,validators +from geometry import assigned_origin,exterior_ipc,exact +from PIL import Image,ImageDraw +LAB=Path(os.environ['SMX_LAB']);KIT=Path(__file__).resolve().parents[1] + +class Scientific(unittest.TestCase): + def rejects(self,fn,*args,**kwargs): + with self.assertRaises((ValueError,AssertionError,RuntimeError,KeyError)):fn(*args,**kwargs) + def snapshot(self,width=800,scroll=800,output='HEADLESS-1',focus=3,count=3,gap=0): + x=-width if output=='HEADLESS-1' else 0;cell=max(640,(width-(count-1)*gap)//count) + return dict(tree=dict(type='workspace',layout='mosaic',name='1',id=20,output=output,rect=dict(x=x,y=0,width=width,height=600),mosaic=dict(rows=[dict(children=list(range(1,count+1)),scroll_x=scroll,focused=focus)]),nodes=[dict(id=i+1,focused=i+1==focus,rect=dict(x=x+i*(cell+gap),y=25,width=cell,height=575)) for i in range(count)])) + def state(self): + rect=dict(x=-800,y=0,width=800,height=600) + owner=dict(type='con',id=40,pid=123,rect=dict(x=-800,y=0,width=400,height=600),nodes=[],floating=None) + peer=dict(type='con',id=41,pid=124,rect=dict(x=-400,y=0,width=400,height=600),nodes=[]) + group=dict(type='con',id=30,layout='splith',fullscreen_mode=1,nodes=[owner,peer],rect=rect,floating_nodes=[]) + ws=dict(type='workspace',id=20,name='1',output='HEADLESS-1',layout='splith',nodes=[group],fullscreen_mode=1) + return dict(owner=owner,workspace=ws,snapshot=dict(tree=ws,outputs=[dict(name='HEADLESS-1',rect=rect)])) + def applied(self):return dict(source='post-command-xdg-configure-ack-map',group_id=30,owner_id=40,workspace_id=20) + def test_fullscreen_exact_group(self): + st=self.state();fullscreen.group_candidate(st,41,30,applied=self.applied()) + changes=[lambda g:g.update(fullscreen_mode=0),lambda g:g.update(nodes=g['nodes'][:1]),lambda g:g['nodes'][1].update(id=99),lambda g:g['nodes'][0]['rect'].update(width=800),lambda g:g.update(layout='splitv')] + for change in changes: + bad=copy.deepcopy(st);change(bad['snapshot']['tree']['nodes'][0]);self.rejects(fullscreen.group_candidate,bad,41,30,applied=self.applied()) + def test_fullscreen_peer_at_workspace(self): + bad=self.state();g=bad['workspace']['nodes'][0];bad['workspace']['nodes'].append(g['nodes'].pop());self.rejects(fullscreen.group_candidate,bad,41,30,applied=self.applied()) + def test_pending_or_other_owner_workspace_not_applied(self): + for mutation in ({'source':'pending'},{'owner_id':99},{'workspace_id':99},{'group_id':99}): + a=self.applied();a.update(mutation);self.rejects(fullscreen.group_candidate,self.state(),41,30,applied=a) + self.rejects(fullscreen.group_candidate,self.state(),41,30) + def test_fullscreen_marker_alone_not_enough(self): + st=self.state();st['workspace']['nodes'][0]['fullscreen_mode']=0;self.rejects(fullscreen.group_candidate,st,41,30,applied=self.applied()) + def field(self): + baseline=Image.new('RGB',(800,600),(48,48,56));g=dict(origin=[-798,25],configured_A=[350,80,900,170],configured_B=[100,60,900,130],A=[-448,105,900,170],B=[-348,165,900,130]) + return baseline,g,plans.H[1] + def test_whole_field_prediction_occlusion_and_A_only(self): + b,g,o=self.field();pred=fullscreen.expected_h2(b,g,o,'logical',engine.COLORS,require_crossing=True) + self.assertEqual(pred.getpixel((0,110)),engine.COLORS['POPUP_A']);self.assertEqual(pred.getpixel((0,170)),engine.COLORS['POPUP_B']) + one=fullscreen.expected_h2(b,g,o,'logical',engine.COLORS,('POPUP_A',));self.assertEqual(one.getpixel((0,170)),engine.COLORS['POPUP_A']) + for xy,color in [((799,599),(1,2,3)),((0,170),engine.COLORS['POPUP_A']),((0,110),(48,48,56))]: + wrong=pred.copy();wrong.putpixel(xy,color);self.rejects(exact,pred,wrong) + bad=copy.deepcopy(g);bad['A'][0]=10;self.rejects(fullscreen.expected_h2,b,bad,o,'logical',engine.COLORS,require_crossing=True) + def test_migration_reveal_and_stale_camera(self): + prior=cases.dynamic_geometry(self.snapshot(),expected_focus=3) + moved=self.snapshot(scroll=1120,output='HEADLESS-2');actual=cases.dynamic_geometry(moved,prior,'migration',3);self.assertEqual(actual['expected_scroll'],1120) + self.rejects(cases.dynamic_geometry,self.snapshot(scroll=800,output='HEADLESS-2'),prior,'migration',3) + self.rejects(cases.dynamic_geometry,moved,prior,'resize',3) + def test_current_geometry_varies_count_gap_and_viewport(self): + prior=cases.dynamic_geometry(self.snapshot(scroll=500,count=4,gap=7,focus=4),expected_focus=4) + moved=self.snapshot(width=960,scroll=4*640+3*7-960,output='HEADLESS-2',count=4,gap=7,focus=4) + actual=cases.dynamic_geometry(moved,prior,'migration',4);self.assertEqual(actual['expected_scroll'],1621) + def test_resize800960800_and_input_invalidation(self): + a=cases.dynamic_geometry(self.snapshot(scroll=1100),expected_focus=3) + b=cases.dynamic_geometry(self.snapshot(width=960,scroll=960),a,'resize',3) + c=cases.dynamic_geometry(self.snapshot(scroll=960),b,'resize',3);self.assertEqual(c['expected_scroll'],960) + self.rejects(cases.dynamic_geometry,self.snapshot(width=960,scroll=1100),a,'resize',3) + def test_missing_or_other_refocus_order_bounds_pending(self): + prior=cases.dynamic_geometry(self.snapshot(),expected_focus=3) + for mutate in [lambda w:w['nodes'][2].update(focused=False),lambda w:w['mosaic']['rows'][0].update(focused=1),lambda w:w['mosaic']['rows'][0].update(scroll_x=9999),lambda w:w['mosaic']['rows'][0].update(children=[3,2,1]),lambda w:w['nodes'][2].update(focused=False)]: + snap=self.snapshot(scroll=1120,output='HEADLESS-2');mutate(snap['tree']);self.rejects(cases.dynamic_geometry,snap,prior,'migration',3) + bad=self.snapshot(scroll=0,focus=1);self.rejects(cases.dynamic_geometry,bad,prior,'migration',3) + bad=self.snapshot(scroll=1120);bad['state_source']='pending-as-current';self.rejects(cases.dynamic_geometry,bad,prior,'migration',3) + def close_log(self,roles=('TOPLEVEL','POPUP_A'),maps=()): + out=[] + for i,name in enumerate(roles): + a=10+i*3;out.append(f'{name} CREATE wl={a} xdg={a+1} role={a+2}') + if name in maps:out.append(name+' MAP wl='+str(a)) + out+=['CLOSE_RECEIVED','COMMAND close_owner'] + for i,name in reversed(list(enumerate(roles))): + a=10+i*3;out += [f'{name} DESTROY role={a+2} xdg={a+1} wl={a}',f'{"xdg_toplevel" if i==0 else "xdg_popup"}#{a+2}.destroy()',f'xdg_surface#{a+1}.destroy()',f'wl_surface#{a}.destroy()'] + return '\n'.join(out+['SHM_CLOSURE created=1 released=1','EXIT error=0']) + def test_root_only_close_is_valid_and_distinct(self): + raw=self.close_log();validators.client_exit(raw,0,'root_failure');self.rejects(validators.client_exit,raw,0,'nested') + self.rejects(validators.client_exit,raw+'\nPOPUP_B CREATE wl=99 xdg=98 role=97',0,'root_failure') + self.rejects(validators.client_exit,raw.replace('CLOSE_RECEIVED',''),0,'root_failure') + def test_nested_failure_A_mapped_B_unmapped(self): + raw=self.close_log(('TOPLEVEL','POPUP_A','POPUP_B'),('POPUP_A',));validators.client_exit(raw,0,'nested_failure') + self.rejects(validators.client_exit,raw+'\nPOPUP_B MAP wl=16',0,'nested_failure') + self.rejects(validators.client_exit,raw.replace('POPUP_A MAP wl=13',''),0,'nested_failure') + def test_close_negative_hierarchy_error_release_signal(self): + raw=self.close_log() + for bad in [raw.replace('POPUP_A DESTROY','TOPLEVEL DESTROY'),raw.replace('released=1','released=0'),raw.replace('error=0','error=12'),raw.replace('xdg_popup#15.destroy()',''),raw+'\nwl_display#1.error(wl_display#1, 1, "no memory")',raw+'\nPOPUP_A MAP wl=13']: + self.rejects(validators.client_exit,bad,0,'root_failure') + self.rejects(validators.client_exit,raw,-15,'root_failure') + def test_wire_real_grabs_and_partial_maps(self): + raw=(KIT/'wire-fixture.txt').read_text();validators.grab(raw) + import re + grab=re.search(r'POPUP_A GRAB_REQUEST[^\n]+',raw)[0];serial=re.search(r'serial=(\d+)',grab)[1] + changes=[raw.replace('used='+serial,'used=999',1),raw.replace('POPUP_B MAP ','POPUP_B NOT_MAP ',1),re.sub(r'(POPUP_B CREATE[^\n]+parent_xdg=)\d+',r'\g<1>999',raw),re.sub(r'xdg_popup#\d+\.grab',r'xdg_popup#999.grab',raw,count=1)] + for bad in changes:self.assertNotEqual(raw,bad);self.rejects(validators.grab,bad) + def test_failed_popup_real_wire_no_map(self): + raw=(KIT/'wire-fixture.txt').read_text();oom_handler.opening_before_map(raw,'POPUP_A','TOPLEVEL') + grab=__import__('re').search(r'POPUP_A GRAB_REQUEST[^\n]+',raw)[0];serial=__import__('re').search(r'serial=(\d+)',grab)[1] + self.rejects(oom_handler.opening_before_map,raw.replace('used='+serial,'used=999',1),'POPUP_A','TOPLEVEL') + def test_no_memory_requires_wire(self): + raw='wl_display#1.error(wl_display#1, 1, "no memory")\nEXIT error=12\n';oom_handler.expected_disconnect(raw,0) + for bad in [raw.replace('.error(','.no_error('),raw.replace('error=12','error=0'),raw.replace(', 1,',', 2,')]:self.rejects(oom_handler.expected_disconnect,bad,0) + def test_native_counter_and_NULL_association(self): + env=dict(SMX_OOM_NONCE='a'*32,SMX_OOM_ROLE='root_popup') + raw='SMX_OOM ENOMEM role=root_popup nonce='+env['SMX_OOM_NONCE']+' pid=123 xdg=0x10 xdg_parent=0x20 parent=0x30 tree=0x40 children=0 matched=1 failures=1 successes=0 initialized=0 listeners=0 published=0\nSMX_OOM result role=root_popup pid=123 xdg=0x10 xdg_parent=0x20 parent=0x30 tree=(nil) successes=0 failures=1' + oom.native_failure(raw,env,123) + for old,new in [('listeners=0','listeners=1'),('published=0','published=1'),('initialized=0','initialized=1'),('pid=123','pid=999'),('tree=(nil)','tree=0x40'),('xdg_parent=0x20','xdg_parent=0x99'),('matched=1','matched=2'),('nonce='+env['SMX_OOM_NONCE'],'nonce='+'b'*32)]: + bad=raw.replace(old,new,1);self.rejects(oom.native_failure,bad,env,123) + def test_B5_native_SHM_and_destination_titlebar_distinct(self): + owner=dict(rect=dict(x=-800,y=25,width=640,height=575),window_rect=dict(x=2,y=0,width=636,height=573),deco_rect=dict(height=25)) + self.assertEqual(assigned_origin(owner),[-798,25]);self.assertEqual(exterior_ipc(owner),[-800,0,640,600]) + self.assertNotEqual([420,320],[owner['window_rect']['width'],owner['window_rect']['height']]) + def test_floating_null_tiled_and_actual_floating_rejected(self): + st=self.state();path=[st['workspace'],st['workspace']['nodes'][0],st['owner']];st['workspace']['layout']='mosaic' + self.assertTrue(validators.camera_eligible(path,st['workspace']) is False) # fullscreen exemption + path[1]['fullscreen_mode']=0;self.assertTrue(validators.camera_eligible(path,st['workspace'])) + path[1]['floating_nodes']=[st['owner']];self.assertFalse(validators.camera_eligible(path,st['workspace'])) + + def test_B5_terminal_raw_codes_not_rewritten(self): + terminal=dict(ipc_exit=1,stdout='',stderr='Unable to receive IPC response') + self.assertTrue(validators.terminal_transport(terminal,0));self.assertEqual(terminal['ipc_exit'],1) + self.assertFalse(validators.terminal_transport(terminal,1));self.assertFalse(validators.terminal_transport(dict(ipc_exit=1),0)) + self.assertFalse(validators.terminal_transport(dict(ipc_exit=1,stdout='',stderr=''),0)) + def test_positioner_adjust_same_actual_wire_required(self): + raw=(KIT/'wire-fixture.txt').read_text() + import re + adapted=re.sub(r'(xdg_wm_base#\d+\.create_positioner\(new id xdg_positioner#(\d+)\))',lambda m:m[0]+'\nxdg_positioner#'+m[2]+'.set_constraint_adjustment(0)',raw) + self.assertEqual(fullscreen.positioner_adjust(adapted),[0,0]) + self.rejects(fullscreen.positioner_adjust,adapted.replace('set_constraint_adjustment(0)','set_constraint_adjustment(1)',1)) + self.rejects(fullscreen.positioner_adjust,raw) + + def test_native_availability_missing_identity_IPC_input_socket(self): + expected=dict(pid=123,binary='/private/sway',binary_sha256='a'*64,build_id='b'*40,wlroots='/usr/lib/libwlroots-0.20.so') + ident=dict(pid=123,exe=expected['binary'],sha256=expected['binary_sha256'],build_id=expected['build_id'],wlroots=[expected['wlroots']],maps='mapped '+expected['wlroots'],swaysock='/tmp/smx-offline/sway-ipc.123.sock',captured_before_cleanup=True) + snap=dict(tree=dict(type='root',nodes=[dict(id=1)]),outputs=[dict(active=True),dict(active=True)]) + buttons=dict(reference=['BUTTON ns=100 serial=11 button=273 state=1','BUTTON ns=101 serial=12 button=273 state=0']) + raw='wl_pointer#9.button(11, 100, 273, 1)\nwl_pointer#9.button(12, 101, 273, 0)' + oom_handler.availability_certificate(ident,expected,snap,raw,buttons) + for change in ({'pid':999},{'sha256':'c'*64},{'build_id':'c'*40},{'maps':'wrong'},{'captured_before_cleanup':False},{'swaysock':'/run/user/1000/sway.sock'}): + bad=dict(ident,**change);self.rejects(oom_handler.availability_certificate,bad,expected,snap,raw,buttons) + self.rejects(oom_handler.availability_certificate,ident,expected,dict(tree={},outputs=[]),raw,buttons) + self.rejects(oom_handler.availability_certificate,ident,expected,snap,raw,{}) + self.rejects(oom_handler.availability_certificate,ident,expected,snap,raw.replace('273, 0','273, 1'),buttons) + def test_predictions_precede_runtime_captures(self): + # The real acquisition helper calls the production oracle first; a capture + # side-effect changes the geometry to prove it cannot influence prediction. + b,g,o=self.field();expected=fullscreen.expected_h2(b,g,o,'logical',engine.COLORS) + from types import SimpleNamespace + events=[] + def capture(tag): + events.append('capture');g['A'][0]=999;return [b,expected] + session=SimpleNamespace(outputs=plans.H,frames={'HEADLESS-2':'logical'},record=lambda *args,**kwargs:events.append('prediction'),pair_capture=capture) + fullscreen.measured_pair(session,'offline',b,g);self.assertEqual(events,['prediction','capture']) + +class RepairPredicates(unittest.TestCase): + rejects=Scientific.rejects + def test_oom_pixels_both_outputs_and_final(self): + b=Image.new('RGB',(40,40),(48,48,56));clean=[b.copy(),b.copy()] + for failed in ('POPUP_A','POPUP_B'): + oom_handler.popup_pixels(clean,b,failed) + for output in (0,1): + bad=[im.copy() for im in clean];bad[output].putpixel((7,9),engine.COLORS[failed]) + self.rejects(oom_handler.popup_pixels,bad,b,failed) + alive=[b.copy(),b.copy()];alive[0].putpixel((7,9),engine.COLORS['POPUP_A']) + oom_handler.popup_pixels(alive,b,'POPUP_B') + for role in ('POPUP_A','POPUP_B'): + for output in (0,1): + bad=[im.copy() for im in clean];bad[output].putpixel((7,9),engine.COLORS[role]) + self.rejects(oom_handler.popup_pixels,bad,b,final=True) + oom_handler.popup_pixels(clean,b,final=True) + def test_root_input_real_delivery_wrong_wire_owner_point(self): + from types import SimpleNamespace + raw=(KIT/'wire-fixture.txt').read_text() + # Real fixture establishes configured root identity; use its measured rectangle. + proof=oom_handler.mapped_root(raw);x,y,w,h=proof['configure'] + origin=[-x,-y];image=Image.new('RGB',(800,600),engine.COLORS['POPUP_A']) + def exercise(wrong=None): + def click(name,point,action): + self.assertEqual((name,action),('owner','right')) + local=[point[0],point[1]] + if wrong=='point':local[0]+=1 + target='TOPLEVEL' if wrong=='role' else 'POPUP_A' + lines=[f'POINTER_BUTTON target={target} serial={i+10} button=273 state={v}' for i,v in enumerate((1,0))] + tail=f'POINTER_MOTION target=POPUP_A x={local[0]} y={local[1]}\n'+'\n'.join(f'wl_pointer#9.button({i+10}, 100, 273, {v})' for i,v in enumerate((1,0))) + if wrong=='wire':tail=tail.replace('273, 0','273, 1') + buttons={'owner':lines,'reference':lines if wrong=='owner' else []} + return {'owner':tail},buttons + obj=SimpleNamespace(log=lambda name:raw,owner=lambda name:dict(origin=origin),outputs=[dict(scale=1,logical=[0,0,800,600])],frames={'HEADLESS-1':'logical'},click=click) + obj.delivery=lambda *args:engine.Session.delivery(obj,*args) + return oom_handler.surviving_root_input(obj,[image,image]) + self.assertEqual(exercise()['status'],'PARTIALLY_FEASIBLE') + for wrong in ('wire','owner','point','role'):self.rejects(exercise,wrong) + def test_git_actual_tree_inventory_and_modes(self): + import subprocess + top=KIT.parents[3];prefix=Path('tooling/probes/step20c-applied-state-input') + raw=subprocess.run(['git','-C',str(top),'ls-tree','-rz','--full-tree','HEAD','--',str(prefix)],check=True,capture_output=True).stdout + self.assertTrue(raw) + files={};modes={} + for row in raw.split(b'\0')[:-1]: + meta,path=row.split(b'\t',1);rel=path[len(str(prefix).encode())+1:].decode();files[rel]=gate.sha(top/path.decode());modes[rel]='0o755' if meta.startswith(b'100755') else '0o644' + gate.git_inventory(raw,str(prefix),files,modes) + gate.published_sources(top,'HEAD',prefix,files,modes,gate.clean_environment(LAB)) + first=raw.split(b'\0')[0];meta,path=first.split(b'\t',1);mode,kind,oid=meta.split(b' ') + for replacement in (b'100755' if mode==b'100644' else b'100644',b'120000'): + bad=raw.replace(first,replacement+b' '+kind+b' '+oid+b'\t'+path,1) + self.rejects(gate.git_inventory,bad,str(prefix),files,modes) + self.rejects(gate.git_inventory,raw+first.replace(path,path+b'-extra')+b'\0',str(prefix),files,modes) + self.rejects(gate.git_inventory,raw[len(first)+1:],str(prefix),files,modes) + self.rejects(gate.git_inventory,raw,str(prefix)+'-wrong',files,modes) + for invalid in ('0644','0o600','100644',644): + bad=dict(modes);bad[next(iter(bad))]=invalid;self.rejects(gate.git_inventory,raw,str(prefix),files,bad) + def test_source_modes_reject_nonportable_and_symlink(self): + root=Path(tempfile.mkdtemp(dir=os.environ['TMPDIR']));p=root/'file';p.write_text('source');p.chmod(0o644) + self.assertEqual(gate.source_modes(root),{'file':'0o644'}) + p.chmod(0o755);self.assertEqual(gate.source_modes(root),{'file':'0o755'}) + p.chmod(0o600);self.rejects(gate.source_modes,root) + p.unlink();p.symlink_to('/dev/null');self.rejects(gate.source_modes,root) + +class Admission(Scientific): + def setUp(self): + self.root=Path(tempfile.mkdtemp(prefix='gate-',dir=os.environ.get('TMPDIR',str(LAB/'scratch'))));self.ns=self.root/'OFFLINE_TEST_ONLY';self.ns.mkdir(mode=0o700) + self.manifest=self.ns/'manifest.json';self.review=self.ns/'review.json';self.publication=self.ns/'publication.json' + self.files=gate.source_files();self.m=dict(ticket=gate.TICKET,schema='native-closure-source-v1',author='fixture-author',files=self.files,modes=gate.source_modes(),plans=plans.RUNS,limits=plans.LIMITS) + gate.durable(self.manifest,self.m) + elf=self.ns/'fake-offline-ELF';elf.write_bytes(b'OFFLINE_TEST_ONLY never executed') + identity=dict(binary=str(elf),binary_sha256=gate.sha(elf),wlroots='/usr/lib/libwlroots-0.20.so',wlroots_sha256=gate.WL_SHA,build_review='ACCEPTED',source_qualification='ACCEPTED',build_id='offline') + runtime=self.ns/'runtime.json';gate.durable(runtime,dict(ticket=gate.TICKET,identities={name:identity for name in ('pristine','core','oom')})) + # Fake payloads are explicitly OFFLINE; only validation and budget state are + # exercised. No receipt can authorize Session (offline namespace is rejected). + self.build=LAB/'followup-work/client-build';self.generated={name:gate.sha(self.build/name) for name in ('fullscreen-client','nested-client','static-reference','pointer-daemon')} + compile=self.ns/'compile.json';gate.durable(compile,dict(status='PASS',runtime='NOT_RUN',generated=self.generated,inputs={str(p.relative_to(KIT)):gate.sha(p) for folder in ('clients','protocols') for p in (KIT/folder).iterdir()})) + test=self.ns/'tests.json';gate.durable(test,dict(status='PASS',tests_run=1,failures=0,source_digest=gate.digest(self.files))) + case=dict(oom_source_review='ACCEPTED',variant_identity='COREC_V2_NATIVE_OOM_DIAGNOSTIC',source_manifest_sha256=gate.sha(self.manifest),decision='ACCEPTED',client_build_review='ACCEPTED',client_build_receipt=str(compile),client_build_receipt_sha256=gate.sha(compile)) + self.r=dict(ticket=gate.TICKET,decision='ACCEPTED',independent=True,reviewer='offline-independent-fixture',manifest_sha256=gate.sha(self.manifest),runtime_manifest=str(runtime),runtime_manifest_sha256=gate.sha(runtime),runs={rid:case for rid in plans.RUNS},gates={name:dict(decision='ACCEPTED',manifest_sha256=gate.sha(self.manifest),receipt=str(test),receipt_sha256=gate.sha(test)) for name in ('FULLSCREEN_COMPARABILITY_OFFLINE','MIGRATION_REFOCUS_OFFLINE','OOM_ROOT_NESTED_ORACLE_OFFLINE')}) + gate.durable(self.review,self.r);commit='a'*40 + self.p=dict(ticket=gate.TICKET,decision='VERIFIED',offline_test_only=True,source_commit=commit,origin_sha=commit,github_sha=commit,manifest_sha256=gate.sha(self.manifest),source_digest=gate.digest(self.files),remote_observations={name:dict(exit=0,argv=['git','ls-remote',name,'refs/heads/main'],stdout=commit+'\trefs/heads/main\n') for name in ('origin','github')});gate.durable(self.publication,self.p) + self.ledger=gate.Ledger(LAB,self.ns/'budget.json',offline=True);self.ledger.initialize() + def validate(self,rid='OM-ROOT'):return gate.validate_review(LAB,self.manifest,self.review,self.publication,rid,offline=True) + def admit(self,rid='OM-ROOT'):return self.ledger.admit(rid,self.manifest,self.review,self.publication) + def test_real_validator_offline_namespace_and_unique_reservation(self): + self.validate();a=self.admit();self.ledger.action_debit(a,'capture','hook');self.rejects(self.admit) + self.rejects(self.ledger.action_debit,a,'hook');self.ledger.complete(a,dict(status='INCONCLUSIVE'));self.rejects(self.ledger.action_debit,a,'capture') + self.rejects(engine.Session,plans.RUNS['OM-ROOT'],a) + def test_historical_ids_and_tokens(self): + for rid in ('O1','O2','C7','P1','C8','G2'):self.rejects(self.validate,rid) + auth=self.admit();bad=dict(auth,token='historic-token');self.rejects(self.ledger.action_debit,bad,'capture') + def test_self_review_and_wrong_SHA_reject_before_reservation(self): + for mutate in [lambda r:r.update(reviewer='fixture-author'),lambda r:r.update(decision='PASS'),lambda r:r.update(manifest_sha256='0'*64),lambda r:r.update(gates={})]: + bad=copy.deepcopy(self.r);mutate(bad);gate.durable(self.review,bad);self.rejects(self.admit) + self.assertEqual(json.loads(self.ledger.path.read_text())['runs'],[]) + def test_publication_remote_observation_and_clone_bytes_binding(self): + for mutate in [lambda p:p.update(github_sha='b'*40),lambda p:p.update(remote_observations={}),lambda p:p.update(source_digest='0'*64),lambda p:p.update(offline_test_only=False)]: + bad=copy.deepcopy(self.p);mutate(bad);gate.durable(self.publication,bad);self.rejects(self.admit) + def test_stale_source_manifest_and_superficial_gate(self): + self.m['files']['graphics/plans.py']='0'*64;gate.durable(self.manifest,self.m);self.rejects(self.admit) + gate.durable(self.manifest,{**self.m,'files':self.files});self.r['gates']['OOM_ROOT_NESTED_ORACLE_OFFLINE']['decision']='PASS';gate.durable(self.review,self.r);self.rejects(self.admit) + def test_root_dependency_unresolved_cannot_consume_nested(self): + self.rejects(self.admit,'OM-NESTED');root=self.admit();self.ledger.complete(root,dict(status='INCONCLUSIVE',scientific_cleanup=True));self.rejects(self.admit,'OM-NESTED') + self.assertEqual(len(json.loads(self.ledger.path.read_text())['runs']),1) + def test_root_qualified_allows_one_nested(self): + root=self.admit();self.ledger.complete(root,dict(status='PARTIALLY_FEASIBLE',scientific_cleanup=True));self.admit('OM-NESTED');self.rejects(self.admit,'OM-NESTED') + def test_optional_gpu_never_auto_admitted(self): + for rid in ('GPU-G3','GPU-CTRL','OM-EXTRA'):self.rejects(self.admit,rid) + def test_caps_duration_and_failed_reservation_spent(self): + auth=self.admit() + for _ in range(10):self.ledger.action_debit(auth,'capture') + self.rejects(self.ledger.action_debit,auth,'capture') + def expire(d):d['runs'][0]['started_ns']=time.monotonic_ns()-151_000_000_000;return None,d + self.ledger.transaction(expire);self.rejects(self.ledger.action_debit,auth,'move');self.rejects(self.admit) + def test_mode_symlink_hardlink_lock_attacks(self): + self.ledger.path.chmod(0o644);self.rejects(self.admit);self.ledger.path.chmod(0o600) + alias=self.ns/'hardlink';os.link(self.ledger.path,alias);self.rejects(self.admit);alias.unlink() + lock=self.ledger.lock;lock.unlink();lock.symlink_to(self.manifest) + with self.assertRaises(OSError):self.admit() + def test_clean_environment_no_daily_authority(self): + with patch.dict(os.environ,dict(SWAYSOCK='/run/user/daily',WAYLAND_DISPLAY='daily',DISPLAY=':0',LD_PRELOAD='evil',LD_LIBRARY_PATH='daily',WLR_RENDERER='daily',SMX_OOM_NONCE='daily')): + e=gate.clean_environment(LAB) + self.assertFalse(set(e)&{'SWAYSOCK','WAYLAND_DISPLAY','DISPLAY','LD_PRELOAD','LD_LIBRARY_PATH','WLR_RENDERER','SMX_OOM_NONCE'}) + self.assertTrue(all(Path(e[k]).is_relative_to(LAB) for k in ('TMPDIR','TMP','TEMP','XDG_CACHE_HOME','XDG_DATA_HOME','PYTHONPYCACHEPREFIX','CCACHE_DIR'))) + + def test_concurrent_reservation_single_winner(self): + from concurrent.futures import ThreadPoolExecutor + def attempt(_): + try:self.admit();return 'reserved' + except ValueError:return 'refused' + with ThreadPoolExecutor(max_workers=2) as pool:out=list(pool.map(attempt,range(2))) + self.assertEqual(sorted(out),['refused','reserved']);self.assertEqual(len(json.loads(self.ledger.path.read_text())['runs']),1) + def test_source_addition_and_mutation_after_freeze(self): + import shutil + source=self.ns/'portable-source';shutil.copytree(gate.SOURCE_ROOT,source) + with patch.object(gate,'SOURCE_ROOT',source): + self.validate();(source/'unexpected.py').write_text('unreviewed=True\n');self.rejects(self.admit) + (source/'unexpected.py').unlink();(source/'graphics/plans.py').chmod(0o755);self.rejects(self.admit);(source/'graphics/plans.py').chmod(0o644);(source/'graphics/plans.py').write_text('# mutated\n');self.rejects(self.admit) + def test_real_watchdog_independently_kills_owned_child(self): + import signal,subprocess,threading + from types import SimpleNamespace + child=subprocess.Popen(['prlimit','--core=0:0','--',sys.executable,'-B','-c','import signal,time; signal.signal(signal.SIGTERM,signal.SIG_IGN); print("READY",flush=True); time.sleep(20)'],stdout=subprocess.PIPE,stderr=subprocess.PIPE,text=True,start_new_session=True,env=gate.clean_environment(LAB)) + self.assertEqual(child.stdout.readline().strip(),'READY') + obj=SimpleNamespace(finished=threading.Event(),started=time.monotonic()-142.9,run=self.ns,proc_lock=threading.RLock(),expired=False,proc={'offline-owned-child':child},transient={}) + obj.signal_owned=lambda sig:engine.Session.signal_owned(obj,sig) + thread=threading.Thread(target=engine.Session.watchdog,args=(obj,));thread.start() + try:self.assertEqual(child.wait(timeout=4),-signal.SIGKILL);thread.join(timeout=1);self.assertTrue(obj.expired);self.assertFalse(thread.is_alive()) + finally: + if child.poll() is None:os.killpg(child.pid,signal.SIGKILL);child.wait(timeout=1) + obj.finished.set();thread.join(timeout=1);child.stdout.close();child.stderr.close() + + def test_GPU_environment_and_same_identity_adapter(self): + import gpu_handler + from types import SimpleNamespace + calls=[];session=SimpleNamespace(lab=LAB,spec=plans.RUNS['GPU-G3'],env=gate.clean_environment(LAB),debit=lambda *args:calls.append(args)) + adapter=gpu_handler.AdapterSession(session);self.assertEqual(adapter.spec['binary'],'gpu') + adapter.env.update(MOSAIC_G3_DIR=str(LAB/'gpu-work/acquisition'),MOSAIC_G3_NONCE='a'*64,WLR_RENDERER='gles2') + self.assertNotIn('MOSAIC_G3_DIR',session.env);self.assertEqual(session.compositor_environment['MOSAIC_G3_NONCE'],'a'*64) + adapter.debit('mutation');self.assertEqual(calls,[('mutation','hook')]);self.rejects(adapter.debit,'hook') + self.rejects(adapter.env.update,MOSAIC_G3_DIR='/run/user/daily',MOSAIC_G3_NONCE='a'*64,WLR_RENDERER='gles2') + session.spec=dict(session.spec,binary='core');self.rejects(gpu_handler.AdapterSession,session) + def test_GPU_gate_requires_independent_B4(self): + self.rejects(self.admit,'GPU-G3');self.assertEqual(json.loads(self.ledger.path.read_text())['runs'],[]) + self.r['gates']['B4']=dict(status='PASS');gate.durable(self.review,self.r);self.rejects(self.admit,'GPU-G3') + def test_GPU_binding_rejects_empty_or_unreviewed(self): + import gpu_handler + for case in ({},{'gpu_source_review':'ACCEPTED'},{'gpu_source_review':'ACCEPTED','gl_observability_preflight':'ACCEPTED','build_review':'ACCEPTED','native_gl_status':'FEASIBLE'}): + self.rejects(gpu_handler.validate_binding,LAB,case,{},gate.sha(self.manifest)) + def test_GPU_module_origin_attack(self): + import gpu_handler + from types import SimpleNamespace + from contextlib import ExitStack + with ExitStack() as stack: + for name in ('protocol','scenario','validate_source'):stack.enter_context(patch.dict(sys.modules,{name:SimpleNamespace(__file__='/foreign/'+name+'.py')})) + self.rejects(gpu_handler.modules,LAB) + +if __name__=='__main__':unittest.main(verbosity=2) diff --git a/tooling/probes/step20c-corec-v2-followup/graphics/oom.py b/tooling/probes/step20c-corec-v2-followup/graphics/oom.py new file mode 100644 index 0000000..93f85b7 --- /dev/null +++ b/tooling/probes/step20c-corec-v2-followup/graphics/oom.py @@ -0,0 +1,59 @@ +"""Private source-only adapter to the independently reviewed graphics-kit Session. +No native run entry point. O1/O2 remain NOT_ADMITTED until separate review. +""" +import os,re +from pathlib import Path +PLANS={ + 'OM-ROOT':dict(status='NOT_ADMITTED',role='root_popup',max_captures=8,max_injections=1,requires_rootA_stable=False), + 'OM-NESTED':dict(status='NOT_ADMITTED',role='nested_popup',max_captures=8,max_injections=1,requires_rootA_stable=True), +} +CAPTURES=('reference-H2-a','reference-H2-b','pre-H1','pre-H2','post-H1','post-H2','final-H1','final-H2') +def process_environment(lab,run,nonce): + assert run in PLANS and re.fullmatch('[0-9a-f]{32}',nonce) + lab=Path(lab);assert lab.is_absolute() and lab.resolve()==lab and lab.stat().st_uid==os.getuid() + # Parent must use this update solely in Session's compositor spawn environment. + # Do not put it into Session.env inherited by scientific clients/helpers. + path=lab/'graphics-runs'/run/'run'/'oom-token' + assert not path.exists() and not path.is_symlink() + return dict(SMX_OOM_TOKEN_PATH=str(path),SMX_OOM_NONCE=nonce,SMX_OOM_ROLE=PLANS[run]['role']) +def arm(session,env,admission,stable): + run=session.spec['id'];assert run in PLANS + # Separate reviewed admission must bind variant binary + OOM source freeze, + # graphics engine freeze, independent R-OOM, build review and publication. + assert admission==session.auth['prospective'] + assert admission['decision']=='ACCEPTED' and admission['source_manifest_sha256']==session.auth['manifest_sha256'] + assert admission.get('oom_source_review')=='ACCEPTED' and session.auth['build_review']=='ACCEPTED' + assert session.auth['publication_sha256'] and admission['variant_identity']=='COREC_V2_NATIVE_OOM_DIAGNOSTIC' + assert stable['startup'] and stable['owner'] and stable['neighbor'] and stable['reference_H2_twice'] + if PLANS[run]['requires_rootA_stable']:assert stable['rootA'] and stable['rootA_native_success'] + assert env['SMX_OOM_ROLE']==PLANS[run]['role'] + path=Path(env['SMX_OOM_TOKEN_PATH']);assert path.parent.resolve()==session.run.resolve() + assert path.name=='oom-token' and not path.is_symlink() + # The parent's reviewed OOM ledger extension must provide hook cap=1. Debit + # precedes O_EXCL write; failed arm stays spent, with no retry/refund. + session.debit('hook') + fd=os.open(path,os.O_CREAT|os.O_EXCL|os.O_WRONLY|os.O_NOFOLLOW,0o600) + try: + os.fchmod(fd,0o600) + token=f"SMXOOM1 {env['SMX_OOM_NONCE']} {env['SMX_OOM_ROLE']}\n".encode('ascii') + assert os.write(fd,token)==len(token);os.fsync(fd) + finally:os.close(fd) + session.record('oom-arm',role=env['SMX_OOM_ROLE'],nonce=env['SMX_OOM_NONCE'],path=str(path)) +def native_failure(log,env,pid): + lines=[line for line in log.splitlines() if 'SMX_OOM ENOMEM ' in line] + assert len(lines)==1 + fields=dict(re.findall(r'([a-z_]+)=([^ ]+)',lines[0])) + for key,value in dict(role=env['SMX_OOM_ROLE'],nonce=env['SMX_OOM_NONCE'],pid=str(pid),matched='1',failures='1',initialized='0',listeners='0',published='0').items():assert fields.get(key)==value,(key,fields) + assert all(fields.get(k) not in (None,'(nil)','0x0') for k in ['xdg','xdg_parent','parent','tree']) + assert fields.get('successes')==('0' if env['SMX_OOM_ROLE']=='root_popup' else '1') + assert re.fullmatch(r'\d+',fields.get('children','')) + # WHY: native children counts existing PARENT children, not the failed tree. + # CONTRACT: preserve it without demanding impossible nested parent_children0. + # INVARIANT: NULL result must bind same role/PID/xdg/parent, unpublished node. + results=[line for line in log.splitlines() if 'SMX_OOM result ' in line and 'role='+env['SMX_OOM_ROLE']+' ' in line] + assert len(results)==1,'unique targeted NULL caller result' + result=dict(re.findall(r'([a-z_]+)=([^ ]+)',results[0])) + for key in ('role','pid','xdg','xdg_parent','parent','successes','failures'):assert result.get(key)==fields[key] + assert result.get('tree') in ('(nil)','0x0'),'failed tree returned non-NULL' + fields.update(parent_children=fields['children'],children_scope='existing parent children, not failed tree',caller_result=result) + return fields diff --git a/tooling/probes/step20c-corec-v2-followup/graphics/oom_handler.py b/tooling/probes/step20c-corec-v2-followup/graphics/oom_handler.py new file mode 100644 index 0000000..256b0ac --- /dev/null +++ b/tooling/probes/step20c-corec-v2-followup/graphics/oom_handler.py @@ -0,0 +1,173 @@ +"""EXPERIMENTAL prospective O1/O2 handler. No allocator or admission policy.""" +import errno,re,time,subprocess +from pathlib import Path +import cases,oom +from gate import sha,need +from geometry import solid_point,exact,count +from engine import COLORS + +def opening_before_map(raw,popup,parent): + # WHY: native no-memory can precede configure/MAP. CONTRACT/INVARIANT: + # validate actual serial/grab/parent wire, never await the failed popup MAP. + rows=re.findall(rf'{popup} CREATE wl=(\d+) xdg=(\d+) role=(\d+) parent={parent} parent_xdg=(\d+)',raw) + need(len(rows)==1,'unique failed popup identity');wl,xdg,role,parent_xdg=map(int,rows[0]) + parent_ids=re.findall(rf'{parent} CREATE wl=(\d+) xdg=(\d+) role=(\d+)',raw) + need(len(parent_ids)==1 and int(parent_ids[0][1])==parent_xdg,'failed popup exact parent') + grabs=re.findall(rf'{popup} GRAB_REQUEST origin=wl_pointer.button target={parent} serial=(\d+) used=(\d+)',raw) + need(len(grabs)==1 and grabs[0][0]==grabs[0][1] and int(grabs[0][0])>0,'failed popup real grab') + serial=int(grabs[0][0]);button=raw.index(f'POINTER_BUTTON target={parent} serial={serial} button=272 state=1') + wire=re.search(rf'wl_pointer#\d+\.button\({serial}, \d+, 272, 1\)',raw) + create=re.search(rf'xdg_surface#{xdg}\.get_popup\(new id xdg_popup#{role}, xdg_surface#{parent_xdg}, xdg_positioner#\d+\)',raw) + grab=re.search(rf'xdg_popup#{role}\.grab\(wl_seat#\d+, {serial}\)',raw) + need(wire is not None and create is not None and grab is not None and wire.start()=6 and line.split()[-1].startswith('/') and 'libwlroots' in line.split()[-1]} + need(paths=={s.auth['wlroots']} and sha(s.auth['wlroots'])==s.auth['wlroots_sha256'],'critical mapped wlroots changed') + build=s.command_process(['readelf','-n',str(exe)]) + ids=re.findall(r'Build ID: ([0-9a-f]+)',build.stdout) + need(build.returncode==0 and len(ids)==1 and ids[0]==s.auth['build_id'],'actual availability BuildID mismatch') + need(not any('libscenefx' in line for line in maps.splitlines()),'unexpected dynamic SceneFX in OOM binary') + return dict(pid=s.comp.pid,exe=str(exe.resolve()),sha256=sha(exe),build_id=ids[0],wlroots=list(paths),maps=maps,scenefx_link='static',swaysock=str(s.sock),captured_before_cleanup=True) + +def availability_certificate(identity,expected,snapshot,neighbor_raw,buttons): + # WHY: a final exit0 is not a post-injection live availability observation. + # CONTRACT: exact surviving PID/exe/BuildID/maps + private IPC + fresh wire + # press/release, saved before cleanup. INVARIANT: no superficial PASS receipt. + need(identity.get('captured_before_cleanup') is True and identity['pid']==expected['pid'],'availability PID/time scope') + need(identity['sha256']==expected['binary_sha256'] and identity['build_id']==expected['build_id'] and identity['exe']==expected['binary'],'availability ELF identity') + need(identity['wlroots']==[expected['wlroots']] and expected['wlroots'] in identity['maps'] and 'libscenefx' not in identity['maps'],'availability mappings') + sock=Path(identity['swaysock']);need(sock.parent.name.startswith('smx-') and sock.parent.parent in (Path('/tmp'),Path('/var/tmp')),'daily/foreign socket') + need(snapshot['tree'].get('type')=='root' and snapshot['tree'].get('nodes') and len([o for o in snapshot['outputs'] if o.get('active')])==2,'live IPC tree/outputs absent') + lines=buttons.get('reference',[]);events=[re.search(r'BUTTON ns=\d+ serial=(\d+) button=(\d+) state=(\d+)',line) for line in lines] + need(len(events)==2 and all(events) and [list(map(int,e.groups()[1:])) for e in events]==[[273,1],[273,0]] and int(events[0][1])0 and count(pre[1],COLORS['POPUP_A'])==0,'nested precondition rootA sampled clip absent') + s.save('oom-rootA-before-injection',dict(native_success=root_success,wire=proof,painted_H1=True,H2='EXACT',clip_scope='sampled; native generation NOT_PROVEN')) + target='TOPLEVEL' if s.spec['id']=='OM-ROOT' else 'POPUP_A';popup='POPUP_A' if target=='TOPLEVEL' else 'POPUP_B' + xy=solid_point(pre[0],COLORS[target],s.outputs[0],s.frames['HEADLESS-1']) + stable=dict(startup=True,owner=True,neighbor=s.find('reference') is not None,reference_H2_twice=True,rootA=root_success is not None,rootA_native_success=root_success is not None) + oom.arm(s,s.oom_env,s.auth['prospective'],stable) + tails,buttons=s.click('owner',xy,'left',True) + s.wait(lambda:'SMX_OOM ENOMEM ' in s.log('compositor')) + wire=opening_before_map(s.log('owner'),popup,target) + native=oom.native_failure(s.log('compositor'),s.oom_env,s.comp.pid) + results=[line for line in s.log('compositor').splitlines() if 'SMX_OOM result ' in line and 'role='+s.oom_env['SMX_OOM_ROLE']+' ' in line] + need(len(results)==1 and 'tree=(nil)' in results[0] and 'failures=1' in results[0],'native NULL caller outcome absent') + ident=availability(s);s.save('oom-availability-identity',ident);after=s.snapshot('oom-after');s.save('oom-availability-ipc',dict(identity=ident,snapshot=after,captured_before_cleanup=True)) + need(popup+' MAP ' not in s.log('owner'),'failed popup unexpectedly mapped') + owner_raw_exit=s.proc['owner'].poll();disconnected=None + if owner_raw_exit is None: + time.sleep(.15);owner_raw_exit=s.proc['owner'].poll() + if owner_raw_exit is not None:disconnected=retire_enomem(s) + post=s.pair_capture('oom-post');post_pixels=popup_pixels(post,baseline,popup) + old_root=dict(status='NOT_EXERCISED',reason='O1 has no old rootA' if root_success is None else ('EXPECTED_ENOMEM_DISCONNECT' if disconnected else 'ROOT_A_DISMISSED'),raw=s.log('owner')) + if root_success is not None and disconnected is None and 'POPUP_A DESTROY ' not in s.log('owner') and 'POPUP_A POPUP_DONE ' not in s.log('owner'): + need(count(post[0],COLORS['POPUP_A'])>0 and count(post[1],COLORS['POPUP_A'])==0,'surviving rootA sampled clipping') + old_root=surviving_root_input(s,post);old_root['native_success']=root_success + s.save('oom-rootA-post-input',old_root) + neighbor=s.find('reference');need(neighbor is not None,'surviving neighbor absent') + xy=solid_point(post[1],(48,48,56),s.outputs[1],s.frames['HEADLESS-2']) + tails,buttons=s.click('reference',xy) + # WHY/CONTRACT/INVARIANT: a surviving real popup grab may consume the + # outside click to dismiss; only a measured dismissal permits a fresh + # neighbor click. This does not claim universal click-through. + if not buttons.get('reference'): + need('POPUP_DONE ' in tails.get('owner',''),'neighbor input missing without measured outside dismissal') + s.save('oom-neighbor-outside-dismissal',dict(tails=tails,buttons=buttons)) + tails,buttons=s.click('reference',xy) + lines=buttons.get('reference',[]) + events=[re.search(r'BUTTON ns=\d+ serial=(\d+) button=(\d+) state=(\d+)',line) for line in lines] + need(len(events)==2 and all(events) and [list(map(int,e.groups()[1:])) for e in events]==[[273,1],[273,0]],'fresh neighbor button pair') + for e in events:need(re.search(rf'wl_pointer#\d+\.button\({e[1]}, \d+, 273, {e[3]}\)',tails['reference']) is not None,'neighbor real wire') + need(not any(v for k,v in buttons.items() if k!='reference'),'neighbor button not exclusive') + live=availability_certificate(ident,dict(s.auth,pid=s.comp.pid),after,tails['reference'],buttons) + s.save('oom-availability-live',live) + if disconnected is None:s.close_client('owner',scenario=s.closure_scenarios['owner']) + final=s.pair_capture('oom-final');final_pixels=popup_pixels(final,baseline,final=True) + s.save('oom-measurements',dict(post_pixels=post_pixels,final_pixels=final_pixels,native=native,wire=wire,rootA_success=root_success,old_rootA=old_root,before=before,after=after,availability=ident,owner_raw_exit=owner_raw_exit,owner_disconnect=disconnected,neighbor_buttons=buttons,H2_pre_post_final='EXACT',captures=8,closure_scenario=s.closure_scenarios['owner'],complete_damage='NOT_PROVEN',native_stale_listeners='NOT_PROVEN',native_no_partial_popup='NOT_PROVEN; IPC has no popup-node schema',atomic_generation='NOT_PROVEN')) + return 'PARTIALLY_FEASIBLE' diff --git a/tooling/probes/step20c-corec-v2-followup/graphics/pixels.py b/tooling/probes/step20c-corec-v2-followup/graphics/pixels.py new file mode 100644 index 0000000..e5660b6 --- /dev/null +++ b/tooling/probes/step20c-corec-v2-followup/graphics/pixels.py @@ -0,0 +1,21 @@ +"""Exact binary RGB masks with explicit output origin, clip and occlusion. + +Screenshots prove color/visible geometry only, never native-buffer identity. +""" +from PIL import Image,ImageChops,ImageDraw +from model import * +COLORS=dict(TOPLEVEL=(35,68,204),POPUP_A=(224,192,32),POPUP_B=(32,208,160)) + + +def color_geometry(im,target,rect,output,clip,occluders=()): + require(im.size==(output.rectangle.width,output.rectangle.height),'replay-output-size') + visible=intersect(rect,clip) if clip else rect + expected=Image.new('L',im.size,0);draw=ImageDraw.Draw(expected) + box=to_png(visible,output) + if box.width and box.height:draw.rectangle((box.x,box.y,box.end[0]-1,box.end[1]-1),fill=255) + for occ in occluders: + ob=to_png(occ,output) + if ob.width and ob.height:draw.rectangle((ob.x,ob.y,ob.end[0]-1,ob.end[1]-1),fill=0) + actual=Image.new('L',im.size);actual.putdata([255 if px==COLORS[target] else 0 for px in im.get_flattened_data()]) + require(ImageChops.difference(expected,actual).getbbox() is None,'replay-paint-'+target) + return dict(count=sum(actual.histogram()[1:]),bbox=actual.getbbox()) diff --git a/tooling/probes/step20c-corec-v2-followup/graphics/plans.py b/tooling/probes/step20c-corec-v2-followup/graphics/plans.py new file mode 100644 index 0000000..f43c75f --- /dev/null +++ b/tooling/probes/step20c-corec-v2-followup/graphics/plans.py @@ -0,0 +1,17 @@ +"""EXPERIMENTAL new-ticket intent; no plan grants admission by itself.""" +from copy import deepcopy +CAPS={'FS-P':12,'FS-C':16,'DY-C':16,'OM-ROOT':10,'OM-NESTED':10,'GPU-G3':12,'OM-EXTRA':12,'GPU-CTRL':12} +LIMITS=dict(runs=8,captures=110,captures_per_run=18,duration_s=150) +H=[dict(name='HEADLESS-1',logical=[-800,0,800,600],mode=[800,600],scale=1,transform='normal'),dict(name='HEADLESS-2',logical=[0,0,800,600],mode=[800,600],scale=1,transform='normal')] +def config(spec): + return '\n'.join(f"output {o['name']} resolution {o['mode'][0]}x{o['mode'][1]} scale {o['scale']} transform {o['transform']} position {o['logical'][0]} {o['logical'][1]}" for o in spec['outputs'])+'\nworkspace 1 output HEADLESS-1\nworkspace 2 output HEADLESS-2\nfocus_follows_mouse no\nseat seat0 hide_cursor 100\nshadows disable\nblur disable\n' +RUNS={rid:dict(id=rid,binary='pristine' if rid=='FS-P' else 'core',mode='pristine' if rid=='FS-P' else 'mosaic',outputs=deepcopy(H),client='nested-client',placement='static_cross',axis=0,boundary=0,capture_limit=cap,action_limits=dict(spawn=12,tool=160,configuration=80,mutation=26,move=24,click=12,grab=4,camera=6,chain=2,source_marker=1),admission='INDEPENDENT_REVIEW_REQUIRED') for rid,cap in CAPS.items()} +for rid in ('FS-P','FS-C'):RUNS[rid].update(client='fullscreen-client',placement='fullscreen_cross') +for rid in ('OM-ROOT','OM-NESTED','OM-EXTRA'): + RUNS[rid]['binary']='oom';RUNS[rid]['action_limits']['hook']=1 +for rid in ('GPU-G3','GPU-CTRL'): + RUNS[rid].update(binary='gpu',admission='NOT_ADMITTED',reason='B4 and separately reviewed GPU adapter required') + RUNS[rid]['action_limits']['hook']=3 +RUNS['OM-EXTRA'].update(admission='NOT_ADMITTED',reason='Distinct question, handler and additional independent review required') +for spec in RUNS.values():spec['config']=config(spec) +assert len(RUNS)==8 and sum(CAPS.values())==100 and max(CAPS.values())<=18 diff --git a/tooling/probes/step20c-corec-v2-followup/graphics/predicates.py b/tooling/probes/step20c-corec-v2-followup/graphics/predicates.py new file mode 100644 index 0000000..50c85c9 --- /dev/null +++ b/tooling/probes/step20c-corec-v2-followup/graphics/predicates.py @@ -0,0 +1,124 @@ +"""Shared offline/live predicates: facts precede scientific classification. + +CONTRACT: full RGB comparisons have no mask/tolerance; grabs require real logs. +INVARIANT: dismissal, client destruction, clipping and layout remain distinct. +""" +import re +from model import Rect,Space,Provenance,Output,popup,crossing,local_clip_to_global,require +from pixels import color_geometry,COLORS +from PIL import Image,ImageChops + +class Defect(RuntimeError): + """A directly measured scientific contract failure, distinct from fixture ambiguity.""" + +def paint(im,target): + im=im.convert('RGB');assert im.size==(800,600) + mask=Image.new('L',im.size);mask.putdata([255 if p==COLORS[target] else 0 for p in im.get_flattened_data()]) + return dict(count=sum(mask.histogram()[1:]),bbox=mask.getbbox()) + +def point(im,target,origin): + box=paint(im,target)['bbox'];assert box,('no measured paint',target) + # WHY: A may be occluded by B; bbox center alone is not a valid hit point. + x1,y1,x2,y2=box + for y in range(y1+10,y2-10,5): + for x in range(x1+10,x2-10,5): + if all(im.getpixel((xx,yy))==COLORS[target] for yy in range(y-8,y+9) for xx in range(x-8,x+9)): + return [x+origin,y] + raise AssertionError('no solid measured interior '+target) + +def equal(a,b): + assert a.size==b.size==(800,600) + rows=[(i%800,i//800,x,y) for i,(x,y) in enumerate(zip(a.get_flattened_data(),b.get_flattened_data())) if x!=y] + return dict(equal=not rows,changed=len(rows),bbox=ImageChops.difference(a,b).getbbox(),pixels=rows) + +def reference(a,b): + result=equal(a,b) + assert result['equal'] and all(c==(48,48,56) for c in a.get_flattened_data()),'unstable reference' + return result + +def configured(text,name): + rows=re.findall(rf'{name} ROLE_CONFIGURE x=(-?\d+) y=(-?\d+) w=(\d+) h=(\d+)',text) + assert rows,(name,'no configure') + return list(map(int,rows[-1])) + +def geometry(text,origin,provenance=None,axis=0,boundary=0): + a=configured(text,'POPUP_A');b=configured(text,'POPUP_B') + require(provenance is not None,'popup-owner-provenance-required') + p=provenance + top=Rect(Space.GLOBAL,*origin,0,0,p) + ids={n:list(map(int,re.search(rf'{n} CREATE wl=(\d+) xdg=(\d+) role=(\d+)',text).groups())) for n in ('TOPLEVEL','POPUP_A','POPUP_B')} + result={} + parent=top + for key,name,parent_name,c in [('A','POPUP_A','TOPLEVEL',a),('B','POPUP_B','POPUP_A',b)]: + parent_id=int(re.search(rf'{name} CREATE .*parent_xdg=(\d+)',text)[1]) + ack=int(re.findall(rf'{name} XDG_CONFIGURE serial=(\d+) ack',text)[-1]) + parent=popup(parent,Rect(Space.POPUP,*c,p),parent_id,ids[parent_name][1],ack,name+' MAP ' in text) + result[key]=parent.values() + result.update(configured_A=a,configured_B=b,origin=origin,provenance=dict(owner=p.owner,workspace=p.workspace,generation=p.generation,evidence=p.evidence,state=p.state), + crossing_axis=axis,crossing_boundary=boundary,crossing={key:(result[key][axis]=2 + assert request.start() POPUP_A -> POPUP_B',status='PASS') + +def pair(lines,target): + events=[] + for line in lines: + m=re.fullmatch(r'POINTER_BUTTON target=(\w+) serial=(\d+) button=(\d+) state=(\d+)',line);assert m + events.append([m[1],*map(int,m.groups()[1:])]) + if len(events)!=2 or any(e[0]!=target for e in events) or [e[2:] for e in events]!=[[273,1],[273,0]]: + raise Defect('wrong actual button recipient/pair: '+str(events)) + assert 0 + + + Copyright © 2019 Josef Gajdusek + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice (including the next + paragraph) shall be included in all copies or substantial portions of the + Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + + + + + This protocol allows clients to emulate a physical pointer device. The + requests are mostly mirror opposites of those specified in wl_pointer. + + + + + + + + + + The pointer has moved by a relative amount to the previous request. + + Values are in the global compositor space. + + + + + + + + + The pointer has moved in an absolute coordinate frame. + + Value of x can range from 0 to x_extent, value of y can range from 0 + to y_extent. + + + + + + + + + + + A button was pressed or released. + + + + + + + + + Scroll and other axis requests. + + + + + + + + + Indicates the set of events that logically belong together. + + + + + + Source information for scroll and other axis. + + + + + + + Stop notification for scroll and other axes. + + + + + + + + Discrete step information for scroll and other axes. + + This event allows the client to extend data normally sent using the axis + event with discrete value. + + + + + + + + + + + + + + + This object allows clients to create individual virtual pointer objects. + + + + + Creates a new virtual pointer. The optional seat is a suggestion to the + compositor. + + + + + + + + + + + + + Creates a new virtual pointer. The seat and the output arguments are + optional. If the seat argument is set, the compositor should assign the + input device to the requested seat. If the output argument is set, the + compositor should map the input device to the requested output. + + + + + + + diff --git a/tooling/probes/step20c-corec-v2-followup/graphics/protocols/xdg-shell.xml b/tooling/probes/step20c-corec-v2-followup/graphics/protocols/xdg-shell.xml new file mode 100644 index 0000000..f8b78ee --- /dev/null +++ b/tooling/probes/step20c-corec-v2-followup/graphics/protocols/xdg-shell.xml @@ -0,0 +1,1420 @@ + + + + + Copyright © 2008-2013 Kristian Høgsberg + Copyright © 2013 Rafael Antognolli + Copyright © 2013 Jasper St. Pierre + Copyright © 2010-2013 Intel Corporation + Copyright © 2015-2017 Samsung Electronics Co., Ltd + Copyright © 2015-2017 Red Hat Inc. + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice (including the next + paragraph) shall be included in all copies or substantial portions of the + Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + + + + + The xdg_wm_base interface is exposed as a global object enabling clients + to turn their wl_surfaces into windows in a desktop environment. It + defines the basic functionality needed for clients and the compositor to + create windows that can be dragged, resized, maximized, etc, as well as + creating transient windows such as popup menus. + + + + + + + + + + + + + + + Destroy this xdg_wm_base object. + + Destroying a bound xdg_wm_base object while there are surfaces + still alive created by this xdg_wm_base object instance is illegal + and will result in a defunct_surfaces error. + + + + + + Create a positioner object. A positioner object is used to position + surfaces relative to some parent surface. See the interface description + and xdg_surface.get_popup for details. + + + + + + + This creates an xdg_surface for the given surface. An xdg_surface is + used as basis to define a role to a given surface, such as xdg_toplevel + or xdg_popup. It also manages functionality shared between xdg_surface + based surface roles. + + While xdg_surface itself is not a role, the corresponding surface may + only be assigned a role extending xdg_surface, such as xdg_toplevel or + xdg_popup. It is illegal to create an xdg_surface for a wl_surface which + already has anassigned role and this will result in a role error. + + See the documentation of xdg_surface for more details about what an + xdg_surface is and how it is used. + + + + + + + + A client must respond to a ping event with a pong request or + the client may be deemed unresponsive. See xdg_wm_base.ping + and xdg_wm_base.error.unresponsive. + + + + + + + The ping event asks the client if it's still alive. Pass the + serial specified in the event back to the compositor by sending + a "pong" request back with the specified serial. See xdg_wm_base.pong. + + Compositors can use this to determine if the client is still + alive. It's unspecified what will happen if the client doesn't + respond to the ping request, or in what timeframe. Clients should + try to respond in a reasonable amount of time. The “unresponsive” + error is provided for compositors that wish to disconnect unresponsive + clients. + + A compositor is free to ping in any way it wants, but a client must + always respond to any xdg_wm_base object it created. + + + + + + + + The xdg_positioner provides a collection of rules for the placement of a + child surface relative to a parent surface. Rules can be defined to ensure + the child surface remains within the visible area's borders, and to + specify how the child surface changes its position, such as sliding along + an axis, or flipping around a rectangle. These positioner-created rules are + constrained by the requirement that a child surface must intersect with or + be at least partially adjacent to its parent surface. + + See the various requests for details about possible rules. + + At the time of the request, the compositor makes a copy of the rules + specified by the xdg_positioner. Thus, after the request is complete the + xdg_positioner object can be destroyed or reused; further changes to the + object will have no effect on previous usages. + + For an xdg_positioner object to be considered complete, it must have a + non-zero size set by set_size, and a non-zero anchor rectangle set by + set_anchor_rect. Passing an incomplete xdg_positioner object when + positioning a surface raises an invalid_positioner error. + + + + + + + + + Notify the compositor that the xdg_positioner will no longer be used. + + + + + + Set the size of the surface that is to be positioned with the positioner + object. The size is in surface-local coordinates and corresponds to the + window geometry. See xdg_surface.set_window_geometry. + + If a zero or negative size is set the invalid_input error is raised. + + + + + + + + Specify the anchor rectangle within the parent surface that the child + surface will be placed relative to. The rectangle is relative to the + window geometry as defined by xdg_surface.set_window_geometry of the + parent surface. + + When the xdg_positioner object is used to position a child surface, the + anchor rectangle may not extend outside the window geometry of the + positioned child's parent surface. + + If a negative size is set the invalid_input error is raised. + + + + + + + + + + + + + + + + + + + + + + Defines the anchor point for the anchor rectangle. The specified anchor + is used to derive an anchor point that the child surface will be + positioned relative to. If a corner anchor is set (e.g. 'top_left' or + 'bottom_right'), the anchor point will be at the specified corner; + otherwise, the derived anchor point will be centered on the specified + edge, or in the center of the anchor rectangle if no edge is specified. + + + + + + + + + + + + + + + + + + + Defines in what direction a surface should be positioned, relative to + the anchor point of the parent surface. If a corner gravity is + specified (e.g. 'bottom_right' or 'top_left'), then the child surface + will be placed towards the specified gravity; otherwise, the child + surface will be centered over the anchor point on any axis that had no + gravity specified. If the gravity is not in the ‘gravity’ enum, an + invalid_input error is raised. + + + + + + + The constraint adjustment value define ways the compositor will adjust + the position of the surface, if the unadjusted position would result + in the surface being partly constrained. + + Whether a surface is considered 'constrained' is left to the compositor + to determine. For example, the surface may be partly outside the + compositor's defined 'work area', thus necessitating the child surface's + position be adjusted until it is entirely inside the work area. + + The adjustments can be combined, according to a defined precedence: 1) + Flip, 2) Slide, 3) Resize. + + + + Don't alter the surface position even if it is constrained on some + axis, for example partially outside the edge of an output. + + + + + Slide the surface along the x axis until it is no longer constrained. + + First try to slide towards the direction of the gravity on the x axis + until either the edge in the opposite direction of the gravity is + unconstrained or the edge in the direction of the gravity is + constrained. + + Then try to slide towards the opposite direction of the gravity on the + x axis until either the edge in the direction of the gravity is + unconstrained or the edge in the opposite direction of the gravity is + constrained. + + + + + Slide the surface along the y axis until it is no longer constrained. + + First try to slide towards the direction of the gravity on the y axis + until either the edge in the opposite direction of the gravity is + unconstrained or the edge in the direction of the gravity is + constrained. + + Then try to slide towards the opposite direction of the gravity on the + y axis until either the edge in the direction of the gravity is + unconstrained or the edge in the opposite direction of the gravity is + constrained. + + + + + Invert the anchor and gravity on the x axis if the surface is + constrained on the x axis. For example, if the left edge of the + surface is constrained, the gravity is 'left' and the anchor is + 'left', change the gravity to 'right' and the anchor to 'right'. + + If the adjusted position also ends up being constrained, the resulting + position of the flip_x adjustment will be the one before the + adjustment. + + + + + Invert the anchor and gravity on the y axis if the surface is + constrained on the y axis. For example, if the bottom edge of the + surface is constrained, the gravity is 'bottom' and the anchor is + 'bottom', change the gravity to 'top' and the anchor to 'top'. + + The adjusted position is calculated given the original anchor + rectangle and offset, but with the new flipped anchor and gravity + values. + + If the adjusted position also ends up being constrained, the resulting + position of the flip_y adjustment will be the one before the + adjustment. + + + + + Resize the surface horizontally so that it is completely + unconstrained. + + + + + Resize the surface vertically so that it is completely unconstrained. + + + + + + + Specify how the window should be positioned if the originally intended + position caused the surface to be constrained, meaning at least + partially outside positioning boundaries set by the compositor. The + adjustment is set by constructing a bitmask describing the adjustment to + be made when the surface is constrained on that axis. + + If no bit for one axis is set, the compositor will assume that the child + surface should not change its position on that axis when constrained. + + If more than one bit for one axis is set, the order of how adjustments + are applied is specified in the corresponding adjustment descriptions. + + The default adjustment is none. + + + + + + + Specify the surface position offset relative to the position of the + anchor on the anchor rectangle and the anchor on the surface. For + example if the anchor of the anchor rectangle is at (x, y), the surface + has the gravity bottom|right, and the offset is (ox, oy), the calculated + surface position will be (x + ox, y + oy). The offset position of the + surface is the one used for constraint testing. See + set_constraint_adjustment. + + An example use case is placing a popup menu on top of a user interface + element, while aligning the user interface element of the parent surface + with some user interface element placed somewhere in the popup surface. + + + + + + + + + + When set reactive, the surface is reconstrained if the conditions used + for constraining changed, e.g. the parent window moved. + + If the conditions changed and the popup was reconstrained, an + xdg_popup.configure event is sent with updated geometry, followed by an + xdg_surface.configure event. + + + + + + Set the parent window geometry the compositor should use when + positioning the popup. The compositor may use this information to + determine the future state the popup should be constrained using. If + this doesn't match the dimension of the parent the popup is eventually + positioned against, the behavior is undefined. + + The arguments are given in the surface-local coordinate space. + + + + + + + + Set the serial of an xdg_surface.configure event this positioner will be + used in response to. The compositor may use this information together + with set_parent_size to determine what future state the popup should be + constrained using. + + + + + + + + An interface that may be implemented by a wl_surface, for + implementations that provide a desktop-style user interface. + + It provides a base set of functionality required to construct user + interface elements requiring management by the compositor, such as + toplevel windows, menus, etc. The types of functionality are split into + xdg_surface roles. + + Creating an xdg_surface does not set the role for a wl_surface. In order + to map an xdg_surface, the client must create a role-specific object + using, e.g., get_toplevel, get_popup. The wl_surface for any given + xdg_surface can have at most one role, and may not be assigned any role + not based on xdg_surface. + + A role must be assigned before any other requests are made to the + xdg_surface object. + + The client must call wl_surface.commit on the corresponding wl_surface + for the xdg_surface state to take effect. + + Creating an xdg_surface from a wl_surface which has a buffer attached or + committed is a client error, and any attempts by a client to attach or + manipulate a buffer prior to the first xdg_surface.configure call must + also be treated as errors. + + After creating a role-specific object and setting it up (e.g. by sending + the title, app ID, size constraints, parent, etc), the client must + perform an initial commit without any buffer attached. The compositor + will reply with initial wl_surface state such as + wl_surface.preferred_buffer_scale followed by an xdg_surface.configure + event. The client must acknowledge it and is then allowed to attach a + buffer to map the surface. + + Mapping an xdg_surface-based role surface is defined as making it + possible for the surface to be shown by the compositor. Note that + a mapped surface is not guaranteed to be visible once it is mapped. + + For an xdg_surface to be mapped by the compositor, the following + conditions must be met: + (1) the client has assigned an xdg_surface-based role to the surface + (2) the client has set and committed the xdg_surface state and the + role-dependent state to the surface + (3) the client has committed a buffer to the surface + + A newly-unmapped surface is considered to have met condition (1) out + of the 3 required conditions for mapping a surface if its role surface + has not been destroyed, i.e. the client must perform the initial commit + again before attaching a buffer. + + + + + + + + + + + + + + Destroy the xdg_surface object. An xdg_surface must only be destroyed + after its role object has been destroyed, otherwise + a defunct_role_object error is raised. + + + + + + This creates an xdg_toplevel object for the given xdg_surface and gives + the associated wl_surface the xdg_toplevel role. + + See the documentation of xdg_toplevel for more details about what an + xdg_toplevel is and how it is used. + + + + + + + This creates an xdg_popup object for the given xdg_surface and gives + the associated wl_surface the xdg_popup role. + + If null is passed as a parent, a parent surface must be specified using + some other protocol, before committing the initial state. + + See the documentation of xdg_popup for more details about what an + xdg_popup is and how it is used. + + + + + + + + + The window geometry of a surface is its "visible bounds" from the + user's perspective. Client-side decorations often have invisible + portions like drop-shadows which should be ignored for the + purposes of aligning, placing and constraining windows. Note that + in some situations, compositors may clip rendering to the window + geometry, so the client should avoid putting functional elements + outside of it. + + The window geometry is double-buffered state, see wl_surface.commit. + + When maintaining a position, the compositor should treat the (x, y) + coordinate of the window geometry as the top left corner of the window. + A client changing the (x, y) window geometry coordinate should in + general not alter the position of the window. + + Once the window geometry of the surface is set, it is not possible to + unset it, and it will remain the same until set_window_geometry is + called again, even if a new subsurface or buffer is attached. + + If never set, the value is the full bounds of the surface, + including any subsurfaces. This updates dynamically on every + commit. This unset is meant for extremely simple clients. + + The arguments are given in the surface-local coordinate space of + the wl_surface associated with this xdg_surface, and may extend outside + of the wl_surface itself to mark parts of the subsurface tree as part of + the window geometry. + + When applied, the effective window geometry will be the set window + geometry clamped to the bounding rectangle of the combined + geometry of the surface of the xdg_surface and the associated + subsurfaces. + + The effective geometry will not be recalculated unless a new call to + set_window_geometry is done and the new pending surface state is + subsequently applied. + + The width and height of the effective window geometry must be + greater than zero. Setting an invalid size will raise an + invalid_size error. + + + + + + + + + + When a configure event is received, if a client commits the + surface in response to the configure event, then the client + must make an ack_configure request sometime before the commit + request, passing along the serial of the configure event. + + For instance, for toplevel surfaces the compositor might use this + information to move a surface to the top left only when the client has + drawn itself for the maximized or fullscreen state. + + If the client receives multiple configure events before it + can respond to one, it only has to ack the last configure event. + Acking a configure event that was never sent raises an invalid_serial + error. + + A client is not required to commit immediately after sending + an ack_configure request - it may even ack_configure several times + before its next surface commit. + + A client may send multiple ack_configure requests before committing, but + only the last request sent before a commit indicates which configure + event the client really is responding to. + + Sending an ack_configure request consumes the serial number sent with + the request, as well as serial numbers sent by all configure events + sent on this xdg_surface prior to the configure event referenced by + the committed serial. + + It is an error to issue multiple ack_configure requests referencing a + serial from the same configure event, or to issue an ack_configure + request referencing a serial from a configure event issued before the + event identified by the last ack_configure request for the same + xdg_surface. Doing so will raise an invalid_serial error. + + + + + + + The configure event marks the end of a configure sequence. A configure + sequence is a set of one or more events configuring the state of the + xdg_surface, including the final xdg_surface.configure event. + + Where applicable, xdg_surface surface roles will during a configure + sequence extend this event as a latched state sent as events before the + xdg_surface.configure event. Such events should be considered to make up + a set of atomically applied configuration states, where the + xdg_surface.configure commits the accumulated state. + + Clients should arrange their surface for the new states, and then send + an ack_configure request with the serial sent in this configure event at + some point before committing the new surface. + + If the client receives multiple configure events before it can respond + to one, it is free to discard all but the last event it received. + + + + + + + + + This interface defines an xdg_surface role which allows a surface to, + among other things, set window-like properties such as maximize, + fullscreen, and minimize, set application-specific metadata like title and + id, and well as trigger user interactive operations such as interactive + resize and move. + + An xdg_toplevel by default is responsible for providing the full intended + visual representation of the toplevel, which depending on the window + state, may mean things like a title bar, window controls and drop shadow. + + Unmapping an xdg_toplevel means that the surface cannot be shown + by the compositor until it is explicitly mapped again. + All active operations (e.g., move, resize) are canceled and all + attributes (e.g. title, state, stacking, ...) are discarded for + an xdg_toplevel surface when it is unmapped. The xdg_toplevel returns to + the state it had right after xdg_surface.get_toplevel. The client + can re-map the toplevel by performing a commit without any buffer + attached, waiting for a configure event and handling it as usual (see + xdg_surface description). + + Attaching a null buffer to a toplevel unmaps the surface. + + + + + This request destroys the role surface and unmaps the surface; + see "Unmapping" behavior in interface section for details. + + + + + + + + + + + + Set the "parent" of this surface. This surface should be stacked + above the parent surface and all other ancestor surfaces. + + Parent surfaces should be set on dialogs, toolboxes, or other + "auxiliary" surfaces, so that the parent is raised when the dialog + is raised. + + Setting a null parent for a child surface unsets its parent. Setting + a null parent for a surface which currently has no parent is a no-op. + + Only mapped surfaces can have child surfaces. Setting a parent which + is not mapped is equivalent to setting a null parent. If a surface + becomes unmapped, its children's parent is set to the parent of + the now-unmapped surface. If the now-unmapped surface has no parent, + its children's parent is unset. If the now-unmapped surface becomes + mapped again, its parent-child relationship is not restored. + + The parent toplevel must not be one of the child toplevel's + descendants, and the parent must be different from the child toplevel, + otherwise the invalid_parent protocol error is raised. + + + + + + + Set a short title for the surface. + + This string may be used to identify the surface in a task bar, + window list, or other user interface elements provided by the + compositor. + + The string must be encoded in UTF-8. + + + + + + + Set an application identifier for the surface. + + The app ID identifies the general class of applications to which + the surface belongs. The compositor can use this to group multiple + surfaces together, or to determine how to launch a new application. + + For D-Bus activatable applications, the app ID is used as the D-Bus + service name. + + The compositor shell will try to group application surfaces together + by their app ID. As a best practice, it is suggested to select app + ID's that match the basename of the application's .desktop file. + For example, "org.freedesktop.FooViewer" where the .desktop file is + "org.freedesktop.FooViewer.desktop". + + Like other properties, a set_app_id request can be sent after the + xdg_toplevel has been mapped to update the property. + + See the desktop-entry specification [0] for more details on + application identifiers and how they relate to well-known D-Bus + names and .desktop files. + + [0] https://standards.freedesktop.org/desktop-entry-spec/ + + + + + + + Clients implementing client-side decorations might want to show + a context menu when right-clicking on the decorations, giving the + user a menu that they can use to maximize or minimize the window. + + This request asks the compositor to pop up such a window menu at + the given position, relative to the local surface coordinates of + the parent surface. There are no guarantees as to what menu items + the window menu contains, or even if a window menu will be drawn + at all. + + This request must be used in response to some sort of user action + like a button press, key press, or touch down event. + + + + + + + + + + Start an interactive, user-driven move of the surface. + + This request must be used in response to some sort of user action + like a button press, key press, or touch down event. The passed + serial is used to determine the type of interactive move (touch, + pointer, etc). + + The server may ignore move requests depending on the state of + the surface (e.g. fullscreen or maximized), or if the passed serial + is no longer valid. + + If triggered, the surface will lose the focus of the device + (wl_pointer, wl_touch, etc) used for the move. It is up to the + compositor to visually indicate that the move is taking place, such as + updating a pointer cursor, during the move. There is no guarantee + that the device focus will return when the move is completed. + + + + + + + + These values are used to indicate which edge of a surface + is being dragged in a resize operation. + + + + + + + + + + + + + + + Start a user-driven, interactive resize of the surface. + + This request must be used in response to some sort of user action + like a button press, key press, or touch down event. The passed + serial is used to determine the type of interactive resize (touch, + pointer, etc). + + The server may ignore resize requests depending on the state of + the surface (e.g. fullscreen or maximized). + + If triggered, the client will receive configure events with the + "resize" state enum value and the expected sizes. See the "resize" + enum value for more details about what is required. The client + must also acknowledge configure events using "ack_configure". After + the resize is completed, the client will receive another "configure" + event without the resize state. + + If triggered, the surface also will lose the focus of the device + (wl_pointer, wl_touch, etc) used for the resize. It is up to the + compositor to visually indicate that the resize is taking place, + such as updating a pointer cursor, during the resize. There is no + guarantee that the device focus will return when the resize is + completed. + + The edges parameter specifies how the surface should be resized, and + is one of the values of the resize_edge enum. Values not matching + a variant of the enum will cause the invalid_resize_edge protocol error. + The compositor may use this information to update the surface position + for example when dragging the top left corner. The compositor may also + use this information to adapt its behavior, e.g. choose an appropriate + cursor image. + + + + + + + + + The different state values used on the surface. This is designed for + state values like maximized, fullscreen. It is paired with the + configure event to ensure that both the client and the compositor + setting the state can be synchronized. + + States set in this way are double-buffered, see wl_surface.commit. + + + + The surface is maximized. The window geometry specified in the configure + event must be obeyed by the client, or the xdg_wm_base.invalid_surface_state + error is raised. + + The client should draw without shadow or other + decoration outside of the window geometry. + + + + + The surface is fullscreen. The window geometry specified in the + configure event is a maximum; the client cannot resize beyond it. For + a surface to cover the whole fullscreened area, the geometry + dimensions must be obeyed by the client. For more details, see + xdg_toplevel.set_fullscreen. + + + + + The surface is being resized. The window geometry specified in the + configure event is a maximum; the client cannot resize beyond it. + Clients that have aspect ratio or cell sizing configuration can use + a smaller size, however. + + + + + Client window decorations should be painted as if the window is + active. Do not assume this means that the window actually has + keyboard or pointer focus. + + + + + The window is currently in a tiled layout and the left edge is + considered to be adjacent to another part of the tiling grid. + + The client should draw without shadow or other decoration outside of + the window geometry on the left edge. + + + + + The window is currently in a tiled layout and the right edge is + considered to be adjacent to another part of the tiling grid. + + The client should draw without shadow or other decoration outside of + the window geometry on the right edge. + + + + + The window is currently in a tiled layout and the top edge is + considered to be adjacent to another part of the tiling grid. + + The client should draw without shadow or other decoration outside of + the window geometry on the top edge. + + + + + The window is currently in a tiled layout and the bottom edge is + considered to be adjacent to another part of the tiling grid. + + The client should draw without shadow or other decoration outside of + the window geometry on the bottom edge. + + + + + The surface is currently not ordinarily being repainted; for + example because its content is occluded by another window, or its + outputs are switched off due to screen locking. + + + + + The left edge of the window is currently constrained, meaning it + shouldn't attempt to resize from that edge. It can for example mean + it's tiled next to a monitor edge on the constrained side of the + window. + + + + + The right edge of the window is currently constrained, meaning it + shouldn't attempt to resize from that edge. It can for example mean + it's tiled next to a monitor edge on the constrained side of the + window. + + + + + The top edge of the window is currently constrained, meaning it + shouldn't attempt to resize from that edge. It can for example mean + it's tiled next to a monitor edge on the constrained side of the + window. + + + + + The bottom edge of the window is currently constrained, meaning it + shouldn't attempt to resize from that edge. It can for example mean + it's tiled next to a monitor edge on the constrained side of the + window. + + + + + + + Set a maximum size for the window. + + The client can specify a maximum size so that the compositor does + not try to configure the window beyond this size. + + The width and height arguments are in window geometry coordinates. + See xdg_surface.set_window_geometry. + + Values set in this way are double-buffered, see wl_surface.commit. + + The compositor can use this information to allow or disallow + different states like maximize or fullscreen and draw accurate + animations. + + Similarly, a tiling window manager may use this information to + place and resize client windows in a more effective way. + + The client should not rely on the compositor to obey the maximum + size. The compositor may decide to ignore the values set by the + client and request a larger size. + + If never set, or a value of zero in the request, means that the + client has no expected maximum size in the given dimension. + As a result, a client wishing to reset the maximum size + to an unspecified state can use zero for width and height in the + request. + + Requesting a maximum size to be smaller than the minimum size of + a surface is illegal and will result in an invalid_size error. + + The width and height must be greater than or equal to zero. Using + strictly negative values for width or height will result in an + invalid_size error. + + + + + + + + Set a minimum size for the window. + + The client can specify a minimum size so that the compositor does + not try to configure the window below this size. + + The width and height arguments are in window geometry coordinates. + See xdg_surface.set_window_geometry. + + Values set in this way are double-buffered, see wl_surface.commit. + + The compositor can use this information to allow or disallow + different states like maximize or fullscreen and draw accurate + animations. + + Similarly, a tiling window manager may use this information to + place and resize client windows in a more effective way. + + The client should not rely on the compositor to obey the minimum + size. The compositor may decide to ignore the values set by the + client and request a smaller size. + + If never set, or a value of zero in the request, means that the + client has no expected minimum size in the given dimension. + As a result, a client wishing to reset the minimum size + to an unspecified state can use zero for width and height in the + request. + + Requesting a minimum size to be larger than the maximum size of + a surface is illegal and will result in an invalid_size error. + + The width and height must be greater than or equal to zero. Using + strictly negative values for width and height will result in an + invalid_size error. + + + + + + + + Maximize the surface. + + After requesting that the surface should be maximized, the compositor + will respond by emitting a configure event. Whether this configure + actually sets the window maximized is subject to compositor policies. + The client must then update its content, drawing in the configured + state. The client must also acknowledge the configure when committing + the new content (see ack_configure). + + It is up to the compositor to decide how and where to maximize the + surface, for example which output and what region of the screen should + be used. + + If the surface was already maximized, the compositor will still emit + a configure event with the "maximized" state. + + If the surface is in a fullscreen state, this request has no direct + effect. It may alter the state the surface is returned to when + unmaximized unless overridden by the compositor. + + + + + + Unmaximize the surface. + + After requesting that the surface should be unmaximized, the compositor + will respond by emitting a configure event. Whether this actually + un-maximizes the window is subject to compositor policies. + If available and applicable, the compositor will include the window + geometry dimensions the window had prior to being maximized in the + configure event. The client must then update its content, drawing it in + the configured state. The client must also acknowledge the configure + when committing the new content (see ack_configure). + + It is up to the compositor to position the surface after it was + unmaximized; usually the position the surface had before maximizing, if + applicable. + + If the surface was already not maximized, the compositor will still + emit a configure event without the "maximized" state. + + If the surface is in a fullscreen state, this request has no direct + effect. It may alter the state the surface is returned to when + unmaximized unless overridden by the compositor. + + + + + + Make the surface fullscreen. + + After requesting that the surface should be fullscreened, the + compositor will respond by emitting a configure event. Whether the + client is actually put into a fullscreen state is subject to compositor + policies. The client must also acknowledge the configure when + committing the new content (see ack_configure). + + The output passed by the request indicates the client's preference as + to which display it should be set fullscreen on. If this value is NULL, + it's up to the compositor to choose which display will be used to map + this surface. + + If the surface doesn't cover the whole output, the compositor will + position the surface in the center of the output and compensate with + border fill covering the rest of the output. The content of the + border fill is undefined, but should be assumed to be in some way that + attempts to blend into the surrounding area (e.g. solid black). + + If the fullscreened surface is not opaque, the compositor must make + sure that other screen content not part of the same surface tree (made + up of subsurfaces, popups or similarly coupled surfaces) are not + visible below the fullscreened surface. + + + + + + + Make the surface no longer fullscreen. + + After requesting that the surface should be unfullscreened, the + compositor will respond by emitting a configure event. + Whether this actually removes the fullscreen state of the client is + subject to compositor policies. + + Making a surface unfullscreen sets states for the surface based on the following: + * the state(s) it may have had before becoming fullscreen + * any state(s) decided by the compositor + * any state(s) requested by the client while the surface was fullscreen + + The compositor may include the previous window geometry dimensions in + the configure event, if applicable. + + The client must also acknowledge the configure when committing the new + content (see ack_configure). + + + + + + Request that the compositor minimize your surface. There is no + way to know if the surface is currently minimized, nor is there + any way to unset minimization on this surface. + + If you are looking to throttle redrawing when minimized, please + instead use the wl_surface.frame event for this, as this will + also work with live previews on windows in Alt-Tab, Expose or + similar compositor features. + + + + + + This configure event asks the client to resize its toplevel surface or + to change its state. The configured state should not be applied + immediately. See xdg_surface.configure for details. + + The width and height arguments specify a hint to the window + about how its surface should be resized in window geometry + coordinates. See set_window_geometry. + + If the width or height arguments are zero, it means the client + should decide its own window dimension. This may happen when the + compositor needs to configure the state of the surface but doesn't + have any information about any previous or expected dimension. + + The states listed in the event specify how the width/height + arguments should be interpreted, and possibly how it should be + drawn. + + The states are sent as an array of 32-bit unsigned integers in + native endianness. State values are defined in the state enum. + + Clients must send an ack_configure in response to this event. See + xdg_surface.configure and xdg_surface.ack_configure for details. + + + + + + + + + The close event is sent by the compositor when the user + wants the surface to be closed. This should be equivalent to + the user clicking the close button in client-side decorations, + if your application has any. + + This is only a request that the user intends to close the + window. The client may choose to ignore this request, or show + a dialog to ask the user to save their data, etc. + + + + + + + + The configure_bounds event may be sent prior to a xdg_toplevel.configure + event to communicate the bounds a window geometry size is recommended + to constrain to. + + The passed width and height are in surface coordinate space. If width + and height are 0, it means bounds is unknown and equivalent to as if no + configure_bounds event was ever sent for this surface. + + The bounds can for example correspond to the size of a monitor excluding + any panels or other shell components, so that a surface isn't created in + a way that it cannot fit. + + The bounds may change at any point, and in such a case, a new + xdg_toplevel.configure_bounds will be sent, followed by + xdg_toplevel.configure and xdg_surface.configure. + + + + + + + + + + + + + + + + + This event advertises the capabilities supported by the compositor. If + a capability isn't supported, clients should hide or disable the UI + elements that expose this functionality. For instance, if the + compositor doesn't advertise support for minimized toplevels, a button + triggering the set_minimized request should not be displayed. + + The compositor will ignore requests it doesn't support. For instance, + a compositor which doesn't advertise support for minimized will ignore + set_minimized requests. + + Compositors must send this event once before the first + xdg_surface.configure event. When the capabilities change, compositors + must send this event again and then send an xdg_surface.configure + event. + + The configured state should not be applied immediately. See + xdg_surface.configure for details. + + The capabilities are sent as an array of 32-bit unsigned integers in + native endianness. Capability values are defined in the wm_capabilities enum. + + + + + + + + A popup surface is a short-lived, temporary surface. It can be used to + implement for example menus, popovers, tooltips and other similar user + interface concepts. + + A popup can be made to take an explicit grab. See xdg_popup.grab for + details. + + When the popup is dismissed, a popup_done event will be sent out, and at + the same time the surface will be unmapped. See the xdg_popup.popup_done + event for details. + + Explicitly destroying the xdg_popup object will also dismiss the popup and + unmap the surface. Clients that want to dismiss the popup when another + surface of their own is clicked should dismiss the popup using the destroy + request. + + A newly created xdg_popup will be stacked on top of all previously created + xdg_popup surfaces associated with the same xdg_toplevel. + + The parent of an xdg_popup must be mapped (see the xdg_surface + description) before the xdg_popup itself. + + The client must call wl_surface.commit on the corresponding wl_surface + for the xdg_popup state to take effect. + + + + + + + + + This destroys the popup. Explicitly destroying the xdg_popup + object will also dismiss the popup, and unmap the surface. + + If this xdg_popup is not the "topmost" popup, the + xdg_wm_base.not_the_topmost_popup protocol error will be sent. + + + + + + This request makes the created popup take an explicit grab. An explicit + grab will be dismissed when the user dismisses the popup, or when the + client destroys the xdg_popup. This can be done by the user clicking + outside the surface, using the keyboard, or even locking the screen + through closing the lid or a timeout. + + If the compositor denies the grab, the popup will be immediately + dismissed. + + This request must be used in response to some sort of user action like a + button press, key press, or touch down event. The serial number of the + event should be passed as 'serial'. + + The parent of a grabbing popup must either be an xdg_toplevel surface or + another xdg_popup with an explicit grab. If the parent is another + xdg_popup it means that the popups are nested, with this popup now being + the topmost popup. + + Nested popups must be destroyed in the reverse order they were created + in, e.g. the only popup you are allowed to destroy at all times is the + topmost one. + + When compositors choose to dismiss a popup, they may dismiss every + nested grabbing popup as well. When a compositor dismisses popups, it + will follow the same dismissing order as required from the client. + + If the topmost grabbing popup is destroyed, the grab will be returned to + the parent of the popup, if that parent previously had an explicit grab. + + If the parent is a grabbing popup which has already been dismissed, this + popup will be immediately dismissed. If the parent is a popup that did + not take an explicit grab, an error will be raised. + + During a popup grab, the client owning the grab will receive pointer + and touch events for all their surfaces as normal (similar to an + "owner-events" grab in X11 parlance), while the top most grabbing popup + will always have keyboard focus. + + + + + + + + This event asks the popup surface to configure itself given the + configuration. The configured state should not be applied immediately. + See xdg_surface.configure for details. + + The x and y arguments represent the position the popup was placed at + given the xdg_positioner rule, relative to the upper left corner of the + window geometry of the parent surface. + + For version 2 or older, the configure event for an xdg_popup is only + ever sent once for the initial configuration. Starting with version 3, + it may be sent again if the popup is setup with an xdg_positioner with + set_reactive requested, or in response to xdg_popup.reposition requests. + + + + + + + + + + The popup_done event is sent out when a popup is dismissed by the + compositor. The client should destroy the xdg_popup object at this + point. + + + + + + + + Reposition an already-mapped popup. The popup will be placed given the + details in the passed xdg_positioner object, and a + xdg_popup.repositioned followed by xdg_popup.configure and + xdg_surface.configure will be emitted in response. Any parameters set + by the previous positioner will be discarded. + + The passed token will be sent in the corresponding + xdg_popup.repositioned event. The new popup position will not take + effect until the corresponding configure event is acknowledged by the + client. See xdg_popup.repositioned for details. The token itself is + opaque, and has no other special meaning. + + If multiple reposition requests are sent, the compositor may skip all + but the last one. + + If the popup is repositioned in response to a configure event for its + parent, the client should send an xdg_positioner.set_parent_configure + and possibly an xdg_positioner.set_parent_size request to allow the + compositor to properly constrain the popup. + + If the popup is repositioned together with a parent that is being + resized, but not in response to a configure event, the client should + send an xdg_positioner.set_parent_size request. + + + + + + + + The repositioned event is sent as part of a popup configuration + sequence, together with xdg_popup.configure and lastly + xdg_surface.configure to notify the completion of a reposition request. + + The repositioned event is to notify about the completion of a + xdg_popup.reposition request. The token argument is the token passed + in the xdg_popup.reposition request. + + Immediately after this event is emitted, xdg_popup.configure and + xdg_surface.configure will be sent with the updated size and position, + as well as a new configure serial. + + The client should optionally update the content of the popup, but must + acknowledge the new popup configuration for the new position to take + effect. See xdg_surface.ack_configure for details. + + + + + + diff --git a/tooling/probes/step20c-corec-v2-followup/graphics/run.py b/tooling/probes/step20c-corec-v2-followup/graphics/run.py new file mode 100644 index 0000000..2437e1b --- /dev/null +++ b/tooling/probes/step20c-corec-v2-followup/graphics/run.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +"""WHY/CONTRACT/INVARIANT: one new ID, reviewed source, no retry or auto-review.""" +import argparse,json,os,time +from pathlib import Path +from gate import Ledger,need,disable_dump,validate_review +from plans import RUNS +from engine import Session,storage_guard +import cases + + +def normal_remaining(s): + for name in list(s.proc): + if name not in ('owner','compositor','pointer') and s.proc[name].poll() is None:s.close_client(name,'close') + + +def execute(rid,lab,manifest,review,publication): + disable_dump();storage_guard(Path(lab)) + # WHY: missing replay inputs must not consume a run. The independent gate is + # validated again inside admit, then before every action. Never repair a freeze. + validate_review(lab,manifest,review,publication,rid) + need(not (Path(lab)/'graphics-runs'/rid).exists(),'existing run directory; no overwrite/retry') + auth=Ledger(lab).admit(rid,manifest,review,publication) + s=None;failure=None;status='INCONCLUSIVE' + try: + if rid=='GPU-G3': + from gpu_handler import GPUSceneSession,run as gpu_run + s=GPUSceneSession(RUNS[rid],auth);status=gpu_run(s) + else: + s=Session(RUNS[rid],auth);s.start() + if rid=='GPU-G3':pass + elif rid in ('FS-P','FS-C'): + from fullscreen import run,comparable + status=run(s) + if rid=='FS-C' and status=='PARTIALLY_FEASIBLE': + control=s.lab/'graphics-runs/FS-P/run/fullscreen-comparability.json' + need(control.is_file(),'FS-P measured comparable source missing') + s.save('fullscreen-comparison',comparable(json.loads(control.read_text()),json.loads((s.run/'fullscreen-comparability.json').read_text()))) + elif rid=='DY-C':status=cases.dynamic_campaign(s) + elif rid in ('OM-ROOT','OM-NESTED'): + from oom_handler import run + status=run(s) + else:raise ValueError('NOT_ADMITTED: independent diagnostic adapter absent') + normal_remaining(s) + except Exception as exc: + failure=f'{type(exc).__name__}: {exc}';status='INCONCLUSIVE' + if s is not None: + s.save('partial-failure',dict(status=status,failure=failure,events=s.events,captures_observed=sum(e['kind']=='capture' for e in s.events),compositor_exit=s.comp.poll() if hasattr(s,'comp') else None,owner_exit=s.proc['owner'].poll() if 'owner' in s.proc else None,critical_or_unresolved=True,no_retry=True)) + finally: + if s is None:Ledger(lab).complete(auth,dict(status='INCONCLUSIVE',failure=failure,session_constructed=False)) + else: + try:status=s.cleanup(status,failure) + except Exception as exc: + # Cleanup exceptions remain failures; autonomous watchdog still owns groups. + failure=(failure or '')+f'; cleanup {type(exc).__name__}: {exc}' + Ledger(lab).complete(auth,dict(status='INCONCLUSIVE',failure=failure,scientific_cleanup=False)) + status='INCONCLUSIVE' + return dict(status=status,failure=failure,runner_exit=0 if status=='PARTIALLY_FEASIBLE' else 1) + +if __name__=='__main__': + p=argparse.ArgumentParser(description='One independent new-ticket run, never retry/all') + p.add_argument('run_id',choices=RUNS) + for name in ('lab','manifest','review','publication'):p.add_argument('--'+name,required=True) + a=p.parse_args();result=execute(a.run_id,a.lab,a.manifest,a.review,a.publication);print(json.dumps(result));raise SystemExit(result['runner_exit']) diff --git a/tooling/probes/step20c-corec-v2-followup/graphics/validate_offline.py b/tooling/probes/step20c-corec-v2-followup/graphics/validate_offline.py new file mode 100644 index 0000000..1370919 --- /dev/null +++ b/tooling/probes/step20c-corec-v2-followup/graphics/validate_offline.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +"""Run exact offline validators; report is never an independent acceptance.""" +import argparse,copy,importlib.util,json,sys,unittest +from pathlib import Path +from gate import canonical_lab,clean_environment,disable_dump,durable,source_files,digest,sha + +def historical_replay(directory): + import cases + root=Path(directory);a=root/'resized-back-snapshot.json';b=root/'migrated-H2-snapshot.json' + before=json.loads(a.read_text());after=json.loads(b.read_text());previous=cases.dynamic_geometry(before) + current=cases.dynamic_geometry(after,previous,'migration',previous['focused']) + mutant=copy.deepcopy(after) + from ipc_predicates import workspace + workspace(mutant['tree'],'1')['mosaic']['rows'][0]['scroll_x']=previous['scroll'] + try:cases.dynamic_geometry(mutant,previous,'migration',previous['focused']) + except ValueError:negative=True + else:raise AssertionError('stale historical C7 camera accepted') + return dict(status='PASS',scope='historical IPC replay only; no runtime upgrade',inputs={str(a):sha(a),str(b):sha(b)},positive=current,negative_stale_camera_rejected=negative) + +def validate(lab,historical=None): + disable_dump();lab=canonical_lab(lab);clean_environment(lab) + path=Path(__file__).parent/'offline/test_followup.py';spec=importlib.util.spec_from_file_location('test_followup',path);module=importlib.util.module_from_spec(spec);spec.loader.exec_module(module) + suite=unittest.defaultTestLoader.loadTestsFromModule(module);result=unittest.TextTestRunner(verbosity=2).run(suite) + replay=historical_replay(historical) if historical else dict(status='NOT_RUN',reason='historical replay input not supplied') + receipt=dict(status='PASS' if result.wasSuccessful() else 'FAIL',tests_run=result.testsRun,failures=len(result.failures)+len(result.errors),skipped=len(result.skipped),source_digest=digest(source_files()),runtime='NOT_RUN',real_reservations=0,independent_review='NOT_PROVEN',historical_C7=replay) + durable(lab/'followup-work/offline-tests.json',receipt);return result.wasSuccessful() +if __name__=='__main__': + p=argparse.ArgumentParser();p.add_argument('--lab',required=True);p.add_argument('--historical-c7');a=p.parse_args();raise SystemExit(0 if validate(a.lab,a.historical_c7) else 1) diff --git a/tooling/probes/step20c-corec-v2-followup/graphics/validators.py b/tooling/probes/step20c-corec-v2-followup/graphics/validators.py new file mode 100644 index 0000000..c492c9e --- /dev/null +++ b/tooling/probes/step20c-corec-v2-followup/graphics/validators.py @@ -0,0 +1,99 @@ +"""Imported live/offline scientific predicates, not a parallel test model.""" +import re +from geometry import need,exact,crossing,expected_outputs +from predicates import protocol + +def client_exit(log,code,scenario='observed'): + # WHY: root OOM creates A, never B. CONTRACT: hierarchy-sensitive close; + # no normal error0 close is substituted for a wire-proven no_memory path. + # INVARIANT: every created role is destroyed exactly once, child first. + need(scenario in ('observed','root_failure','nested_failure','nested','toplevel'),'unknown closure scenario') + if 'TOPLEVEL CREATE ' in log: + created=re.findall(r'^(POPUP_[AB]|TOPLEVEL) CREATE ',log,re.M) + need(len(created)==len(set(created)) and created and created[0]=='TOPLEVEL','ambiguous created hierarchy') + expected={'root_failure':['TOPLEVEL','POPUP_A'],'nested_failure':['TOPLEVEL','POPUP_A','POPUP_B'],'nested':['TOPLEVEL','POPUP_A','POPUP_B'],'toplevel':['TOPLEVEL']}.get(scenario,created) + need(created==expected,'scenario created hierarchy differs') + order=re.findall(r'^(POPUP_[AB]|TOPLEVEL) DESTROY',log,re.M) + need(order==list(reversed(created)),'actual created hierarchy child-first destruction') + failed={'root_failure':'POPUP_A','nested_failure':'POPUP_B'}.get(scenario) + if failed:need(failed+' MAP ' not in log,'failed popup mapped despite NULL') + if scenario=='nested_failure':need('POPUP_A MAP ' in log,'nested failure without mapped root') + need('CLOSE_RECEIVED' in log and log.index('CLOSE_RECEIVED')0 and releases[0][0]==releases[0][1],'client SHM release incomplete/unobserved') + errors=re.findall(r'EXIT[^\n]*error=(\d+)',log) + need(code==0 and len(errors)==1 and errors==['0'],'normal client exit/error differs') + need(not re.search(r'protocol error|wl_display[^\n]*\.error\(',log,re.I),'wire error cannot be ordinary close') + return dict(status='PASS',scenario=scenario,exit=code,wire_error=False) + +def window(start,end,events): + need(start wl_display#1.get_registry(new id wl_registry#2) +[22:46:38.282546] {Default Queue} -> wl_display#1.sync(new id wl_callback#3) +[22:46:38.282664] {Display Queue} wl_display#1.delete_id(3) +[22:46:38.282677] {Default Queue} wl_registry#2.global(1, "wl_fixes", 1) +[22:46:38.282685] {Default Queue} wl_registry#2.global(2, "wl_shm", 2) +[22:46:38.282692] {Default Queue} -> wl_registry#2.bind(2, "wl_shm", 1, new id [unknown]#4) +[22:46:38.282699] {Default Queue} wl_registry#2.global(3, "zwp_linux_dmabuf_v1", 5) +[22:46:38.282703] {Default Queue} wl_registry#2.global(4, "wp_linux_drm_syncobj_manager_v1", 1) +[22:46:38.282708] {Default Queue} wl_registry#2.global(5, "wl_compositor", 6) +[22:46:38.282712] {Default Queue} -> wl_registry#2.bind(5, "wl_compositor", 4, new id [unknown]#5) +[22:46:38.282716] {Default Queue} wl_registry#2.global(6, "wl_subcompositor", 1) +[22:46:38.282720] {Default Queue} wl_registry#2.global(7, "wl_data_device_manager", 3) +[22:46:38.282724] {Default Queue} wl_registry#2.global(8, "zwlr_gamma_control_manager_v1", 1) +[22:46:38.282728] {Default Queue} wl_registry#2.global(9, "zxdg_output_manager_v1", 3) +[22:46:38.282732] {Default Queue} wl_registry#2.global(10, "ext_idle_notifier_v1", 2) +[22:46:38.282736] {Default Queue} wl_registry#2.global(11, "zwp_idle_inhibit_manager_v1", 1) +[22:46:38.282740] {Default Queue} wl_registry#2.global(12, "zwlr_layer_shell_v1", 5) +[22:46:38.282744] {Default Queue} wl_registry#2.global(13, "xdg_wm_base", 5) +[22:46:38.282751] {Default Queue} -> wl_registry#2.bind(13, "xdg_wm_base", 3, new id [unknown]#6) +[22:46:38.282756] {Default Queue} wl_registry#2.global(14, "zwp_tablet_manager_v2", 1) +[22:46:38.282760] {Default Queue} wl_registry#2.global(15, "org_kde_kwin_server_decoration_manager", 1) +[22:46:38.282764] {Default Queue} wl_registry#2.global(16, "zxdg_decoration_manager_v1", 1) +[22:46:38.282768] {Default Queue} wl_registry#2.global(17, "zwp_relative_pointer_manager_v1", 1) +[22:46:38.282772] {Default Queue} wl_registry#2.global(18, "zwp_pointer_constraints_v1", 1) +[22:46:38.282776] {Default Queue} wl_registry#2.global(19, "wp_presentation", 2) +[22:46:38.282780] {Default Queue} wl_registry#2.global(20, "wp_alpha_modifier_v1", 1) +[22:46:38.282784] {Default Queue} wl_registry#2.global(21, "zwlr_output_manager_v1", 4) +[22:46:38.282788] {Default Queue} wl_registry#2.global(22, "zwlr_output_power_manager_v1", 1) +[22:46:38.282792] {Default Queue} wl_registry#2.global(23, "zwp_input_method_manager_v2", 1) +[22:46:38.282796] {Default Queue} wl_registry#2.global(24, "zwp_text_input_manager_v3", 1) +[22:46:38.282799] {Default Queue} wl_registry#2.global(25, "ext_foreign_toplevel_list_v1", 1) +[22:46:38.282803] {Default Queue} wl_registry#2.global(26, "zwlr_foreign_toplevel_manager_v1", 3) +[22:46:38.282807] {Default Queue} wl_registry#2.global(27, "ext_session_lock_manager_v1", 1) +[22:46:38.282812] {Default Queue} wl_registry#2.global(28, "ext_workspace_manager_v1", 1) +[22:46:38.282816] {Default Queue} wl_registry#2.global(29, "zwlr_export_dmabuf_manager_v1", 1) +[22:46:38.282820] {Default Queue} wl_registry#2.global(30, "zwlr_screencopy_manager_v1", 3) +[22:46:38.282824] {Default Queue} wl_registry#2.global(31, "ext_image_copy_capture_manager_v1", 1) +[22:46:38.282828] {Default Queue} wl_registry#2.global(32, "ext_output_image_capture_source_manager_v1", 1) +[22:46:38.282832] {Default Queue} wl_registry#2.global(33, "zwlr_data_control_manager_v1", 2) +[22:46:38.282836] {Default Queue} wl_registry#2.global(34, "ext_data_control_manager_v1", 1) +[22:46:38.282840] {Default Queue} wl_registry#2.global(35, "wp_security_context_manager_v1", 1) +[22:46:38.282844] {Default Queue} wl_registry#2.global(36, "wp_viewporter", 1) +[22:46:38.282848] {Default Queue} wl_registry#2.global(37, "wp_single_pixel_buffer_manager_v1", 1) +[22:46:38.282852] {Default Queue} wl_registry#2.global(38, "wp_content_type_manager_v1", 1) +[22:46:38.282856] {Default Queue} wl_registry#2.global(39, "wp_fractional_scale_manager_v1", 1) +[22:46:38.282860] {Default Queue} wl_registry#2.global(40, "ext_foreign_toplevel_image_capture_source_manager_v1", 1) +[22:46:38.282872] {Default Queue} wl_registry#2.global(41, "wp_tearing_control_manager_v1", 1) +[22:46:38.282886] {Default Queue} wl_registry#2.global(42, "zxdg_exporter_v1", 1) +[22:46:38.282890] {Default Queue} wl_registry#2.global(43, "zxdg_importer_v1", 1) +[22:46:38.282894] {Default Queue} wl_registry#2.global(44, "zxdg_exporter_v2", 1) +[22:46:38.282898] {Default Queue} wl_registry#2.global(45, "zxdg_importer_v2", 1) +[22:46:38.282903] {Default Queue} wl_registry#2.global(46, "xdg_activation_v1", 1) +[22:46:38.282907] {Default Queue} wl_registry#2.global(47, "xdg_toplevel_tag_manager_v1", 1) +[22:46:38.282911] {Default Queue} wl_registry#2.global(48, "wp_cursor_shape_manager_v1", 2) +[22:46:38.282915] {Default Queue} wl_registry#2.global(49, "wp_color_representation_manager_v1", 1) +[22:46:38.282919] {Default Queue} wl_registry#2.global(50, "zwp_virtual_keyboard_manager_v1", 1) +[22:46:38.282923] {Default Queue} wl_registry#2.global(51, "zwlr_virtual_pointer_manager_v1", 2) +[22:46:38.282927] {Default Queue} wl_registry#2.global(52, "zwp_keyboard_shortcuts_inhibit_manager_v1", 1) +[22:46:38.282931] {Default Queue} wl_registry#2.global(53, "zwp_pointer_gestures_v1", 3) +[22:46:38.282935] {Default Queue} wl_registry#2.global(54, "ext_transient_seat_manager_v1", 1) +[22:46:38.282939] {Default Queue} wl_registry#2.global(55, "wl_seat", 9) +[22:46:38.282944] {Default Queue} -> wl_registry#2.bind(55, "wl_seat", 5, new id [unknown]#7) +[22:46:38.282948] {Default Queue} wl_registry#2.global(57, "zwp_primary_selection_device_manager_v1", 1) +[22:46:38.282953] {Default Queue} wl_registry#2.global(58, "wl_output", 4) +[22:46:38.282957] {Default Queue} wl_registry#2.global(59, "wl_output", 4) +[22:46:38.282961] {Default Queue} wl_callback#3.done(16) +[22:46:38.282966] {Default Queue} -> wl_display#1.sync(new id wl_callback#3) +[22:46:38.283205] {Display Queue} wl_display#1.delete_id(3) +[22:46:38.283217] {Default Queue} discarded wl_shm#4.format(0) +[22:46:38.283222] {Default Queue} discarded wl_shm#4.format(1) +[22:46:38.283227] {Default Queue} discarded wl_shm#4.format(875709016) +[22:46:38.283231] {Default Queue} discarded wl_shm#4.format(875708993) +[22:46:38.283235] {Default Queue} discarded wl_shm#4.format(875710274) +[22:46:38.283239] {Default Queue} discarded wl_shm#4.format(842094674) +[22:46:38.283242] {Default Queue} discarded wl_shm#4.format(842088786) +[22:46:38.283246] {Default Queue} discarded wl_shm#4.format(892426322) +[22:46:38.283249] {Default Queue} discarded wl_shm#4.format(892420434) +[22:46:38.283253] {Default Queue} discarded wl_shm#4.format(909199186) +[22:46:38.283257] {Default Queue} discarded wl_shm#4.format(808665688) +[22:46:38.283260] {Default Queue} discarded wl_shm#4.format(808665665) +[22:46:38.283264] {Default Queue} discarded wl_shm#4.format(1213351746) +[22:46:38.283268] {Default Queue} discarded wl_shm#4.format(1211384408) +[22:46:38.283271] {Default Queue} discarded wl_shm#4.format(1211384385) +[22:46:38.283275] {Default Queue} discarded wl_shm#4.format(942950210) +[22:46:38.283279] {Default Queue} discarded wl_shm#4.format(942948952) +[22:46:38.283282] {Default Queue} discarded wl_shm#4.format(942948929) +[22:46:38.283286] {Default Queue} wl_seat#7.name("seat0") +[22:46:38.283291] {Default Queue} wl_seat#7.capabilities(1) +[22:46:38.283296] {Default Queue} -> wl_seat#7.get_pointer(new id wl_pointer#8) +[22:46:38.283300] {Default Queue} wl_callback#3.done(16) +[22:46:38.283305] {Default Queue} -> wl_compositor#5.create_surface(new id wl_surface#3) +[22:46:38.283310] {Default Queue} -> xdg_wm_base#6.get_xdg_surface(new id xdg_surface#9, wl_surface#3) +[22:46:38.283319] {Default Queue} -> xdg_surface#9.get_toplevel(new id xdg_toplevel#10) +[22:46:38.283324] {Default Queue} -> xdg_toplevel#10.set_title("Mosaic Nested Grab Probe") +[22:46:38.283328] {Default Queue} -> xdg_toplevel#10.set_app_id("org.mosaic.NestedGrabProbe") +TOPLEVEL CREATE wl=3 xdg=9 role=10 placement=v_both_cross +[22:46:38.283338] {Default Queue} -> wl_surface#3.commit() +[22:46:38.283679] {Default Queue} xdg_wm_base#6.ping(17) +[22:46:38.283698] {Default Queue} -> xdg_wm_base#6.pong(17) +[22:46:38.283708] {Default Queue} xdg_toplevel#10.configure(0, 0, array[0]) +TOPLEVEL ROLE_CONFIGURE w=0 h=0 +[22:46:38.283716] {Default Queue} xdg_surface#9.configure(18) +TOPLEVEL XDG_CONFIGURE serial=18 ack +[22:46:38.283724] {Default Queue} -> xdg_surface#9.ack_configure(18) +[22:46:38.284502] {Default Queue} -> wl_shm#4.create_pool(new id wl_shm_pool#11, fd 5, 966000) +[22:46:38.284513] {Default Queue} -> wl_shm_pool#11.create_buffer(new id wl_buffer#12, 0, 420, 575, 1680, 1) +[22:46:38.284520] {Default Queue} -> wl_surface#3.attach(wl_buffer#12, 0, 0) +[22:46:38.284525] {Default Queue} -> wl_surface#3.damage_buffer(0, 0, 420, 575) +[22:46:38.284530] {Default Queue} -> wl_surface#3.commit() +[22:46:38.284534] {Default Queue} -> wl_buffer#12.destroy() +[22:46:38.284538] {Default Queue} -> wl_shm_pool#11.destroy() +TOPLEVEL MAP wl=3 xdg=9 +[22:46:38.285508] discarded [unknown]#12.[event 0](0 fd, 8 byte) +[22:46:38.285514] {Display Queue} wl_display#1.delete_id(12) +[22:46:38.285519] {Display Queue} wl_display#1.delete_id(11) +[22:46:38.285524] {Default Queue} xdg_toplevel#10.configure(636, 573, array[20]) +TOPLEVEL ROLE_CONFIGURE w=636 h=573 +[22:46:38.285532] {Default Queue} xdg_surface#9.configure(19) +TOPLEVEL XDG_CONFIGURE serial=19 ack +[22:46:38.285539] {Default Queue} -> xdg_surface#9.ack_configure(19) +[22:46:38.583097] {Default Queue} wl_pointer#8.enter(24, wl_surface#3, 238.00000000, 275.00000000) +POINTER_ENTER target=TOPLEVEL wl=3 serial=24 x=238.00 y=275.00 +[22:46:38.583124] {Default Queue} wl_pointer#8.frame() +[22:46:38.583129] {Default Queue} wl_pointer#8.leave(25, wl_surface#3) +POINTER_LEAVE target=TOPLEVEL serial=25 +[22:46:38.583137] {Default Queue} wl_pointer#8.frame() +[22:46:39.867771] {Default Queue} wl_pointer#8.enter(30, wl_surface#3, 3.00000000, 5.00000000) +POINTER_ENTER target=TOPLEVEL wl=3 serial=30 x=3.00 y=5.00 +[22:46:39.867787] {Default Queue} wl_pointer#8.frame() +[22:46:39.867792] {Default Queue} wl_pointer#8.frame() +[22:46:39.968604] {Default Queue} wl_pointer#8.leave(31, wl_surface#3) +POINTER_LEAVE target=TOPLEVEL serial=31 +[22:46:39.968649] {Default Queue} wl_pointer#8.frame() +[22:46:40.117908] {Default Queue} wl_pointer#8.enter(32, wl_surface#3, 3.00000000, 5.00000000) +POINTER_ENTER target=TOPLEVEL wl=3 serial=32 x=3.00 y=5.00 +[22:46:40.117939] {Default Queue} wl_pointer#8.frame() +[22:46:40.117943] {Default Queue} wl_pointer#8.button(33, 1002, 272, 1) +POINTER_BUTTON target=TOPLEVEL serial=33 button=272 state=1 +[22:46:40.117951] {Default Queue} -> wl_compositor#5.create_surface(new id wl_surface#11) +[22:46:40.117956] {Default Queue} -> xdg_wm_base#6.get_xdg_surface(new id xdg_surface#12, wl_surface#11) +[22:46:40.117960] {Default Queue} -> xdg_wm_base#6.create_positioner(new id xdg_positioner#13) +[22:46:40.117964] {Default Queue} -> xdg_positioner#13.set_size(250, 170) +[22:46:40.117967] {Default Queue} -> xdg_positioner#13.set_anchor_rect(50, 480, 1, 1) +[22:46:40.117970] {Default Queue} -> xdg_positioner#13.set_anchor(5) +[22:46:40.117973] {Default Queue} -> xdg_positioner#13.set_gravity(8) +[22:46:40.117976] {Default Queue} -> xdg_surface#12.get_popup(new id xdg_popup#14, xdg_surface#9, xdg_positioner#13) +POPUP_A CREATE wl=11 xdg=12 role=14 parent=TOPLEVEL parent_xdg=9 +POPUP_A GRAB_REQUEST origin=wl_pointer.button target=TOPLEVEL serial=33 used=33 +[22:46:40.117984] {Default Queue} -> xdg_popup#14.grab(wl_seat#7, 33) +[22:46:40.117988] {Default Queue} -> wl_surface#11.commit() +[22:46:40.117991] {Default Queue} -> xdg_positioner#13.destroy() +[22:46:40.117994] {Default Queue} wl_pointer#8.frame() +[22:46:40.118126] {Display Queue} wl_display#1.delete_id(13) +[22:46:40.118129] {Default Queue} xdg_popup#14.configure(50, 480, 250, 170) +POPUP_A ROLE_CONFIGURE x=50 y=480 w=250 h=170 +[22:46:40.118135] {Default Queue} xdg_surface#12.configure(34) +POPUP_A XDG_CONFIGURE serial=34 ack +[22:46:40.118140] {Default Queue} -> xdg_surface#12.ack_configure(34) +[22:46:40.118286] {Default Queue} -> wl_shm#4.create_pool(new id wl_shm_pool#13, fd 5, 170000) +[22:46:40.118298] {Default Queue} -> wl_shm_pool#13.create_buffer(new id wl_buffer#15, 0, 250, 170, 1000, 1) +[22:46:40.118313] {Default Queue} -> wl_surface#11.attach(wl_buffer#15, 0, 0) +[22:46:40.118317] {Default Queue} -> wl_surface#11.damage_buffer(0, 0, 250, 170) +[22:46:40.118320] {Default Queue} -> wl_surface#11.commit() +[22:46:40.118322] {Default Queue} -> wl_buffer#15.destroy() +[22:46:40.118325] {Default Queue} -> wl_shm_pool#13.destroy() +POPUP_A MAP wl=11 xdg=12 +[22:46:40.118959] discarded [unknown]#15.[event 0](0 fd, 8 byte) +[22:46:40.118962] {Display Queue} wl_display#1.delete_id(15) +[22:46:40.118965] {Display Queue} wl_display#1.delete_id(13) +[22:46:40.197365] {Default Queue} wl_pointer#8.button(35, 1003, 272, 0) +POINTER_BUTTON target=TOPLEVEL serial=35 button=272 state=0 +[22:46:40.197412] {Default Queue} wl_pointer#8.frame() +[22:46:40.298184] {Default Queue} wl_pointer#8.leave(36, wl_surface#3) +POINTER_LEAVE target=TOPLEVEL serial=36 +[22:46:40.298225] {Default Queue} wl_pointer#8.frame() +[22:46:40.585174] {Default Queue} wl_pointer#8.enter(37, wl_surface#3, 3.00000000, 5.00000000) +POINTER_ENTER target=TOPLEVEL wl=3 serial=37 x=3.00 y=5.00 +[22:46:40.585201] {Default Queue} wl_pointer#8.frame() +[22:46:40.585208] {Default Queue} wl_pointer#8.leave(38, wl_surface#3) +POINTER_LEAVE target=TOPLEVEL serial=38 +[22:46:40.585221] {Default Queue} wl_pointer#8.frame() +[22:46:40.585227] {Default Queue} wl_pointer#8.enter(39, wl_surface#11, 4.00000000, 5.00000000) +POINTER_ENTER target=POPUP_A wl=11 serial=39 x=4.00 y=5.00 +[22:46:40.585240] {Default Queue} wl_pointer#8.frame() +[22:46:40.585246] {Default Queue} wl_pointer#8.frame() +[22:46:40.685838] {Default Queue} wl_pointer#8.leave(40, wl_surface#11) +POINTER_LEAVE target=POPUP_A serial=40 +[22:46:40.685909] {Default Queue} wl_pointer#8.frame() +[22:46:40.835646] {Default Queue} wl_pointer#8.enter(41, wl_surface#11, 4.00000000, 5.00000000) +POINTER_ENTER target=POPUP_A wl=11 serial=41 x=4.00 y=5.00 +[22:46:40.835675] {Default Queue} wl_pointer#8.frame() +[22:46:40.835682] {Default Queue} wl_pointer#8.button(42, 1005, 272, 1) +POINTER_BUTTON target=POPUP_A serial=42 button=272 state=1 +[22:46:40.835696] {Default Queue} -> wl_compositor#5.create_surface(new id wl_surface#13) +[22:46:40.835704] {Default Queue} -> xdg_wm_base#6.get_xdg_surface(new id xdg_surface#15, wl_surface#13) +[22:46:40.835712] {Default Queue} -> xdg_wm_base#6.create_positioner(new id xdg_positioner#16) +[22:46:40.835719] {Default Queue} -> xdg_positioner#16.set_size(170, 130) +[22:46:40.835726] {Default Queue} -> xdg_positioner#16.set_anchor_rect(20, 80, 1, 1) +[22:46:40.835733] {Default Queue} -> xdg_positioner#16.set_anchor(5) +[22:46:40.835738] {Default Queue} -> xdg_positioner#16.set_gravity(8) +[22:46:40.835745] {Default Queue} -> xdg_surface#15.get_popup(new id xdg_popup#17, xdg_surface#12, xdg_positioner#16) +POPUP_B CREATE wl=13 xdg=15 role=17 parent=POPUP_A parent_xdg=12 +POPUP_B GRAB_REQUEST origin=wl_pointer.button target=POPUP_A serial=42 used=42 +[22:46:40.835761] {Default Queue} -> xdg_popup#17.grab(wl_seat#7, 42) +[22:46:40.835768] {Default Queue} -> wl_surface#13.commit() +[22:46:40.835775] {Default Queue} -> xdg_positioner#16.destroy() +[22:46:40.835781] {Default Queue} wl_pointer#8.frame() +[22:46:40.835930] {Display Queue} wl_display#1.delete_id(16) +[22:46:40.835937] {Default Queue} xdg_popup#17.configure(20, 80, 170, 130) +POPUP_B ROLE_CONFIGURE x=20 y=80 w=170 h=130 +[22:46:40.835949] {Default Queue} xdg_surface#15.configure(43) +POPUP_B XDG_CONFIGURE serial=43 ack +[22:46:40.835960] {Default Queue} -> xdg_surface#15.ack_configure(43) +[22:46:40.836118] {Default Queue} -> wl_shm#4.create_pool(new id wl_shm_pool#16, fd 5, 88400) +[22:46:40.836128] {Default Queue} -> wl_shm_pool#16.create_buffer(new id wl_buffer#18, 0, 170, 130, 680, 1) +[22:46:40.836135] {Default Queue} -> wl_surface#13.attach(wl_buffer#18, 0, 0) +[22:46:40.836142] {Default Queue} -> wl_surface#13.damage_buffer(0, 0, 170, 130) +[22:46:40.836149] {Default Queue} -> wl_surface#13.commit() +[22:46:40.836155] {Default Queue} -> wl_buffer#18.destroy() +[22:46:40.836168] {Default Queue} -> wl_shm_pool#16.destroy() +POPUP_B MAP wl=13 xdg=15