Files
pyxis/lib/tdeck_ui/UI/LXMF/NomadNetImageDecoder.cpp
T
Torlando 929fa2c7ab feat(nomadnet): inline page images — parser, WebP decode, /media transport, LVGL render
Cumulative candidate on top of origin/main (7ae47d5).
Slices 1-5: compact-model image records, vendored decode-only libwebp,
wire contract + loader state machine, owner-loop /media dispatch with
retained page-Link reuse, screen render with bounded PSRAM LRU pixel store.

Local checkpoint only — not pushed.
2026-09-16 18:11:18 +00:00

114 lines
4.9 KiB
C++

// Bounded WebP -> RGB565 decoder implementation. See NomadNetImageDecoder.h
// for the bounds contract. Vendor source: lib/webp (libwebp v1.3.2
// decode-only subset, VENDOR_MANIFEST.json).
#include "NomadNetImageDecoder.h"
// Vendored copy of libwebp v1.3.2 src/webp/decode.h, placed next to this
// file because PlatformIO exposes the webp library's C sources to dependent
// translation units but not its nested src/webp include prefix.
#include "webp_decode_vendored.h"
#include <cstdlib>
#include <cstring>
#if defined(ARDUINO_ARCH_ESP32)
#include <esp_heap_caps.h>
// MapScreen convention: large UI buffers go to PSRAM (8 MB OPI on the
// T-Deck Plus) so they do not compete with the internal heap.
#define PYXIS_IMAGE_MALLOC(size) \
static_cast<std::uint8_t*>(heap_caps_malloc((size), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT))
#define PYXIS_IMAGE_FREE(ptr) heap_caps_free(ptr)
#else
#define PYXIS_IMAGE_MALLOC(size) static_cast<std::uint8_t*>(std::malloc((size)))
#define PYXIS_IMAGE_FREE(ptr) std::free(ptr)
#endif
namespace UI::LXMF::NomadNet {
namespace {
// Convert one packed RGBA pixel as read little-endian (memory order R,G,B,A,
// so r = bits 0-7, g = 8-15, b = 16-23) to 16-bit RGB565 (R:11 G:10 B:5).
// Each 8-bit channel is truncated to its high 5/6 bits (>>3, >>2, >>3).
// C++11-safe: single-expression constexpr body.
constexpr std::uint16_t rgba_to_rgb565(std::uint32_t rgba) {
return static_cast<std::uint16_t>(
(((rgba & 0xffU) >> 3) << 11) |
((((rgba >> 8) & 0xffU) >> 2) << 5) |
(((rgba >> 16) & 0xffU) >> 3));
}
// Staging bound: RGBA bytes for the largest decodable image (640x640).
constexpr long long MAX_STAGING_BYTES =
(long long)MAX_DECODABLE_DIMENSION * (long long)MAX_DECODABLE_DIMENSION * 4LL;
} // namespace
ImageDecodeResult decode_webp_rgb565(const std::uint8_t* data, std::size_t bytes,
std::uint16_t max_dimension,
DecodedImage& out) {
out = DecodedImage{};
if (data == nullptr || bytes == 0) return ImageDecodeResult::NOT_IMAGE;
if (bytes > MAX_IMAGE_INPUT_BYTES) return ImageDecodeResult::INPUT_TOO_LARGE;
if (max_dimension == 0) max_dimension = MAX_DECODABLE_DIMENSION;
// 1. Validate the container and enforce the dimension cap before any
// decode staging is allocated.
int width = 0;
int height = 0;
if (!WebPGetInfo(data, bytes, &width, &height))
return ImageDecodeResult::NOT_IMAGE;
if (width <= 0 || height <= 0) return ImageDecodeResult::NOT_IMAGE;
const long long dim_max = (long long)width > (long long)height ? width : height;
if (dim_max > max_dimension) return ImageDecodeResult::DIMENSIONS;
const long long pixel_bytes = (long long)width * height * 4;
if (pixel_bytes > MAX_STAGING_BYTES) return ImageDecodeResult::DIMENSIONS;
// 2. Allocate RGBA staging (PSRAM on device) and decode directly into it.
// WebPDecodeRGBAInto writes RGBA: R,G,B,A per pixel, stride in bytes.
std::uint8_t* staging = PYXIS_IMAGE_MALLOC(static_cast<std::size_t>(pixel_bytes));
if (staging == nullptr) return ImageDecodeResult::ALLOC_FAILED;
const uint8_t* result =
WebPDecodeRGBAInto(data, bytes, staging,
static_cast<size_t>(pixel_bytes),
static_cast<int>(width * 4));
if (result == nullptr) {
PYXIS_IMAGE_FREE(staging);
return ImageDecodeResult::DECODE_FAILED;
}
// 3. Convert RGBA -> RGB565 in place, forward. Source pixel k spans bytes
// [4k, 4k+4); the already-written destination pixels 0..k-1 occupy
// bytes [0, 2k). Since 4k >= 2k for all k, the source read for pixel k
// is strictly above every prior destination write, so no read touches
// a byte already converted. (Right-to-left would corrupt pixel 0, whose
// source bytes 2..3 sit under dst[1].) No scratch buffer needed, so no
// added stack pressure for the FreeRTOS/UI task.
for (int y = 0; y < height; ++y) {
const std::size_t row = (std::size_t)y * (std::size_t)width;
const std::uint8_t* src = staging + row * 4;
std::uint16_t* dst = reinterpret_cast<std::uint16_t*>(staging) + row;
for (int x = 0; x < width; ++x) {
const std::size_t o = (std::size_t)x * 4;
const std::uint32_t rgba =
static_cast<std::uint32_t>(src[o]) |
(static_cast<std::uint32_t>(src[o + 1]) << 8) |
(static_cast<std::uint32_t>(src[o + 2]) << 16) |
(static_cast<std::uint32_t>(src[o + 3]) << 24);
dst[x] = rgba_to_rgb565(rgba);
}
}
out.width = (std::uint32_t)width;
out.height = (std::uint32_t)height;
out.pixels = reinterpret_cast<std::uint16_t*>(staging);
return ImageDecodeResult::OK;
}
void release_decoded_image(DecodedImage& image) {
if (image.pixels != nullptr) PYXIS_IMAGE_FREE(image.pixels);
image = DecodedImage{};
}
} // namespace UI::LXMF::NomadNet