acdream/docs/research/2026-04-23-sky-decompile-hunt-B.md
Erik 1d54880213 sky(phase-8): retail-faithful night sky + README refresh
Iteration on the sky rendering pipeline to restore stars/moon visibility
at night and fix washed-out grey daytime clouds. Key fixes:

* sky.frag: disable fog-mix on sky meshes. Retail's keyframe FogEnd
  (0..400m at midnight, up to 2400m during day) is calibrated for
  terrain; sky meshes are authored at radii 1050-14271m which sits
  past FogEnd universally, causing every sky pixel to saturate to
  fogColor (dark navy). Stars, moon, dome texture all got
  obliterated. The horizon-glow trade-off is noted in the shader
  comment; research item to find retail's sky-specific fog range
  later.

* SkyRenderer + sky.frag: promote rep.Luminosity into uEmissive so the
  vertex lighting saturates properly for bright keyframes. Retail's
  FUN_0059da60 non-luminous path writes rep.Luminosity into
  material.Emissive via the cache +0x3c slot; we were instead using
  it as a post-fragment multiply which could only dim, never brighten.
  Net effect: daytime clouds now render saturated white, dome dims
  correctly at night (rep.Luminosity=0.11 → Emissive=0.11), stars
  and moon unchanged.

* terrain.vert: MIN_FACTOR 0.08 -> 0.0 per retail FUN_00532440 decompile
  (DAT_00796344 ambient-floor = 0.0). Back-lit terrain now falls to
  pure ambient rather than getting an 8% sun floor.

New research / tooling (no runtime impact):

* docs/research/2026-04-24-lambert-brightness-split.md — retail's
  ambient-brightness formula pinned from PE .rdata read + live
  RetailTimeProbe capture: effAmbBright = AmbBright + |sunDir| * 0.2
  where scale constant 0x0079a1e8 = 0.2f exactly.

* docs/research/2026-04-23-lightning-real.md — research note on the
  dat-baked PhysicsScript-driven lightning path (Rainy DayGroup has
  explicit PES-triggered flash SkyObjects with 5ms time windows).

* Corrections stapled to sky-decompile-hunt-{B,C}.md: DAT_00842778 is
  DirColor, DAT_0084277c is AmbColor (the hunt docs had the swap
  backwards).

* tools/RetailTimeProbe/Program.cs: extended with pid=NNNN selector,
  sky global probe (DirColor/AmbColor/AmbBright/sunDir/cache.amb),
  and the 0x0079a1e8 scale-factor readout.

* tools/SkyObjectInspect/: throwaway dat-inspector built by the Opus
  deep-dive agent. Identified GfxObj 0x010015EF as the stars layer
  (A8R8G8B8 128x128 texture, 4% bright-pixel ratio).

* src/AcDream.App/Rendering/TextureCache.cs: per-texture alpha
  histogram dump under ACDREAM_DUMP_SKY=1 for diagnosing "are the
  clouds decoded with proper alpha" type questions.

README: rewrite to reflect current state (playable pre-alpha rendering
Dereth with animated characters, day-night cycle, weather, etc.)
instead of the stale "Phase 0 dat inventory only" description.

All 742 tests green.
2026-04-24 20:34:36 +02:00

19 KiB
Raw Permalink Blame History

Sky Decompile Hunt B — D3D Render-State Signature Trace

Date: 2026-04-23 Hunter: Hunt Agent B (render-state signatures) Status: SIGNIFICANT FINDINGS — but NOT a "celestial-body iteration draw loop"

⚠ 2026-04-24 correction: Any occurrences in this doc that call DAT_00842778 the "ambient" colour are backwards. DAT_00842778 = DirColor (directional / sun), DAT_0084277c = AmbColor, DAT_00842780 = AmbBright. Cross-verified against SkyTimeOfDay.Unpack and FUN_00501600's output mapping. Full re-analysis: docs/research/2026-04-24-lambert-brightness-split.md.

TL;DR

The retail acclient does NOT appear to have a classical "sky dome + iterate celestial meshes with per-mesh blend swaps" render function. Instead, sky is implemented as:

  1. A per-frame keyframe sampler (FUN_005062e0) that interpolates the current RegionDesc keyframe (sun angle, sun color, ambient color, fog color/near/far) and stashes results into globals.
  2. Per-mesh ambient color pushed via _DAT_008682bc/c0/c4 (RGB floats, no D3DRS_AMBIENT call).
  3. D3D fog state (D3DRS_FOGCOLOR/FOGSTART/FOGEND) set via FUN_005a41b0 — this is the ONLY sky-keyframe → D3D state write path.
  4. A "pre-world" pass (FUN_00507a50(0)) that disables Zwrite and iterates weather volume objects (fog/rain/snow, NOT sun/moon/stars).

No "celestial body draw loop" found. No D3DRS_AMBIENT call found. No matrix-translation-zero (camera-anchor) found. No lightning RNG found. No huge far-plane constant found at the sky call site. This strongly suggests retail AC is far simpler than modern sky demos assume — the sun/moon/stars are either baked into the RegionDesc scene geometry (treated as regular objects that happen to be positioned far away) or do not exist as discrete draw calls at all, with "sun color" being expressed purely via per-vertex lighting on world meshes.

D3D Vtable Map

All state writes go through pIDirect3DDevice at (param_1 + 0x468), vtable offset 0xe4 = SetRenderState, offset 0xd4 = SetTextureStageState, offset 0xc4 = SetMaterial, offset 0x1c (of a PARENT wrapper object at DAT_0086734c) = SetPerspective/SetFrustum.

Discovered D3D state wrappers in chunk_005A0000.c:

Wrapper Addr D3DRS Meaning Cache offset
FUN_005a3ba0 0x005A3BA0 0x1b=27 ALPHABLENDENABLE +0x475
FUN_005a3be0 0x005A3BE0 0x0f=15 ALPHATESTENABLE +0x476
FUN_005a3c20 0x005A3C20 0x19=25 ALPHAFUNC +0x478
FUN_005a3c60 0x005A3C60 0x18=24 ALPHAREF +0x47c
FUN_005a3ca0 0x005A3CA0 0x13=19 src, 0x14=20 dst, 0xab=171 op blend state +0x4f0/+0x49c
FUN_005a3d80 0x005A3D80 0x16=22 CULLMODE +0x488
FUN_005a3dc0 0x005A3DC0 0xaf=175 (fog-gated) +0x48c
FUN_005a3e00 0x005A3E00 7 ZENABLE none (stateless)
FUN_005a3e20 0x005A3E20 0x17=23 (ZFUNC) + 0xe=14 (ZWRITE) combined ZFunc+ZWrite +0x494/+0x498
FUN_005a3eb0 0x005A3EB0 0x8b=139 AMBIENT +0x4a0
FUN_005a3ef0 0x005A3EF0 0x91=145 COLORVERTEX +0x4a8
FUN_005a3f40 0x005A3F40 0x93=147 ? +0x4a4
FUN_005a3f90 0x005A3F90 0x1c=28 FOGENABLE +0x4bc
FUN_005a4080 0x005A4080 0x22=34 (FOGCOLOR), 0x24=36 (FOGSTART), 0x25=37 (FOGEND) fog triple +0x4ac/+0x4c0/+0x4c4
FUN_005a41b0 0x005A41B0 wrapper → FUN_005a4080 SetFog(color, start, end)
FUN_005a41f0 0x005A41F0 0x89=137 LIGHTING +0x4c8
FUN_005a4280 0x005A4280 0x92=146 LOCALVIEWER +0x4d8
FUN_005a4350 0x005A4350 0x3c=60 ? +0x4e0
FUN_005a4390 0x005A4390 0x08 SHADEMODE +0x4e4
FUN_005a4420 0x005A4420 stages SetTextureStageState blend args
FUN_005a4010 0x005A4010 internal flag at +0x4be → re-calls FUN_005a3f90 fog-master-enable gate +0x4be

Device-reset-to-default function: FUN_005a10f0 at 0x005A10F0, lines 687-740 of chunk_005A0000.c. This is NOT the sky render; it is called once on device init and full-state rebuild.

AMBIENT (D3DRS=0x8b=139) — The Key Negative Finding

I searched every call to vtable 0xe4 with state 0x8b across the entire 688K-line decompile. Results:

  • chunk_005A0000.c:3388 — ...0x8b,0 — inside default-init reset
  • chunk_005A0000.c:2772 — ...0x8b,param_2 — inside wrapper FUN_005a3eb0

External callers of FUN_005a3eb0:

chunk_005A0000.c:704:  FUN_005a3eb0(0);   // default-reset, ambient=black

ONLY ONE CALLER. The sky keyframe sampler (FUN_005062e0) does NOT call FUN_005a3eb0. D3DRS_AMBIENT is set to 0 once and never changed. This falsifies the previous research hypothesis (2026-04-22 doc) that clouds are tinted by a per-keyframe D3DRS_AMBIENT write.

Sky color instead lives in _DAT_008682bc/c0/c4 (three floats, RGB, set from DAT_00842950/54/58). See below.

Sky Keyframe Sampler — FUN_005062e0 @ 0x005062E0

chunk_00500000.c, lines 6213-6333. Called each frame from the scene renderer (FUN_00455a50 path, around chunk_00450000.c:4341, 4468).

Stripped pseudocode:

void FUN_005062e0(int worldRenderer) {
  if (worldRenderer.layerBits != 0 && worldRenderer.skyEnabled != 0)
  {
    FUN_00508010();  // advance sky timer
    double now = _DAT_008379a8;  // game-time seconds
    if (_DAT_00842798 <= now && currentRegionDesc != 0) {
      _DAT_00842798 = regionDesc[+0x50].nextKeyTime + now;  // reschedule

      float turbidity = (weatherSys ? weatherSys[+0x48] : _DAT_00795610);
      if (_DAT_008427a0 < now) {
        // ---- BLOCK 1: sample ambient/sun ----
        int ok = FUN_004ff440(turbidity,
                              &fogDensity,  // out float
                              &sunColor,    // out ARGB
                              sunDirVec,    // out float[3]
                              &ambientColor); // out ARGB
        if (ok) {
          if (fogDensity < DAT_0084295c) fogDensity = DAT_0084295c;
          // ---- WEATHER FOG BLEND (if weather active) ----
          if (DAT_008427a9 /* weatherFogEnabled */) {
            if (1.0f <= _DAT_008427b8 /* blendProgress */) {
              // blend complete; snap to weather values
              fogDensity = DAT_008427ac; // weather density
              sunColor   = DAT_00842788; // weather fog color
            } else {
              // lerp currentColor → weatherColor per channel (bytes),
              // lerp density toward DAT_008427ac
              // advance _DAT_008427b8 by _DAT_007c7208 (blend rate)
            }
          }
          FUN_00505f30(fogDensity, sunColor, sunDirVec, ambientColor);
        }
        _DAT_008427a0 = now + (currentRegionDesc ?
                               regionDesc[+0x50].keyInterval : _DAT_007c7200);
      }

      // ---- BLOCK 2: sample fog near/far/color ----
      FUN_005a4010(!DAT_0081dbf8);  // disable fog if master fog flag clear
      if (DAT_0081dbf8) {
        FUN_005a3f90(DAT_0081dbf8); // FOGENABLE = true
        int ok2 = FUN_004ff480(turbidity,
                               &fogNear,  // out float
                               &fogFar,   // out float
                               &fogColor);// out ARGB
        if (ok2) {
          if (DAT_008427a9 /* weatherFogEnabled */) {
            // blend fogNear, fogFar, fogColor toward weather values
            // identical lerp structure
          }
          FUN_005a41b0(&fogColor, fogNear, fogFar);
          // → SetRenderState(FOGCOLOR, ...), (FOGSTART, ...), (FOGEND, ...)
        }
      }
    }
  }
}

This is the ONLY per-frame writer of fog D3D state. Called by FUN_00455a50 (scene entry) via FUN_00506270 etc.

Block-1 Keyframe Layout — FUN_00501600 @ 0x00501600

chunk_00500000.c, lines 1151-1232. Decoded keyframe struct:

Offset Field Type Notes
+0x04 SunMagnitude float lerp → param_4 (radius)
+0x08 SunYAngle float degrees; * (π/180) gives radians
+0x0c SunZAngle float degrees; * (π/180) gives radians
+0x100x12 AmbientColor byte[3] R,G,B (packed via byte-shuffles)
+0x14 FogDensity float lerp → param_2
+0x180x1a SunColor byte[3] R,G,B

From chunk_00500000.c:1190-1216:

// out sunDirVec (param_4):
fVar9 = lerp(nextKey[+0x04], prevKey[+0x04], param_1);  // magnitude
float sinAngY = lerp(nextKey[+0x08], prevKey[+0x08]) * DEG2RAD;
float sinAngZ = lerp(nextKey[+0x0c], prevKey[+0x0c]) * DEG2RAD;
float cosY = cos(sinAngY);
float sinY = sin(sinAngY);
float cosZ = cos(sinAngZ);
float sinZ = sin(sinAngZ);
param_4[0] = fVar9 * sinZ * cosY;
param_4[1] = fVar9 * cosZ * cosY;
param_4[2] = fVar9 * sinY;

// out sunColor ARGB (param_3):  from bytes at +0x18,0x19,0x1a
// out ambientColor ARGB (param_5):  from bytes at +0x10,0x11,0x12

(The repeated FUN_005df4c4() calls with no args are Ghidra's artifact for a union/reinterpret-cast byte shuffle — they are NOT an RNG and NOT a lightning modulator.)

Block-2 Keyframe Layout — FUN_00501860 @ 0x00501860

chunk_00500000.c, lines 1236-1268. Second block of the keyframe struct:

Offset Field Type Notes
+0x20 FogNear float
+0x24 FogFar float
+0x280x2a FogColor byte[3] R,G,B

Apply-Block-1 — FUN_00505f30 @ 0x00505F30

chunk_00500000.c, lines 6026-6092. Stores the sampled values into global cache and into the _DAT_008682bc/c0/c4 "per-vertex ambient" slot:

DAT_00842780 = fogDensity;      // param_2
DAT_0084277c = sunColor;        // param_3 (ARGB)
DAT_00842950 = sunDir[0];       // param_4[0]
DAT_00842954 = sunDir[1];
DAT_00842958 = sunDir[2];
DAT_00842778 = ambientColor;    // param_5 (ARGB)

_DAT_008682c8 = sunDir[0];      // duplicated
_DAT_008682d0 = sunDir[2];
_DAT_008682cc = sunDir[1];
FUN_00451a60(&_something, ambientColor);  // unpacks ARGB → 3 floats
_DAT_008682bc = unpacked R;     // RGB ambient floats for mesh lighting
_DAT_008682c0 = unpacked G;
_DAT_008682c4 = unpacked B;
DAT_008682d4 = 0;               // ambient dirty flag

_DAT_008682bc/c0/c4 is the THING the sky tints the world with. It is consumed per-vertex by mesh-draw, not by a D3D state write.

Candidate "Sky Pass" — FUN_00507a50 @ 0x00507A50

chunk_00500000.c, lines 7250-7299.

void FUN_00507a50(weatherMgr, phase) {
  if ((FUN_00451ec0() || phase == 0)) {  // skyEnabled or pass-0
    weatherMgr.renderingFlag = 1;
    char oldAlpha = FUN_005a1560();
    FUN_005a3f90(DAT_008427a9 != '\0');     // FOGENABLE ← weatherFog flag
    float oldFar = DAT_0081fc98;
    FUN_0054bf30(DAT_0081fc98 * _DAT_007c6f14);  // multiply far plane
    FUN_005a3e20(8, 0);                      // ZFUNC=ALWAYS, ZWRITE=0
    if (phase == 0) {
      // iterate weather volume objects (rain/snow/fog cells)
      for (uint i = 0; i < weatherMgr.count; ++i) {
        int obj = *(int*)(weatherMgr.pArray + i*4);
        uint flags = *(uint*)(weatherMgr.pFlags + i*4);
        if (obj && !(flags & 1) &&
            (DAT_0081dbf9 || !(flags & 4)) &&
            (!DAT_008427a9 || !FUN_005a1560() || !(flags_byte & 2)))
        {
          FUN_00511720(obj);   // scene-graph walk (update per-frame)
          FUN_00511760(obj);   // scene-graph walk (draw)
        }
      }
    } else if (DAT_0081dbf9) {
      // phase 1: just let the device callback at +0x64 run
      (**(code **)(*DAT_00870340 + 0x64))(weatherMgr[+0x28]);
    }
    FUN_0054bf30(oldFar);                    // restore far plane
    FUN_005a3e20(4, 1);                      // ZFUNC=LESSEQUAL, ZWRITE=1
    FUN_005a3f90(oldAlpha);                  // restore FOGENABLE
    weatherMgr.renderingFlag = 0;
  }
}

Called by FUN_00506d90 (chunk_00500000.c:6683):

  • FUN_00507a50(0) = phase-0 (sky-ish volume draw, before terrain/entity)
  • FUN_00507a50(1) = phase-1 (post-draw overlay, gated by weather flag)

The DAT_0081fc98 * _DAT_007c6f14 line IS the far-plane multiplier — however _DAT_007c6f14 here is ambiguous, used elsewhere as a small rotation coefficient (chunk_005E0000.c:258 etc). This deserves a follow-up: check if that DAT is a configurable sky-far-plane multiplier or a recycled constant.

No matrix-translation-zero (camera-anchor) is present in this function. The scene graph objects iterated via FUN_00511720/60 are rain/snow/fog-shaft volumes — they are positioned in world space and expected to follow the camera via normal scene-graph propagation.

No per-mesh blend swap (alpha for clouds vs additive for sun) is present. Blend mode is set once for the whole pass.

Fog Write Path — SUMMARY

The complete trail:

  1. FUN_00505f30 stores sun/ambient globals (no D3D call).
  2. FUN_005062e0 lerps current-keyframe fog → weather-fog if needed.
  3. FUN_005a41b0(&fogColor, fogNear, fogFar)
  4. FUN_005a4080(pFogColor_as_ARGB, near, far)
  5. Writes D3DRS_FOGCOLOR=0x22, D3DRS_FOGSTART=0x24, D3DRS_FOGEND=0x25.

Fog ENABLE is wrapped by FUN_005a4010 (master gate at +0x4be) + FUN_005a3f90 (actual D3DRS_FOGENABLE=0x1c write). Both driven by the DAT_0081dbf8 master-fog flag.

D3DRS_FOGTABLEMODE=0x23, FOGVERTEXMODE=0x8c, FOGDENSITY=0x26 — these are only set once in the default-init (FUN_005a10f0) and never per-frame. Retail uses linear fog (FOGSTART/FOGEND), not exponential (FOGDENSITY).

Lightning Flash — NOT FOUND

Searched for:

  • FUN_005df4c4 (the RNG/byte-shuffle) — all hits are byte-packing for ARGB color interpolation, not random modulation.
  • _DAT_008427b8 — it's a weather-fog-blend progress (0 → 1 ramp), not a per-frame random value.
  • Time-based sine/cos on small periods — none found coupled to sky color.
  • Any RNG call near fog/ambient writes — none found.

Lightning is likely a separate system not found in this hunt, OR does not exist in retail and is purely decorative in modern ports.

Matrix-Anchor (Camera-Centered Sky) — NOT FOUND

Searched for patterns like:

  • *(undefined4*)(mat + 0x30) = 0;
  • SetTransform(D3DTS_VIEW=2, ...) via any vtable offset
  • A matrix copy that zeroes three consecutive 4-byte fields

Nothing found anchored to the sky pass. The view matrix is NOT rewritten with zero translation before the sky draw. This is consistent with the conclusion that there is no discrete "sky dome" — the weather/fog volume objects follow the camera by being placed in camera-relative world position by their parent scene-graph node.

Huge Far-Plane Constants — NOT FOUND

Searched for:

  • 0x49742400 (1e6f) — no hits
  • 0x47c35000 (1e5f) — no hits
  • 0x4b189680 (1e7f) — no hits

The only far-plane modulator is DAT_0081fc98 * _DAT_007c6f14 at chunk_00500000.c:7272. Without knowing _DAT_007c6f14's numeric value from a debugger or disassembly of .rdata, we can't tell if this is

1 (sky-far-plane multiplier) or ~1 (no-op). A follow-up disassembly of the address 0x007c6f14 should nail this down.

D3D State Inventory — Sky-Relevant Per-Frame Writes

Only four D3D states are written per-frame in the sky-adjacent code path:

State Hex Name Writer Source of value
34 0x22 FOGCOLOR FUN_005a4080 current keyframe+weather lerp
36 0x24 FOGSTART FUN_005a4080 current keyframe+weather lerp
37 0x25 FOGEND FUN_005a4080 current keyframe+weather lerp
28 0x1c FOGENABLE FUN_005a3f90 DAT_0081dbf8 / weather flag

Plus, inside FUN_00507a50 (phase-0 weather-volume draw only):

State Hex Name Value
23+14 0x17+0x0e ZFUNC + ZWRITE ALWAYS + OFF → restore LESSEQUAL + ON

Gaps / Unresolved

  1. The classical sky dome / celestial body draw — NOT FOUND. Either retail literally does not have one (sky is just a clear-color glimpse plus fog between camera and terrain), or it's hidden inside the RegionDesc.Scene entries and rendered as a regular object through the normal scene-graph path.
  2. _DAT_007c6f14 — used as a far-plane multiplier in the sky pass, but cross-referenced use in chunks 0x5E0000 suggests it's a unit-scale float (< 10). Needs raw .rdata dump to confirm its value.
  3. Is _DAT_008682bc/c0/c4 consumed by D3D material or per-vertex color? The writers are clear; the readers need a separate hunt (likely in chunk_00590000.c or the mesh-draw path at FUN_0054c9c0).
  4. Lightning flash — not found in the keyframe/fog paths. If it exists, it's in a separate code path (maybe tied to weather presets 6 "thunderstorm" — DAT_008427a9=1 case in chunk_00550000.c:11886).

Files / Addresses cited (all absolute paths)

  • C:\Users\erikn\source\repos\acdream\docs\research\decompiled\chunk_005A0000.c
    • lines 687-740: FUN_005a10f0 device reset (AMBIENT=0 here, only caller)
    • lines 2606-3035: D3D state wrapper zoo (all vtable-0xe4 writers)
    • lines 2868-2907: FUN_005a4080 = SetFog triple
    • lines 2911-2921: FUN_005a41b0 = SetFog wrapper
    • lines 3346-3400: default state-init defaults
    • lines 3860-3970: FUN_005a5950 world-scene top-level renderer
  • C:\Users\erikn\source\repos\acdream\docs\research\decompiled\chunk_00500000.c
    • lines 1151-1232: FUN_00501600 Block-1 keyframe sampler
    • lines 1236-1268: FUN_00501860 Block-2 (fog) keyframe sampler
    • lines 6026-6092: FUN_00505f30 apply-keyframe-1 to globals
    • lines 6213-6333: FUN_005062e0 per-frame sky+fog update (MAIN)
    • lines 6683-6708: FUN_00506d90 scene renderer (calls sky phase 0/1)
    • lines 7250-7299: FUN_00507a50 the phase-0 weather-volume draw
  • C:\Users\erikn\source\repos\acdream\docs\research\decompiled\chunk_00450000.c
    • lines 608-622: FUN_00451a60 ARGB → 3-float unpack
    • lines 4300-4479: per-landblock ambient setup (_DAT_008682bc writers)
  • C:\Users\erikn\source\repos\acdream\docs\research\decompiled\chunk_00550000.c
    • lines 11835-12016: FUN_0055eb40 weather-preset-id → fog constants (0=clear, 1=light rain, 2=medium, 3=heavy, 4=blizzard, 5=?, 6=thunderstorm)
  • C:\Users\erikn\source\repos\acdream\docs\research\decompiled\chunk_004F0000.c
    • lines 10692-10720: FUN_004ff440 / FUN_004ff480 keyframe trampolines

Recommendation for acdream port

Given these findings, our C# SkyRenderer should:

  1. Stop expecting a celestial-body draw loop in the decompile — Hunt A will likely confirm via the Region-loader angle.
  2. Port the keyframe sampler faithfully from FUN_00501600 / 1860 — the struct layout above is the ground truth for RegionDesc keyframe blobs.
  3. Apply the sun direction and colors to world-mesh vertex lighting, not to a D3DRS_AMBIENT equivalent. The values live in _DAT_008682bc/c0/c4 and are consumed by the mesh pipeline.
  4. Set fog via SetFog(color, start, end) + FogEnable per frame from the Block-2 sample. This matches FUN_005a41b0.
  5. Do NOT add a matrix translation-zero — retail does not anchor the sky dome, because retail does not appear to HAVE a sky dome draw in the D3D state sense. If acdream renders a dome mesh, it's a modern addition.
  6. Tint clouds (if rendered) via the per-mesh ambient global, not via the current uniform-one approach. The previous research doc's conclusion on "clouds multiply by ambient" is likely correct in spirit — just know that "ambient" here means _DAT_008682bc/c0/c4, not D3DRS_AMBIENT.