defold / defold/defold

Render script: GPU copy/blit from render targets into textures (mips / array layers / 3D slices)

Open
#11,756 0 comments 6 reactions 0 assignees View on GitHub
3d engine feature request render
Dominant language
C++
Stars
6.3k
Forks
455
Avg merge
2d 2h
Merged PRs (30d)
129

Description

### Summary

In Defold’s render script API, there’s currently no way to **GPU-copy (blit) the contents of a render target** into an **existing texture resource**, e.g.:

* into a specific **mip level** of a 2D texture
* into a specific **layer/page** of a **texture array**
* into a specific **slice (z-layer)** of a **3D texture**

Having a fast, hardware-backed copy/blit would significantly simplify a lot of modern rendering techniques and remove the need for “atlas packing” workarounds.

### Motivation / Why this matters

Many rendering workflows produce intermediate images in render targets, then need to store them in a **single texture object** in a structured way (mip chain, array layers, 3D volume slices). On most graphics APIs this is a standard operation (copy image / blit image / resolve).

Without this, the only option is to do extra full-screen passes to manually assemble results (e.g. packing multiple downsampled images into one big atlas), which is:

* architecturally awkward
* more error-prone
* often more expensive than a direct GPU copy
* makes certain pipelines much harder to implement cleanly

### Concrete use cases

#### 1) Pre-filtered environment map (roughness mip chain)

A typical PBR pipeline needs a **prefiltered environment map** where different mip levels represent different roughness/blur levels. A common approach is:

1. render env map into a series of progressively smaller render targets (or reuse one RT and downsample repeatedly)
2. **copy each result into the corresponding mip level** of a texture

In older DX-style pipelines this is trivial via surface/texture copy. In Defold it’s currently not possible directly.

#### 2) Screen Space Local Reflections (previous-frame pyramid)

For SSR/SSLR it’s very useful to keep the **previous fully rendered frame** and build a **mip pyramid** (downsampled + optionally blurred versions). Ideally:

* do ~6–8 downsample passes
* **copy each downsampled RT into mip levels of a single texture**
* sample that texture in SSR according to roughness

Right now, the workaround is something like:

* still do the downsample passes
* then do an extra “packing” pass to place all levels side-by-side in one big atlas texture

This works, but it’s a hack and complicates everything (UV logic, padding, filtering issues, wasted space, etc.).

#### 3) Writing into texture arrays / 3D textures

Dynamic techniques sometimes need to write 2D results into:

* a **texture array layer** (paged texture)
* a **3D texture slice** (volume accumulation, cascades, history buffers, etc.)

A render-target-to-texture copy/blit would enable this cleanly.

### Requested feature

Add a render-script accessible function to perform a **GPU copy/blit** from a render target (or texture) into a destination texture subresource.

Key requirements:

* Source: render target attachment (color) (optionally depth later, but color is already huge)
* Destination: 2D texture / texture array / 3D texture
* Addressing destination subresource:

* **mip level**
* **array layer**
* **3D slice**
* Ability to specify optional rectangles (copy region) and filtering mode for scaling blits (nearest/linear), if supported.

### Possible API shape (example)

Something along these lines (names just illustrative):

```lua
-- Copy or blit from a render target attachment into a texture subresource.
render.blit({
src_render_target = rt,
src_attachment = render.BUFFER_COLOR0, -- or attachment index
src_x = 0, src_y = 0, src_w = w, src_h = h,

dst_texture = tex,
dst_mip = mip,
dst_layer = layer, -- for texture arrays (optional)
dst_slice = slice, -- for 3D textures (optional)
dst_x = 0, dst_y = 0, dst_w = w2, dst_h = h2,

filter = render.FILTER_LINEAR, -- optional
})
```

Notes:

* If `src_w/h != dst_w/h`, the operation is a scaling blit (if supported).
* If formats are incompatible, the function could error or fall back to a shader blit internally (engine choice).
* Even a **restricted “copy only, same size, same format”** version would already be extremely useful.

### Expected backend behavior

* On modern backends (Vulkan/Metal/D3D via bgfx), this maps well to native copy/blit operations.
* On OpenGL it maps to framebuffer blit / texture copy variants.
* On WebGL there are limitations, but even partial support (or a defined fallback path) would help; it’s fine if some platforms can’t support every variant.

### Workarounds today (and why they’re not great)

* “Atlas packing pass”: render multiple downsampled RTs into a single big texture by drawing quads.

* wastes space, adds complexity (UV remapping, padding, filtering artifacts)
* requires additional render passes and shader logic
* Keeping many separate textures alive:

* increases binding pressure / slot usage
* complicates sampling logic and resource management

### Why this is a good engine-level feature

This is one of those small primitives that unlocks many techniques:

* SSR/SSLR history pyramids
* environment prefiltering for PBR
* bloom/DOF pyramids stored in a single mipmapped texture
* temporal accumulation buffers
* writing into arrays/volumes for advanced effects

It makes these techniques **cleaner**, **faster**, and **more idiomatic** than forcing custom packing hacks.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.