acdream/docs/research/2026-06-11-holistic-map/wf2-sky-weather-scenery.md
Erik 5e2f99d08e docs: Phase A comparison + Phase B port plan (holistic building-render investigation)
Deliverable 1: docs/research/2026-06-11-building-render-acdream-vs-retail-
comparison.md - the acdream-vs-retail architecture comparison synthesized
from two ultracode mapping fan-outs (11/12 areas, ~90 agents, every retail
claim Ghidra/pc-cited, every acdream claim file:line, 40/76 divergences
adversarially verified so far; raw per-area evidence committed under
docs/research/2026-06-11-holistic-map/).

Headline findings: (1) retail flattens GfxObjs/cells at load exactly like
us (ConstructMesh + RemoveNonPortalNodes) - the MDI pipeline survives;
(2) the phantom/door mechanism is the skipNoTexture draw-time surface gate
(dat-confirmed); (3) retail never geometrically clips world geometry -
aperture exactness is a DEPTH discipline (punch maxZ1 / seal maxZ2 / gated
clear + far-to-near whole-mesh draws) - reframes #114; (4) flood admission
is already faithful, the trigger/depth/multi-view/cone-culling layers are
missing; (5) #115 root cause verified (boom damping severed from the
published collided viewer); collision A6.P4 design verified with
corrections (signed other_portal_id >= 0 gate).

Deliverable 2: docs/plans/2026-06-11-building-render-port-plan.md - the
phased port plan (BR-1 surface gate, BR-2 depth punch/seal, BR-3 delete
the shell chop, BR-4 draw-driven floods, BR-5 viewconeCheck, BR-6 one
gate, BR-7 collision A6.P4, BR-8 camera/lighting/LOD) with per-phase
acceptance criteria, bug closures, keep-list, and a playable-after-every-
phase migration order. AWAITING USER APPROVAL - no implementation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 05:54:12 +02:00

22 KiB
Raw Blame History

2.3 — Sky, weather, and procedural scenery vs the portal-view discipline

RETAIL

FRAME ENTRY — SmartBox::RenderNormalMode (Ghidra 0x453aa0). The branch key is the VIEWER (collided camera) cell: (viewer.objcell_id & 0xffff) < 0x100 = viewer outdoors. Outdoors: LScape::update_viewpoint(viewer cell) → Render::set_default_view (no portal clip list) → useSunlightSet(1) → LScape::draw. Indoors: if viewer_cell->seen_outside, LScape::update_viewpoint(Position::get_outside_cell_id(viewer)) — this only re-centers the 2D landblock draw list under the indoor viewer, it draws nothing — then RenderDevice vtable+0x48 = RenderDeviceD3D::DrawInside (vtable map pc:1037065; impl 0x0059f0d0) which tail-calls PView::DrawInside(indoor_pview, viewer_cell).

SKY/WEATHER DRAW SITE — LScape::draw (Ghidra 0x00506330): (1) GameSky::Draw(sky, 0) FIRST, (2) back-to-front DrawBlock per landblock with in_view != OUTSIDE, (3) if LScape::weather_enabled: GameSky::Draw(sky, 1) LAST. GameSky::Draw (Ghidra 0x00506ff0) gates its whole body on is_player_outside() || pass==0: the sky pass is unconditional (whenever LScape::draw runs at all), the weather pass additionally requires the PLAYER to be outdoors. SmartBox::is_player_outside (Ghidra 0x00451e80) = (player cell & 0xffff) < 0x100. Depth state for BOTH passes: SetDepthBufferMode(DEPTHTEST_ALWAYS, z-write OFF), zfar ×4, fog forced per LScape::m_override_enabled; restored to (LESSEQUAL, write on) after (0x00507063/0x005070fc). Sky pass iterates sky_obj skipping property bit 0x01 (after-cell members), bit 0x04 objects when !weather_enabled, bit 0x02 under the admin fog override, calling CPhysicsObj::DrawRecursive each (pc:268704-268760). Weather pass = RenderDevice::DrawObjCellForDummies(after_sky_cell) (0x005070da; impl 0x005a0760 = UpdateObjCell + shadow-part sort + DrawObjCell). GameSky owns two dummy CEnvCells, before_sky_cell/after_sky_cell (acclient.h:35426): MakeObject (0x00506ee0) puts props&1 objects in after_sky_cell, the rest in before_sky_cell, and refuses to create props&4 (weather) objects while weather is disabled (also created/deleted on toggle by CreateDeletePhysicsObjects 0x005073c0, pc:268912-269036).

SKY POSITION — SmartBox::set_viewer (0x00452c40) calls LScape::set_sky_position(this->lscape, &this->viewer) (pc:91830 / 0x00452d45; impl 0x00504c30) → GameSky::UpdatePosition (0x00506dd0, BN pc:268569-268618): both dummy cells snap to the VIEWER's cell id + frame; weather objects (bit 0x04) snap x,y to the viewer origin and, when bit 0x08 is clear, pin z := 120.0f (constant 0xc2f00000 stored at 0x00506e96-e98 — absolute world height, not camera-relative). So the rain cylinders (GfxObj 0x01004C42/0x01004C44, ~815 m tall) ride the camera in x,y but hang at fixed world z 120.

INDOOR LOOKING OUT — PView::DrawInside (0x005a5860, pc:433793) → ConstructView (0x005a57b0: zero outside_view.view_count, BFS flood via InitCell/ClipPortals/AddViewToPortals) → PView::DrawCells (0x005a4840, pc:432709). ClipPortals (0x005a5520): a portal whose other_cell_id == 0xFFFFFFFF is the OUTSIDE; if this->draw_landscape, its accumulated clip view is merged into the pview's outside_view via Render::copy_view (clipped when global cliplandscape != 0, unclipped otherwise; 0x005a566c-5711). PView::DrawCells then runs FOUR stages: (a) if outside_view.view_count > 0 → useSunlightSet(1), Render::PortalList = pview, LScape::draw(lscape) — the ENTIRE outdoor world draws through the doorway: sky pass, terrain blocks, buildings, scenery, outdoor statics/creatures, and the weather pass which self-gates on is_player_outside (player indoors → no rain even through the door). Every mesh in this slice is portal-clipped: DrawMesh (0x005a0860) loops PortalList views, viewconeCheck per view, and draws per intersecting view under that view's clip planes. (b) Clear(4 /depth/) — a FULL depth-buffer clear, conditional on portalsDrawnCount/forceClear (0x005a48a9) — then per cell (reverse cell_draw_list) per view, the OUTSIDE portal polys (other_cell == 1) are re-drawn via D3DPolyRender::DrawPortalPolyInternal (0x005a49af-49b7): a "z-stamp" that writes the doorway aperture's true depth back so indoor geometry lying beyond the doorway can't paint over the outside image. (c) useSunlightSet(0) + restore_all_lighting; shells per cell per view via DrawEnvCell (0x0059f170; planeMask=0xffffffff submit pc:427922). (d) per cell: Render::PortalList = the cell's last portal_view; DrawObjCellForDummies(cell) — cell contents portal-clipped.

OUTDOOR CONTENT (what the through-door slice contains) — RenderDeviceD3D::DrawBlock (0x005a17c0): per LandCell, DrawLandCell (0x0059f120 — terrain polys) then DrawSortCell (0x0059f140): if the cell has a building → DrawBuilding, then DrawObjCell (the cell's object list). DrawBuilding (0x0059f2a0, pc:429282-429295) installs outdoor_pview->outdoor_portal_list = building->portals before CPhysicsPart::Draw; portal polys inside the building's DrawingBSP dispatch RenderDeviceD3D::DrawPortal (0x0059f0e0) → PView::DrawPortal (0x005a5ab0, pc:433895) → ConstructView(CBldPortal) (0x005a59a0) → DrawCells of that building's interior. I.e. NESTED recursion: standing inside looking out, another building's interior renders through its open door/window because DrawBuilding runs inside the through-door LScape::draw.

SCENERY — CLandBlock::get_land_scenes (0x00530460, pc:314322): pseudo-random ObjectDesc::Place per scene-type slot; skips cells holding buildings (CSortCell::has_building 0x00530865), road cells (on_road 0x005307ce), too-steep terrain (CheckSlope 0x0053089a); then CPhysicsObj::makeObject → set_initial_frame → add_obj_to_cell(landcell) (0x00530923) + CLandBlock::add_static_object. Scenery is therefore an ordinary per-LandCell object — drawn through DrawSortCell/DrawObjCell and portal-clipped through doorways exactly like every other static. There is no separate scenery draw path.

ACDREAM

ROOT PICK — GameWindow.OnRender: clipRoot = viewerRoot ?? _outdoorNode (src/AcDream.App/Rendering/GameWindow.cs:7497). Steady-state frames (eye indoors OR outdoors) ALL go through RetailPViewRenderer.DrawInside; the clipRoot is null "Outdoor LScape entry" (GameWindow.cs:7546-7587 sky+terrain, 7874-7889 weather) survives only as the pre-spawn/login safety path. Sky policy gate: renderSky = viewerRoot is null || rootSeenOutside (GameWindow.cs:7423).

DRAWINSIDE — src/AcDream.App/Rendering/RetailPViewRenderer.cs:44-109: PortalVisibilityBuilder.Build floods from the root; exit portals (OtherCellId==0xFFFF) union their clipped screen regions into OutsideView (src/AcDream.App/Rendering/PortalVisibilityBuilder.cs:279, 87); the synthetic outdoor node seeds a FULL-SCREEN OutsideView quad (PortalVisibilityBuilder.cs:80-89). Outdoor-node roots additionally merge per-building interior floods within 48 m (RetailPViewRenderer.cs:60-61, 115-145) — interior roots do NOT. ClipFrameAssembler produces ≤8-plane slices + an NDC AABB each; InteriorEntityPartition (src/AcDream.App/Rendering/InteriorEntityPartition.cs:22-79) buckets every landblock entity: indoor ParentCellId→ByCell, outdoor/none→Outdoor, server-guid-without-cell→LiveDynamic.

OUTSIDE SLICE — DrawLandscapeThroughOutsideView (RetailPViewRenderer.cs:214-238) loops slices: SetTerrainClip(slice.Planes) + entity clip routing, then GameWindow.DrawRetailPViewLandscapeSlice (GameWindow.cs:9465-9551): scissor to the slice NDC AABB → SkyRenderer.RenderSky (9486, clip distances ON) → SkyPreScene particles → terrain Draw (9496) → the WHOLE Outdoor bucket (scenery + dat stabs + building exteriors + outdoor-parented live entities) via WbDrawDispatcher, frustum cull + slice clip routing (9503-9512) → outdoor-filtered Scene particles with clip distances DISABLED, scissor only (9518-9530) → SkyRenderer.RenderWeather (9535) + SkyPostScene particles, gated ONLY on renderSky. After all slices: a SCISSORED depth clear per slice, interior roots only (GameWindow.cs:7644-7652; explicitly null for outdoor-node roots); then DrawExitPortalMasks — declared on the context (RetailPViewRenderer.cs:534) but NEVER assigned by GameWindow, so the pass no-ops (RetailPViewRenderer.cs:331-332); then DrawEnvCellShells (GL-clipped only for outdoor roots per #114 scope, :104-105, 378-398); then DrawCellObjectLists + per-cell particles (scissor only, clip off; GameWindow.cs:9553-9580).

SKYRENDERER — src/AcDream.App/Rendering/Sky/SkyRenderer.cs (WB SkyboxRenderManager port): RenderSky draws the pre-scene partition (Properties bit 0x01 clear), RenderWeather the post-scene partition (bit set) (:106-144, 219-235 — correct retail bit semantics, citing MakeObject 0x00506ee0). GL state: depth test OFF + depth mask OFF + cull off + per-submesh additive/alpha blend (:194-210) — equivalent to retail's DEPTHTEST_ALWAYS/no-write. View translation zeroed = camera-centred sky (:175-178). Weather 120 offset applied as a CAMERA-RELATIVE model translation for bit4 && !bit8 objects (:307-308). Fog-override bit 0x02 skip implemented (:240-241). sky.vert writes gl_ClipDistance from the terrain-clip UBO (src/AcDream.App/Rendering/Shaders/sky.vert:153) so the doorway slices genuinely clip sky and rain.

SCENERY — SceneryGenerator.Generate (src/AcDream.Core/World/SceneryGenerator.cs:86 via WbSceneryAdapter) → GameWindow.BuildSceneryEntitiesForStreaming (GameWindow.cs:5290-5473): scenery becomes WorldEntity ids 0x80XXYYII with MeshRefs and NO ParentCellId (5463-5472) → lands in the Outdoor partition bucket → drawn once per outside slice with per-entity frustum cull + slice clip planes. Building suppression at generation uses a 9×9 vertex-grid set derived from building origins (5310-5316). There are no per-LandCell object lists outdoors — the bucket is flat.

DIVERGENCES

[CRITICAL] outside-portal-zstamp-missing (UNVERIFIED (verifier hit token limit)) — No depth re-stamp of outside portal polys after the outside-view draw (and the depth clear is scissored, not full)

  • blastRadius: #108 (grass texture sweeping across the upstairs door opening during the cellar ascent — the outside image / terrain drawn through the doorway region is never re-fenced in depth) and #109 (far exit door oscillating between door texture and background — a per-frame depth race in the doorway rectangle between the slice's cleared depth, the door entity, and shell geometry). Both are top entries in the 2026-06-11 holistic mandate.
  • retailEvidence: PView::DrawCells (0x005a4840, pc:432709): after LScape::draw through outside_view it issues Clear(4 /depth, FULL buffer/) conditional on portalsDrawnCount (0x005a48a9), then per cell per view re-draws every OUTSIDE portal poly (other_cell == 0xFFFFFFFF) via D3DPolyRender::DrawPortalPolyInternal (0x005a49a0-49b7) — writing the doorway aperture's real depth back so indoor geometry beyond the doorway cannot overpaint the outside image, while geometry in front of it still occludes normally.
  • acdreamEvidence: Depth clear is scissored to each slice's NDC AABB and only for interior roots (GameWindow.cs:7644-7652). The z-stamp stage exists as RetailPViewRenderer.DrawExitPortalMasks (RetailPViewRenderer.cs:325-343) but the callback is null — GameWindow's RetailPViewDrawContext initializer (GameWindow.cs:7604-7663) never assigns DrawExitPortalMasks, so the pass no-ops every frame.
  • portShape: Wire the masks stage: after the slice depth-clears, draw each visible cell's exit-portal polygons depth-only (color mask off) per slice — the portal quads already exist as PortalRef polys per the e223325 dat finding. Match retail's full-buffer Clear(depth) gated on any-slice-drawn rather than per-slice scissored clears (or prove the scissored equivalent identical and document it). Order: outside slices → depth clear → portal z-stamp → shells → objects, exactly DrawCells.

[HIGH] weather-indoor-gate (UNVERIFIED (verifier hit token limit)) — Weather pass not gated on is_player_outside — rain draws through doorways while the player is inside

  • blastRadius: Rain/snow visibly composites in the doorway slice (depth-test off) when standing inside a building looking out; retail shows zero weather in that situation. Part of the 'indoor world feels right' gap; no dedicated issue number yet.
  • retailEvidence: GameSky::Draw (0x00506ff0) gates its body on is_player_outside() || pass==0 (0x00507009) — the weather pass (pass 1, DrawObjCellForDummies(after_sky_cell) at 0x005070da) requires the PLAYER cell to be outdoor (SmartBox::is_player_outside 0x00451e80: (cell & 0xffff) < 0x100), independently of the viewer/outside_view. LScape::draw additionally requires LScape::weather_enabled (0x0050638b-96).
  • acdreamEvidence: DrawRetailPViewLandscapeSlice calls _skyRenderer.RenderWeather inside every OutsideView slice gated only on renderSky (GameWindow.cs:9533-9536), and renderSky is the viewer-root seen_outside policy (GameWindow.cs:7423) — the player's indoor/outdoor state is never consulted.
  • portShape: Pass a playerIsOutside bool (player CellId & 0xFFFF < 0x100 — already computed for lighting at GameWindow.cs:7291-7320) into the slice context; skip RenderWeather + the SkyPostScene weather particles when false. RenderSky stays ungated (retail pass-0 is unconditional).

[HIGH] particles-not-portal-clipped (UNVERIFIED (verifier hit token limit)) — Particles draw with clip distances disabled — scissor rectangle only, no portal-plane clipping

  • blastRadius: The reported particles-through-walls bug: an emitter whose particles fall inside the doorway's bounding RECTANGLE but outside the portal WEDGE paints across interior walls; same for per-cell particles bleeding across neighbor cells inside the AABB.
  • retailEvidence: Retail particles are cell objects drawn through DrawObjCellForDummies under the active Render::PortalList — DrawMesh (0x005a0860) loops the portal views and draws only per intersecting view with that view's clip planes installed (Render::set_view), i.e. polygon-level portal clipping identical to statics.
  • acdreamEvidence: Both particle draw sites explicitly DisableClipDistances() before drawing and rely on the slice/cell NDC-AABB scissor alone: outdoor slice particles at GameWindow.cs:9518-9530, per-cell particles at GameWindow.cs:9568-9579 (DrawRetailPViewCellParticles).
  • portShape: Give the particle pipeline the same slice clip the sky already has: write gl_ClipDistance in the particle vertex/billboard shader from the slice planes (the terrain-clip UBO is already bound), enable clip distances around the draw, keep the scissor as a cheap pre-cull.

[MEDIUM] no-nested-building-flood-through-outside-view (UNVERIFIED (verifier hit token limit)) — Interior roots never flood neighbor buildings — their interiors are absent from the through-door outside view

  • blastRadius: Standing inside looking out a doorway at another building with an open door or window: retail renders that building's interior through its aperture; acdream shows the aperture as background/unsealed (same artifact family the outdoor look-in had pre-R-A2). Contributes to 'indoor world feels right'; not yet a numbered issue.
  • retailEvidence: The through-door slice is the full LScape::draw, whose DrawSortCell (0x0059f140) calls DrawBuilding (0x0059f2a0, pc:429282-429295); DrawBuilding installs outdoor_pview->outdoor_portal_list and the building DrawingBSP's portal polys dispatch RenderDeviceD3D::DrawPortal (0x0059f0e0) → PView::DrawPortal (0x005a5ab0, pc:433895) → ConstructView(CBldPortal) (0x005a59a0) → DrawCells of the neighbor interior — nested recursion reachable from inside-looking-out.
  • acdreamEvidence: MergeNearbyBuildingFloods runs only when ctx.RootCell.IsOutdoorNode (RetailPViewRenderer.cs:60-61); for interior roots NearbyBuildingCells is null by construction (GameWindow.cs:7610). The Outdoor bucket draws neighbor buildings' EXTERIOR entities through the slice (GameWindow.cs:9503-9512) but no interior cells flood.
  • portShape: When an interior root has OutsideView slices, run the existing BuildFromExterior per-building seeding (the R-A2 machinery, keep-listed) for buildings whose entrance portals intersect a slice, intersect the resulting views with the slice planes, and append to the frame — the renderer already merges per-building frames (MergeBuildingFrame, RetailPViewRenderer.cs:151-160).

[LOW] outdoor-objects-flat-bucket (UNVERIFIED (verifier hit token limit)) — Outdoor scenery/statics drawn as one flat bucket per slice instead of per-LandCell object lists

  • blastRadius: No single named bug; costs are structural: the full outdoor entity set is re-dispatched once per doorway slice (perf when multiple doorways visible), alpha-blended outdoor objects have no near-to-far cell ordering, and per-cell semantics retail relies on (cell in_view, building-cell suppression at draw time) have no home. Mostly masked today by clip planes + scissor + depth buffer.
  • retailEvidence: Scenery is placed INTO land cells (CPhysicsObj::add_obj_to_cell, 0x00530923 in CLandBlock::get_land_scenes 0x00530460) and drawn per cell in block draw order: DrawBlock (0x005a17c0) → per-LandCell DrawLandCell (0x0059f120) + DrawSortCell (0x0059f140) → DrawObjCell, with per-view viewcone checks in DrawMesh (0x005a0860).
  • acdreamEvidence: Scenery entities carry no ParentCellId (GameWindow.cs:5463-5472) → InteriorEntityPartition.Outdoor (InteriorEntityPartition.cs:42-49, 61-64); the whole bucket is drawn in one WbDrawDispatcher call per OutsideView slice with frustum cull only (GameWindow.cs:9503-9512).
  • portShape: Long-term: per-LandCell object lists (the render twin of the A6.P4 per-cell shadow architecture), letting the slice walk cells in draw order like DrawBlock. Near-term acceptable as-is; do not re-fix under this area alone.

[LOW] rain-anchor-z-relative (UNVERIFIED (verifier hit token limit)) — Rain cylinder z pinned camera-relative (120 below camera) instead of world-absolute z = 120

  • blastRadius: At high terrain/camera altitude the rain volume rides up with the camera (acdream span camZ120..camZ+695 vs retail fixed 120..+695 world) — subtle density/coverage differences on mountains; nothing user-reported.
  • retailEvidence: GameSky::UpdatePosition (0x00506dd0, BN pc:268596-268618): weather objects (props bit 4) snap x,y to the viewer origin; when bit 8 is clear the frame z slot is OVERWRITTEN with the constant 0xc2f00000 = 120.0f (0x00506e96-e98) — an absolute height, not an offset.
  • acdreamEvidence: SkyRenderer.RenderPass applies Matrix4x4.CreateTranslation(0,0,120) to the model in a sky view whose translation is zeroed — i.e. 120 relative to the camera (SkyRenderer.cs:175-178, 307-308; the doc comment at 284-306 itself reads the decomp as an offset).
  • portShape: In the weather branch, translate by (0,0,120 cameraWorldPos.Z) so the cylinder's base sits at world z 120 while x,y stay camera-snapped — a two-line change in RenderPass.

[LOW] weather-enabled-toggle-absent (UNVERIFIED (verifier hit token limit)) — No weather_enabled client toggle (weather objects always instantiated)

  • blastRadius: None vs retail defaults (retail ships weather on, GameSky::s_weatherEnabled init 0x1 at pc:1098001); only matters for the user-options parity pass.
  • retailEvidence: LScape::weather_enabled gates the weather draw (0x0050638b-96, 0x005070d8) and GameSky::CreateDeletePhysicsObjects (0x005073c0, pc:268912-268917) creates/destroys the weather physics objects when the flag flips.
  • acdreamEvidence: SkyDescLoader.cs:46-51 documents the missing toggle ('we don't honor a weather_enabled toggle yet'); SkyRenderer partitions purely on the dat property bits.
  • portShape: A RuntimeOptions/settings bool consulted at both the object-build site (SkyDescLoader/DayGroup hydration) and the RenderWeather call sites — mirroring retail's create/delete + draw double gate.

OPEN QUESTIONS

  • Is D3DPolyRender::DrawPortalPolyInternal in DrawCells stage (b) (0x005a49b7, second arg 0) strictly a depth-only write? The position in the sequence (after the full z-clear, before shells) and its use with arg 0 vs the ConstructView call site (arg = arg5==1, 0x005a5a7b) strongly imply a z-stamp with color off, but I did not decompile DrawPortalPolyInternal itself to confirm the write mask — confirm before porting the masks stage.
  • Default and source of the retail global cliplandscape (branch at 0x005a5681 in PView::ClipPortals): when 0 the outside view merges UNCLIPPED (Render::copy_view(this, nullptr, 0) at 0x005a5699). Presumably a registry/debug toggle defaulting to clipped, but I could not find its initializer.
  • Does retail's weather pass really composite rain over near buildings outdoors? GameSky::Draw sets DEPTHTEST_ALWAYS around DrawObjCellForDummies(after_sky_cell), but the poly pipeline below DrawObjCell (CShadowPart::draw / D3DPolyRender surface setup) may re-set depth state per surface — not traced to the draw-call level.
  • Exact draw-time role of before_sky_cell: GameSky::Draw pass 0 iterates sky_obj directly (skipping bit-0x01 objects) rather than drawing before_sky_cell as a cell, so the cell looks like a positioning/lighting container only — no draw site for it was found, but I did not exhaustively xref it.
  • Retail re-centers the landscape block draw list on get_outside_cell_id(viewer) while the viewer is indoors (RenderNormalMode 0x453aa0, gated on viewer_cell->seen_outside); acdream's streamed terrain window is centered on the player landblock. Whether this matters at landblock edges (viewer inside a building near a block boundary, doorway facing the un-streamed direction) is untested.
  • #108's final mechanism still needs its own capture: this map identifies the unwired portal z-stamp and the scissored-vs-full depth clear as the faithful-port gaps sitting exactly in that code path, but does not prove which of the two produces the grass sweep during the cellar ascent.
  • DrawMesh's per-view loop draws a mesh once per intersecting portal view (0x005a08ae-096f) — when two doorways show the same outdoor object, retail composites it twice under disjoint clips, same as acdream's per-slice redraw; I treated this as a match, but alpha-blended objects at overlapping view edges were not verified pixel-level.