Shadow Draw Calls are broken

Introduction & Previous Contact

Recently, during the summer, I asked a colleague to create a post regarding severe FPS drops in games caused by an excessive number of “Draw Shadow Calls.” This forces the engine to calculate massive amounts of data on seemingly non-complex locations, especially when compared to modern-day games. Our goal was to determine if this behavior was correct or if it constituted a bug. We did not receive a response from Roblox Staff or a Developer. Time has passed, but the problem remains the same. Additionally, a strangely high percentage of “Schedule Render” has appeared, reaching up to 90% (in my case, it consumes almost 50 FPS from the total).

2. System Configuration

Primary Workstation:

    OS: Windows 11 Home (24H2, Build 26100.4946)

    CPU: Intel Core i5-12400F

    GPU: AMD Radeon RX 6700 XT (Stable Driver)

    RAM: 32GB (4x8GB) DDR4 @ 3200MHz

    Storage: SSD

    Connection: Wired Internet

    Browser: Firefox

Secondary Workstation:

    OS: Windows 11 Home (24H2, Build 26100.4946)

    CPU: Intel Core i5-14600KF

    GPU: AMD Radeon RX 6700 XT (Stable Driver)

    RAM: 32GB (2x16GB) DDR4 @ 4400MHz

    Storage: SSD

    Connection: Wired Internet

    Browser: Firefox

Internet Speed Test: https://www.speedtest.net/result/18194927265 (Provided as requested by support)

Actual Behavior

When any dynamic object moves (e.g., doors, wheels, or BaseParts), an intense shadow recalculation is triggered. This is most noticeable on large maps. As a result, the shadow draw count skyrockets to 5,000 – 6,000 calls (representing approximately ~1 billion triangles), and the FPS is halved. On some less powerful devices, this leads to unacceptable lag, making the game unplayable. This occurs even with minor movements (such as resizing a part in Studio) and can last for over 3 seconds per object. Notably, the movement of a player’s avatar typically causes only 200-300 draw calls, which proves that optimization is possible.

Where It Occurs

  • In our game: A “Wheel of Fortune” script caused ~6,000 shadow draws. A single rotating part consumed ~6,000 draw counts.

  • In popular experiences like “Doors”: Shadow draws spike to ~1,500, capping the FPS at around 120.

  • The issue is reproducible in minimal, controlled scenes (see the attached file for a reproducible test place).

When It Occurs / Frequency

We first fully encountered this issue in July-August of this year. It persists to the present day without any change. I have contacted Roblox technical support, and we filed a report, but received no response. The issue triggers 100% of the time when an object moves or spawns (observing this requires a scene with active lighting and geometry).

Reproduction Steps

  1. Open Roblox Studio and create a new baseplate place (using standard “Future” graphics settings, etc.).

  2. Add approximately 16 or more PointLight objects (with Shadows.Enabled = true) and 50 or more assets (e.g., PBR meshes, shelves, barrels, or other complex geometry).

  3. Create a single moving part (driven by physics or a script) within or near the illuminated area.

  4. Press Shift + Ctrl + F2 to open the performance stats and navigate to Draw Counts > Shadows.

Observation

The shadow draw count will skyrocket to 800-1,000+ calls (varies depending on the test setup), causing a 30-40% FPS drop (up to 100% on complex scenes, making them unplayable).

I have attached videos below that clearly demonstrate the problem in both our project and in the “Doors” experience. If more detailed information is required, I also have a private YouTube video created specifically on this topic.





Expected behavior

Expected Behavior

I expect that when an object moves under light sources, it should only force those specific lights to recalculate their shadows, rather than causing a near-global recalculation of the entire shadow map. For instance, when a player character moves and we observe them from a third-person view, in most cases, it does not cause such massive recalculations compared to, for example, a simple unanchored Part.

A private message is associated with this bug report

16 Likes

I also encountered this problem. Bump

I’m still encountering this problem, No updates from staff or anything. Bump.

I believe this is a problem apart of you’re shadow driver. (Don’t know that the thing is called)

There are a couple things going on here.

But TL;DR: since your game seems to be fully inside, turn off Lighting → GlobalShadows. Since this disables shadows from the sun, which by and far use most draw calls due to cascaded shadowmap.

** Why does the draw call count increase so much? **

there is a split here between “Sun” and other lights “local lights”

SUN

The sun light (controlled through Lighting → GlobalShadows) uses a cascaded shadow map. This is a good resource to understand more of it: Cascaded Shadow Maps - Win32 apps | Microsoft Learn

Roblox has some custom optimizations on top of it, that makes it so that not the whole shadow map needs to be regenerated when an object moves. However the base case remains that if any object moves inside the shadow volume, then that part of the shadow map needs to be regenerated. In the case of cascaded shadow maps, this often means that multiple cascades need to be updated. So you can see multiple draw calls for a single object changing, one for each cascade.

Roblox does NOT cache static vs dynamic parts of the shadow map (currently) due to Roblox having no notion of a “static” object. Anchored != static. The other reason being that that would require another shadow map for the static parts which can then get copied to the shadow map for dynamic parts, which would then be re-rendering only the dynamic parts. (More information on how this works is here: DOOM (2016) - Graphics Study - Adrian Courrèges in the “Shadow map”)

** LOCAL LIGHTS **

For local lights, e.g. point lights, spot lights, etc. They have limited range, so the amount of objects that can affect them is usually limited. It is important to understand though that point lights are usually implemented through rendering through cube maps. Which means there are 6 faces, and hence some parts can show up in more than 1 face. Contributing to more draw calls.

** Caching **

In order to not continuously recalculate all the shadow maps all the time and to balance the amount of draw calls for shadows vs the main scene. There is a caching and throttling system that prioritizes updates of shadow maps based on multiple factors (think how much of the light can be seen on the screen, etc.)

This will throttle shadow map updates so it doesn’t completely destroy performance… proportional to the amount of objects touching various light volumes. But it obviously costs some performance as it is rendering stuff into shadow maps.

I hope this helps.

2 Likes

How can we implement caching in our game? By the way, we turned off the sunlight a long time ago; it usually does cause a lot of draw calls.

If you mean the caching of shadow maps, that’s an automated system and you have no manual control. This is by design as we do not want to burden creators with fine level details like you often see exposed in other game engines out there. The reason being that this adds a lot of technical complexity and know-how expectations on the creator’s side, which correlates with time it takes to create sometime (and therefore cost)