Skip to content

DWM frame pipeline

ALTaleX edited this page Aug 25, 2026 · 1 revision

DWM frame pipeline

This page describes the part of the Desktop Window Manager frame pipeline that matters when implementing backdrop glass. Names and object layouts change between Windows generations, but the ordering remains recognizable:

  1. apply pending MIL/DirectComposition resource and property updates;
  2. traverse the visual tree in PreCompute and assemble a dirty-region set;
  3. optimize that set into a bounded collection of dirty rectangles;
  4. traverse again with COcclusionContext, using drawing-like interfaces to collect occlusion and glass coverage;
  5. render each unoccluded dirty region through RenderDirtyRegion and DrawVisualTree into the selected target;
  6. present the updated desktop surface.

OpenGlass needs to participate in stages 2 through 5. A custom shader alone cannot make backdrop glass correct: the dirty and occlusion stages decide whether the source pixels required by that shader are updated or discarded before the draw begins.

Pipeline overview

flowchart TD
    A[Pending channel batches and animated properties] --> B[Apply MIL / DComp object updates]
    B --> C[CleanTree / CPreComputeContext traversal]
    C --> D[Collect local changes and transform them to target space]
    D --> E[CDirtyRegion Optimize and merge rectangles]
    E --> F[COcclusionContext traversal over dirty coverage]
    F --> G[Simulated drawing through occlusion interfaces]
    G --> H[Occluder set plus glass coverage set]
    H --> I[BeginFrame and select the current device target]
    I --> J[RenderDirtyRegion selects an unoccluded dirty rectangle]
    J --> K[CDrawingContext::DrawVisualTree]
    K --> L[Render nodes, overlays and custom glass]
    L --> M[EndFrame and present]
Loading

The exact owner of each step differs. Windows 7 keeps much of the sequence inside CDrawingContext::Render; Windows 10 separates visual-tree cleaning, target-specific RenderDirtyRegion, overlay/direct-flip decisions, and final drawing. The conceptual boundaries are more durable than any one function name.

The BeginFrame boundary is target- and generation-specific. In the Windows 7-era path, the target begins the frame before CDrawingContext::Render performs PreCompute and occlusion. In the Windows 10 path examined here, tree cleaning/PreCompute is separated into PreRender, while BeginFrame selects the target immediately before target-specific RenderDirtyRegion. The invariant is that actual drawing occurs only after the target is selected and the dirty/occlusion data needed by that draw has been prepared.

1. Apply control-side updates

uDWM and DirectComposition are the logical control side. They create resources and visuals, change properties, and submit command batches. dwmcore processes those commands before using the visual tree for the next frame.

In the Windows 7-era implementation, CCrossThreadComposition::OnBeginComposition obtains pending batches and calls ProcessBatches. CComposition::ProcessCommandBatch dispatches each command to the corresponding resource update routine. Functions such as CCombinedGeometry::ProcessUpdate, brush updates, transform updates, and render-data updates mutate the server-side MIL objects.

In the Windows 10 implementation, CComposition::ProcessCommandBatch similarly validates a batch and dispatches messages through ProcessMessage to the target resources. The class hierarchy and packet format are larger, but the responsibility is unchanged.

In the examined Windows 11 25H2 sample, the command surface has grown substantially: CGlobalComposition::ProcessMessage is the large resource-command dispatcher, while CComposition::UpdateAnimateResources advances animated state. The implementation has changed, but retained object updates still precede tree cleaning and target rendering.

Animated values are a separate source of per-frame change. On the Windows 7-era sample, CComposition::Render calls the schedule manager and UpdateAnimateResources before rendering targets. Modern builds retain equivalent animation/property advancement even when the function boundaries differ.

The important distinction is:

  • resource update changes the retained visual/resource graph;
  • rendering consumes the resulting graph;
  • dirty propagation connects the two by recording which previous pixels are no longer valid.

A property update that does not cause the correct visual or subtree to become dirty can leave a perfectly valid resource graph with stale output on screen.

2. PreCompute: turn visual changes into dirty regions

PreCompute is a non-rendering traversal. CPreComputeContext walks the visual tree, maintains transform and clip stacks, examines visual dirty flags and bounds, and contributes rectangles to a CDirtyRegion/CDirtyRegion2 accumulator.

The Windows 7-era CPreComputeContext::PreCompute sequence is explicit:

  1. initialize CDirtyRegion2 with the render-surface bounds and an allowed-overhead threshold;
  2. add externally invalidated target rectangles;
  3. push the device transform;
  4. walk the visual graph through CGraphIterator;
  5. use PreSubgraph/PostSubgraph to maintain transform, clip, interpolation, and dirty state;
  6. transform local dirty bounds conservatively into page/target space;
  7. add them to CDirtyRegion2.

CPreComputeContext::AddToDirtyRegion reads the current transform, converts local bounds to target space, and forwards the result to CDirtyRegion2::Add. CDirtyRegion2::_Add can intersect, union, annotate, and compare the cost of separate rectangles against the overhead of their union.

The Windows 10 path makes the final optimization boundary particularly clear. CLegacyRenderTarget::PreRender calls CDesktopTree::CleanTree, which reaches CVisualTree::PreCompute. That function:

  • initializes CDirtyRegion;
  • calls CPreComputeContext::PreCompute;
  • lets registered helpers add additional dirty rectangles;
  • calls CDirtyRegion::Optimize;
  • stores the resulting optimized collection for target rendering.

Windows 11 25H2 reorganizes this stage again. CComposition::CleanTrees coordinates all affected trees, calls CVisualTree::BeginPreCompute, constructs a CPreComputeContext, and runs CPreComputeContext::PreCompute for each tree. Dirty state is represented by CTreeDirty; visual methods such as AddBoundsToDirtyRegion and subtree-context methods add transformed rectangles. The old single CVisualTree::PreCompute entry is no longer the useful top-level anchor, but the retained-tree traversal and dirty-collection responsibility remain.

PreCompute does not decide what is visible. It answers a different question: which target-space pixels might have changed because the retained tree changed?

3. Dirty-region optimization and where expansion belongs

A frame rarely needs one full-screen rectangle. DWM maintains a collection so unrelated changes can remain separate. Too many tiny rectangles increase traversal, command, and presentation overhead; one large union redraws unchanged pixels. CDirtyRegion::Optimize/CDirtyRegion2::Union balances those costs and merges regions when the estimated overhead favors it.

Backdrop blur complicates the bounds. A glass pixel samples pixels around it, so a nominal dirty rectangle is too small by the sampling radius. If a changed object lies just outside the glass rectangle but inside the blur kernel, the glass output still changes.

A correct glass integration expands dirty rectangles with these rules:

  • convert the blur sampling radius into the current target's pixel space;
  • expand before occlusion is calculated, because the newly included background must not be discarded;
  • clip the expansion to the render-surface/device bounds;
  • merge overlapping expanded rectangles when possible;
  • preserve target-specific transforms, DPI, rotation, and supersampling;
  • do not replace the collection with an unconditional full-screen dirty region.

The best expansion point is normally at, or immediately around, the final dirty-rectangle optimization boundary before RenderDirtyRegion consumes the set. Expanding before optimization lets overlapping samples collapse into fewer rectangles. Expanding an already optimized span can also work, but overlapping results should be re-integrated or the renderer may draw the same area repeatedly.

Windows 11 25H2 exposes this boundary through CTargetDirtyBase<8>::AddDirtyRegionAndCalcOcclusion. It copies existing target rectangles and the new CTreeDirty rectangles into CMergedRectBase<8>, calls Optimize, calculates occlusion over the merged span, then asks CTreeDirty::GetOptimizedRect for each occlusion-adjusted result and accumulates the surviving target rectangles. This is concrete evidence that dirty merging, occlusion, and target-specific dirty storage are one coordinated pipeline rather than independent fixes.

Expanding only the clip passed to DrawVisualTree is too late if occlusion has already removed backdrop contributors. Expanding only the glass geometry is also insufficient when content behind stationary glass changes.

OpenGlass uses GlassKernel::GetBlurExpansion and architecture-specific GlassIntegrity hooks to carry this requirement into modern dirty and occlusion paths. The exact hook can move, but the invariant does not: the final regions presented to occlusion must already include the blur sampling expansion; overlaps introduced by expansion should be integrated before drawing.

4. Occlusion: a simulated draw

Occlusion is another visual-tree traversal, but it does not normally emit GPU pixels. It asks each visual or render resource what it would cover if drawn, while maintaining transform, clip, opacity, Z/depth, and dirty bounds.

The Windows 7-era implementation makes the simulation literal:

  1. COcclusionContext::Compute clears the ordinary occlusion set and the glass coverage set, records the dirty rectangle, pushes the device transform, and walks the graph.
  2. COcclusionContext::CollectOcclusion calls the visual's virtual CollectOcclusion method.
  3. CVisual::CollectOcclusion checks for render-data content and calls CRenderData::Draw, but supplies COcclusionContext through its IDrawingContext interface instead of a real drawing context.
  4. Render commands therefore call methods such as DrawRectangle, DrawImage, or DrawGlass on COcclusionContext.
  5. Those methods transform and clip geometry and append temporary coverage records instead of drawing pixels.
  6. CollectOcclusion classifies the records into the ordinary occlusion set or the glass coverage set with the current Z value.

This is why "simulated drawing" is the useful mental model: the same retained render data is replayed against a context whose draw methods collect coverage.

The modern Windows 10 implementation changes the interfaces but keeps the idea. COcclusionContext::Compute walks the CVisualTree; CVisual::CollectOcclusion, CWindowNode::CollectOcclusion, and resource methods such as CBrush::AddOcclusionInformation describe what would cover the current dirty region. Overlay candidates and composition surfaces are evaluated in the same broad phase.

Windows 11 25H2 confirms the modern model more directly. COcclusionContext::Compute still walks the visual tree and coordinates overlay collection, while CVisual::CollectOcclusion calls CRenderData::AddOcclusionInformation or CBrush::AddOcclusionInformation rather than replaying legacy render data through an IDrawingContext. CTargetDirtyBase<8>::AddDirtyRegionAndCalcOcclusion supplies the merged dirty span to this calculation. The implementation is no longer literally the Windows 7 drawing interface, but each resource is still asked to describe the coverage it would contribute during a real draw.

Why glass is collected here

Glass is visually in front but depends on pixels behind it. Treating it as an ordinary opaque occluder prevents its backdrop from being rendered. Disabling all occlusion preserves correctness but wastes substantial work.

The occlusion traversal already has the information glass needs:

  • exact transformed geometry;
  • clip state;
  • dirty rectangle;
  • Z/depth order;
  • visual/resource identity;
  • ordinary occluders above and below it.

OpenGlass therefore records marked glass geometry during this simulated draw and builds a depth-aware glass coverage set. Ordinary occlusion remains enabled outside regions that can contribute to visible glass.

This is also where the abandoned ShapeVisual prototype failed. ShapeVisual did not override the virtual CVisual::CollectOcclusion, so its vtable reached the empty base implementation and contributed no shape coverage. Implementing the missing path required recovering the private shape through a large object graph, which created too much version-sensitive Layout surface.

5. RenderDirtyRegion: consume the prepared frame

After dirty and occlusion data are ready, a render target begins a frame and draws only the required target regions.

On Windows 7, CHwndRenderTarget::Render obtains a CDrawingContext, begins the frame, and calls CDrawingContext::Render. That function performs PreCompute, obtains the dirty-region collection, calculates occlusion for each relevant rectangle, obtains the unoccluded dirty rectangle, copies front/back-buffer content when needed, and calls CDrawingContext::DrawVisualTree.

On Windows 10, the responsibilities are split:

  1. CLegacyRenderTarget::PreRender cleans the desktop tree and triggers CVisualTree::PreCompute.
  2. CLegacyRenderTarget::Render calls CDrawingContext::BeginFrame, selects the current IDeviceTarget, and passes the prepared CDirtyRegion to RenderDirtyRegion.
  3. RenderDirtyRegion consumes optimized dirty rectangles, applies target/HDR/overlay/direct-flip decisions, obtains each GetUnOccludedDirtyRect, pushes the active render target and GPU clip, and calls CDrawingContext::DrawVisualTree.
  4. DrawVisualTree traverses the renderable subtree using the dirty bounds and COcclusionContext and emits the actual D2D/D3D work.
  5. EndFrame and the render-target manager decide whether presentation is required.

In the examined 19041.3693 sample, CLegacyRenderTarget::RenderDirtyRegion calls CDirtyRegion::GetUnOccludedDirtyRect before CDrawingContext::DrawVisualTree. CDDisplayRenderTarget::RenderDirtyRegion has the same high-level relationship while adding modern display-swap-chain, HDR, and overlay handling.

Windows 11 25H2 moves more state into target-owned dirty objects. CLegacyRenderTarget::RenderDirtyRegion no longer receives a CDirtyRegion parameter; it obtains the COcclusionContext from CMonitorDirty, performs another bounded CMergedRectBase<8>::Optimize where required, pushes the current device target, and calls the expanded CDrawingContext::DrawVisualTree interface. CLegacyRenderTarget::Render still brackets this with BeginFrame and EndFrame. The desktop-display target has its own CDDisplayRenderTarget::RenderDirtyRegion, reinforcing that target selection and target-specific dirty processing must be audited together.

This stage is attractive for custom rendering because the current target is known. It is also late: dirty and occlusion inputs have already been prepared. A hook here can use the selected D2D/D3D target, but it cannot repair missing backdrop work unless earlier pipeline stages were integrated correctly.

6. DrawVisualTree: actual rendering

CDrawingContext::DrawVisualTree is the real traversal rather than the simulation. It receives the selected dirty bounds and occlusion context, establishes GPU clips and transforms, walks the visual tree, skips covered nodes, clears when required, and invokes the actual resource/brush drawing paths.

OpenGlass should not replace the entire traversal. It uses narrow hooks where a marked glass resource and the current target coexist:

  • Legacy reaches marked MIL geometry through render-data and IDrawingContext::DrawGeometry/D2D fill;
  • MILComp recognizes a tracked visual/clip and replaces CColorBrush::Draw/D2D fill;
  • shared realizers perform backdrop copy, blur, colorization, material, and reflection;
  • scoped state restoration returns the D2D/D3D context to the native traversal.

See How OpenGlass renders glass for those architecture-specific marker and draw paths.

What can be hooked at each stage

Stage Good uses Poor uses
Command/property update Capture marker state, associate resource handles, observe lifetime Draw pixels; no current target exists yet
PreCompute Add/transform dirty bounds, observe invalidation, expand before optimization Decide final visibility without Z/occlusion
Dirty optimization Expand and merge blur-dependent regions, bound redraw cost Identify glass without a durable marker
Occlusion simulation Collect glass coverage with geometry and Z, preserve backdrop contributors Execute expensive blur or mutate GPU state
RenderDirtyRegion Use optimized/unoccluded rectangles and current target Recover backdrop work already discarded earlier
DrawVisualTree/resource draw Execute custom D2D/D3D rendering for a recognized marker Globally hook every draw without target/visual scoping
Present Measure/present completed work Insert geometry-dependent backdrop effects

The strongest implementation usually has one small responsibility at several stages rather than one large hook attempting to reconstruct the entire frame late.

Debugging the pipeline on an unknown build

A practical investigation order is:

  1. Find command-batch processing and one known resource ProcessUpdate method.
  2. Find the visual-tree clean/pre-render entry and the dirty-region owner.
  3. Find the PreCompute traversal and final dirty optimization call.
  4. Find COcclusionContext construction/clear and its visual-tree walk.
  5. Prove how a normal solid rectangle contributes occlusion information.
  6. Prove whether the desired glass carrier overrides or reaches that path.
  7. Find every RenderDirtyRegion variant for desktop, legacy, local-app, remote, capture, and off-screen targets.
  8. Identify the desktop variant and the point where it selects its current device target.
  9. Locate the call to DrawVisualTree and the resource draw used by the marker.
  10. Test HDR, overlays, thumbnails, remote/capture targets, multiple displays, and device loss before treating one path as universal.

Do not assume a function is the desktop path merely because it exposes an ID2D1DeviceContext. Intermediate, thumbnail, capture, and remote targets use many of the same interfaces.

Reference evidence

This document was cross-checked read-only against three named dwmcore IDA databases:

Sample SHA-256 Decisive functions
Win7-era dwmcore.dll 6.1.7762.0 754ceca0adcf03b7a188229fef357f0318bcc639283d1131d456a5de31355268 CComposition::ProcessComposition 0x180001770; CPreComputeContext::PreCompute 0x180007070; COcclusionContext::Compute 0x1800090FC; CVisual::CollectOcclusion 0x180008630; CDrawingContext::Render 0x180006A28; CDrawingContext::DrawVisualTree 0x18000CFAC
Windows 10 22H2 dwmcore.dll 10.0.19041.3693 44a113c5f308f726de0a537607fc1ad5bca4f971280b34b5d830d80302437d31 CComposition::ProcessCommandBatch 0x1800ADCE0; CVisualTree::CleanTree 0x18009A380; CVisualTree::PreCompute 0x180099148; COcclusionContext::Compute 0x180081884; CLegacyRenderTarget::RenderDirtyRegion 0x18009BCF8; CDrawingContext::DrawVisualTree 0x18007FD90
Windows 11 25H2 dwmcore.dll 10.0.26100.8972 ce7a9da65154e004fbf483d9f6b9cb2f1a4dcf6493aa2952622449fb74e8a2ef CComposition::CleanTrees 0x18008AE50; CVisualTree::BeginPreCompute 0x18008AA70; CPreComputeContext::PreCompute 0x18008D8B0; CTargetDirtyBase<8>::AddDirtyRegionAndCalcOcclusion 0x1801615FC; COcclusionContext::Compute 0x18002F410; CVisual::CollectOcclusion 0x1800106D0; CLegacyRenderTarget::RenderDirtyRegion 0x180011580; CDrawingContext::DrawVisualTree 0x180013018

Addresses are sample-local evidence, not reusable offsets or hook declarations. The IDBs were not modified. The Windows 7-era database supplied the clearest semantic model; Windows 10 independently confirmed the split between tree cleaning, dirty optimization, target-specific rendering, and DrawVisualTree; Windows 11 25H2 confirmed the same responsibilities after dirty/occlusion state moved into CTreeDirty, CTargetDirtyBase, and CMonitorDirty.

Related documentation

Clone this wiki locally