Anisotropic Scaling in Indiana Jones and the Great Circle and Doom the Dark Ages

2 hours ago 2

While working on Indiana Jones and the Great Circle and later DOOM: The Dark Ages, it became apparent that both titles are particularly sensitive to anisotropic sampling cost on the Xbox Series consoles. This is despite both having very different renderers, Indiana Jones uses what I would best describe as a semi-forward system, while Doom use deferred texturing, with the novel development of a barycentric buffer. (See GPC 2025: Visibility Buffer and Deferred Rendering in Doom the Dark Ages) The fact that both are so sensitive to anisotropic tap count I ascribe partly to the system of layering materials, and partly to the fact that both games use Samper Feedback Streaming (SFS) on Xbox Series consoles. (SFS adds an additional texture fetch to discover what the most detailed mip is in memory for a given texture region, which is then used to limit the access of a subsequent texture read)

As usual, the problem arises due to variation in scene content. Some scenes could afford high levels of anisotropic sampling, while others benefitted from reducing the count to ensure a locked 60hz output. I set about one Saturday (immediately prior to code lock) to attempt to tie anisotropic sampling level to the current dynamic resolution scale, not entirely convinced this was a visually acceptable thing to do.

Most non-engineers would be amazed how much DRS resolution choice can jump around even with a steady camera. A good TAA/super resolution implementation ensures nobody notices per-frame resolution changes. In fact, I think it can help, providing additional temporal information on a macro scale, while sub-pixel jitter provides additional information on a micro scale.

The problem with changing anisotropic sample count is that it’s extremely obvious, visually nasty, and particularly distracting if the count flip flops. The difficulty therefore was to create an algorithm that will change the count, but is reluctant to do so, and will even stick with a setting it considers to be ‘wrong’ rather than change it.

The solution I arrived at is two part; the first is a filter, which is used to ensure the same recommendation for change has been made for a number of frames, less frames the higher the confidence. This on its own however was not enough. The second part of the algorithm is an aid to prevent flip flopping, so if the last change was to increase the anisotropic tap count, increasing it again is easier than switching direction and decreasing the tap count.

This algorithm varied the anisotropic tap count between 1 (bilinear) and 8 on Xbox Series S and between 4 and 10 on Xbox Series X. (10 being in effect is a bias towards 8)

Something I expected I might need to implement was an additional reluctance to up sample unless the camera transform had altered significantly. However, I did not find this necessary, the algorithm has enough resistance to change that in practice, it rarely changes the sample count with a steady camera anyway, and when it does, it’s hard to argue the choice. Anisotropic scaling wasn’t applied to the terrain system as this used different samplers, but likely could have been for additional savings. Indiana Jones does vary the sample count on terrain overlays such as road tracks, which is where changing the level was most noticeable due to these overlays often running at max sample count and on a large screen area.

The code ported straight over from Indiana Jones to DOOM: The Dark Ages and shipped in that title also with only a minor tweak to parameters. Both games shipped with a perf or quality win depending on scene load, and nobody noticed or complained (that I noticed). Indiana Jones won Digital Foundry’s technology of the year 2024 (despite the ‘knocked up in a day’ anisotropic scaling algorithm), huge congrats to everyone involved!

The same algorithm can be used to tie any graphical setting to DRS where a frequent change or flip-flip of state is objectionable, but an infrequent change particularly with a moving camera may go unnoticed. For example, I fully expect the same algorithm could be used for shadow map size or shadow filtering quality. Other parameters can be tied to DRS scale directly without this algorithm, e.g. VRS shading rate, which we did on Doom. (Another shameless plug! See GPC 2025: Variable Rate Compute Shaders in Doom the Dark Ages)

Always great to work with the talented teams at Machine Games and id Software and thank you to both for allowing me to share.

The code below is of course trivial to generalise to other settings, not just anisotropic sampling. The code is modified from as-shipped only to aid portability and understanding.

/* MIT License Copyright (c) Microsoft Corporation. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE */ float GetAnisotropicSampleCount( float minAniso, // min sample count for current platform settings float maxAniso, // max sample count for current platform settings float currentResScale, // current resolution scale float minAnisoResScale, // resolution scale <= which we return minAniso float maxAnisoResScale, // resolution scale >= which we return maxAnsio float bias, // prevents flip flop of ansio settings, higher numbers reduce oscillation, 1.25f default, suggested range 0..2 float smoothFactor, // smooth factor, closer to one takes longer to smooth, default 0.925f float smoothFactorPanic // smooth factor when resolution is less than min, closer to one takes longer to smooth, default 0.8f, should be less than smoothFactor ) { static float currentAniso = -1.0f; static float smoothedAniso = -1.0f; static float upBias = 0.0f; static float downBias = 0.0f; if (currentAniso < 0.0) { // first frame initialization currentAniso = minAniso; smoothedAniso = minAniso; } else { float anisoThisFrame; float smooth; if (currentResScale <= minAnisoResScale) { // panic, favor this frames preferred aniso and make it easy to scale down anisoThisFrame = minAniso; upBias = 2.0f; downBias = 0.0f; smooth = smoothFactorPanic; } else { // ok, everything is normal, no panic if (currentResScale >= maxAnisoResScale) { anisoThisFrame = maxAniso; } else { // could precalculate reciprocal float t = (currentResScale - minAnisoResScale) / (maxAnisoResScale - minAnisoResScale); anisoThisFrame = minAniso + (t * (maxAniso - minAniso)); } smooth = smoothFactor; } // smooth the running target, takes out a lot of per frame noise smoothedAniso *= smooth; smoothedAniso += (1.0f - smooth) * anisoThisFrame; } // check if we should scale up or scale down, we don't want to do this often, but we need to do it sometimes! float candidateAniso = roundf(smoothedAniso + 0.499f); // Might want to prevent scaling up if no or very low camera movement, that check would go here if ((smoothedAniso - (upBias * bias)) > currentAniso) { if (currentAniso != candidateAniso) { // scale up currentAniso = candidateAniso; // easy to scale up again next frame, but hard to scale down upBias = 0.5f; downBias = 2.0f; } } if ((smoothedAniso + (downBias * bias)) < currentAniso) { if (currentAniso != candidateAniso) { // scale down currentAniso = candidateAniso; // easy to scale down again next frame, but hard to scale up upBias = 2.0f; downBias = 0.5f; } } return currentAniso; }
Read Entire Article