Repository navigation
Sky modernization 2: the star field as points, culled by cell and brightness - #582
Merged
Merged
Conversation
LLVOWLSky::initStars laid the field out inline, from the fixed seed Phase 0 gave it. The star rework that follows bins, sorts and culls that field, and its tests need to build the same field the viewer draws without a viewer, so the generation moves to alrender as ALStarField: generate(count, dome radius) returns each star's centre and its colour already packed as the vertex stage takes it (the black-body sRGB in rgb, the intensity in alpha, rounded as LLColor4U rounds an LLColor4). The draws are made in the same order from the same generators, the fixed-seed std::mt19937_64 drawing once a star the seed of the star's own SplitMix64, so the field is the one Phase 0 laid out. Phase 0's code and ALStarField's are compiled apart, each inlining the band's density its own way, so where they round one of a star's band comparisons the other way, as expf may, that star differs, and no other. The black-body colour and the hemisphere sampling move with it; the meteors still use both, drawing from the global generator as before. How the field is drawn is a template over the Milky Way band's density, ALStarField::detail::generate(count, radius, density) in alstarfieldgenerate.h: generate calls it with milkyWayDensity, the band's Gaussian about the galactic plane, and the tests with densities of their own. alstarfield_test pins five stars of the default field across all three tiers, so a change of seed, draw order or tier shows, and checks that the field does not move with the global generator or a count in between, that each tier scales with the count, and that every star sits on the upper hemisphere in its tier's distance and intensity range. Through detail::generate it forces one band comparison the other way in each tier and checks that the star alone moves; with one stream feeding every star, the same flips move 15,387, 13,712 and 3,976 of a tenth-size field's 15,500 stars. generate's own field, its density compiled apart from the test's, differs from the test's at no more than one star in a thousand. alstarfield_gl_test draws the stars two ways on the hidden window from that field: through the star shaders as they are now, copied into the test as the reference the rework must keep and built as the viewer builds its own, by createShader from a tree of the viewer's class1 shaders with them beside, the engine's matrix blocks spliced in where they say //[ENGINE_BLOCK Matrices]; and through the product's deferred/starsV.glsl and starsF.glsl as LLViewerShaderMgr builds them, into a G-buffer of each path (four attachments with the emissive buffer, three without). At this commit the two are the same shaders, and the pictures match exactly in all six scenes (night looking up, at the horizon, wide at the zenith, moonlit with the moon in view, dusk, dusk with the moon up) on both paths. The moonlit scenes check that the moon's mask hides stars at all. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
starsF ran, for every fragment of every star's quad, five value noises (forty hashes), the Kasten-Young airmass (an asin and a pow), the moon's glare and wash, and the moon mask's texture tap, though every one of them reads only the star's direction, which is the same at all six of its vertices. They move to starsV: the extinction, the scintillation and its colour jitter, the moon's glare and wash, the subpixel fade and both paths' gains fold into one flat colour, vary_star_color, and the spikes' strength into vary_spike. The fragment stage keeps only the shape (core, halo and spikes) times that colour. The moon's mask was a per-fragment discard of a per-star test, so it always took a star whole; starsV now leaves such a star out before it is rasterized, by placing it outside the clip volume, and reads the moon's texture with textureLod as starsF did. alstarfield_gl_test now bounds each pixel of the stars' attachment by one step of its storage (8 bits, or R11G11B10F's mantissa at the pixel's value) plus half a step of 8 bits, and reports each scene. Here 0 to 5 pixels of 15k to 133k lit ones differ per scene and path, each by one storage step, where the reordered arithmetic rounds the other way; nothing passes the bound. With the vertex stage's moon cull taken out, both moonlit scenes fail on both paths (up to 128/255 over 927 to 1458 pixels). glslangValidator -l links all four programs (emissive and not, reverse-Z and not) with their attached objects; a control that renames a varying fails all four. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every star was a quad of six non-indexed vertices of 28 bytes each: 155k stars made 930k vertices and 26 MB. Now a star is one GL_POINTS vertex of 16 bytes: its centre in xyz and its colour's four bytes in the position's fourth word, where LLVertexBuffer keeps the texture index (MAP_VERTEX | MAP_TEXTURE_INDEX), unpacked by unpackUnorm4x8 in starsV. The default field's buffer is 2.5 MB. GL drops a point whose centre leaves the screen, which would pop the arms of the stars with diffraction spikes (up to 19 pixels across) at the edge, so those draw as quads in a second, small draw (D4): the 295 stars of the default field whose intensity byte is 230 or more, the first past starsV's smoothstep(0.90, 1.0, intensity). ALStarField lays the buffer out with their quads first, six vertices each, from vertex 0, so the quad program (gDeferredStarQuadProgram, starsV and starsF under STAR_QUADS) takes each corner from gl_VertexID % 6; then the rest, one vertex each. A point is sized by gl_PointSize under GL_PROGRAM_POINT_SIZE, but GL rounds that size to its granularity (0.125 pixels on NVIDIA) and measures gl_PointCoord across the rounded square, which scaled a 2.6-pixel star's shape by up to 5% and, in the A/B below, changed more pixels than this does. So starsV passes the star's centre in window coordinates and half its extent, draws the point a pixel wider than the star, and starsF works each pixel's offset from the centre from gl_FragCoord and leaves out the pixels past the star's square: the same pixels as the quad and the same shape, whatever the driver's granularity. The viewport that maps the centre is the bound target's (LLRenderTarget::sCurResX/Y), which is also what now sizes the quads. alstarfield_test pins SPIKE_INTENSITY to starsV's gate for every intensity byte and checks that the layout puts every spiked star first, keeps the field's order within each kind, and writes each star's centre and colour bytes, six times for a quad and once for a point, and nothing past the end. The A/B against the old quads now measures what may differ by the old picture itself moved one step of the rasterizer's sub-pixel grid (1/256 of a pixel), since the old quads' corners are snapped to it where a point's centre is exact. The new picture differs from the old at 1.8 to 4.8 times fewer pixels than that moved picture does in every scene and path, by at most 3/255 on the albedo path (the moved old picture, 4/255) and, on the emissive path, by at most one R11G11B10F step more, which is the stars' blends rounding in a different order where they overlap (the old shaders in the new order alone give that). The plain gl_PointCoord form fails the A/B. glslangValidator -l links all eight programs (points and quads, emissive and not, reverse-Z and not); a control that renames the quads' corner output fails the four quad programs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every star was drawn every frame wherever the camera looked, though a normal field of view takes in about a sixth of the upper hemisphere. ALStarField now bins the points (not the 295 spiked quads, which stay one small draw) into 512 cells, 16 bands of equal height in z, so of equal area, each cut into 32 arcs of azimuth, and lays the buffer out cell by cell. Each cell keeps its run of vertices and a cone about the camera holding every one of its stars' directions: about their mean, as wide as the farthest. Each frame the star draw takes the side planes of its own modelview-projection, which already turns the agent's frame into the stars' (the translation to the camera and the slow turn about z), so the cull runs in the stars' frame with no rotation of its own. A star is thousands of times farther than the camera is from the stars' origin, so a direction is in view where each plane's normal makes a non-negative product with it, and a cell is out where one plane has its whole cone outside. The planes are moved out by POINT_REACH (4 pixels: half the widest point and the pixel starsV widens it by), so no point a driver would draw from a centre just off the edge is lost. The cells in view become glMultiDrawArrays runs, those that meet merged, through LLVertexBuffer::drawMultiArrays, which syncs the matrices and counts the draw as drawArrays does. The cull is a CPU zone, "sky stars cull". alstarfield_test checks that the cells lay the points out in order, each star in the cell cellOf names and inside its cell's cone, no cell of the default field empty; that over 47 views (level, up, at the zenith, below the horizon, rolled, from 10 to 120 degrees, and 40 at random) every point that lands within the widened clip square is in a cell the cull keeps, and the draws are exactly the kept cells; and that looking 35 degrees up through 60 degrees at 16:9 the view draws 25,902 of the 154,705 points, 6.0 times fewer, in 17 runs, where 19,182 land in view. alstarfield_gl_test draws every point and then only the culled runs: the pictures are identical in all six scenes on both paths, while the views draw 22k to 84k of the 155k points. The A/B against the old quads is unchanged. With the cone's radius left out of the test, views lose points in view and the culled pictures lose stars. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
At dusk the star brightness (custom_alpha, the sky's star_brightness over 500) falls toward 0, and most of the faint dust adds less to any pixel than the frame can show, yet every star in view was drawn. ALStarField now lays the quads, and each cell's points, out brightest first, and each frame works out the faintest intensity that can still add half a step of 8 bits (0.5/255) to a pixel of an 8-bit attachment: ALStarField::brightest bounds what starsV and starsF give a star of an intensity, every term at its most but the star's own (its colour and extinction at 1, the scintillation and colour jitter at their peaks, +65% and +12%, no moon glare and the moon's wash at its least, the shape at its centre), and it rises with the intensity, so faintestVisible bisects the 256 bytes. Each cell in view then draws only its prefix at that intensity or brighter, and the quads theirs. The pass takes the attachment's gain from the latched shader inputs (mGlobal.mEmissive) and the moon's power from the sky it already reads. The shaders name each bound the arithmetic takes (starsV's STAR_FAINT_PX, STAR_BRIGHT_PX, STAR_SPIKE_START, STAR_SPIKE_PX, STAR_MIN_PX, STAR_TWINKLE_MAX, STAR_CHROMA, STAR_MOON_WASH, STAR_MOON_FADE_MAX and STAR_BLOOM_BOOST, starsF's STAR_HALO and STAR_SPIKE), and brightest reads ALStarField's constants of the same names less STAR_. layOut orders the stars by one counting sort, which is stable: a key for each kind, cell and intensity byte, the spiked stars' first by intensity alone, then each cell's points', brightest first within each, and stars as bright in the field's own order, which is the order stable comparison sorts give. layOut runs on the main thread at login and at every RenderStarCount change, and through the counting sort it takes 3.58 ms at the default count and 17.54 ms at MAX_COUNT, against 14.12 ms and 67.31 ms through those sorts (alstarfield_bench, pinned to one core). Half a step is what a star left out may add. On an 8-bit attachment, the albedo or the emissive without HDR (RGB8), which the stars blend into one at a time, it rounds away and the picture is unchanged. The float one, the emissive under HDR (R11G11B10F), rounds nothing away, and the exposure, the tonemapper and the grade after the sky's resolve may show any of it, so there the pass draws every star in view. A bound for it would have to follow every tonemapper's curve at its live parameters, and the grade; and at dusk the whole field in view costs about 4 us of GPU more than its culled prefix (5.2 us against 1.3 us at a level 60-degree view on NVIDIA). On an 8-bit attachment, at a level 60-degree view the field draws 2.1k to 2.4k of 25.9k points at a star brightness of 0.003, 9.4k to 9.8k at 0.01 and 13.9k to 14.3k at 0.03. From 0.1 up, which takes in the default night's 0.5, every star in view can add half a step and all are drawn, under a full moon too: the moon's wash takes a faint star down to 45% at most, and the default night's faintest dust still adds about 2/255. So moonlit nights at the default brightness draw the whole field in view, and only dusk draws a fraction. alstarfield_test checks that the quads and every cell run brightest first and that the layout's intensities are the stars'; that at every count from 0 to MAX_COUNT the counting sort lays the stars, the runs and, to rounding, the cones out as the stable comparison sorts do; that each STAR_ constant the shaders declare is ALStarField's, and reads starsV and starsF for every term the bound takes; and, through a copy of the shaders' per-star arithmetic over colours, zenith angles, scintillation noises, moon powers, star brightnesses from 0.001 to 2 and both paths, that no star fainter than the threshold reaches half a step, and the faintest kept comes within 30% of it. The draws are each cell in view down to the threshold, and the quads' prefix. alstarfield_gl_test draws every star and then the frame's, with the 8-bit bound on both paths: on the albedo path the pictures are identical in all six scenes; on the emissive path the dusk scenes leave out 14.6k of 29.6k and 21.8k of 55.6k points, and a pixel differs by at most 0.75/255, where two stars left out overlap (8 of 84.5k lit pixels pass half a step). The A/B against the old quads now measures its sub-pixel criterion with every star drawn, and bounds the frame's picture pixel by pixel. With the scintillation's margin left out of the bound, stars left out reach 0.58/255. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The stars blended into all four attachments of the G-buffer: zeros into the albedo (with the emissive buffer) and the specular, (0, 1, 0, SKIP_ATMOS) into the normal, and their colour into the one the sky's colour is in. Only that one changed anything the frame reads: the others' additions were zero, or the normal's y, which nothing reads at the sky (the luminance meter reads only the flags in its w, and SKIP_ATMOS is 0). Each star pixel still read and wrote all four. starsF now writes that output alone, frag_data[3] with the emissive buffer and frag_data[0] without, and the pass narrows the bound G-buffer's draw buffers to it through LLRenderTarget::setDrawAttachment, which sends a fragment output to the attachment of the same index and leaves every other attachment unread and unwritten (and a clear clears that one alone), and gives them all back with setDrawBuffers(). It takes the place of setDrawOutput, which nothing called: it sent one named output into a target's first colour attachment. The pass takes the attachment from the latched shader inputs, and draws no stars into a target without it, which would otherwise take undefined outputs. Under the stars the sky still draws all four, so the attachment's clear colour stays as it is until Phase 3 stops the dome writing the G-buffer. llrendertarget_gl_test draws a four-output program into a four- attachment target narrowed to each attachment in turn: only that attachment takes its output, a clear takes it alone, and setDrawBuffers() restores all four; with the output sent to attachment 0 instead, 12 checks fail. setDrawOutput's case goes with it. alstarfield_gl_test now draws the stars as the pass does and checks every other attachment is exactly as the sky left it; without the narrowing, 12 fail. The A/B against the old stars, as all of Phase 2 now draws them: - albedo path: 77 to 9,062 of 14.2k to 117.3k lit pixels differ, by at most 1/255 to 3/255, in every scene fewer than the old stars themselves change when moved 1/256 of a pixel (352 to 31,376 pixels, 1/255 to 4/255); - emissive path: 4,924 to 35,754 pixels at night, by at most 2/255 to 16/255 at bright overlaps, which is one R11G11B10F step where the stars' blends round in a new order (the old shaders in the new order give the same), again fewer than the moved old stars (13.4k to 63.8k); at dusk 9.3k and 16.7k pixels differ, nearly all by under half a step, from the stars the brightness cull leaves out, at most 0.75/255 where two of them overlap. glslangValidator -l links all eight star programs; a control that renames the emissive output fails the four emissive ones. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 59 minutes. View limit details
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
GCC's -Wclass-memaccess, an error under the Linux rows' warnings as errors, refuses a memset over an LLVector4a, whose one member is private. The test sets and compares the guard past the last vertex through getF32ptr() instead, the same sixteen bytes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phase 2 of the sky modernization (
doc/SKY_MODERNIZATION.md): the star field. Stacked on #581 (rye/sky-p0-p1).What changes
ALStarField. It is the same field Phase 0 lays out, from per-star generators, and the band's density is a template inalstarfieldgenerate.h.GL_POINTSvertex instead of a six-vertex quad: 155k stars go from 26 MB to 2.5 MB.gl_FragCoord, so the driver's point-size rounding doesn't move them.LLVertexBuffer::drawMultiArrays. A level 60° view draws 25.9k of 154.7k points.layOut's sort is now a counting sort: 3.6 ms instead of 14.1 ms at the default count.LLRenderTarget::setDrawAttachment).Verified
ctest -LE manualon the stack's previous base. On the current base only the full stack's tip was rebuilt: it builds clean and passes 557/557.alstarfield_testandalstarfield_gl_testcover:Not verified
🤖 Generated with Claude Code