ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
ra8_gfx_dither.h File Reference

Void-and-cluster blue-noise dithering: continuous-tone gray8 -> 16-level panel (#477). More...

#include <stdint.h>
#include "ra8_err.h"
Include dependency graph for ra8_gfx_dither.h:
This graph shows which files directly or indirectly include this file:

Go to the source code of this file.

Enumerations

enum  ra8_gfx_dither_const_t : uint8_t {
  k_ra8_gfx_dither_levels = 16 ,
  k_ra8_gfx_dither_step = 17 ,
  k_ra8_gfx_dither_max_level = 15 ,
  k_ra8_gfx_dither_nib_shift = 4 ,
  k_ra8_gfx_dither_ppb = 2 ,
  k_ra8_gfx_dither_mask_dim = 64 ,
  k_ra8_gfx_dither_mask_index_mask = 63 ,
  k_ra8_gfx_dither_rgb_g_shift = 8 ,
  k_ra8_gfx_dither_rgb_r_shift = 16
}
 16-level palette, packing, and blue-noise mask geometry constants. More...
enum  ra8_gfx_dither_scale_t : uint16_t {
  k_ra8_gfx_dither_byte_levels = 256 ,
  k_ra8_gfx_dither_mask_len = 4096
}
 Fixed-point scales for the unbiased threshold comparison. More...

Functions

uint8_t ra8_gfx_dither_gray4_level (uint8_t gray8, int32_t x, int32_t y)
 Quantise one gray8 sample to a 4-bit panel level via the blue-noise mask.
ra8_err_t ra8_gfx_dither_gray8_to_gray4 (const uint8_t *src, int32_t w, int32_t h, int32_t origin_x, int32_t origin_y, uint8_t *out, uint32_t out_cap, uint32_t *out_size)
 Dither a gray8 tile to packed 4-bpp nibbles (panel-native), seamlessly.
ra8_err_t ra8_gfx_blit_gray8_dither (const uint8_t *src, int32_t w, int32_t h, int32_t dst_x, int32_t dst_y)
 Blit a gray8 image into the bound framebuffer, blue-noise dithered.

Detailed Description

Void-and-cluster blue-noise dithering: continuous-tone gray8 -> 16-level panel (#477).

The shipping e-reader panel (Waveshare 6inch HD, IT8951) shows 16 gray levels (4 bpp); the retained source figures (#476) carry 256. Hard-quantising 256 -> 16 bands smooth gradients into visible steps. This module removes the banding by thresholding every pixel against a precomputed blue-noise mask (ra8_gfx_dither_mask_internal.h, generated by scripts/gen/gen_bluenoise_mask.py) indexed purely by the pixel's absolute (x, y) position plus its own value – Ulichney's void-and-cluster method (1993), NOT error diffusion.

Why a position-indexed mask and not Floyd-Steinberg: the reader is tiled, zoomable, must be deterministic (ra8_emulator byte == silicon, the EIL==HIL rule), and wants Helium/MVE. A mask threshold depends on no neighbour state, so it is:

  • independent per pixel/tile – abutting tiles share one continuous mask phase and never seam (the mask is indexed at absolute panel coordinates);
  • fully deterministic – pure integer arithmetic over a const table, so the host unit test, ra8_emulator, and silicon emit identical bytes;
  • re-dithered cleanly from the retained gray8 at every zoom level.
Quantisation rule
The 16 panel levels sit at gray values n * 17 (n = 0..15), the same even palette and the same (n << 4) | n gray4->gray8 expansion the rest of the reader uses (ra8_gfx_blit_gray8, ra8_gfx_blit_gray4_zoom). For a source value v the base level is v / 17 and the fractional distance to the next level is (v % 17) / 17; the pixel rounds up when the mask threshold falls below that fraction. Averaged over any flat region the reconstructed tone equals v exactly (unbiased), so smooth gradients stay smooth.
Note
All three entry points are pure integer transforms; ra8_gfx_blit_gray8_dither additionally writes through the single ra8_gfx framebuffer binding and is therefore not thread-safe.
See also
ra8_gfx_blit_gray8 Non-dithered 1:1 gray8 blit (hard tone copy).
ra8_gfx_blit_gray4_zoom Packed-gray4 loupe blit the dither can feed.
Since
0.1.0

Definition in file ra8_gfx_dither.h.

Enumeration Type Documentation

◆ ra8_gfx_dither_const_t

enum ra8_gfx_dither_const_t : uint8_t

16-level palette, packing, and blue-noise mask geometry constants.

The panel palette is 16 evenly-spaced levels at n * k_ra8_gfx_dither_step for n in [0, k_ra8_gfx_dither_max_level]. The mask edge is a power of two so the toroidal index is a bitmask (coord & k_ra8_gfx_dither_mask_index_mask) rather than a divide, and negative coordinates wrap correctly under the same two's-complement mask.

Invariant
k_ra8_gfx_dither_step * k_ra8_gfx_dither_max_level == 255.
k_ra8_gfx_dither_mask_index_mask == k_ra8_gfx_dither_mask_dim - 1.
See also
ra8_gfx_dither_gray4_level
Since
0.1.0
Enumerator
k_ra8_gfx_dither_levels 

Panel gray levels (4 bpp).

k_ra8_gfx_dither_step 

Palette step 255/15; also the 4->8 replicate.

k_ra8_gfx_dither_max_level 

Maximum 4-bit level.

k_ra8_gfx_dither_nib_shift 

High-nibble shift (packing + gray8 expand).

k_ra8_gfx_dither_ppb 

Pixels packed per output byte.

k_ra8_gfx_dither_mask_dim 

Blue-noise mask edge (power of two).

k_ra8_gfx_dither_mask_index_mask 

Toroidal index bitmask (mask_dim - 1).

k_ra8_gfx_dither_rgb_g_shift 

Green shift of the 0x00RRGGBB gray expand.

k_ra8_gfx_dither_rgb_r_shift 

Red shift of the 0x00RRGGBB gray expand.

Definition at line 68 of file ra8_gfx_dither.h.

◆ ra8_gfx_dither_scale_t

enum ra8_gfx_dither_scale_t : uint16_t

Fixed-point scales for the unbiased threshold comparison.

The rounding decision mask * step < rem * byte_levels keeps the per-pixel round-up probability exactly rem / step when the mask value is uniform over [0, k_ra8_gfx_dither_byte_levels).

See also
ra8_gfx_dither_gray4_level
Since
0.1.0
Enumerator
k_ra8_gfx_dither_byte_levels 

Threshold texture depth (mask range +1).

k_ra8_gfx_dither_mask_len 

Mask entry count (mask_dim * mask_dim).

Definition at line 89 of file ra8_gfx_dither.h.

Function Documentation

◆ ra8_gfx_blit_gray8_dither()

ra8_err_t ra8_gfx_blit_gray8_dither ( const uint8_t * src,
int32_t w,
int32_t h,
int32_t dst_x,
int32_t dst_y )
nodiscard

Blit a gray8 image into the bound framebuffer, blue-noise dithered.

The render entry point: for each pixel of the w x h row-major gray8 source it computes the dithered level with ra8_gfx_dither_gray4_level, expands that level to the 8-bit gray (n << 4) | n, broadcasts it to 0x00RRGGBB, and writes it through the shared clipped plotter at framebuffer coordinate (dst_x + col, dst_y + row) – so the visible output is the panel's 16 levels arranged as blue-noise grain, down-converted to the bound pixel format exactly as ra8_gfx_blit_gray8 down-converts a hard tone. The mask is indexed at absolute framebuffer coordinates, so repainting a damaged sub-rectangle (via ra8_gfx_set_clip) reproduces the same grain that a full-frame paint would – dirty-region updates never seam.

Parameters
[in]srcRow-major gray8 source of at least w * h bytes.
[in]wSource width in pixels (> 0; also the row stride).
[in]hSource height in pixels (> 0).
[in]dst_xDestination column of the source top-left in the framebuffer.
[in]dst_yDestination row of the source top-left in the framebuffer.
Returns
Error code.
Return values
k_ra8_okVisible pixels written (or fully clipped out).
k_ra8_err_not_initializedra8_gfx_init() was not called.
k_ra8_err_invalid_argsrc is NULL, or w / h <= 0.
Precondition
ra8_gfx_init() returned k_ra8_ok.
src holds at least w * h readable bytes.
Postcondition
Each in-clip destination pixel equals its dithered, down-converted level.
Pixels outside the clip rectangle are left unchanged.
Note
Not thread-safe; shares the single ra8_gfx bind state.
See also
ra8_gfx_blit_gray8 Same geometry, hard tone (no dither).
Since
0.1.0

Definition at line 220 of file ra8_gfx_dither.c.

References g_gfx_text_state, internal_level_to_color(), internal_mask_index(), internal_quantise(), k_ra8_err_invalid_arg, k_ra8_err_not_initialized, k_ra8_ok, priv_gfx_text_plot(), and s_ra8_gfx_dither_mask.

◆ ra8_gfx_dither_gray4_level()

uint8_t ra8_gfx_dither_gray4_level ( uint8_t gray8,
int32_t x,
int32_t y )

Quantise one gray8 sample to a 4-bit panel level via the blue-noise mask.

The atomic dither operation and the seam that pins the quantisation maths. Looks up the blue-noise threshold for the pixel at absolute coordinates (x, y) – s_ra8_gfx_dither_mask[(y & 63) * 64 + (x & 63)] – and returns v / 17 rounded up to v / 17 + 1 when that threshold is below the pixel's fractional distance to the next level. Indexing at absolute coordinates (not tile-local) is what makes tiled rendering seamless: the same pixel gets the same threshold regardless of which tile drew it. Pure: reads only its arguments and the const mask, mutates nothing.

Parameters
[in]gray8Source luminance sample, 0 (black) .. 255 (white).
[in]xAbsolute panel/framebuffer column (any int32; wraps mod 64).
[in]yAbsolute panel/framebuffer row (any int32; wraps mod 64).
Returns
The dithered 4-bit level.
Return values
0The pixel quantised to black.
15The pixel quantised to white (the maximum level).
Precondition
None – every uint8 value and every int32 coordinate is in range.
The blue-noise mask table is linked (compile-time const).
Postcondition
The result is in [0, k_ra8_gfx_dither_max_level].
No memory is modified (pure function).
Note
Thread-safe: reads only its arguments and immutable data; ISR-safe.
Example:
uint8_t level = ra8_gfx_dither_gray4_level(src[y * w + x], tile_x + x, tile_y + y);
uint8_t ra8_gfx_dither_gray4_level(uint8_t gray8, int32_t x, int32_t y)
Quantise one gray8 sample to a 4-bit panel level via the blue-noise mask.
See also
ra8_gfx_dither_gray8_to_gray4 Bulk tile -> packed 4 bpp using this rule.
Since
0.1.0

Definition at line 183 of file ra8_gfx_dither.c.

References internal_mask_index(), internal_quantise(), and s_ra8_gfx_dither_mask.

◆ ra8_gfx_dither_gray8_to_gray4()

ra8_err_t ra8_gfx_dither_gray8_to_gray4 ( const uint8_t * src,
int32_t w,
int32_t h,
int32_t origin_x,
int32_t origin_y,
uint8_t * out,
uint32_t out_cap,
uint32_t * out_size )
nodiscard

Dither a gray8 tile to packed 4-bpp nibbles (panel-native), seamlessly.

Applies ra8_gfx_dither_gray4_level to every pixel of the w x h row-major gray8 tile and packs the resulting levels two per byte: the pixel at flat index i occupies the high nibble when i is even and the low nibble when odd (byte = (level[2k] << 4) | level[2k+1]), the same layout ra8_gfx_blit_gray4_zoom unpacks and ra8_rabook_gray4_encode produces. The mask is indexed at absolute coordinates (origin_x + col, origin_y + row), so a large image split into tiles dithers identically to the whole image – no tile-boundary seams. Output size is (w * h + 1) / 2 bytes; out must be at least that large.

Parameters
[in]srcRow-major gray8 tile of at least w * h bytes.
[in]wTile width in pixels (> 0; also the source row stride).
[in]hTile height in pixels (> 0).
[in]origin_xAbsolute panel column of the tile's left edge (mask phase).
[in]origin_yAbsolute panel row of the tile's top edge (mask phase).
[out]outPacked-gray4 output; >= (w * h + 1) / 2 writable bytes.
[in]out_capCapacity of out in bytes.
[out]out_sizeOn success, the byte count written ((w * h + 1) / 2).
Returns
Error code.
Return values
k_ra8_okTile dithered and packed.
k_ra8_err_null_ptrsrc, out, or out_size is NULL.
k_ra8_err_invalid_argw <= 0 or h <= 0.
k_ra8_err_no_memout_cap < (w * h + 1) / 2.
Precondition
src holds at least w * h readable bytes.
out and out_size are non-NULL and out does not overlap src.
Postcondition
On k_ra8_ok, *out_size == (w * h + 1) / 2 and every packed nibble is in [0, k_ra8_gfx_dither_max_level].
On any error return, out and *out_size are unmodified past the point of the failing check.
Note
Not thread-safe only in that it writes out; holds no shared state.
See also
ra8_gfx_blit_gray8_dither Dither straight into the bound framebuffer.
Since
0.1.0

Definition at line 188 of file ra8_gfx_dither.c.

References internal_pack_tile(), k_ra8_err_invalid_arg, k_ra8_err_no_mem, k_ra8_gfx_dither_ppb, k_ra8_ok, RA8_CHECK_NULL_PTR, and ra8_log_error.

Referenced by internal_emit_strip().