Add mip map lod bias and bilinear types (#10701) 608fb2781f
* Add mip map lod bias and bilinear types

* minor updates

* Removed MAX enums

* Added Image Mesh LOD Bias

* Add metal support

* Add VK support

* WebGPU filter

* Remove lodbias from metal

* clang format

* clang format

* turn off bilinear on GL as a test

* Remove trilinear and miplodbias from sampler states

* Moved miplodbias into shader instructions

* removed trilinear from webgpu

* clang the shaders

* Update image_filter_options.cpp

* fix missing $ in metal.glsl

* fix lua WRT trilinear

* fix missing flushuniforms in metal.glsl for image draw

* Remove lodbias from FRAG_DATA_MAIN

* fix #define

* Update rive_lua_libs.cpp

* Update metal.glsl

* Removed lodbias from image mesh as a test

* Update atomic_draw.glsl

* Update draw_image_mesh.glsl

* Fix unit tests

* Remove whitespace

* Update draw_image_mesh.glsl

* Update image_sampler.hpp

* Update bindings_c2d.cpp

* clang format

* Update render_context_d3d_impl.cpp

* Readd lodbias to draw image mesh

* Update glsl.glsl

* Update draw_image_mesh.glsl

* Update glsl

* Update glsl.glsl

* Update glsl.glsl

* Update glsl.glsl

* Update rhi.glsl

* more lodbias removal

* more lodbias removal

* Update glsl.glsl

* Update glsl.glsl

* Update glsl.glsl

* Remove one image call lodbias

* Update glsl.glsl

* Update glsl.glsl

* Update glsl.glsl

* Update glsl.glsl

* Update glsl.glsl

* Update draw_image_mesh.glsl

* clang format

* Update hlsl.glsl

* Update metal.glsl

* Update rhi.glsl

* clang format

* Update Goldens

* update golden

* more goldens

* Test for metal

* goldens

* metal test fix

* Update gpu.cpp

* revert changes

* Update gpu.cpp

* Update linux golden

* Metal test

* metal test

* Update gpu.cpp

* Update gpu.cpp

* clang format and ios goldens

* Update goldesn

* linux goldens

* move mipmaplodbias in uniforms

* Addres Chris' comments

* More comments addressed

* clang format

Co-authored-by: John White <aliasbinman@gmail.com>
diff --git a/.rive_head b/.rive_head
index 2338ff4..9f9cb77 100644
--- a/.rive_head
+++ b/.rive_head
@@ -1 +1 @@
-9c66afdd2a7e9ee1a08b2da8da4679cfa3e4545d
+608fb2781f361c0c795b4522ac6d9662eb42b4b8
diff --git a/include/rive/lua/rive_lua_libs.hpp b/include/rive/lua/rive_lua_libs.hpp
index 2358c4b..3cc8e54 100644
--- a/include/rive/lua/rive_lua_libs.hpp
+++ b/include/rive/lua/rive_lua_libs.hpp
@@ -56,7 +56,7 @@
     clamp,
     repeat,
     mirror,
-    trilinear,
+    bilinear,
     nearest,
 
     // Paint
diff --git a/include/rive/shapes/paint/image_sampler.hpp b/include/rive/shapes/paint/image_sampler.hpp
index 8fe04c7..e929a42 100644
--- a/include/rive/shapes/paint/image_sampler.hpp
+++ b/include/rive/shapes/paint/image_sampler.hpp
@@ -7,11 +7,10 @@
 {
 enum class ImageFilter : uint8_t
 {
-    // High fidelity linear filter in all 3 directions: x, y, and between mip
-    // levels
-    trilinear = 0,
+    // High fidelity linear filter in all 2 directions: x, y
+    bilinear = 0,
     // Sample with low fidelity, good for things like pixel art.
-    nearest = 1
+    nearest = 1,
 };
 
 constexpr size_t NUM_IMAGE_FILTERS = 2;
@@ -27,7 +26,7 @@
     mirror = 2,
 };
 
-constexpr size_t NUM_IMAGE_WRAP = 4;
+constexpr size_t NUM_IMAGE_WRAP = 3;
 
 struct ImageSampler
 {
@@ -38,7 +37,7 @@
     ImageWrap wrapX = ImageWrap::clamp;
     ImageWrap wrapY = ImageWrap::clamp;
     // How to sample the texture, this will be for both MIN and MAG filtering.
-    ImageFilter filter = ImageFilter::trilinear;
+    ImageFilter filter = ImageFilter::bilinear;
 
     bool operator==(const ImageSampler other) const
     {
@@ -54,14 +53,15 @@
     // The maximum number of possible combinations of sampler options. Used for
     // array length in implementations.
     static constexpr size_t MAX_SAMPLER_PERMUTATIONS =
-        NUM_IMAGE_FILTERS * NUM_IMAGE_FILTERS * NUM_IMAGE_WRAP;
+        NUM_IMAGE_FILTERS * NUM_IMAGE_WRAP * NUM_IMAGE_WRAP;
 
     // Convert struct to a key that can be used to index an array to get a
     // unique sampler that represents these options.
     const uint8_t asKey() const
     {
-        return static_cast<int>(wrapX) + (static_cast<int>(wrapY) * 3) +
-               (static_cast<int>(filter) * 9);
+        return static_cast<int>(wrapX) +
+               (static_cast<int>(wrapY) * NUM_IMAGE_WRAP) +
+               (static_cast<int>(filter) * NUM_IMAGE_WRAP * NUM_IMAGE_WRAP);
     }
 
     static ImageSampler SamplerFromKey(uint8_t key)
@@ -79,17 +79,18 @@
 
     static ImageWrap GetWrapXOptionFromKey(uint8_t key)
     {
-        return static_cast<ImageWrap>(key % 3);
+        return static_cast<ImageWrap>(key % NUM_IMAGE_WRAP);
     }
 
     static ImageWrap GetWrapYOptionFromKey(uint8_t key)
     {
-        return static_cast<ImageWrap>((key % 9) / 3);
+        return static_cast<ImageWrap>((key / NUM_IMAGE_WRAP) % NUM_IMAGE_WRAP);
     }
 
     static ImageFilter GetFilterOptionFromKey(uint8_t key)
     {
-        return static_cast<ImageFilter>((key - (key % 9)) % 2);
+        return static_cast<ImageFilter>(key /
+                                        (NUM_IMAGE_WRAP * NUM_IMAGE_WRAP));
     }
 };
 } // namespace rive
diff --git a/renderer/include/rive/renderer/d3d11/render_context_d3d_impl.hpp b/renderer/include/rive/renderer/d3d11/render_context_d3d_impl.hpp
index 4a48a78..409dfbf 100644
--- a/renderer/include/rive/renderer/d3d11/render_context_d3d_impl.hpp
+++ b/renderer/include/rive/renderer/d3d11/render_context_d3d_impl.hpp
@@ -204,7 +204,7 @@
     RenderContextD3DImpl(ComPtr<ID3D11Device>,
                          ComPtr<ID3D11DeviceContext>,
                          const D3DCapabilities&,
-                         ShaderCompilationMode);
+                         const D3DContextOptions&);
 
     rcp<RenderBuffer> makeRenderBuffer(RenderBufferType,
                                        RenderBufferFlags,
diff --git a/renderer/include/rive/renderer/gpu.hpp b/renderer/include/rive/renderer/gpu.hpp
index 88a9ec9..0c80e88 100644
--- a/renderer/include/rive/renderer/gpu.hpp
+++ b/renderer/include/rive/renderer/gpu.hpp
@@ -39,6 +39,10 @@
 class RenderTarget;
 class Texture;
 
+// Global MipMap LOD Bias to apply to samplers. Going lower leads to sharper
+// filtering at the expense of potential shimmering.
+constexpr static float MIP_MAP_LOD_BIAS = -.5f;
+
 // Tessellate in parametric space until each segment is within 1/4 pixel of the
 // true curve.
 constexpr static int kParametricPrecision = 4;
@@ -1284,9 +1288,10 @@
     // Spacing between adjacent path IDs (1 if IEEE compliant).
     WRITEONLY uint32_t m_pathIDGranularity;
     WRITEONLY float m_vertexDiscardValue;
+    WRITEONLY float m_mipMapLODBias;
     WRITEONLY uint32_t m_wireframeEnabled; // Forces coverage to solid.
     // Uniform blocks must be multiples of 256 bytes in size.
-    WRITEONLY uint8_t m_padTo256Bytes[256 - 80];
+    WRITEONLY uint8_t m_padTo256Bytes[256 - 84];
 };
 static_assert(sizeof(FlushUniforms) == 256);
 
diff --git a/renderer/src/d3d11/render_context_d3d_impl.cpp b/renderer/src/d3d11/render_context_d3d_impl.cpp
index 066f1a7..07c0263 100644
--- a/renderer/src/d3d11/render_context_d3d_impl.cpp
+++ b/renderer/src/d3d11/render_context_d3d_impl.cpp
@@ -388,10 +388,10 @@
 {
     switch (option)
     {
-        case ImageFilter::trilinear:
-            return D3D11_FILTER::D3D11_FILTER_MIN_MAG_MIP_LINEAR;
         case ImageFilter::nearest:
             return D3D11_FILTER::D3D11_FILTER_MIN_MAG_MIP_POINT;
+        case ImageFilter::bilinear:
+            return D3D11_FILTER::D3D11_FILTER_MIN_MAG_LINEAR_MIP_POINT;
     }
 
     RIVE_UNREACHABLE();
@@ -493,7 +493,7 @@
         new RenderContextD3DImpl(std::move(gpu),
                                  std::move(gpuContext),
                                  d3dCapabilities,
-                                 contextOptions.shaderCompilationMode));
+                                 contextOptions));
     return std::make_unique<RenderContext>(std::move(renderContextImpl));
 }
 
@@ -501,8 +501,11 @@
     ComPtr<ID3D11Device> gpu,
     ComPtr<ID3D11DeviceContext> gpuContext,
     const D3DCapabilities& d3dCapabilities,
-    ShaderCompilationMode shaderCompilationMode) :
-    m_pipelineManager(gpuContext, gpu, d3dCapabilities, shaderCompilationMode),
+    const D3DContextOptions& d3dContextOptions) :
+    m_pipelineManager(gpuContext,
+                      gpu,
+                      d3dCapabilities,
+                      d3dContextOptions.shaderCompilationMode),
     m_d3dCapabilities(d3dCapabilities),
     m_gpu(std::move(gpu)),
     m_gpuContext(std::move(gpuContext))
@@ -689,9 +692,9 @@
         mipmapSamplerDesc.AddressV =
             address_mode_for_sampler_filter_options(yWrap);
         mipmapSamplerDesc.AddressW = D3D11_TEXTURE_ADDRESS_CLAMP;
-        mipmapSamplerDesc.MipLODBias = 0.0f;
         mipmapSamplerDesc.MaxAnisotropy = 1;
         mipmapSamplerDesc.ComparisonFunc = D3D11_COMPARISON_NEVER;
+        mipmapSamplerDesc.MipLODBias = 0.0f;
         mipmapSamplerDesc.MinLOD = 0;
         mipmapSamplerDesc.MaxLOD = D3D11_FLOAT32_MAX;
         VERIFY_OK(m_gpu->CreateSamplerState(
diff --git a/renderer/src/d3d12/render_context_d3d12_impl.cpp b/renderer/src/d3d12/render_context_d3d12_impl.cpp
index 219586b..7f92c60 100644
--- a/renderer/src/d3d12/render_context_d3d12_impl.cpp
+++ b/renderer/src/d3d12/render_context_d3d12_impl.cpp
@@ -25,10 +25,10 @@
 {
     switch (option)
     {
-        case ImageFilter::trilinear:
-            return D3D12_FILTER_COMPARISON_MIN_MAG_MIP_LINEAR;
         case ImageFilter::nearest:
             return D3D12_FILTER_COMPARISON_MIN_MAG_MIP_POINT;
+        case ImageFilter::bilinear:
+            return D3D12_FILTER_COMPARISON_MIN_MAG_LINEAR_MIP_POINT;
     }
 
     RIVE_UNREACHABLE();
diff --git a/renderer/src/gl/gl_utils.cpp b/renderer/src/gl/gl_utils.cpp
index 8f307c1..9d59cab 100644
--- a/renderer/src/gl/gl_utils.cpp
+++ b/renderer/src/gl/gl_utils.cpp
@@ -306,8 +306,8 @@
 {
     switch (filter)
     {
-        case rive::ImageFilter::trilinear:
-            return GL_LINEAR_MIPMAP_LINEAR;
+        case rive::ImageFilter::bilinear:
+            return GL_LINEAR_MIPMAP_NEAREST;
         case rive::ImageFilter::nearest:
             return GL_NEAREST;
     }
@@ -319,10 +319,10 @@
 {
     switch (filter)
     {
-        case rive::ImageFilter::trilinear:
-            return GL_LINEAR;
         case rive::ImageFilter::nearest:
             return GL_NEAREST;
+        case rive::ImageFilter::bilinear:
+            return GL_LINEAR;
     }
 
     RIVE_UNREACHABLE();
diff --git a/renderer/src/gpu.cpp b/renderer/src/gpu.cpp
index a10ca33..97d2c3e 100644
--- a/renderer/src/gpu.cpp
+++ b/renderer/src/gpu.cpp
@@ -489,6 +489,7 @@
     m_coverageBufferPrefix(flushDesc.coverageBufferPrefix),
     m_pathIDGranularity(platformFeatures.pathIDGranularity),
     m_vertexDiscardValue(std::numeric_limits<float>::quiet_NaN()),
+    m_mipMapLODBias(MIP_MAP_LOD_BIAS),
     m_wireframeEnabled(flushDesc.wireframe)
 {}
 
@@ -625,7 +626,8 @@
                 // Instead of finding sqrt(maxScaleFactorPow2), just multiply
                 // the log by .5.
                 m_imageTextureLOD =
-                    log2f(std::max(maxScaleFactorPow2, 1.f)) * .5f;
+                    (log2f(std::max(maxScaleFactorPow2, 1.f)) * .5f) +
+                    MIP_MAP_LOD_BIAS;
             }
             else
             {
diff --git a/renderer/src/metal/render_context_metal_impl.mm b/renderer/src/metal/render_context_metal_impl.mm
index bf68042..4626399 100644
--- a/renderer/src/metal/render_context_metal_impl.mm
+++ b/renderer/src/metal/render_context_metal_impl.mm
@@ -70,7 +70,7 @@
 {
     switch (option)
     {
-        case ImageFilter::trilinear:
+        case ImageFilter::bilinear:
             return MTLSamplerMinMagFilterLinear;
         case ImageFilter::nearest:
             return MTLSamplerMinMagFilterNearest;
@@ -83,9 +83,8 @@
 {
     switch (option)
     {
-        case ImageFilter::trilinear:
-            return MTLSamplerMipFilterLinear;
         case ImageFilter::nearest:
+        case ImageFilter::bilinear:
             return MTLSamplerMipFilterNearest;
     }
 
diff --git a/renderer/src/shaders/common.glsl b/renderer/src/shaders/common.glsl
index 38343bd..9c81ca5 100644
--- a/renderer/src/shaders/common.glsl
+++ b/renderer/src/shaders/common.glsl
@@ -242,6 +242,7 @@
 uint pathIDGranularity; // Spacing between adjacent path IDs (1 if IEEE
                         // compliant).
 float vertexDiscardValue;
+float mipMapLODBias;
 // Debugging.
 uint wireframeEnabled;
 UNIFORM_BLOCK_END(uniforms)
diff --git a/renderer/src/shaders/draw_image_mesh.glsl b/renderer/src/shaders/draw_image_mesh.glsl
index c732d40..b606c15 100644
--- a/renderer/src/shaders/draw_image_mesh.glsl
+++ b/renderer/src/shaders/draw_image_mesh.glsl
@@ -124,8 +124,11 @@
     VARYING_UNPACK(v_clipRect, float4);
 #endif
 
-    half4 color =
-        TEXTURE_SAMPLE_DYNAMIC(@imageTexture, imageSampler, v_texCoord);
+    half4 color = TEXTURE_SAMPLE_DYNAMIC_LODBIAS(@imageTexture,
+                                                 imageSampler,
+                                                 v_texCoord,
+                                                 uniforms.mipMapLODBias);
+
     half coverage = 1.;
 
 #ifdef @ENABLE_CLIP_RECT
@@ -179,9 +182,11 @@
 {
     VARYING_UNPACK(v_texCoord, float2);
 
-    half4 color =
-        TEXTURE_SAMPLE_DYNAMIC(@imageTexture, imageSampler, v_texCoord) *
-        imageDrawUniforms.opacity;
+    half4 color = TEXTURE_SAMPLE_DYNAMIC_LODBIAS(@imageTexture,
+                                                 imageSampler,
+                                                 v_texCoord,
+                                                 uniforms.mipMapLODBias) *
+                  imageDrawUniforms.opacity;
 
 #if defined(@ENABLE_ADVANCED_BLEND) && !defined(@FIXED_FUNCTION_COLOR_OUTPUT)
     if (@ENABLE_ADVANCED_BLEND)
diff --git a/renderer/src/shaders/glsl.glsl b/renderer/src/shaders/glsl.glsl
index 336bb40..0f0c113 100644
--- a/renderer/src/shaders/glsl.glsl
+++ b/renderer/src/shaders/glsl.glsl
@@ -213,6 +213,8 @@
     texture(sampler2D(NAME, SAMPLER_NAME), COORD)
 #define TEXTURE_SAMPLE_LOD(NAME, SAMPLER_NAME, COORD, LOD)                     \
     textureLod(sampler2D(NAME, SAMPLER_NAME), COORD, LOD)
+#define TEXTURE_SAMPLE_LODBIAS(NAME, SAMPLER_NAME, COORD, LODBIAS)             \
+    texture(sampler2D(NAME, SAMPLER_NAME), COORD, LODBIAS)
 #define TEXTURE_SAMPLE_GRAD(NAME, SAMPLER_NAME, COORD, DDX, DDY)               \
     textureGrad(sampler2D(NAME, SAMPLER_NAME), COORD, DDX, DDY)
 #if defined(@FRAGMENT) && defined(@RENDER_MODE_MSAA)
@@ -232,6 +234,8 @@
 #define TEXTURE_SAMPLE(NAME, SAMPLER_NAME, COORD) texture(NAME, COORD)
 #define TEXTURE_SAMPLE_LOD(NAME, SAMPLER_NAME, COORD, LOD)                     \
     textureLod(NAME, COORD, LOD)
+#define TEXTURE_SAMPLE_LODBIAS(NAME, SAMPLER_NAME, COORD, LODBIAS)             \
+    texture(NAME, COORD, LODBIAS)
 #define TEXTURE_SAMPLE_GRAD(NAME, SAMPLER_NAME, COORD, DDX, DDY)               \
     textureGrad(NAME, COORD, DDX, DDY)
 #define DST_COLOR_FETCH(NAME) texelFetch(NAME, ivec2(floor(_fragCoord.xy)), 0)
@@ -241,6 +245,8 @@
     TEXTURE_SAMPLE(TEXTURE, SAMPLER_NAME, COORD)
 #define TEXTURE_SAMPLE_DYNAMIC_LOD(TEXTURE, SAMPLER_NAME, COORD, LOD)          \
     TEXTURE_SAMPLE_LOD(TEXTURE, SAMPLER_NAME, COORD, LOD)
+#define TEXTURE_SAMPLE_DYNAMIC_LODBIAS(TEXTURE, SAMPLER_NAME, COORD, LODBIAS)  \
+    TEXTURE_SAMPLE_LODBIAS(TEXTURE, SAMPLER_NAME, COORD, LODBIAS)
 
 // Polyfill the feather texture as a sampler2D since ES doesn't support
 // sampler1DArray. This is why the macro needs "ARRAY_INDEX_NORMALIZED": when
diff --git a/renderer/src/shaders/hlsl.glsl b/renderer/src/shaders/hlsl.glsl
index 2dbe6c0..a42dbd9 100644
--- a/renderer/src/shaders/hlsl.glsl
+++ b/renderer/src/shaders/hlsl.glsl
@@ -141,6 +141,8 @@
     NAME.$Sample(SAMPLER_NAME, COORD)
 #define TEXTURE_SAMPLE_LOD(NAME, SAMPLER_NAME, COORD, LOD)                     \
     NAME.$SampleLevel(SAMPLER_NAME, COORD, LOD)
+#define TEXTURE_SAMPLE_LODBIAS(NAME, SAMPLER_NAME, COORD, LODBIAS)             \
+    NAME.$SampleBias(SAMPLER_NAME, COORD, LODBIAS)
 #define TEXTURE_SAMPLE_GRAD(NAME, SAMPLER_NAME, COORD, DDX, DDY)               \
     NAME.$SampleGrad(SAMPLER_NAME, COORD, DDX, DDY)
 #define TEXTURE_GATHER(NAME, SAMPLER_NAME, COORD, TEXTURE_INVERSE_SIZE)        \
@@ -157,7 +159,8 @@
     TEXTURE_SAMPLE(TEXTURE, SAMPLER_NAME, COORD)
 #define TEXTURE_SAMPLE_DYNAMIC_LOD(TEXTURE, SAMPLER_NAME, COORD, LOD)          \
     TEXTURE_SAMPLE_LOD(TEXTURE, SAMPLER_NAME, COORD, LOD)
-
+#define TEXTURE_SAMPLE_DYNAMIC_LODBIAS(TEXTURE, SAMPLER_NAME, COORD, LODBIAS)  \
+    TEXTURE_SAMPLE_LODBIAS(TEXTURE, SAMPLER_NAME, COORD, LODBIAS)
 #define PLS_INTERLOCK_BEGIN
 #define PLS_INTERLOCK_END
 
diff --git a/renderer/src/shaders/metal.glsl b/renderer/src/shaders/metal.glsl
index f568670..c1c39b8 100644
--- a/renderer/src/shaders/metal.glsl
+++ b/renderer/src/shaders/metal.glsl
@@ -156,6 +156,8 @@
     _textures.TEXTURE.$sample(SAMPLER_NAME, COORD)
 #define TEXTURE_SAMPLE_LOD(TEXTURE, SAMPLER_NAME, COORD, LOD)                  \
     _textures.TEXTURE.$sample(SAMPLER_NAME, COORD, $level(LOD))
+#define TEXTURE_SAMPLE_LODBIAS(TEXTURE, SAMPLER_NAME, COORD, LODBIAS)          \
+    _textures.TEXTURE.$sample(SAMPLER_NAME, COORD, $bias(LODBIAS))
 #define TEXTURE_SAMPLE_GRAD(TEXTURE, SAMPLER_NAME, COORD, DDX, DDY)            \
     _textures.TEXTURE.$sample(SAMPLER_NAME, COORD, $gradient2d(DDX, DDY))
 #define TEXTURE_GATHER(TEXTURE, SAMPLER_NAME, COORD, TEXTURE_INVERSE_SIZE)     \
@@ -164,6 +166,10 @@
     _textures.TEXTURE.$sample(_dynamicSampler.SAMPLER_NAME, COORD)
 #define TEXTURE_SAMPLE_DYNAMIC_LOD(TEXTURE, SAMPLER_NAME, COORD, LOD)          \
     _textures.TEXTURE.$sample(_dynamicSampler.SAMPLER_NAME, COORD, $level(LOD))
+#define TEXTURE_SAMPLE_DYNAMIC_LODBIAS(TEXTURE, SAMPLER_NAME, COORD, LODBIAS)  \
+    _textures.TEXTURE.$sample(_dynamicSampler.SAMPLER_NAME,                    \
+                              COORD,                                           \
+                              $bias(LODBIAS))
 #define TEXTURE_SAMPLE_LOD_1D_ARRAY(TEXTURE,                                   \
                                     SAMPLER_NAME,                              \
                                     X,                                         \
@@ -442,6 +448,8 @@
     PLS_METAL_MAIN(                                                            \
         NAME,                                                                  \
         PLS _inpls,                                                            \
+        $constant @FlushUniforms& uniforms                                     \
+        [[$buffer(METAL_BUFFER_IDX(FLUSH_UNIFORM_BUFFER_IDX))]],               \
         Varyings _varyings [[$stage_in]],                                      \
         FragmentTextures _textures,                                            \
         FragmentStorageBuffers _buffers,                                       \
@@ -467,16 +475,21 @@
         PLS _pls;
 
 #define PLS_FRAG_COLOR_MAIN(NAME)                                              \
-    PLS_FRAG_COLOR_METAL_MAIN(NAME,                                            \
-                              PLS _inpls,                                      \
-                              Varyings _varyings [[$stage_in]],                \
-                              FragmentTextures _textures,                      \
-                              FragmentStorageBuffers _buffers)
+    PLS_FRAG_COLOR_METAL_MAIN(                                                 \
+        NAME,                                                                  \
+        PLS _inpls,                                                            \
+        $constant @FlushUniforms& uniforms                                     \
+        [[$buffer(METAL_BUFFER_IDX(FLUSH_UNIFORM_BUFFER_IDX))]],               \
+        Varyings _varyings [[$stage_in]],                                      \
+        FragmentTextures _textures,                                            \
+        FragmentStorageBuffers _buffers)
 
 #define PLS_FRAG_COLOR_MAIN_WITH_IMAGE_UNIFORMS(NAME)                          \
     PLS_FRAG_COLOR_METAL_MAIN(                                                 \
         NAME,                                                                  \
         PLS _inpls,                                                            \
+        $constant @FlushUniforms& uniforms                                     \
+        [[$buffer(METAL_BUFFER_IDX(FLUSH_UNIFORM_BUFFER_IDX))]],               \
         Varyings _varyings [[$stage_in]],                                      \
         FragmentTextures _textures,                                            \
         FragmentStorageBuffers _buffers,                                       \
diff --git a/renderer/src/shaders/rhi.glsl b/renderer/src/shaders/rhi.glsl
index 021128d..35a7d9b 100644
--- a/renderer/src/shaders/rhi.glsl
+++ b/renderer/src/shaders/rhi.glsl
@@ -148,6 +148,8 @@
     NAME.$Sample(SAMPLER_NAME, COORD)
 #define TEXTURE_SAMPLE_LOD(NAME, SAMPLER_NAME, COORD, LOD)                     \
     NAME.$SampleLevel(SAMPLER_NAME, COORD, LOD)
+#define TEXTURE_SAMPLE_LODBIAS(NAME, SAMPLER_NAME, COORD, LODBIAS)             \
+    NAME.$SampleBias(SAMPLER_NAME, COORD, LODBIAS)
 #define TEXTURE_REF_SAMPLE_LOD TEXTURE_SAMPLE_LOD
 #define TEXTURE_SAMPLE_GRAD(NAME, SAMPLER_NAME, COORD, DDX, DDY)               \
     NAME.$SampleGrad(SAMPLER_NAME, COORD, DDX, DDY)
@@ -165,6 +167,8 @@
     TEXTURE_SAMPLE(TEXTURE, SAMPLER_NAME, COORD)
 #define TEXTURE_SAMPLE_DYNAMIC_LOD(TEXTURE, SAMPLER_NAME, COORD, LOD)          \
     TEXTURE_SAMPLE_LOD(TEXTURE, SAMPLER_NAME, COORD, LOD)
+#define TEXTURE_SAMPLE_DYNAMIC_LODBIAS(TEXTURE, SAMPLER_NAME, COORD, LODBIAS)  \
+    TEXTURE_SAMPLE_LODBIAS(TEXTURE, SAMPLER_NAME, COORD, LODBIAS)
 
 #define PLS_INTERLOCK_BEGIN
 #define PLS_INTERLOCK_END
diff --git a/renderer/src/vulkan/pipeline_manager_vulkan.cpp b/renderer/src/vulkan/pipeline_manager_vulkan.cpp
index 88e994a..b563563 100644
--- a/renderer/src/vulkan/pipeline_manager_vulkan.cpp
+++ b/renderer/src/vulkan/pipeline_manager_vulkan.cpp
@@ -12,9 +12,8 @@
 {
     switch (option)
     {
-        case rive::ImageFilter::trilinear:
-            return VK_SAMPLER_MIPMAP_MODE_LINEAR;
         case rive::ImageFilter::nearest:
+        case rive::ImageFilter::bilinear:
             return VK_SAMPLER_MIPMAP_MODE_NEAREST;
     }
 
@@ -40,7 +39,7 @@
 {
     switch (option)
     {
-        case rive::ImageFilter::trilinear:
+        case rive::ImageFilter::bilinear:
             return VK_FILTER_LINEAR;
         case rive::ImageFilter::nearest:
             return VK_FILTER_NEAREST;
diff --git a/renderer/src/webgpu/render_context_webgpu_impl.cpp b/renderer/src/webgpu/render_context_webgpu_impl.cpp
index 4c240ea..a354144 100644
--- a/renderer/src/webgpu/render_context_webgpu_impl.cpp
+++ b/renderer/src/webgpu/render_context_webgpu_impl.cpp
@@ -1056,7 +1056,7 @@
 {
     switch (filter)
     {
-        case rive::ImageFilter::trilinear:
+        case rive::ImageFilter::bilinear:
             return wgpu::FilterMode::Linear;
         case rive::ImageFilter::nearest:
             return wgpu::FilterMode::Nearest;
@@ -1070,9 +1070,8 @@
 {
     switch (filter)
     {
-        case rive::ImageFilter::trilinear:
-            return wgpu::MipmapFilterMode::Linear;
         case rive::ImageFilter::nearest:
+        case rive::ImageFilter::bilinear:
             return wgpu::MipmapFilterMode::Nearest;
     }
 
diff --git a/src/lua/renderer/lua_image.cpp b/src/lua/renderer/lua_image.cpp
index b644190..ed6108a 100644
--- a/src/lua/renderer/lua_image.cpp
+++ b/src/lua/renderer/lua_image.cpp
@@ -53,7 +53,7 @@
     }
     ImageWrap wrapX = ImageWrap::clamp;
     ImageWrap wrapY = ImageWrap::clamp;
-    ImageFilter filter = ImageFilter::trilinear;
+    ImageFilter filter = ImageFilter::bilinear;
     switch (wrapXAtom)
     {
         case (int)LuaAtoms::clamp:
@@ -85,7 +85,7 @@
 
     switch (imageFilterAtom)
     {
-        case (int)LuaAtoms::trilinear:
+        case (int)LuaAtoms::bilinear:
             break;
         case (int)LuaAtoms::nearest:
             filter = ImageFilter::nearest;
diff --git a/src/lua/rive_lua_libs.cpp b/src/lua/rive_lua_libs.cpp
index 7ab877f..35156af 100644
--- a/src/lua/rive_lua_libs.cpp
+++ b/src/lua/rive_lua_libs.cpp
@@ -34,7 +34,7 @@
     {"clamp", (int16_t)LuaAtoms::clamp},
     {"repeat", (int16_t)LuaAtoms::repeat},
     {"mirror", (int16_t)LuaAtoms::mirror},
-    {"trilinear", (int16_t)LuaAtoms::trilinear},
+    {"bilinear", (int16_t)LuaAtoms::bilinear},
     {"nearest", (int16_t)LuaAtoms::nearest},
     {"style", (int16_t)LuaAtoms::style},
     {"join", (int16_t)LuaAtoms::join},
diff --git a/tests/gm/image_filter_options.cpp b/tests/gm/image_filter_options.cpp
index 391329a..1d3ef1d 100644
--- a/tests/gm/image_filter_options.cpp
+++ b/tests/gm/image_filter_options.cpp
@@ -35,7 +35,7 @@
                    img.get(),
                    {rive::ImageWrap::clamp,
                     rive::ImageWrap::clamp,
-                    rive::ImageFilter::trilinear},
+                    rive::ImageFilter::bilinear},
                    r);
         ren->restore();
         ren->save();
@@ -53,7 +53,7 @@
                    img.get(),
                    {rive::ImageWrap::clamp,
                     rive::ImageWrap::clamp,
-                    rive::ImageFilter::trilinear},
+                    rive::ImageFilter::bilinear},
                    r);
         ren->restore();
         ren->save();
diff --git a/tests/unit_tests/renderer/pls_renderer_test.cpp b/tests/unit_tests/renderer/pls_renderer_test.cpp
index cfb3d25..ed9a90a 100644
--- a/tests/unit_tests/renderer/pls_renderer_test.cpp
+++ b/tests/unit_tests/renderer/pls_renderer_test.cpp
@@ -1222,38 +1222,37 @@
     CHECK(wrapy(repeatNearestOptionsKey) == rive::ImageWrap::repeat);
     CHECK(filter(repeatNearestOptionsKey) == rive::ImageFilter::nearest);
 
-    rive::ImageSampler clampMirrorLinearOptions = {
-        rive::ImageWrap::clamp,
-        rive::ImageWrap::mirror,
-        rive::ImageFilter::trilinear};
+    rive::ImageSampler clampMirrorLinearOptions = {rive::ImageWrap::clamp,
+                                                   rive::ImageWrap::mirror,
+                                                   rive::ImageFilter::bilinear};
 
     auto clampMirrorLinearOptionsKey = clampMirrorLinearOptions.asKey();
     CHECK(wrapx(clampMirrorLinearOptionsKey) == rive::ImageWrap::clamp);
     CHECK(wrapy(clampMirrorLinearOptionsKey) == rive::ImageWrap::mirror);
-    CHECK(filter(clampMirrorLinearOptionsKey) == rive::ImageFilter::trilinear);
+    CHECK(filter(clampMirrorLinearOptionsKey) == rive::ImageFilter::bilinear);
     CHECK(clampMirrorLinearOptions != repeatNearestOptions);
 
     rive::ImageSampler repeatClampMipLinearOptions = {
         rive::ImageWrap::repeat,
         rive::ImageWrap::clamp,
-        rive::ImageFilter::trilinear};
+        rive::ImageFilter::bilinear};
 
     auto repearClampMipLinearOptionsKey = repeatClampMipLinearOptions.asKey();
     CHECK(wrapx(repearClampMipLinearOptionsKey) == rive::ImageWrap::repeat);
     CHECK(wrapy(repearClampMipLinearOptionsKey) == rive::ImageWrap::clamp);
     CHECK(filter(repearClampMipLinearOptionsKey) ==
-          rive::ImageFilter::trilinear);
+          rive::ImageFilter::bilinear);
 
     rive::ImageSampler clampRepeatMipNearestOptions = {
         rive::ImageWrap::clamp,
         rive::ImageWrap::repeat,
-        rive::ImageFilter::trilinear};
+        rive::ImageFilter::bilinear};
 
     auto clampRepeatMipNearestOptionsKey = clampRepeatMipNearestOptions.asKey();
     CHECK(wrapx(clampRepeatMipNearestOptionsKey) == rive::ImageWrap::clamp);
     CHECK(wrapy(clampRepeatMipNearestOptionsKey) == rive::ImageWrap::repeat);
     CHECK(filter(clampRepeatMipNearestOptionsKey) ==
-          rive::ImageFilter::trilinear);
+          rive::ImageFilter::bilinear);
 
     rive::ImageSampler mirrorClampMipNearestOptions = {
         rive::ImageWrap::mirror,