Perspective-Correct Texture Mapping in the Build Engine
I have always wondered whether Ken Silverman’s Build engine used perspective-correct texture mapping while rendering floors and ceilings as horizontal lines, or whether it found some other shortcut—perhaps something related to the constant-distance-line technique I remembered from one of Ken’s six-degrees-of-freedom experiments.
I first asked Google. The original version of this post came from Google Gemini and included a long, confident explanation with many citations. I then asked GPT to audit the answer against Ken Silverman’s released source code, the Build development log, period processor documentation, and later source ports.
The high-level answer survived, but many of Gemini’s implementation details did not. The revised and expanded explanation below separates what Build actually does from what merely sounded plausible.
![]()
The short answer
Yes—with an important qualification.
Build maps sloped floors and ceilings projectively, so their textures remain visually attached to the plane as it recedes into the distance. However, the optimized DOS renderer does not calculate an exact perspective divide for every pixel. Its active assembly routine obtains an approximate reciprocal at short intervals—normally eight pixels apart—and linearly interpolates the texture coordinates inside each interval.
So there are two useful answers:
- If “perspective-correct” means that the mapping follows the projective geometry of the sloped plane rather than applying one affine map to the whole surface, then yes.
- If it means that Build evaluates exact
U/Z,V/Z, and1/Zdivisions independently at every pixel, then no.
The most revealing way to understand the engine is not “affine floors versus perspective slopes.” Build instead chooses a drawing direction that makes each supported kind of surface as cheap as possible:
- vertical screen columns for vertical walls;
- horizontal screen rows for flat floors and ceilings; and
- vertical screen columns, subdivided into short projective intervals, for sloped floors and ceilings.
Why ordinary Build surfaces are so cheap
Perspective texture mapping is expensive when both texture coordinates must be divided by a changing depth at every pixel. Build avoids that general problem by exploiting the restricted geometry of its world.
Vertical walls
A vertical wall is naturally drawn in vertical screen columns. At a fixed screen x-coordinate, every pixel in that column belongs to the same horizontal location on the wall and therefore has the same forward-depth scale. The texture’s horizontal coordinate is fixed for the column, while its vertical coordinate can be advanced linearly down the screen. Under classic Build’s upright-camera model, that is an exact perspective result for a vertical wall, not the globally affine shortcut associated with warped PlayStation polygons.
Flat floors and ceilings
For a horizontal world plane, the reciprocal-depth term depends on the screen row but not on screen x. Build’s ceilscan and florscan routines turn the visible gaps beneath and above walls into horizontal spans. The hline setup selects a row-dependent depth scale and then advances both texture coordinates linearly across that row.
This is the elegant special case behind my original intuition. Under Build’s upright-camera model—where looking up and down shifts the horizon rather than applying true pitch—an entire horizontal row has a constant projective denominator on a flat floor or ceiling. The points are not all the same Euclidean distance from the eye, but the quantity that would otherwise force a perspective divide is constant across the row. Linear interpolation is therefore perspective-correct along that row; the scale changes when the renderer moves to another row.
Calling this simply “affine texture mapping” is technically misleading. The stepping is linear within one horizontal span, but it is linear because the projective equation has simplified exactly for that geometry.
How Build represents a slope
A Build slope is still deliberately constrained. It is one mathematical plane clipped to a sector polygon, not an arbitrary mesh whose vertices can each have unrelated heights.
The sector stores a base ceiling or floor z-coordinate and a signed slope value named ceilingheinum or floorheinum. The renderer uses the sector’s first wall, referenced by sector.wallptr, as the zero-rise or “hinge” line. Conceptually, the height calculation is:
height at (x, y) = base height
+ slope amount × signed perpendicular distance from the first wall
The actual source normalizes that distance by the wall’s length and uses fixed-point scaling. Ken’s getceilzofslope, getflorzofslope, and alignment functions expose the calculation directly.
The common description that a map author can choose a hinge wall is also substantially correct, but the file format does not hold a separate arbitrary hinge pointer. The Build editor’s Alt-F command changes which wall is the sector’s first wall; the renderer then uses that first wall as the slope reference. This detail appears in the contemporary Build development notes.
What changes when the plane is sloped
A slope destroys the flat-plane shortcut. Across a horizontal screen row, the plane now rises or falls in world space, so the projective denominator generally changes with x. Build could have searched for slanted constant-depth contours and rasterized along those, but the released renderer does not do that.
Instead, the engine dispatches a sloped floor or ceiling to grouscan. That routine walks from left to right over screen columns. For every x-coordinate, it finds the visible top and bottom of the slope, prepares the shade lookup information, and sends the vertical run to slopevlin. The DOS routine starts at the bottom pixel and moves upward through the framebuffer.
For a fixed screen column, the texture coordinates have the projective form:
u(y) = u base + u coefficient / D(y)
v(y) = v base + v coefficient / D(y)
Here D(y) changes linearly with screen y and generally also changes as grouscan advances to the next column. This equation is algebraically equivalent to intersecting a viewing ray with the plane. That is why some explanations—including Fabien Sanglard’s otherwise valuable Build engine analysis—use the word “raytracing.”
That word should be read only as a mathematical analogy. Build does not switch the scene renderer into a general ray tracer, cast rays to discover visible walls, or ray-march the level. Its portal and wall-projection machinery still determines visibility and clips the floor or ceiling. grouscan merely evaluates the already visible plane with a specialized projective rasterizer.
What slopevlin actually does
The readable C replacement makes the intended relationship obvious: for each pixel it obtains a reciprocal of a changing denominator, multiplies that reciprocal by two mapping coefficients, and adds the results to base u and v coordinates. Ken explicitly warned that this separately written 1996 C version was not an exact replacement for the optimized assembly, however. It explains the formula, not the instruction cost of the original DOS path.
The active A.ASM slope routine uses a faster approximation.
1. Start with a reciprocal at the bottom of the column
grouscan calculates the denominator for the bottom pixel and calls krecipasm. This is not a hardware division. Build has a 2,048-entry reciptable prepared when its tables are loaded.
The reciprocal lookup is more sophisticated than using raw depth as an array index. The code converts the value to a floating-point representation, extracts high mantissa bits to choose an entry in the table, uses the exponent to shift the result to the proper scale, and restores the sign. In effect, it normalizes a large range of denominators into a compact reciprocal table.
2. Correct the reciprocal every eight pixels
The assembly defines BITSOFPRECISION as 3, making the normal block size 2^3, or eight pixels. It advances the projective denominator to the next block boundary, obtains another table-assisted reciprocal, and uses the difference between the old and new reciprocals to derive fixed-point u and v increments.
The inner loop then draws up to eight pixels using those increments. A shorter final block handles whatever remains at the top of the column. In simplified pseudocode:
get the reciprocal at the bottom of the column
while pixels remain:
choose a block of at most 8 pixels
get the reciprocal at the next block boundary
derive fixed-point u and v steps between the two boundaries
draw the block by adding those steps for each pixel
This is piecewise-affine interpolation of a projective mapping. Correcting every eight pixels keeps the error small while amortizing the expensive reciprocal work. It is far more accurate than stretching one affine map over the entire slope, but it is still an approximation rather than exact per-pixel division.
3. Keep the pixel loop simple
Inside each short block, the routine mostly performs integer work:
- advance the fixed-point u and v accumulators with additions;
- form a texture address with prepatched shifts and a mask;
- fetch an 8-bit texel;
- apply the preselected shade and palette lookup; and
- write the final byte to the framebuffer.
There is still a normal loop branch. The important win is that there is no DIV, IDIV, or FDIV in this pixel loop—or anywhere in the active slopevlin routine.
![]()
Assembly optimization: avoiding division, not speeding it up
The original Gemini answer framed the problem as making a 46-cycle division faster. The better description is that Silverman arranged the slope mapper so that division never enters the hot loop in the first place.
Exact instruction timings depend on the processor, operand size, and instruction form. On a 486 or original Pentium, integer division took tens of cycles and was dramatically slower than addition or a simple shift. The repeated “46-cycle DIV” figure applies, at most, to a particular processor and instruction case; it is not a universal timing. None of those exact numbers is needed to understand the optimization: a reciprocal lookup once per short block, two integer multiplies to establish the coordinate steps, and additions per pixel are much cheaper than a divide per pixel.
Fixed-point arithmetic and lookup tables
Build is overwhelmingly a fixed-point engine. The slope path uses integer coordinate accumulators, precomputed reciprocal values, texture-size shifts and masks, framebuffer row offsets, and palette tables. grouscan also prepares shade-table pointers by screen row so the inner loop can convert a texel to a lit palette value with a lookup rather than calculate lighting from scratch.
These techniques matter together. No single lookup table explains the renderer’s speed; the entire data path is designed so that the per-pixel work reduces to shifts, masks, additions, memory reads, and a byte write.
The x87 FPU is present—but it does not divide
The active slope path is mostly integer code, but describing it as completely FPU-free is also wrong. setupslopevlin and slopevlin contain FILD, FADD, FST, and FSTP. The x87 stack holds and advances the changing denominator and makes its floating-point exponent and mantissa available to the reciprocal-table trick. It never executes FDIV there.
This small amount of x87 work explains the documented 486SX behavior. A 486SX had no on-chip floating-point unit, so those instructions had to be serviced through software emulation in a compatible Watcom build. Slopes invoke the x87-assisted reciprocal machinery densely enough that a visible sloped surface could become disproportionately expensive. For context, Duke Nukem 3D’s published minimum processor was a 486DX2/66, which did include an FPU. An integer-only routine named slopevlin2 exists in A.ASM, but the released renderer does not call it.
The careful conclusion is therefore not “perspective mapping required floating point.” It did not. Silverman chose a hybrid implementation: x87-assisted denominator normalization and accumulation around a fixed-point integer texture loop.
Self-modifying code: real, but not generated unrolled blitters
Build really does use self-modifying code. Before drawing, setupslopevlin patches operand fields inside the already assembled routine. Among the baked-in values are the texture pointer, framebuffer stride, coordinate shift counts, and texture mask. slopevlin also patches an address displacement used to reach its row-dependent shade information.
This conserves scarce 32-bit x86 registers and lets the hot loop use immediate constants, including faster immediate shift counts. It does not generate a new temporary program containing one blit instruction for every pixel. A 14-pixel column does not cause Build to synthesize 14 copies of the drawing code, and self-modification does not remove every branch. The source visibly retains both its per-pixel and per-block loops.
What about the Pentium U-pipe and V-pipe?
The original Pentium could issue certain compatible, independent integer instructions together through its U and V pipelines. Hand-written assembly could benefit from arranging adjacent operations so that they paired, and A.ASM uses aligned loops and interleaves independent coordinate and address work in a style that gives the Pentium pairing opportunities.
However, the evidence does not support the stronger claim that slopevlin kept both pipes “completely packed” or that dual-pipe scheduling was the secret replacement for division. Some operations, including integer multiply and divide, were not generally pairable; shifts had additional restrictions. The source’s demonstrable slope-specific optimization is the reciprocal table plus eight-pixel subdivision. Pentium-aware scheduling is a useful supporting optimization, not the central algorithm.
How fast was it on period hardware?
Ken’s own development log provides a better data point than speculative instruction arithmetic. On September 22, 1995, he recorded that a full-screen slope in VESA 2.0 at 320×200 improved from 24 frames per second to 35 frames per second on a 486-66 with a local-bus video system. The next log entry mentions still more assembly optimization. Slopes had first appeared in the log only a few weeks earlier, on August 29.
The more complex source path explains why slopes cost more than Build’s flat-plane fast path; the measurement shows that optimizing that path mattered. The resulting implementation was already practical on the hardware Silverman was targeting. Rendering cost follows the number of visible slope pixels and columns, not merely the number of sectors in the map. I found no primary source establishing the stronger Gemini claim that level designers deliberately rationed slopes for performance, so I would not present that as fact.
The C version and the source-port story
The optimized DOS engine’s dependence on 32-bit x86 assembly and writable code certainly made portability harder. That part of the original answer was fair. Its history of A.C, however, was backwards.
The C replacement later used as a foundation by JFBuild-derived ports was not reverse-engineered by EDuke32 developers. According to Ken Silverman’s official source page, Ken himself ported A.ASM to A.C in June 1996 for Macintosh and console porting teams. He publicly released that code in BUILDC.ZIP in 2003. Jonathon Fowler’s later JFBuild file describes itself as an A.ASM replacement written mainly by Ken, combined with Fowler’s substantial porting work.
The C routine is intentionally easier to understand and is not cycle-for-cycle equivalent to the assembly. In particular, its straightforward slope loop obtains a reciprocal for each pixel. That is useful evidence for the mathematical mapping, but it must not be mistaken for proof that the original DOS assembly performed a hardware divide per pixel.
The real portability problems were the 32-bit calling conventions and pointer assumptions, the use of self-modifying writable code, and support for non-x86 targets. They were serious, but “modern 64-bit processors could not read the assembly” is not an accurate explanation.
Fact-checking the original Gemini answer
What it got right
- Build represents a sloped sector surface as a mathematical plane.
- The slope is tied to a wall used as a hinge or reference line.
- Flat floors and ceilings have a much cheaper specialized rendering path.
- Sloped texture mapping accounts for perspective rather than applying one affine map to the whole surface.
- The engine relies heavily on fixed-point arithmetic, lookup tables, hand-written assembly, and self-modifying code.
- x87 instructions can make the slope path especially painful on a 486SX.
What needed qualification
- The hinge is specifically the sector’s first wall; the editor can make another wall first.
- Flat-plane interpolation is linear along each horizontal span, but it is perspective-correct for that special geometry rather than merely an affine compromise.
- Calling the slope calculation “raytracing” is acceptable only as shorthand for ray/plane-equivalent math, not as a description of Build’s scene-rendering architecture.
- The slope mapper approximates perspective correction through short-span subdivision rather than evaluating the exact projective equation at every pixel.
- Pentium dual-pipeline scheduling is relevant background, but the claim that the slope loop completely filled both pipelines was not demonstrated.
What was wrong
- The released slope renderer does not search for constant-distance lines across the plane.
- It does not perform
U/Z,V/Z, or any other division for every pixel. - It does not use
DIV,IDIV, orFDIVinsideslopevlin. - Its self-modifying code does not manufacture a separately unrolled blitter for each column length.
- Ken—not a later source-port team—wrote the original C translation of
A.ASM.
Final verdict
Build’s achievement was not brute-force perspective division. It was recognizing where perspective math collapses into a cheap linear walk and choosing the raster direction accordingly.
Under Build’s upright-camera model, vertical walls are exact when drawn as vertical columns, and horizontal floors and ceilings are exact when drawn as horizontal spans. Slopes generally fit neither of those axis-aligned constant-depth special cases, so Build draws vertical columns and restores the missing projective behavior with a reciprocal-table estimate at intervals of up to eight pixels.
That makes the slope renderer a specialized, approximately perspective-correct plane mapper: more sophisticated than affine texture mapping, far cheaper than a divide per pixel, and still integrated into Build’s portal rasterizer rather than a separate raytracing engine. The most impressive optimization was not speeding up DIV. It was designing the hot path so that DIV was never needed there at all.
Primary sources and further reading
- Ken Silverman’s official Build source-code page — release history and descriptions of
KENBUILD.ZIPandBUILDC.ZIP. - Ken Silverman’s Build engine history — contemporary development milestones, including the arrival of slopes.
- KENBUILD.ZIP — Ken’s published DOS Build source, including
ENGINE.C,A.ASM, andBUILD2.TXT. - BUILDC.ZIP — Ken’s readable C translation of the assembly raster routines and its explanatory notes.
- Vanilla Duke Nukem 3D
ENGINE.C: slope-height functions — the first-wall reference andheinumplane calculation. - Vanilla Duke Nukem 3D
ENGINE.C:grouscan— the vertical-column slope setup and call toslopevlin. - Vanilla Duke Nukem 3D
A.ASM:setupslopevlinandslopevlin— x87 denominator handling, reciprocal lookup, eight-pixel blocks, fixed-point stepping, and patched operands. - JFBuild’s readable C slope mapper — a clear expression of the per-pixel projective formula.
- Build development log for September 22, 1995 — the reported improvement from 24 to 35 frames per second.
- Fabien Sanglard’s Build Engine Internals — an accessible source-guided overview, including the later correction about x87 use and 486SX performance.
- Duke Nukem 3D manual — the published 486DX2/66 minimum processor requirement that puts the 486SX behavior in context.
- Ken Silverman discussing horizontal and vertical rendering directions — useful first-person context for the engine’s surface-specific scan directions.
- Ken Silverman discussing Build’s self-modifying code — why constants and immediate shifts were patched into inner loops.
- Open Watcom x87-emulation test source and the Open Watcom tools documentation — concrete and reference documentation for assembler floating-point emulation modes.
- Intel Pentium Processor Family Developer’s Manual, Volume 1 — the U-pipe/V-pipe execution model.
- Intel AP-526: Optimizations for Intel’s 32-Bit Processors — period instruction-pairing and long-arithmetic guidance.






Leave a Reply
You must be logged in to post a comment.