libs-quartzcore — QuartzCore/CoreAnimation gaps for WebKit Sub-plan

What must be added to gnustep/libs-quartzcore for WebKit v5. Includes 6 open PRs from DTW-Thalion that should be merged first. For the minimal build (ENABLE_GPU_PROCESS=OFF), most gaps are bypassed by software rendering. Parent plan: WebKit v5 Cocoa-native porting plan.

1. TL;DR

Where things stand

2. Post-merge audit — 6 DTW-Thalion PRs merged Done

6 PRs from DTW-Thalion were merged into pkgdemon/libs-quartzcore. All 6 verified as correctly implemented.

What was verified

PR What Verified
#13 CATransform3DInvert — now divides by determinant Confirmed — full cofactor-matrix inversion with epsilon check
#16 CATransform3DMakeAffineTransform, CATransform3DIsAffine, CATransform3DGetAffineTransform Confirmed — all 3 functions present with correct implementation
#15 CAMediaTimingFunction.h imports CGBase.h for CGFloat Confirmed — unconditional import
#11 Heap-allocate pixel buffer in CAGLTexture _writeToPNG Confirmed — uses malloc() with NULL guard
#12 CATransform3D test suite (289 lines) Confirmed — covers identity, translate, scale, rotate, concat, invert
#14 CAMediaTimingFunction test suite (229 lines) Confirmed — covers all 5 named functions + custom + solveForInput

Remaining Phase 2 gaps (blocking initial rendering)

These 3 items are still missing and needed before WebKit can use CALayer:

  1. CALayer.frame — Declared as @property, implemented via @synthesize only. This creates a simple ivar-backed getter/setter, but Apple's CALayer.frame is a computed property derived from position, bounds, and anchorPoint. Setting frame should update position and bounds. The current implementation reads/writes _frame independently — semantically wrong for WebKit.
  2. CALayer geometry conversionconvertPoint:fromLayer:, convertPoint:toLayer:, convertRect:fromLayer:, convertRect:toLayer: are declared in the header but inside #if 0 in CALayer.m (lines 1229–1237). Zero implementation.
  3. CATransaction.completionBlock — Only a TODO comment in the header (kCATransactionCompletionBlock). No ivar, no property, no invocation in commit.

Note: convertTime:fromLayer: and convertTime:toLayer: ARE implemented (the time variants work, the spatial variants don't).

Phase 5+ gaps (unchanged)

CADisplayLink (header-only stub), CATiledLayer, CATextLayer, CAGradientLayer (partial stubs), CALayer.mask (not declared). These are bypassed by software rendering in the minimal build.

Updated coverage

libs-quartzcore coverage is now ~55% (up from ~50% pre-merge). The affine bridge and invert fix are directly relevant to WebKit's compositing layer. The 3 Phase 2 gaps (frame, geometry conversion, completionBlock) remain the items to coordinate with DTW-Thalion.

3. Open PRs to merge first Merged

Seven PRs are open against gnustep/libs-quartzcore as of July 2026. Six are from DTW-Thalion and are in good shape. One (#6, ethanc8) has stalled and needs rework before merging. The pkgdemon fork should cherry-pick or merge all DTW-Thalion PRs immediately — they fix real bugs and improve build quality with no known regressions.

PR Author What it does WebKit impact Priority
#16 DTW-Thalion Add CATransform3DMakeAffineTransform, CATransform3DIsAffine, CATransform3DGetAffineTransform — the bridge between 2D CGAffineTransform and the 4×4 CATransform3D matrix Direct. WebKit frequently bridges CG and CA transforms, e.g. when mapping a layer's geometry into a CGAffineTransform for hit-testing or when setting layer.affineTransform. Without this bridge, any code path involving CATransform3DIsAffine will crash or produce garbage. Merge now
#15 DTW-Thalion Make CAMediaTimingFunction.h self-contained — removes a hidden transitive include dependency so the header can be included in isolation Build quality. WebKit includes QuartzCore headers in piecemeal order; an include cycle or missing transitive header causes hard-to-diagnose build failures. Makes the header safe to include from any translation unit. Merge now
#14 DTW-Thalion CAMediaTimingFunction tests — unit tests for the timing function interpolation logic (linear, ease, ease-in, ease-out, ease-in-out, custom cubic Bézier control points) Test coverage. Catches regressions in animation timing interpolation, which affects CSS transitions and animation-timing-function behavior in WebKit. Merge now
#13 DTW-Thalion Fix CATransform3DInvert — the existing implementation computed the adjugate (cofactor transpose) matrix but omitted the final division by the determinant. The result is a matrix scaled by det(M), not the true inverse M−1. Critical bug. Any code using CATransform3DInvert gets a wrong matrix. In WebKit this corrupts: (1) hit-testing when layers have non-identity transforms (the point-in-layer conversion uses the inverse of the layer's accumulated transform), (2) animation keyframe interpolation, (3) convertPoint:toLayer:/convertPoint:fromLayer: which walk the transform tree in both directions. Every transformed layer on-screen has broken input handling until this is fixed. Merge now — critical
#12 DTW-Thalion CATransform3D tests — comprehensive test coverage for all CATransform3D math functions (concatenation, inversion, rotation, scale, translation, affine conversion) Test coverage. Guards against future regressions in transform math. Also serves as executable documentation of expected behavior. Merge now
#11 DTW-Thalion Heap-allocate the pixel buffer in _writeToPNG — previously the buffer was stack-allocated with a fixed size, causing a stack overflow for large textures (e.g. a retina-density web page tile) Crash fix. WebKit renders at contentsScale 2.0 on HiDPI displays. A 1024×1024 tile at 2× scale = 2048×2048 RGBA = 16 MB — far beyond any reasonable stack limit. Without this fix, any PNG-capture code path (used for debugging, screenshots, and potentially for software compositing transfer) will stack-overflow silently. Merge now
#6 ethanc8 Build fixes — corrects various compile errors found when building libs-quartzcore on non-macOS systems Build quality. Stalled — the PR has unresolved review comments and may conflict with the DTW-Thalion changes. Needs a rebase and a fresh review pass before merging. Review — needs rework
Fork strategy: Create pkgdemon/libs-quartzcore forked from gnustep/libs-quartzcore. Apply PR #11, #12, #13, #14, #15, #16 as a merge or cherry-pick series (in that order, so tests run against the fixed code). Hold #6 until rebased. Build against the WebKit source tree and run the new test suite before tagging a release.

4. Current coverage

libs-quartzcore is approximately 50% of the full QuartzCore surface area that macOS exposes to WebKit. For the minimal build the effective gap is much smaller because ENABLE_GPU_PROCESS=OFF routes rendering through CGBitmapContext, bypassing most CALayer compositing. The table below reflects the state after merging all DTW-Thalion PRs.

Class / API Status Notes
CALayer — basic properties Working bounds, position, anchorPoint, opacity, backgroundColor, hidden, sublayers, superlayer, zPosition, transform, sublayerTransform, contents, contentsRect, delegate, shouldRasterize, contentsScale, masksToBounds, cornerRadius, borderWidth, borderColor, shadowColor, shadowOffset, shadowOpacity, shadowRadius
CALayer.frame Missing Derived property (computed from bounds/position/anchorPoint/transform). WebKit uses this constantly. See §5a.
CALayer.mask Missing Alpha-channel masking. Used in CSS clip-path and mask compositing.
CALayer — geometry conversion Missing convertPoint:fromLayer:, convertPoint:toLayer:, convertRect:fromLayer:, convertRect:toLayer:, convertTime:fromLayer:, convertTime:toLayer:. See §5b.
CALayer — animations dict Working addAnimation:forKey:, removeAnimationForKey:, removeAllAnimations, animationForKey:, animationKeys
CALayer — presentation/model Working presentationLayer, modelLayer
CALayer — hit testing Partial hitTest: exists but depends on CATransform3DInvert (broken until PR #13 merges) and missing geometry conversion methods.
CABasicAnimation Working Full: fromValue, toValue, byValue, interpolation
CAKeyframeAnimation Working Full: values, keyTimes, timingFunctions, calculationMode (linear/discrete/paced)
CASpringAnimation Working Full: mass, stiffness, damping, initialVelocity, settlingDuration
CATransition Partial MoveIn type implemented; Push, Reveal, Fade not implemented. WebKit uses fade transitions.
CAPropertyAnimation Working Base class for Basic/Keyframe; keyPath, additive, cumulative
CAMediaTiming protocol Working beginTime, duration, speed, timeOffset, repeatCount, repeatDuration, autoreverses, fillMode
CAMediaTimingFunction Working After PR #15: self-contained header. Cubic Bézier interpolation, named presets (linear, easeIn, easeOut, easeInEaseOut, default). PR #14 adds tests.
CATransaction Partial begin, commit, flush, lock, unlock, animationDuration, setAnimationDuration:, disableActions, setDisableActions:, animationTimingFunction. Missing: completionBlock (see §5c).
CATransform3D — math Working After PR #13: all functions correct. Concatenation, rotation, scale, translation, inversion, equality, identity check. After PR #16: CATransform3DMakeAffineTransform, CATransform3DIsAffine, CATransform3DGetAffineTransform. PR #12 adds tests.
CAShapeLayer Working Full: path, fillColor, strokeColor, lineWidth, lineDashPattern, strokeStart, strokeEnd, fillRule, lineCap, lineJoin
CARenderer Working OpenGL-backed compositor. rendererWithCGLContext:options:, setLayer:, beginFrameAtTime:timeStamp:, render, endFrame. Only used when ENABLE_GPU_PROCESS=ON.
CAAction protocol Working runActionForKey:object:arguments:
CAFilter Constants only Filter name constants declared; no actual image-filter processing. Used by WebKit for blur/saturate filters on layers.
CAValueFunction Constants only Function name constants declared; no evaluation logic. Used for animating transform components individually (rotate.x, scale.y, etc.).
CAGradientLayer Header only Declares colors, locations, startPoint, endPoint, type. No drawInContext: implementation.
CATextLayer Header only Declares string, font, fontSize, foregroundColor, alignmentMode, wrapped. No rendering.
CADisplayLink Header only Class declared; no implementation. See §6a.
CAScrollLayer Header only Declares scrollToPoint:, scrollToRect:, scrollMode. No implementation.
CAReplicatorLayer Header only Declares instanceCount, instanceDelay, instanceTransform, instanceColor. No implementation.
CATiledLayer Header only Declares tileSize, levelsOfDetail, levelsOfDetailBias. No implementation. See §6b.
CATransformLayer 24-line stub Subclasses CALayer; no 3D-pass-through compositing.
CAOpenGLLayer Minimal stub Not required for ENABLE_GPU_PROCESS=OFF.
CAContext Not present Apple SPI for cross-process layer hosting. See §7a.

5. Gaps — Phase 2 (needed for initial rendering)

These are the QuartzCore gaps that must be resolved before WebKit can use CALayer at all, even in the software-rendering path. They are not blocked on GPU compositing — they are fundamental layer-tree operations that WebKit's layout and rendering engines call unconditionally.

5a. CALayer.frame property

Missing. CALayer currently exposes only bounds and position. WebKit reads and writes layer.frame in dozens of places — e.g. RenderLayerBacking::updateGeometry(), GraphicsLayerCA::setPosition(), GraphicsLayerCA::setSize().

frame is a derived property. On macOS it is defined as:

Implementation location: Source/CALayer.m

/* Getter — simplified (identity transform fast path) */
- (CGRect)frame
{
    CGRect bounds   = self.bounds;
    CGPoint anchor  = self.anchorPoint;   /* default {0.5, 0.5} */
    CGPoint pos     = self.position;
    CATransform3D t = self.transform;

    if (CATransform3DIsIdentity(t)) {
        return CGRectMake(pos.x - anchor.x * bounds.size.width,
                          pos.y - anchor.y * bounds.size.height,
                          bounds.size.width,
                          bounds.size.height);
    }
    /* Non-identity: transform all four corners, return AABB */
    CGFloat x[4], y[4];
    CGFloat w = bounds.size.width, h = bounds.size.height;
    CGFloat ox = anchor.x * w, oy = anchor.y * h;
    /* ... apply 3D transform to each corner, collect min/max ... */
    return CGRectMake(/* min x */, /* min y */, /* max-min x */, /* max-min y */);
}

/* Setter */
- (void)setFrame:(CGRect)frame
{
    self.bounds   = CGRectMake(0, 0, frame.size.width, frame.size.height);
    CGPoint anchor = self.anchorPoint;
    self.position  = CGPointMake(frame.origin.x + anchor.x * frame.size.width,
                                 frame.origin.y + anchor.y * frame.size.height);
}

Estimated LOC: 40–60 in CALayer.m. The non-identity-transform corner case adds ~20 lines.
Blocking: Phase 2. WebKit cannot use CALayer without this.

5b. CALayer geometry conversion methods

Missing. The six geometry/time conversion methods are not implemented: convertPoint:fromLayer:, convertPoint:toLayer:, convertRect:fromLayer:, convertRect:toLayer:, convertTime:fromLayer:, convertTime:toLayer:.

These are used extensively in WebKit for hit testing (GraphicsLayerCA::pointInLayerRect) and for mapping coordinates between the web-layer hierarchy and the native view hierarchy.

Algorithm: To convert a point from layer A to layer B:

  1. Walk from A to the common ancestor, accumulating the product of each layer's transform scaled by contentsScale and offset by position/anchorPoint. This gives transform TA→root.
  2. Walk from B to the same common ancestor, accumulating transforms to get TB→root.
  3. Result transform = TB→root−1 × TA→root. Apply to the point. (This is why PR #13's fix is required — the inversion must be correct.)

Finding the common ancestor: standard lowest-common-ancestor walk using the superlayer chain. If there is no common ancestor (different layer trees), return an undefined value — macOS documents this as undefined behavior.

Estimated LOC: 100–150 in CALayer.m. The LCA walk is ~40 lines; the transform accumulation is ~40 lines; the point/rect variants share most code.
Blocking: Phase 5 (hit testing), but implementing now avoids a second edit pass.

5c. CATransaction.completionBlock

Stub / TODO. CATransaction has a completionBlock property marked TODO in the implementation. WebKit uses this in GraphicsLayerCA::flushCompositingState and in animation completion teardown paths.

On macOS, completionBlock is called on the main thread after all animations in the transaction have finished (i.e., after the last animation's CAAnimationDelegate animationDidStop:finished: has been called and the layer tree has been committed to the render server).

Implementation plan:

  1. Add a _completionBlock ivar (dispatch_block_t / void (^)(void)) to the CATransaction stack frame.
  2. Expose +completionBlock and +setCompletionBlock: class methods that get/set the block on the current transaction stack frame (same pattern as animationDuration).
  3. In +commit, after all animations for this transaction have been applied and the layer tree has been flushed, invoke the completion block on the main thread.
  4. For the software rendering path (ENABLE_GPU_PROCESS=OFF), animations resolve synchronously during commit, so the completion block can be called immediately after commit returns.

Estimated LOC: 30–50 in CATransaction.m.
Blocking: Phase 2 if any WebKit code path unconditionally sets a completion block before committing. Phase 5 otherwise.

6. Gaps — Phase 5+ (accelerated compositing)

These gaps are not blocking for the minimal software-rendering build but are required for Phase 5 (interactive compositing, smooth animations, 60 Hz rendering) and Phase 6 (full CSS visual effects).

Header only — no implementation. WebKit uses CADisplayLink for vsync-aligned animation frame scheduling in DisplayRefreshMonitorCocoa and WKDisplayLinkRunLoopSource. macOS added CADisplayLink as a public API in macOS 14 (Sonoma); it was previously iOS-only. WebKit on macOS 14+ uses it for main-thread animation timing.

On macOS, CADisplayLink calls its target/selector once per display refresh, at the hardware vsync interval, delivered on a specified NSRunLoop. It exposes timestamp (time of the last frame), targetTimestamp (time of the next frame), duration (frame interval), and preferredFramesPerSecond.

Implementation plan for GNUstep (X11 environment):

  1. Query refresh rate from X11/RandR: Call XRRGetScreenInfo() to get the current monitor's SizeID and then XRRConfigCurrentRate(). Fall back to 60 Hz if RandR is unavailable or returns 0. Cache the result at CADisplayLink creation time; re-query on display configuration change notifications.
  2. Timer source: Use dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue()) with dispatch_source_set_timer(source, DISPATCH_TIME_NOW, interval_ns, leeway_ns) where interval_ns = 1,000,000,000 / refresh_rate and leeway_ns = interval_ns / 10.
  3. Run loop integration: To match the macOS behavior of delivering callbacks on a specific NSRunLoop, schedule the GCD source on a queue associated with that run loop. For the main run loop this is dispatch_get_main_queue().
  4. Timestamp: In the timer handler, query CACurrentMediaTime() (which calls mach_absolute_time() or clock_gettime(CLOCK_MONOTONIC) on GNUstep) to fill timestamp and targetTimestamp = timestamp + duration.
  5. CVDisplayLink fallback: WebKit also accepts a CVDisplayLink (from libs-corevideo). If libs-corevideo implements CVDisplayLinkSetOutputCallback, CADisplayLink can be a thin wrapper. For now, implement CADisplayLink standalone and leave the CVDisplayLink path for later.
@implementation CADisplayLink {
    dispatch_source_t _timer;
    id                _target;
    SEL               _selector;
    CFTimeInterval    _duration;
    CFTimeInterval    _timestamp;
    CFTimeInterval    _targetTimestamp;
    BOOL              _paused;
}

+ (CADisplayLink *)displayLinkWithTarget:(id)target selector:(SEL)sel
{
    CADisplayLink *dl = [[self alloc] init];
    dl->_target   = target;
    dl->_selector = sel;
    dl->_duration = 1.0 / [self _queryRefreshRate];
    return dl;
}

- (void)addToRunLoop:(NSRunLoop *)rl forMode:(NSRunLoopMode)mode
{
    NSTimeInterval iv = _duration;
    uint64_t ns = (uint64_t)(iv * 1e9);
    _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0,
                                    dispatch_get_main_queue());
    dispatch_source_set_timer(_timer, DISPATCH_TIME_NOW, ns, ns / 10);
    __weak CADisplayLink *weakSelf = self;
    dispatch_source_set_event_handler(_timer, ^{
        [weakSelf _fire];
    });
    dispatch_resume(_timer);
}

- (void)_fire
{
    _timestamp       = CACurrentMediaTime();
    _targetTimestamp = _timestamp + _duration;
    [_target performSelector:_selector withObject:self];
}
@end

Estimated LOC: 150–200 in Source/CADisplayLink.m + header updates.
Blocking: Phase 5. Without this, animation frame timing falls back to a fixed timer in WebKit, which is imprecise and not vsync-aligned.

6b. CATiledLayer

Header only — no implementation. Used for tile-based web page rendering. WebKit splits large web pages into a grid of tiles, each rendered independently at the appropriate zoom level. CATiledLayer manages this tile grid and calls the delegate or drawInContext: override for each tile as it becomes visible.

macOS CATiledLayer behavior:

Implementation plan:

  1. Subclass CALayer in Source/CATiledLayer.m.
  2. In display or a custom render hook, determine which tiles intersect the current visibleRect (derived from the superlayer hierarchy).
  3. For each needed tile, dispatch a CGBitmapContext creation + drawInContext: call to a background queue (use dispatch_async with a concurrent queue). Set the CTM to position the context over the tile's rect before calling drawInContext:.
  4. When the tile's context is complete, set it as contents on a sublayer tile (or composite it into the parent layer's backing store) on the main queue.
  5. Manage a tile cache with LRU eviction — keep at most levelsOfDetail * (visible_tiles + margin) tiles in memory.

Estimated LOC: 300–500 in Source/CATiledLayer.m.
Blocking: Phase 6+ (performance optimization for large pages).

6c. CATextLayer

Header only — no implementation. Text rendering in a composited layer. WebKit uses CATextLayer for certain text overlays and in some subframe rendering paths.

Implementation plan:

  1. Subclass CALayer; override drawInContext:.
  2. The string property accepts either an NSString or an NSAttributedString. If plain string, construct an NSAttributedString applying font, fontSize, foregroundColor, alignmentMode as paragraph/character attributes.
  3. Lay out using CoreText: create a CTFramesetter from the attributed string, create a CGPath from bounds, create a CTFrame, call CTFrameDraw(frame, ctx).
  4. If wrapped = NO, truncate at the layer width using a CTLine with kCTLineTruncationEnd.
  5. Handle truncationMode: none, start, middle, end.

Estimated LOC: 200–300 in Source/CATextLayer.m.
Blocking: Phase 6+.

6d. CAGradientLayer

Header only — no implementation. Gradient rendering in a composited layer. Used for CSS background: linear-gradient() and radial-gradient() in composited rendering paths.

Implementation plan:

  1. Subclass CALayer; override drawInContext:.
  2. Build a CGGradient from the colors array (array of CGColorRef) and locations array (array of CGFloat). If locations is nil, distribute stops evenly.
  3. For type = kCAGradientLayerAxial (linear, the default): call CGContextDrawLinearGradient(ctx, gradient, startPt, endPt, kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation). startPoint and endPoint are in unit coordinate space (0,0 = bottom-left, 1,1 = top-right — note: CA uses flipped Y for gradient unit space on macOS). Map to pixel coordinates by multiplying by bounds.size.
  4. For type = kCAGradientLayerRadial: call CGContextDrawRadialGradient with the center derived from startPoint.
  5. For type = kCAGradientLayerConic (macOS 12+): not in WebKit's minimal path; stub with linear fallback.

Estimated LOC: 100–150 in Source/CAGradientLayer.m.
Blocking: Phase 6+.

6e. CALayer.mask

Missing. The mask property sets another CALayer as the alpha-channel mask for the receiver. Used in CSS clip-path, mask, and -webkit-mask properties in composited rendering paths.

Implementation plan:

  1. Add a _mask ivar (CALayer *) to CALayer. Implement -mask / -setMask: accessors.
  2. In the compositor's render path (in CARenderer or CALayer's renderInContext:), after rendering the receiver's subtree into an off-screen buffer:
    1. Render the mask layer into a separate single-channel (alpha-only) bitmap context.
    2. Apply the alpha channel of the mask as a clip mask to the main context using CGContextClipToMask(ctx, bounds, maskImage).
    3. Composite the masked result into the parent context.
  3. The mask layer's position, bounds, and transform are applied relative to the masked layer's coordinate space.
  4. The mask layer is not rendered into the parent layer tree as a visible sublayer — it is invisible except through its effect on the masked layer's alpha.

Estimated LOC: 100–200 across CALayer.m and the render path.
Blocking: Phase 6+.

7. Gaps — Phase 7+ (multi-process compositing)

7a. CAContext (Apple SPI)

Not present. The most architecturally complex missing piece in all of libs-quartzcore. CAContext is private Apple SPI that hosts a CALayer tree across process boundaries using Mach ports. The UIProcess holds a context ID; the WebProcess renders into the matching context; the display compositor merges them without a shared-memory pixel copy.

Why it matters for WebKit: When ENABLE_GPU_PROCESS=ON, the GPU Process renders layer trees and the UIProcess displays them. On macOS this handoff is via CAContext + Mach ports + IOSurface. Without CAContext, the GPU Process path cannot work.

Alternative approaches for GNUstep (in ascending complexity):

Approach Mechanism GPU needed? Phase
Software pixel blit WebProcess renders into a CGBitmapContext backed by a POSIX shared memory region (shm_open / memfd_create). UIProcess maps the same region and blits it to the screen via NSBitmapImageRep + drawInRect:. This is v4's architecture — proven correct in principle. No Phase 3–4 (current target)
DMA-BUF sharing WebProcess renders into a DRM/KMS DMA-BUF surface (Linux kernel primitve). UIProcess imports the same DMA-BUF as an OpenGL texture and composites it. Zero pixel copy. Requires a DRM device and EGL. Yes (EGL) Phase 7+
CAContext over Unix sockets + shm Implement CAContext as a GNUstep extension using a Unix domain socket for control messages and POSIX shm for layer-tree serialization. The compositor in the UIProcess deserializes the tree and renders it. Closest to the macOS architecture; highest implementation cost. Optional Phase 8+

Recommended approach: For Phase 1–6, keep ENABLE_GPU_PROCESS=OFF and use the software pixel blit path. This entirely sidesteps CAContext. Implement CAContext stubs (enough to compile, panic at runtime) to allow the source tree to build with ENABLE_GPU_PROCESS=ON for future work.

Stub implementation:

@interface CAContext : NSObject
+ (CAContext *)contextWithCGSConnection:(uint32_t)cid options:(NSDictionary *)opts;
@property (readonly) uint32_t contextId;
@property (retain) CALayer *layer;
@end

@implementation CAContext
+ (CAContext *)contextWithCGSConnection:(uint32_t)cid options:(NSDictionary *)opts
{
    [NSException raise:NSInternalInconsistencyException
                format:@"CAContext: multi-process compositing not implemented on GNUstep"];
    return nil;
}
@end

7b. CABackdropLayer, CAPresentationModifier, CARenderServer (Apple SPI)

Not present. All three are private Apple SPI used by WebKit's visual effects layer path (CABackdropLayer for backdrop-filter blur, CAPresentationModifier for in-flight animation modification, CARenderServer for direct render server communication).

These are guarded by #if HAVE(CA_BACKDROP_LAYER), #if HAVE(CA_PRESENTATION_MODIFIER), and similar feature-detection macros in WebKit. OptionsGNUstep.cmake should define these as OFF:

set(HAVE_CA_BACKDROP_LAYER    OFF CACHE BOOL "" FORCE)
set(HAVE_CA_PRESENTATION_MODIFIER OFF CACHE BOOL "" FORCE)
set(HAVE_CA_RENDER_SERVER     OFF CACHE BOOL "" FORCE)

With these flags off, WebKit's preprocessor guards will exclude all code that references these SPI classes. No stub implementations are needed — the feature-disable path is the right approach. Priority: Phase 7+ (and even then, only if backdrop-filter CSS is needed).

8. Testing

libs-quartzcore has an existing test infrastructure in the Tests/ directory. The test programs are visual X11 applications that open windows and render animations — useful for interactive verification but not directly CI-runnable without a display.

DTW-Thalion's new test coverage (from PRs #12 and #14)

Headless testing via EGL (extending PR #11's approach)

PR #11 switches the pixel buffer from stack to heap — a side effect is that the PNG capture path becomes usable for automated image comparison tests. The strategy:

  1. Create an EGL/GLES2 offscreen context using EGL_EXT_platform_surfaceless (no X11 required). Mesa's software renderer (llvmpipe or softpipe) supports this on any Linux CI host.
  2. Initialize a CARenderer against the EGL context.
  3. Render a known layer tree via CARenderer.
  4. Read back the pixels with glReadPixels.
  5. Write to PNG via the (now heap-safe) _writeToPNG path and compare against a reference image using a pixel-diff threshold.

This pattern gives visual regression tests that run on CI without a real GPU or X11 server. Extend the existing CARendererTest program to support a --headless flag that activates this path.

Test coverage targets after all PRs merged

ComponentTest typeCoverage after PRs
CATransform3D math Unit (non-visual) Full — PR #12
CATransform3DInvert Unit (algebraic identity check) Full — PR #12 + PR #13
CATransform3D affine bridge Unit (round-trip CGAffineTransform) Full — PR #12 + PR #16
CAMediaTimingFunction Unit (interpolation accuracy) Full — PR #14
CALayer compositing Visual (X11 / EGL headless) Partial — existing visual tests
CALayer.frame Unit (geometry) Not yet — must add with implementation
Geometry conversion methods Unit (coordinate mapping) Not yet — must add with implementation
CATransaction.completionBlock Unit (callback fires) Not yet — must add with implementation
CADisplayLink Integration (timer fires at ~60 Hz) Not yet
CI recommendation: Add a make check target to libs-quartzcore that runs the non-visual unit tests (CATransform3D, CAMediaTimingFunction, CALayer geometry) and the EGL headless visual tests as a single command. These should pass on any Linux host with Mesa installed (pkg install mesa-dri on NextBSD). Visual X11 tests remain opt-in via make check-visual.