Login
You're viewing the discuss.systems public feed.
  • Oct 3, 2026, 9:52 PM

    @GDCPresoReviews GS has the same issue where every vertex produced by the GS is essentially unique and un-shared, and on top of that, your output topology is a triangle strip, which also doesn't let you induce much data sharing _within_ a GS evaluation.

    Some special cases work. E.g. using GS to turn points into sprites is decent, a quad is a 2-tri strip and the 4 corner verts were never gonna be shared with other points anyway.

    💬 4🔄 0⭐ 2

Replies

  • Oct 3, 2026, 9:54 PM

    @GDCPresoReviews Note that this is one of the few common, and endorsed, uses of GS, for this exact reason. It's not adding any waste, and it's doing the data amplification in a shader rather than making a 4x larger vertex buffer or similar that would need to be uploaded and streamed into the shader.

    Okay, final question: I mentioned the memory concerns before, but what memory are we actually talking about? This is where we really get into the weeds.

    💬 2🔄 0⭐ 1
  • Oct 3, 2026, 9:57 PM

    @GDCPresoReviews The key issue that GPUs (and drivers) need to think about a lot, and graphics programmers barely think about at all, is how things like positions or vertex attributes go from vertex shader outputs to pixel/fragment shader inputs.

    💬 1🔄 0⭐ 0
  • Oct 3, 2026, 9:59 PM

    @GDCPresoReviews You're doing vertex shading on some shader core, that produces something like 32-128 vertices in one go that can participate in 100+ triangles, each of these triangles gets rasterized into some number of quads (some of them quite a lot of them, some none), and you need to get all that data from the vertex shader to either the rasterizers (positions) or the pixel/fragment shaders (attributes).

    💬 1🔄 0⭐ 0
  • Oct 3, 2026, 10:01 PM

    @GDCPresoReviews Enough quads are (potentially) generated here that you _need_ a work redistribution stage where the quad shading workload gets distributed to other shader cores.

    Your vertex/domain/geometry shader has vertex attribute data, the thing the pixel shader ultimately needs is plane equations for how the attribute varies across the triangle.

    Typically you just use

    v0 + (v1 - v0) * I + (v2 - v0) * J

    where I, J are perspective-corrected barycentric coords for interpolation.

    💬 1🔄 0⭐ 0
  • Oct 3, 2026, 10:04 PM

    @GDCPresoReviews So _somehow, _someone_ needs to go from a block of vertex attributes computed on one shader core to

    1. figure out which tris are involved in a warp/wavefront worth of quads to be dispatched
    2. fetch the 3 verts for every referenced tri and computing these plane equations
    3. stash those plane equations somewhere the pixel/fragment shader can get at them quickly to do that interpolation.

    💬 1🔄 0⭐ 1
  • Oct 3, 2026, 10:07 PM

    @GDCPresoReviews I can get concrete for AMD HW, since all the stuff I'm about to say is in their public docs.

    There's two large(-ish) dedicated banks of SRAM, the "position cache" and "parameter cache" respectively, that are the centralized staging area for shaded vertex positions (->rasterizer) and parameters/attributes (->shaders), respectively.

    These are conceptually connected to every single shader core in the system. (Some caveats, will cover later.)

    💬 1🔄 0⭐ 1
  • Oct 3, 2026, 10:09 PM

    @GDCPresoReviews Then the shader cores have dedicated setup hardware that is used to prepare shader wavefronts that sets up things like barycentrics and also makes sure things like those plane equations are in place.

    This pulls parameter data from the parameter cache, forms the plane equations, and stores them into LDS. (I.e. same bank as used for shared memory in a compute shader). The "interpolation" instrs use the data in LDS to get the interpolated attributes from there, given barycentrics.

    💬 1🔄 0⭐ 0
  • Oct 3, 2026, 10:13 PM

    @GDCPresoReviews The position/parameter caches are a bottleneck.

    Vertex shaders can't export their shaded positions/parameters until there's a free slot to write that data to. They just stall on those export insns until their output space is available.

    They're also shared between _all_ shader cores (again, some caveats later), so relatively high traffic.

    And un-sharing almost all vertices (as you end up doing with low tess factors or GS) massively increases space used per tri in those caches.

    💬 1🔄 0⭐ 0
  • Oct 3, 2026, 10:16 PM

    @GDCPresoReviews "Why don't you just make them bigger then?"
    Broadly, they're as big now as they have to be. It's up to several megabytes now in the big GPUs, that's a lot of dedicated memory for this, and they absolutely don't want to grow it by another 4-6x or whatever to deal with crappy unshared verts everywhere.

    Plus they'd need ~4-6x the BW in and out as well! It's all bad.

    💬 2🔄 0⭐ 0
  • Oct 3, 2026, 10:17 PM

    @GDCPresoReviews Broadly, increasing usage by a double-digit percentage because you lose cache hits from resetting the vertex cache every 128 or 256 tris or whatever, when you shade in big batches: you can absorb that.

    Absorb "suddenly everything is un-shared": not gonna happen at reasonable cost.

    Finally, the thing I've been dodging: it doesn't have to be a single global thing. You can, and people do, split them a bit.

    💬 1🔄 0⭐ 1
  • Oct 3, 2026, 10:20 PM

    @GDCPresoReviews The thing you see in bigger GPUs is that they are sometimes effectively built as like 2 smaller GPUs in a trenchcoat with a shared command processor and memory interface, and then you sometimes have a setup where there's not a full crossbar and not every vertex shader can generate data used by every fragment shader. Lets you build more, smaller caches (better for BW, contention), limits work redistribution.

    💬 1🔄 0⭐ 1
  • Oct 3, 2026, 10:23 PM

    @GDCPresoReviews i.e. say you have 64 shader cores total, you might build that in 2 "hemispheres" where batches are cut up very early on runs of something like 128-256 prims or so, get assigned to a partition (with 32 shader cores), and then it just can't leave that partition whatsoever until it all re-joins at the blend/ROP/output stage.

    Simpler to build, more scalable, but costs you work efficiency, so a delicate balancing act.

    (end infodump!)

    💬 1🔄 0⭐ 0
  • Oct 3, 2026, 10:36 PM

    @GDCPresoReviews bonus note: if you're looking at AMD HW, you might have noticed that a while back, they added support for making vertex attributes half-floats.

    A lot of attrs really don't need to be float32 (e.g. most normals/tangents, some UVs), and making them float16 takes up half the space in the parameter cache, which as noted is often a bottleneck.

    (It also has savings elsewhere, but the parameter cache is usually the most significant one.)

    💬 1🔄 0⭐ 2
  • Oct 3, 2026, 10:48 PM

    @GDCPresoReviews In the other direction, bonus win from mesh shaders:

    "Classic" vertex shaders don't know what tri they belong to. Things like back face culling happen _after_ storage for vertices is committed, and at that point you're stuck even if all of the tris referencing a vertex end up culled.

    Mesh shaders can cull _before_ allocating output space, and actually remove unreferenced verts entirely.

    💬 4🔄 0⭐ 2
  • 💬 0🔄 0⭐ 1
  • Oct 4, 2026, 9:36 AM

    @rygorous @GDCPresoReviews though the output space allocation strategies also vary by hardware. NVIDIA's current architectures do use a pre allocation (think shared memory) before the mesh shader is run. This is worst case outputs. (And also true for vertex shaders). As result it can limit occupancy.

    From that, the surviving subset is then passed through pipeline.

    NV doesn't support non 32-bit outputs, but one can use shader barycentrics and interpolate custom packed attributes.

    💬 1🔄 0⭐ 0
  • 💬 0🔄 0⭐ 1
  • Oct 4, 2026, 9:51 AM

    @pixeljetstream @GDCPresoReviews we did some experiments with this in the experimental LRB tess pipe; from underlying dynamics it was clear that below a certain number of quads per tri, the plane equation setup step (which we also had to do on "shader cores" since that was all we had!) wasn't worth the cost.

    We tried an alternative interpolation setup for PSOs (well, the internal equivalent) used for high tess where we did full 3D I,J,K barycentrics, K=1-I-J, and interp as

    K*v0+I*v1+J*v2

    💬 1🔄 0⭐ 0
  • Oct 4, 2026, 9:55 AM

    @pixeljetstream @GDCPresoReviews slightly more expensive interp (mul-mad-mad instead of mad-mad, essentially) and the pixel shader had to gather the tri attrs instead of a dedicated setup stage doing it, but a PS was in a better position to hide the gather latency than the dedicated setup did, because it had some reasonable other work to interleave it with.

    Dedicated setup was basically all gathers and then just a sub or two, then back to gathers, so constantly waiting on gathers.

    💬 1🔄 0⭐ 0
  • 💬 0🔄 0⭐ 0
  • Oct 4, 2026, 10:30 AM

    @rygorous

    So it sounds like: if you’re using the GS to:

    1. Generate a many-vertex triangle strip for every input primitive
    2. And every triangle strip has (roughly) the same number of vertices in it

    Then you might be able to get up to half the hit rate of the vertex cache and vertex/parameter buffer utilization/bandwidth as you would get for a traditional fully-connected indexed mesh (because a tri-strip has avg. connectivity = 3, compared to 6)

    💬 2🔄 0⭐ 0
  • Oct 4, 2026, 10:35 AM

    @rygorous

    And if you’re generating 1 triangle for every input triangle, then you’ll have the same problem that tessellation has with low tess factors

    And if you’re generating 0, you’re no worse than tessellation, but you are (probably) worse than mesh, because mesh shaders generally (at least) cull entire meshlets, rather than individual triangles, meaning the vertex/parameter buffer you’re leaving empty is 6x the size for GS compared to MS

    💬 1🔄 0⭐ 0
  • Oct 4, 2026, 10:40 AM

    @rygorous

    So it sounds like the key to mesh shaders being more effective is changing the output topology to use indexed meshlets rather than individual triangles or triangle strips.

    (And I also think that, once you’re generating meshlets, the only sensible programming model is to have one be generated cooperatively by a whole workgroup, rather than the old single-thread programming model)

    That’s an interesting explanation, thank you very much for it.

    💬 1🔄 0⭐ 0
  • Oct 4, 2026, 10:44 AM

    @rygorous

    Hang on, does this mean that you need an index buffer to get good perf, even for traditional draw calls, so the cache can use it as a deduplication key?

    Like if I implement an index buffer myself by doing ‘vertex pulling’ in software in the VS, the draw call itself isn’t an indexed draw call, which means I fall off that same 6x perf cliff, right?

    💬 1🔄 0⭐ 0
  • 💬 1🔄 2⭐ 4
  • 💬 1🔄 0⭐ 1
  • Oct 4, 2026, 6:40 PM

    @GDCPresoReviews Normally if you do vertex pulling in the VS you use SV_VertexID. That ID is the unique cache for the formation of vertex shade warps/wavefronts.

    Non-indexed draws just auto-generate an incrementing counter here, so yes, every vertex ends up treated as unique, and there is no sharing.

    💬 1🔄 0⭐ 0
  • Oct 4, 2026, 7:00 PM

    @GDCPresoReviews There is some uniquely cursed behavior here where non-indexed drawing is specified (in the D3D spec) as generating an incrementing index sequence (as if you'd bound a 0 1 2 .... index buffer).

    I have confirmed, on actual HW, that if you do a non-indexed draw of a triangle strip while the GPU is in 16-bit index mode (it's a toggle in some register on the HW in question), the triangle containing vertex 65535 is skipped, because that is the Primitive Cut Index for 16-bit inds.

    💬 3🔄 0⭐ 0
  • Oct 4, 2026, 6:47 PM

    @GDCPresoReviews GS has a ton of complexity around the whole emit flow. (Likewise, the internal decomposition of hull/domain shaders in D3D into fork/join etc phases is bizarre. Don't know what other GPUs did with it, in the compiler I worked on first thing I did was just flatten it back into a normal control flow, because I had no use for any of it in the form it was presented.)

    💬 0🔄 0⭐ 0
  • Oct 4, 2026, 6:44 PM

    @GDCPresoReviews If you're generating 0 somewhere, you're probably still bad, because the amount of output space allocated can be proportional to the actual vertex count (asymptotically - often there's some allocation granularity, like groups of 4 or 16 or something), but it's also often just based on the maximum output space estimate you provided. When that's there, it's generally for other reasons, to do with the other Big Problem in GPU design other than data logistics: deadlock avoidance

    💬 1🔄 0⭐ 0
  • Oct 4, 2026, 6:53 PM

    @GDCPresoReviews Allocating as late as possible uses memory most efficiently but can lead to deadlocks. When you allocate all resources up front before you even allow a job to spawn, you can statically avoid deadlocks (and other types of ordering problems), but waste a lot.

    A compromise you see sometimes is that you allocate up front for the worst case (guaranteeing forward progress is possible after) but can realloc to shrink post-launch. (Returning unused space to pools.)

    💬 0🔄 0⭐ 0
  • 💬 0🔄 0⭐ 0
  • 💬 0🔄 0⭐ 0
  • Oct 4, 2026, 8:29 AM

    @rygorous @GDCPresoReviews Programmers aren't used to worrying about internal register bandwidth, because in many cases the HW architects go to huge lengths to either make "enough" bandwidth, or to hide those bandwidths with various cunning mini-caches and bypasses, but it's hugely important in cases like this, and it's why too many interpolants going from VS to PS can be such a surprising perf hit.

    💬 0🔄 0⭐ 1
  • Oct 4, 2026, 8:24 AM

    @rygorous @GDCPresoReviews Just a tweak here - the important part of this "blessed path" is a FIXED amplification factor. Every GS invocation always produces the same number of verts/tris.

    Otherwise, the thing that reassembles the triangles after the GS can't reserve the slots deterministically and your perf goes in the toilet.

    💬 0🔄 0⭐ 0
  • Oct 3, 2026, 10:36 PM

    @rygorous @GDCPresoReviews I believe the other big performance problem with geometry shaders is you end up emitting the geometry serially within very few threads, meaning you potentially have a huge chunk of output (that you also need to reserve if the max declared GS outputs are large, even if the GS outputs little at runtime) with very little parallellism.

    💬 1🔄 0⭐ 0
  • Oct 3, 2026, 10:39 PM

    @bas @GDCPresoReviews Yes, that was mentioned before, and that can explain why GS perf is bad if you have large max output size and normally don't use it, but it does not explain why GS perf is still mostly bad if you have a fixed number of output prims each invocation and stick to it (exceptions like the point sprites I mentioned aside), which is what the original question was about.

    💬 0🔄 0⭐ 1
  • Oct 5, 2026, 10:59 AM

    @rygorous @GDCPresoReviews Graphics programming noob here.
    What's the trade-off of converting a point to a quad sprite using GS vs drawing a quad and displacing it using VS to get the same result?

    💬 0🔄 0⭐ 0
  • Oct 5, 2026, 2:47 PM

    @rygorous "Some special cases work. E.g. using GS to turn points into sprites is decent, a quad is a 2-tri strip and the 4 corner verts were never gonna be shared with other points anyway."

    Oh… this was what I actually wanted GS for. But then they were taken away >_>

    💬 1🔄 0⭐ 0
  • 💬 0🔄 0⭐ 0