Skip to content

Add texture mipmap support#1224

Open
StafaH wants to merge 3 commits intoNVIDIA:mainfrom
StafaH:stafah/texture_mipmap
Open

Add texture mipmap support#1224
StafaH wants to merge 3 commits intoNVIDIA:mainfrom
StafaH:stafah/texture_mipmap

Conversation

@StafaH
Copy link
Contributor

@StafaH StafaH commented Feb 8, 2026

Description

For downstream applications, adding mipmap support to the current texture API allows for better texture sampling for detailed textures at various distances. CUDA textures already support this but some modifications need to be done to correctly create and pass in the texture data.

This PR adds:

  • Creating textures with mipmap support, with automatic downscaling of supplied texture data to create the texture maps between the current texture width x height all the way to 1x1
  • Texture sampling using lod (level of detail).
  • CPU logic to mimic the CUDA texture mipmap logic

Example without mip support for a plane with a checkerboard texture with border between squares:

without_mip

After mip support passing lod (level of detail) into texture sample function:

debug

Downstream the API usage changes from this:

# create texture ahead of time
wp.Texture2D(tex_data, filter_mode=wp.TextureFilterMode.LINEAR)

# sample
tex_color = wp.texture_sample(tex, wp.vec2(u, v), dtype=wp.vec4)

to this:

# create texture ahead of time
# specifying 0 means internally we calculate all the texture maps between current resolution and 1x1
wp.Texture2D(tex_data, filter_mode=wp.TextureFilterMode.LINEAR, num_mip_levels=0)

# new arguments lod, can be calculating from raycast data
tex_color = wp.texture_sample(tex, wp.vec2(u, v), dtype=wp.vec4, lod=lod)

TODO:

  • Add documentation for this feature
  • Build the docs once API is finalized

Before your PR is "Ready for review"

  • All commits are signed-off to indicate that your contribution adheres to the Developer Certificate of Origin requirements
  • Necessary tests have been added
  • Documentation is up-to-date
  • Auto-generated files modified by compiling Warp and building the documentation have been updated (e.g. __init__.pyi, docs/api_reference/, docs/language_reference/)
  • Code passes formatting and linting checks with pre-commit run -a

Summary by CodeRabbit

  • New Features

    • Added mipmapping support for 2D and 3D textures with automatic mip-chain generation and configurable mip filter modes.
    • Texture constructors now accept num_mip_levels and mip_filter_mode; Texture exposes a public num_mip_levels property.
    • Texture sampling accepts an optional lod (mipmap level) parameter with a default of 0.0 (no mipmaps).
  • Tests

    • Added comprehensive mipmapping tests covering 2D/3D, multiple channels and data types.

@copy-pr-bot
Copy link

copy-pr-bot bot commented Feb 8, 2026

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@coderabbitai
Copy link

coderabbitai bot commented Feb 8, 2026

📝 Walkthrough

Walkthrough

This pull request adds mipmapping support: an optional lod/load parameter to texture sampling built-ins, mipmap metadata and lifecycle in Texture, extended ctypes and native APIs for mipmap creation/destruction, CUDA driver wrappers for mipmapped arrays, host/device mipmap handling, and extensive tests for 2D/3D mipmap behavior.

Changes

Cohort / File(s) Summary
Python API - Built-ins
warp/_src/builtins.py
Added optional lod: float parameter (default 0.0) to all texture_sample overloads (2D vec2, 2D u/v, 3D vec3, 3D u/v/w).
Python API - Texture Class
warp/_src/texture.py
Added mipmap fields (_mipmap_handle, _num_mip_levels), public num_mip_levels property, constructor params num_mip_levels and mip_filter_mode, mip-chain generation utilities (_compute_mip_count, _downsample_axis, _generate_mip_chain_2d/_3d), updated data processing and create/destroy paths to pass mip arrays and mipmap handle.
Python API - Context Bindings
warp/_src/context.py
Extended ctypes signatures for texture create/destroy host & device bindings to include mip_filter_mode, num_mip_levels, mip_widths, mip_heights, mip_depths (3D) and mipmap_handle_out/mipmap_handle parameters.
Native - CUDA Driver Bindings
warp/native/cuda_util.h, warp/native/cuda_util.cpp
Added driver function pointers and wrapper functions for mipmapped array APIs: cuMipmappedArrayCreate, cuMipmappedArrayGetLevel, cuMipmappedArrayDestroy and their _f wrappers; registered entry points.
Native - Texture Descriptor & Sampling API
warp/native/texture.h
Added mipmap-related fields to CPU texture structs (num_mip_levels, mip_filter_mode, mip_data, mip_widths, mip_heights, mip_depths), added mip-aware sampling signatures and helpers (cpu_sample_*_mip), and extended texture sampling/template specializations to accept an extra load/level parameter.
Native - Texture Implementation
warp/native/texture.cpp
Implemented host/device mipmap creation paths: allocate/partition contiguous mip buffers, populate per-level data, set CUDA mipmapped arrays and texture descriptors, return mipmap handles; updated destroy paths to free mip resources; added cpu texture mip fields and usage.
Native - Public API Declarations
warp/native/warp.h
Updated public C API signatures for 2D/3D texture create/destroy to include mip_filter_mode, num_mip_levels, mip dimension arrays, and mipmap handle parameters.
Tests
warp/tests/cuda/test_texture.py
Added CUDA kernels and extensive tests for mipmap creation and sampling (2D/3D, multi-channel, non-power-of-two, integer types, load-level behavior); wired tests into TestTexture suite.
Project Config / Typo List
pyproject.toml
Added lod to typo-checker extended words list.
Type Stubs
warp/__init__.pyi
Updated texture_sample overloads to include lod: float32 parameter across 2D/3D variants.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant TextureInit as Texture.__init__
    participant MipGen as Mipmap Generation
    participant TexCreate as _create_texture
    participant CUDA as CUDA Driver

    Client->>TextureInit: create(texture, num_mip_levels > 1, mip_filter_mode)
    TextureInit->>MipGen: compute/generate mip chain (_compute_mip_count/_generate_mip_chain)
    MipGen-->>TextureInit: concatenated_data, mip_widths, mip_heights, (mip_depths)
    TextureInit->>TexCreate: _create_texture(data, mip_widths, mip_heights, mip_depths)
    TexCreate->>CUDA: cuMipmappedArrayCreate + cuMipmappedArrayGetLevel (copy per-level)
    CUDA-->>TexCreate: mipmap_handle, level arrays populated
    TexCreate-->>TextureInit: texture_handle, mipmap_handle
    TextureInit-->>Client: Texture object ready (with mipmap state)
Loading
sequenceDiagram
    participant Caller
    participant texture_sample
    participant Helper as texture_sample_helper
    participant Backend as Backend (CUDA/CPU)

    Caller->>texture_sample: sample(tex, coords..., lod)
    texture_sample->>Helper: dispatch specialization (float/vec2/vec4)
    alt lod > 0 (explicit level)
        Helper->>Backend: tex2DLoad/tex3DLoad or cpu_sample_*_mip (use explicit level)
        Backend-->>Helper: texel from specified mip level
    else lod == 0 (default)
        Helper->>Backend: tex2D/tex3D or cpu_sample_* (standard filtering across mips)
        Backend-->>Helper: filtered texel
    end
    Helper-->>texture_sample: sampled value
    texture_sample-->>Caller: return value
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.62% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'Add texture mipmap support' directly and clearly summarizes the main change—comprehensive mipmapping functionality for texture sampling across 2D and 3D textures, including automatic level generation and LOD-based sampling.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link

greptile-apps bot commented Feb 8, 2026

Greptile Overview

Greptile Summary

This PR adds mipmap support to Warp's texture API, enabling automatic generation of mip chains and LOD-based sampling for both 2D and 3D textures. The implementation includes Python-side mipmap generation using box filter downsampling, CUDA mipmap array creation, and CPU software mipmap sampling to match CUDA behavior.

Key Changes

  • Added num_mip_levels and mip_filter_mode parameters to texture creation
  • Automatic mip chain generation when num_mip_levels=0
  • Extended texture_sample() with optional lod parameter (defaults to 0.0)
  • CPU and CUDA paths for mipmap sampling with interpolation between levels

Critical Issues Found

The CPU texture sampling implementation has bugs that will cause incorrect behavior:

  1. Unnormalized coordinate bug: When normalized_coords=False and sampling mip levels, coordinate conversion and clamping use base texture dimensions instead of mip level dimensions (affects lines 294-295, 306-310, 355-357, 370-376)
  2. Mip interpolation bypass: For vec2f and vec4f sampling with lod > 0, the CPU path incorrectly calls cpu_sample_2d_channel() instead of cpu_sample_2d_channel_mip(), which truncates the float LOD to an int and bypasses mip filter interpolation, causing CPU/CUDA disagreement (affects lines 545-546, 567-569, 598-601, 626-629)

Confidence Score: 2/5

  • This PR has critical CPU sampling bugs that cause incorrect behavior with mipmaps
  • Multiple critical bugs in CPU texture sampling will cause incorrect results when using unnormalized coordinates with mip levels, and CPU/CUDA disagreement for vec2f/vec4f sampling with fractional LOD
  • Pay close attention to warp/native/texture.h which contains the CPU mipmap sampling bugs

Important Files Changed

Filename Overview
warp/native/texture.h Added mipmap support with CPU sampling bugs: unnormalized coords use wrong dimensions for mip levels, vec2f/vec4f paths bypass mip interpolation
warp/_src/texture.py Added mipmap generation with box filter downsampling, automatic mip chain creation, and new API parameters for mipmap control
warp/init.pyi Updated type stubs to include lod parameter for all texture_sample overloads
warp/native/texture.cpp Extended texture creation functions to handle mipmap data and CUDA mipmap array creation

Sequence Diagram

sequenceDiagram
    participant User
    participant Texture2D/3D
    participant Python texture.py
    participant Native texture.cpp
    participant CUDA/CPU

    User->>Texture2D/3D: __init__(data, num_mip_levels=0)
    Texture2D/3D->>Python texture.py: _compute_mip_count()
    Python texture.py-->>Texture2D/3D: calculated levels
    Texture2D/3D->>Python texture.py: _generate_mip_chain_2d/3d()
    Python texture.py->>Python texture.py: _downsample_axis() repeatedly
    Python texture.py-->>Texture2D/3D: concatenated mip data
    Texture2D/3D->>Native texture.cpp: wp_texture2d_create_device()
    Native texture.cpp->>CUDA/CPU: create texture with mipmap array
    CUDA/CPU-->>Native texture.cpp: texture handle
    Native texture.cpp-->>Texture2D/3D: texture created
    
    User->>User: wp.launch(kernel)
    User->>CUDA/CPU: texture_sample(tex, uv, dtype, lod)
    alt CUDA Path
        CUDA/CPU->>CUDA/CPU: tex2DLod/tex3DLod (hardware)
    else CPU Path
        CUDA/CPU->>CUDA/CPU: cpu_sample_2d_channel_mip()
        CUDA/CPU->>CUDA/CPU: interpolate between mip levels
    end
    CUDA/CPU-->>User: sampled value
Loading

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

9 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

Comment on lines 293 to 310
@@ -258,7 +309,7 @@ inline float cpu_sample_2d_channel(const cpu_texture2d_data* tex, float u, float
y = cpu_clamp_index(y, tex->height);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mip coords use base dims

When normalized_coords=False, cpu_sample_2d_channel() (and the 3D variant) converts texel-space u/v(/w) to normalized coords using tex->width/height/depth even when sampling a mip level, and the 2D nearest path clamps x/y using tex->width/height as well. For level>0 this will sample the wrong location (and can change border/clamp behavior) because the selected mip has different dimensions. Use the local width/height(/depth) variables for the unnormalized conversion and clamping when level>=0.

Also appears in: warp/native/texture.h lines 354-357 (3D conversion), 305-310 (2D clamping).

Comment on lines 540 to 547
if (tex.tex == 0)
return vec2f(0.0f, 0.0f);
const cpu_texture2d_data* cpu_tex = (const cpu_texture2d_data*)tex.tex;
return vec2f(cpu_sample_2d_channel(cpu_tex, u, v, 0), cpu_sample_2d_channel(cpu_tex, u, v, 1));
if (load <= 0.0f) {
return vec2f(cpu_sample_2d_channel(cpu_tex, u, v, 0, 0), cpu_sample_2d_channel(cpu_tex, u, v, 1, 0));
} else {
return vec2f(cpu_sample_2d_channel(cpu_tex, u, v, 0, load), cpu_sample_2d_channel(cpu_tex, u, v, 1, load));
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CPU vecN mip sampling wrong

In the CPU path for vec2f/vec4f, the mip branch calls cpu_sample_*_channel(..., load) but that last parameter is an int level, so load is implicitly truncated and fractional LOD interpolation + mip_filter_mode are bypassed. This makes CPU results disagree with CUDA and with the intended semantics for non-integer loads. These call sites should use cpu_sample_*_channel_mip(..., load) (or equivalent level0/level1/frac logic) for each channel.

Also appears in: warp/native/texture.h around lines 599-602 (vec4f 2D), 546-547 (vec2f 2D), 627-630 (vec4f 3D), and the corresponding vec2f 3D helper.

Comment on lines 7617 to 7624
# texture_sample for 2D textures with vec2 coordinates
add_builtin(
"texture_sample",
input_types={"tex": Texture2D, "uv": vec2f, "dtype": Any},
input_types={"tex": Texture2D, "uv": vec2f, "dtype": Any, "load": float},
defaults={"load": 0.0},
value_func=texture_sample_2d_value_func,
export_func=lambda input_types: {k: v for k, v in input_types.items() if k != "dtype"},
dispatch_func=texture_sample_2d_dispatch_func,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stubs missing load

This PR adds load as an optional argument to texture_sample, but the generated type stubs (warp/__init__.pyi) still only expose the pre-existing overloads without load. That mismatch will cause IDE/typechecker errors for valid calls like wp.texture_sample(..., load=lod). Regenerate/update the stubs to include load: float = 0.0 for all texture_sample overloads (2D/3D, vec and scalar forms).

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
warp/native/texture.h (1)

300-312: ⚠️ Potential issue | 🔴 Critical

Bug: 2D nearest-neighbor clamp uses base-level dimensions instead of mip-level dimensions.

Lines 306 and 309 clamp to tex->width / tex->height (base-level dimensions), but when sampling a mip level the local variables width / height (set on lines 286–291) hold the correct mip-level dimensions. The 3D counterpart at lines 370–376 already uses the local variables correctly.

This causes incorrect clamping and potential out-of-bounds reads when sampling mip levels with FILTER_CLOSEST on 2D textures, since mip levels are always smaller than the base.

🐛 Fix: use local width/height for clamping
         if (tex->address_mode_u != WP_TEXTURE_ADDRESS_BORDER) {
-            x = cpu_clamp_index(x, tex->width);
+            x = cpu_clamp_index(x, width);
         }
         if (tex->address_mode_v != WP_TEXTURE_ADDRESS_BORDER) {
-            y = cpu_clamp_index(y, tex->height);
+            y = cpu_clamp_index(y, height);
         }
warp/_src/builtins.py (1)

7619-7635: ⚠️ Potential issue | 🟡 Minor

Remove default-value wording for load and avoid “no mipmaps” phrasing.

The docstring repeats the default and implies mipmaps are disabled; better to keep it semantic-only.
Also, since this file changed, regenerate the stubs with uv run build_docs.py.

🧹 Proposed doc tweak
-        load: Level of detail for mipmapped textures. Default is 0.0 (no mipmaps).
+        load: Level of detail for mipmapped textures.

Run:

uv run build_docs.py

As per coding guidelines, "Don't repeat default values from function signatures in docstrings—Sphinx autodoc shows them automatically" and "warp/_src/builtins.py: After modifying warp/_src/builtins.py, run build_docs.py to regenerate warp/init.pyi".

🤖 Fix all issues with AI agents
In `@warp/native/texture.cpp`:
- Around line 144-170: The three mallocs that create tex_data->mip_data,
tex_data->mip_widths and tex_data->mip_heights in the mipmapped 2D path (and the
analogous three mallocs in the 3D mipmapped path) must be null-checked: after
each malloc, verify the returned pointer is non-null; on any failure free any
prior successful allocations (free tex_data->mip_data, tex_data->mip_widths,
tex_data->mip_heights if set), free all_data, and return false (also ensure you
do not dereference null pointers afterward); apply the same pattern for the 3D
mipmap allocation block so both mipmapped paths safely clean up and return false
on allocation failure.

In `@warp/native/texture.h`:
- Around line 484-523: The CPU sampling branches in sample_2d and sample_3d call
cpu_sample_2d_channel / cpu_sample_3d_channel (and the vec2f/vec4f variants)
with level=0 which dereferences null mip arrays for non-mip textures; change
those non-mip calls to pass level = -1 (the default path that uses
tex->data/width/height) instead of 0 for all cpu_sample_*_channel(...) calls in
the load <= 0.0 branches across float, vec2f and vec4f specializations so
non-mipmapped textures no longer dereference mip pointers.

In `@warp/tests/cuda/test_texture.py`:
- Around line 2204-2209: The test_mipmap2d_max_load function currently defines
unused variables width and height while hardcoding the reshape to (4, 4); remove
the unused assignments or use them in the data.reshape call to fix the Ruff F841
warning. Locate the function test_mipmap2d_max_load and either delete the lines
"width, height = 4, 4" or change "data = np.arange(16,
dtype=np.float32).reshape((4, 4))" to use the width and height variables (e.g.,
reshape((height, width))) so the variables are referenced.
🧹 Nitpick comments (4)
warp/_src/texture.py (2)

594-605: _downsample_axis silently drops the last texel for odd-sized dimensions.

When n is odd (e.g., 5), new_n * 2 = 4, so the slice only covers indices 0–3 and index 4 is silently discarded. This gives correct results for power-of-2 dimensions, but for non-power-of-2 textures the mip chain loses edge data without warning.

Since CUDA mipmapped arrays typically require power-of-2 dimensions, consider adding an early validation in __init__ (when num_mip_levels > 1) that the base dimensions are powers of two, or at minimum document this limitation.


607-653: Unused num_channels parameter.

Static analysis correctly flags that num_channels is unused in both _generate_mip_chain_2d (line 609) and _generate_mip_chain_3d (line 657). The channel dimension is handled implicitly by NumPy array shapes. Consider removing the parameter or prefixing with _ if kept for future use.

♻️ Suggested fix
     `@staticmethod`
     def _generate_mip_chain_2d(
-        np_data: np.ndarray, num_levels: int, num_channels: int
+        np_data: np.ndarray, num_levels: int
     ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:

And update call sites accordingly (lines 283–284, 288–289).

warp/tests/cuda/test_texture.py (2)

2275-2309: Consider testing higher LOD levels for non-power-of-two textures.

This test only samples at LOD 0. Non-power-of-two downscaling (e.g., 30→15→7→3→1) is the most error-prone path for mipmap generation. Sampling at higher LOD levels (especially the 1×1 level) would strengthen coverage.


2556-2567: Mipmap test coverage gaps to consider.

A few scenarios aren't covered by the current suite:

  1. 3D mipmap with non-constant datatest_mipmap3d_constant_value only validates that a uniform texture stays uniform across LOD levels. A gradient or pattern-based 3D test would catch downsampling bugs.
  2. Fractional LOD — no test exercises fractional LOD values (e.g., 0.5) to validate trilinear interpolation between mip levels.

These could be added as follow-up.

Comment on lines +144 to +170
if (num_mip_levels > 1 && mip_widths != nullptr && mip_heights != nullptr) {
// Mipmapped path: compute total data size across all levels
size_t total_size = 0;
for (int i = 0; i < num_mip_levels; ++i) {
total_size += (size_t)mip_widths[i] * mip_heights[i] * num_channels * bytes_per_channel;
}

void* all_data = malloc(total_size);
if (all_data == nullptr) {
free(tex_data);
return false;
}
memcpy(all_data, data, total_size);

// Allocate mipmap arrays
tex_data->mip_data = (void**)malloc(sizeof(void*) * num_mip_levels);
tex_data->mip_widths = (int*)malloc(sizeof(int) * num_mip_levels);
tex_data->mip_heights = (int*)malloc(sizeof(int) * num_mip_levels);

// Set up per-level pointers into the contiguous buffer
char* ptr = (char*)all_data;
for (int i = 0; i < num_mip_levels; ++i) {
tex_data->mip_data[i] = ptr;
tex_data->mip_widths[i] = mip_widths[i];
tex_data->mip_heights[i] = mip_heights[i];
ptr += (size_t)mip_widths[i] * mip_heights[i] * num_channels * bytes_per_channel;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing null checks for mipmap sub-allocations.

The three malloc calls at lines 159–161 (and similarly at lines 282–285 for 3D) don't check for null returns. If any allocation fails after all_data succeeds, the code proceeds to dereference null pointers. While small-allocation malloc failures are rare, the non-mip path already validates tex_data->data.

🛡️ Example guard (2D path, lines 159-161)
         tex_data->mip_data = (void**)malloc(sizeof(void*) * num_mip_levels);
         tex_data->mip_widths = (int*)malloc(sizeof(int) * num_mip_levels);
         tex_data->mip_heights = (int*)malloc(sizeof(int) * num_mip_levels);
+        if (tex_data->mip_data == nullptr || tex_data->mip_widths == nullptr || tex_data->mip_heights == nullptr) {
+            free(all_data);
+            free(tex_data->mip_data);
+            free(tex_data->mip_widths);
+            free(tex_data->mip_heights);
+            free(tex_data);
+            return false;
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (num_mip_levels > 1 && mip_widths != nullptr && mip_heights != nullptr) {
// Mipmapped path: compute total data size across all levels
size_t total_size = 0;
for (int i = 0; i < num_mip_levels; ++i) {
total_size += (size_t)mip_widths[i] * mip_heights[i] * num_channels * bytes_per_channel;
}
void* all_data = malloc(total_size);
if (all_data == nullptr) {
free(tex_data);
return false;
}
memcpy(all_data, data, total_size);
// Allocate mipmap arrays
tex_data->mip_data = (void**)malloc(sizeof(void*) * num_mip_levels);
tex_data->mip_widths = (int*)malloc(sizeof(int) * num_mip_levels);
tex_data->mip_heights = (int*)malloc(sizeof(int) * num_mip_levels);
// Set up per-level pointers into the contiguous buffer
char* ptr = (char*)all_data;
for (int i = 0; i < num_mip_levels; ++i) {
tex_data->mip_data[i] = ptr;
tex_data->mip_widths[i] = mip_widths[i];
tex_data->mip_heights[i] = mip_heights[i];
ptr += (size_t)mip_widths[i] * mip_heights[i] * num_channels * bytes_per_channel;
}
if (num_mip_levels > 1 && mip_widths != nullptr && mip_heights != nullptr) {
// Mipmapped path: compute total data size across all levels
size_t total_size = 0;
for (int i = 0; i < num_mip_levels; ++i) {
total_size += (size_t)mip_widths[i] * mip_heights[i] * num_channels * bytes_per_channel;
}
void* all_data = malloc(total_size);
if (all_data == nullptr) {
free(tex_data);
return false;
}
memcpy(all_data, data, total_size);
// Allocate mipmap arrays
tex_data->mip_data = (void**)malloc(sizeof(void*) * num_mip_levels);
tex_data->mip_widths = (int*)malloc(sizeof(int) * num_mip_levels);
tex_data->mip_heights = (int*)malloc(sizeof(int) * num_mip_levels);
if (tex_data->mip_data == nullptr || tex_data->mip_widths == nullptr || tex_data->mip_heights == nullptr) {
free(all_data);
free(tex_data->mip_data);
free(tex_data->mip_widths);
free(tex_data->mip_heights);
free(tex_data);
return false;
}
// Set up per-level pointers into the contiguous buffer
char* ptr = (char*)all_data;
for (int i = 0; i < num_mip_levels; ++i) {
tex_data->mip_data[i] = ptr;
tex_data->mip_widths[i] = mip_widths[i];
tex_data->mip_heights[i] = mip_heights[i];
ptr += (size_t)mip_widths[i] * mip_heights[i] * num_channels * bytes_per_channel;
}
🤖 Prompt for AI Agents
In `@warp/native/texture.cpp` around lines 144 - 170, The three mallocs that
create tex_data->mip_data, tex_data->mip_widths and tex_data->mip_heights in the
mipmapped 2D path (and the analogous three mallocs in the 3D mipmapped path)
must be null-checked: after each malloc, verify the returned pointer is
non-null; on any failure free any prior successful allocations (free
tex_data->mip_data, tex_data->mip_widths, tex_data->mip_heights if set), free
all_data, and return false (also ensure you do not dereference null pointers
afterward); apply the same pattern for the 3D mipmap allocation block so both
mipmapped paths safely clean up and return false on allocation failure.

Comment on lines +194 to 217
// mip_filter_mode: 0=nearest, 1=linear
// address_mode_u, address_mode_v: 0=wrap, 1=clamp, 2=mirror, 3=border (per-axis)
// use_normalized_coords: if true, texture coordinates are in [0,1]; if false, in texel space [0,width/height]
// num_mip_levels: number of mip levels to create
// mip_widths, mip_heights: array of mip widths and heights (must be power of two)
WP_API bool wp_texture2d_create_device(
void* context,
int width,
int height,
int num_channels,
int dtype,
int filter_mode,
int mip_filter_mode,
int address_mode_u,
int address_mode_v,
bool use_normalized_coords,
int num_mip_levels,
const void* data,
const int* mip_widths,
const int* mip_heights,
uint64_t* tex_handle_out,
uint64_t* array_handle_out
uint64_t* array_handle_out,
uint64_t* mipmap_handle_out
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Comment claims "must be power of two" but this isn't enforced.

Lines 198 and 262 state mip widths/heights/depths "must be power of two", but neither the C++ implementation nor the Python side validates this constraint. If this is a CUDA hardware requirement, consider adding validation; otherwise, update the comment to reflect the actual contract (e.g., "typically halved at each level").

Comment on lines +2204 to +2209
def test_mipmap2d_max_load(test, device):
"""Test that sampling at the highest LOAD returns approximately the average of all texels."""
width, height = 4, 4
# Create a gradient pattern
data = np.arange(16, dtype=np.float32).reshape((4, 4))

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove unused width and height variables.

width and height are assigned but never referenced — the data shape is hardcoded as (4, 4). Flagged by Ruff (F841).

Suggested fix
-    width, height = 4, 4
-    # Create a gradient pattern
-    data = np.arange(16, dtype=np.float32).reshape((4, 4))
+    # Create a gradient pattern (4x4)
+    data = np.arange(16, dtype=np.float32).reshape((4, 4))

Alternatively, use the variables in the reshape:

     width, height = 4, 4
     # Create a gradient pattern
-    data = np.arange(16, dtype=np.float32).reshape((4, 4))
+    data = np.arange(width * height, dtype=np.float32).reshape((height, width))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def test_mipmap2d_max_load(test, device):
"""Test that sampling at the highest LOAD returns approximately the average of all texels."""
width, height = 4, 4
# Create a gradient pattern
data = np.arange(16, dtype=np.float32).reshape((4, 4))
def test_mipmap2d_max_load(test, device):
"""Test that sampling at the highest LOAD returns approximately the average of all texels."""
# Create a gradient pattern (4x4)
data = np.arange(16, dtype=np.float32).reshape((4, 4))
🧰 Tools
🪛 Ruff (0.14.14)

[error] 2206-2206: Local variable width is assigned to but never used

Remove assignment to unused variable width

(F841)


[error] 2206-2206: Local variable height is assigned to but never used

Remove assignment to unused variable height

(F841)

🤖 Prompt for AI Agents
In `@warp/tests/cuda/test_texture.py` around lines 2204 - 2209, The
test_mipmap2d_max_load function currently defines unused variables width and
height while hardcoding the reshape to (4, 4); remove the unused assignments or
use them in the data.reshape call to fix the Ruff F841 warning. Locate the
function test_mipmap2d_max_load and either delete the lines "width, height = 4,
4" or change "data = np.arange(16, dtype=np.float32).reshape((4, 4))" to use the
width and height variables (e.g., reshape((height, width))) so the variables are
referenced.

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 files reviewed, no comments

Edit Code Review Agent Settings | Greptile

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
warp/_src/builtins.py (2)

7619-7641: ⚠️ Potential issue | 🟡 Minor

Remove default values from lod docstrings.

The lod descriptions repeat the default 0.0 already shown by Sphinx; please drop the explicit default text.

🧹 Suggested docstring tweak
-        lod: Level of detail for mipmapped textures. Default is 0.0 (no mipmaps).
+        lod: Level of detail for mipmapped textures.
-        lod: Level of detail for mipmapped textures. Default is 0.0 (no mipmaps).
+        lod: Level of detail for mipmapped textures.
-        lod: Level of detail for mipmapped textures. Default is 0.0 (no mipmaps).
+        lod: Level of detail for mipmapped textures.
-        lod: Level of detail for mipmapped textures. Default is 0.0 (no mipmaps).
+        lod: Level of detail for mipmapped textures.

As per coding guidelines, "Don't repeat default values from function signatures in docstrings—Sphinx autodoc shows them automatically".

Also applies to: 7645-7669, 7692-7714, 7718-7744


7619-7626: ⚠️ Potential issue | 🟡 Minor

Regenerate warp/__init__.pyi after modifying builtins.

Please run build_docs.py so the public stubs reflect the new lod parameter.

As per coding guidelines, "After modifying warp/_src/builtins.py, run build_docs.py to regenerate warp/__init__.pyi".

🤖 Fix all issues with AI agents
In `@warp/native/texture.h`:
- Around line 542-546: The CPU sampling paths for vector types pass a float lod
into cpu_sample_2d_channel (which takes an int) causing truncation and skipping
inter-level interpolation; update the vec2f::sample_2d, vec2f::sample_3d,
vec4f::sample_2d, and vec4f::sample_3d branches so that when lod > 0.0f they
call the mip-aware helpers (cpu_sample_2d_channel_mip /
cpu_sample_3d_channel_mip) with the float LOD to perform proper fractional mip
interpolation instead of calling cpu_sample_2d_channel / cpu_sample_3d_channel
directly. Ensure you replace the non-mip calls in those four methods with their
corresponding _mip variants.
- Around line 496-500: The cpu sampling branch currently calls
cpu_sample_2d_channel(cpu_tex, u, v, 0, 0) when lod <= 0.0 which can dereference
tex->mip_widths/mip_heights for single-level textures; change the call to pass
level = -1 so cpu_sample_2d_channel uses the base texture dimensions instead of
accessing mip arrays, and make the same change for the other CPU sampling call
sites mentioned (the spots that call cpu_sample_2d_channel with level 0 in the
non-mip branch); keep the mip path using cpu_sample_2d_channel_mip(cpu_tex, u,
v, 0, lod) unchanged.
🧹 Nitpick comments (1)
warp/tests/cuda/test_texture.py (1)

2084-2098: Consistent "LOAD" → "LOD" typo in comments and docstrings throughout mipmap tests.

The PR commit message explicitly renames "load" to "lod", but many comments and docstrings still say "LOAD" (e.g., "Sample at LOAD 0", "LOAD 1", "max LOAD", "test_mipmap2d_integer_load", "test_mipmap2d_max_load"). While the actual parameter is correctly named lod, the documentation is inconsistent.

Examples: Lines 2152, 2167–2168, 2170, 2185, 2204–2205, 2218, 2221, 2255, 2372, 2404.

Comment on lines +496 to +500
if (lod <= 0.0) {
return cpu_sample_2d_channel(cpu_tex, u, v, 0, 0);
} else {
return cpu_sample_2d_channel_mip(cpu_tex, u, v, 0, lod);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

rg -n "mip_data|mip_widths|mip_heights|num_mip_levels" warp/native/texture.cpp | head -80

Repository: NVIDIA/warp

Length of output: 4306


🏁 Script executed:

rg -A 20 "cpu_sample_2d_channel\(" warp/native/texture.cpp | head -60

Repository: NVIDIA/warp

Length of output: 37


🏁 Script executed:

rg -n -A 15 "cpu_sample_2d_channel\(" warp/native/texture.h

Repository: NVIDIA/warp

Length of output: 4137


Fix null pointer dereference for non-mipmapped textures in CPU sampling code.

The code at line 497 calls cpu_sample_2d_channel(cpu_tex, u, v, 0, 0) with level=0 when lod <= 0.0. However, the cpu_sample_2d_channel function (line 284) accesses tex->mip_widths[level] and tex->mip_heights[level] when level >= 0. For single-level textures (num_mip_levels=1), these mip arrays are set to nullptr in texture.cpp (lines 186-190), causing a null pointer dereference.

Pass level=-1 instead of level=0 in the lod <= 0.0 branch so it uses the base texture dimensions rather than attempting to access the unallocated mip arrays. Apply the same fix at lines 543, 545, 594-600.

🤖 Prompt for AI Agents
In `@warp/native/texture.h` around lines 496 - 500, The cpu sampling branch
currently calls cpu_sample_2d_channel(cpu_tex, u, v, 0, 0) when lod <= 0.0 which
can dereference tex->mip_widths/mip_heights for single-level textures; change
the call to pass level = -1 so cpu_sample_2d_channel uses the base texture
dimensions instead of accessing mip arrays, and make the same change for the
other CPU sampling call sites mentioned (the spots that call
cpu_sample_2d_channel with level 0 in the non-mip branch); keep the mip path
using cpu_sample_2d_channel_mip(cpu_tex, u, v, 0, lod) unchanged.

Comment on lines +542 to +546
if (lod <= 0.0f) {
return vec2f(cpu_sample_2d_channel(cpu_tex, u, v, 0, 0), cpu_sample_2d_channel(cpu_tex, u, v, 1, 0));
} else {
return vec2f(cpu_sample_2d_channel(cpu_tex, u, v, 0, lod), cpu_sample_2d_channel(cpu_tex, u, v, 1, lod));
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Bug: vec2f CPU mip path passes float lod to cpu_sample_2d_channel (expects int level), skipping inter-level interpolation.

The float specialization correctly calls cpu_sample_2d_channel_mip for lod > 0, but the vec2f specialization calls cpu_sample_2d_channel directly, passing the float lod which silently truncates to int. This means fractional LODs won't interpolate between mip levels for 2-channel textures on CPU.

The same bug exists in:

  • vec2f::sample_3d (Lines 567–569)
  • vec4f::sample_2d (Lines 598–601)
  • vec4f::sample_3d (Lines 627–629)
🐛 Proposed fix for vec2f::sample_2d (apply same pattern to all four affected paths)
         } else {
-            return vec2f(cpu_sample_2d_channel(cpu_tex, u, v, 0, lod), cpu_sample_2d_channel(cpu_tex, u, v, 1, lod));
+            return vec2f(cpu_sample_2d_channel_mip(cpu_tex, u, v, 0, lod), cpu_sample_2d_channel_mip(cpu_tex, u, v, 1, lod));
         }

For vec4f::sample_2d:

         } else {
             return vec4f(
-                cpu_sample_2d_channel(cpu_tex, u, v, 0, lod), cpu_sample_2d_channel(cpu_tex, u, v, 1, lod),
-                cpu_sample_2d_channel(cpu_tex, u, v, 2, lod), cpu_sample_2d_channel(cpu_tex, u, v, 3, lod)
+                cpu_sample_2d_channel_mip(cpu_tex, u, v, 0, lod), cpu_sample_2d_channel_mip(cpu_tex, u, v, 1, lod),
+                cpu_sample_2d_channel_mip(cpu_tex, u, v, 2, lod), cpu_sample_2d_channel_mip(cpu_tex, u, v, 3, lod)
             );
         }

Apply the analogous _mip change for the 3D variants (sample_3d) in both vec2f and vec4f.

🤖 Prompt for AI Agents
In `@warp/native/texture.h` around lines 542 - 546, The CPU sampling paths for
vector types pass a float lod into cpu_sample_2d_channel (which takes an int)
causing truncation and skipping inter-level interpolation; update the
vec2f::sample_2d, vec2f::sample_3d, vec4f::sample_2d, and vec4f::sample_3d
branches so that when lod > 0.0f they call the mip-aware helpers
(cpu_sample_2d_channel_mip / cpu_sample_3d_channel_mip) with the float LOD to
perform proper fractional mip interpolation instead of calling
cpu_sample_2d_channel / cpu_sample_3d_channel directly. Ensure you replace the
non-mip calls in those four methods with their corresponding _mip variants.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
warp/__init__.pyi (1)

4674-4690: ⚠️ Potential issue | 🟠 Major

lod parameter should have a default value in stub signatures to match implementation.

The actual implementation (warp/_src/builtins.py) defines all four texture_sample overloads with defaults={"lod": 0.0}, but the stub signatures declare lod: float32 as required. This causes type checkers to incorrectly flag existing call sites that omit lod, breaking backward compatibility. The docstring correctly states "Default is 0.0 (no mipmaps)" but the signature doesn't reflect this.

Proposed fix (apply to all four overloads)
-def texture_sample(tex: Texture2D, uv: vec2f, dtype: Any, lod: float32) -> Any:
+def texture_sample(tex: Texture2D, uv: vec2f, dtype: Any, lod: float32 = ...) -> Any:
-def texture_sample(tex: Texture2D, u: float32, v: float32, dtype: Any, lod: float32) -> Any:
+def texture_sample(tex: Texture2D, u: float32, v: float32, dtype: Any, lod: float32 = ...) -> Any:
-def texture_sample(tex: Texture3D, uvw: vec3f, dtype: Any, lod: float32) -> Any:
+def texture_sample(tex: Texture3D, uvw: vec3f, dtype: Any, lod: float32 = ...) -> Any:
-def texture_sample(tex: Texture3D, u: float32, v: float32, w: float32, dtype: Any, lod: float32) -> Any:
+def texture_sample(tex: Texture3D, u: float32, v: float32, w: float32, dtype: Any, lod: float32 = ...) -> Any:

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 files reviewed, 7 comments

Edit Code Review Agent Settings | Greptile

Comment on lines 294 to 295
float coord_u = tex->use_normalized_coords ? u : (u / (float)tex->width);
float coord_v = tex->use_normalized_coords ? v : (v / (float)tex->height);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unnormalized coords use base texture dims instead of mip dims

Suggested change
float coord_u = tex->use_normalized_coords ? u : (u / (float)tex->width);
float coord_v = tex->use_normalized_coords ? v : (v / (float)tex->height);
float coord_u = tex->use_normalized_coords ? u : (u / (float)width);
float coord_v = tex->use_normalized_coords ? v : (v / (float)height);

Comment on lines 355 to 357
float coord_u = tex->use_normalized_coords ? u : (u / (float)tex->width);
float coord_v = tex->use_normalized_coords ? v : (v / (float)tex->height);
float coord_w = tex->use_normalized_coords ? w : (w / (float)tex->depth);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unnormalized coords use base texture dims instead of mip dims

Suggested change
float coord_u = tex->use_normalized_coords ? u : (u / (float)tex->width);
float coord_v = tex->use_normalized_coords ? v : (v / (float)tex->height);
float coord_w = tex->use_normalized_coords ? w : (w / (float)tex->depth);
float coord_u = tex->use_normalized_coords ? u : (u / (float)width);
float coord_v = tex->use_normalized_coords ? v : (v / (float)height);
float coord_w = tex->use_normalized_coords ? w : (w / (float)depth);

Comment on lines +545 to +546
return vec2f(cpu_sample_2d_channel(cpu_tex, u, v, 0, lod), cpu_sample_2d_channel(cpu_tex, u, v, 1, lod));
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

float lod truncated to int, bypasses mip filter interpolation. CPU disagrees with CUDA

Suggested change
return vec2f(cpu_sample_2d_channel(cpu_tex, u, v, 0, lod), cpu_sample_2d_channel(cpu_tex, u, v, 1, lod));
}
return vec2f(cpu_sample_2d_channel_mip(cpu_tex, u, v, 0, lod), cpu_sample_2d_channel_mip(cpu_tex, u, v, 1, lod));
}

Comment on lines +567 to +569
return vec2f(
cpu_sample_3d_channel(cpu_tex, u, v, w, 0, lod), cpu_sample_3d_channel(cpu_tex, u, v, w, 1, lod)
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

float lod truncated to int, bypasses mip filter interpolation. CPU disagrees with CUDA

Suggested change
return vec2f(
cpu_sample_3d_channel(cpu_tex, u, v, w, 0, lod), cpu_sample_3d_channel(cpu_tex, u, v, w, 1, lod)
);
return vec2f(
cpu_sample_3d_channel_mip(cpu_tex, u, v, w, 0, lod), cpu_sample_3d_channel_mip(cpu_tex, u, v, w, 1, lod)
);

Comment on lines +598 to +601
return vec4f(
cpu_sample_2d_channel(cpu_tex, u, v, 0, lod), cpu_sample_2d_channel(cpu_tex, u, v, 1, lod),
cpu_sample_2d_channel(cpu_tex, u, v, 2, lod), cpu_sample_2d_channel(cpu_tex, u, v, 3, lod)
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

float lod truncated to int, bypasses mip filter interpolation. CPU disagrees with CUDA

Suggested change
return vec4f(
cpu_sample_2d_channel(cpu_tex, u, v, 0, lod), cpu_sample_2d_channel(cpu_tex, u, v, 1, lod),
cpu_sample_2d_channel(cpu_tex, u, v, 2, lod), cpu_sample_2d_channel(cpu_tex, u, v, 3, lod)
);
return vec4f(
cpu_sample_2d_channel_mip(cpu_tex, u, v, 0, lod), cpu_sample_2d_channel_mip(cpu_tex, u, v, 1, lod),
cpu_sample_2d_channel_mip(cpu_tex, u, v, 2, lod), cpu_sample_2d_channel_mip(cpu_tex, u, v, 3, lod)
);

Comment on lines +626 to +629
return vec4f(
cpu_sample_3d_channel(cpu_tex, u, v, w, 0, lod), cpu_sample_3d_channel(cpu_tex, u, v, w, 1, lod),
cpu_sample_3d_channel(cpu_tex, u, v, w, 2, lod), cpu_sample_3d_channel(cpu_tex, u, v, w, 3, lod)
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

float lod truncated to int, bypasses mip filter interpolation. CPU disagrees with CUDA

Suggested change
return vec4f(
cpu_sample_3d_channel(cpu_tex, u, v, w, 0, lod), cpu_sample_3d_channel(cpu_tex, u, v, w, 1, lod),
cpu_sample_3d_channel(cpu_tex, u, v, w, 2, lod), cpu_sample_3d_channel(cpu_tex, u, v, w, 3, lod)
);
return vec4f(
cpu_sample_3d_channel_mip(cpu_tex, u, v, w, 0, lod), cpu_sample_3d_channel_mip(cpu_tex, u, v, w, 1, lod),
cpu_sample_3d_channel_mip(cpu_tex, u, v, w, 2, lod), cpu_sample_3d_channel_mip(cpu_tex, u, v, w, 3, lod)
);

@greptile-apps
Copy link

greptile-apps bot commented Feb 8, 2026

Additional Comments (1)

warp/native/texture.h
clamping uses base dims instead of mip dims

        if (tex->address_mode_u != WP_TEXTURE_ADDRESS_BORDER) {
            x = cpu_clamp_index(x, width);
        }
        if (tex->address_mode_v != WP_TEXTURE_ADDRESS_BORDER) {
            y = cpu_clamp_index(y, height);
        }

@shi-eric shi-eric requested a review from nvtw February 9, 2026 17:40
@nvtw
Copy link
Contributor

nvtw commented Feb 10, 2026

The feature sounds useful and the proposed API looks reasonable. Is the MR already ready for review? I still see a couple of TODO.

@StafaH
Copy link
Contributor Author

StafaH commented Feb 10, 2026

Great! I can finalize the MR then if it looks reasonable to implement it this way. Give me a couple days and I can get it ready for review

@shi-eric
Copy link
Contributor

Great! I can finalize the MR then if it looks reasonable to implement it this way. Give me a couple days and I can get it ready for review

Thanks! FYI, the cutoff for changes going into v1.12.0 is February 23, so the changes should be merged into main prior to that date for the feature to be included in the next release.

@shi-eric shi-eric added this to the 1.13.0 milestone Feb 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants