From ced0a1462ecc9b90a5f4ec64ab81df35edb80be0 Mon Sep 17 00:00:00 2001 From: cepetr Date: Fri, 4 Apr 2025 11:53:25 +0200 Subject: [PATCH] feat(core): emulate hw jpeg decoder using libjpeg [no changelog] --- core/SConscript.bootloader_emu | 5 + core/SConscript.unix | 5 + core/embed/gfx/jpegdec/unix/jpegdec.c | 345 ++++++++++++++++++++++++ core/site_scons/models/T3W1/emulator.py | 6 + 4 files changed, 361 insertions(+) create mode 100644 core/embed/gfx/jpegdec/unix/jpegdec.c diff --git a/core/SConscript.bootloader_emu b/core/SConscript.bootloader_emu index b65cdbac23..4cbf99c19c 100644 --- a/core/SConscript.bootloader_emu +++ b/core/SConscript.bootloader_emu @@ -226,6 +226,11 @@ env.Replace( ] + CPPDEFINES_MOD + CPPDEFINES_HAL, ASPPFLAGS='$CFLAGS $CCFLAGS', ) +try: + env.ParseConfig('pkg-config --cflags --libs libjpeg') +except OSError: + print("libjpeg not installed, Emulator build is not possible") + try: env.ParseConfig('pkg-config --cflags --libs sdl2 SDL2_image') except OSError: diff --git a/core/SConscript.unix b/core/SConscript.unix index 5af4d82574..76f7ee0803 100644 --- a/core/SConscript.unix +++ b/core/SConscript.unix @@ -527,6 +527,11 @@ env.Replace( ] + CPPDEFINES_MOD + CPPDEFINES_HAL, ASPPFLAGS='$CFLAGS $CCFLAGS', ) +try: + env.ParseConfig('pkg-config --cflags --libs libjpeg') +except OSError: + print("libjpeg not installed, Emulator build is not possible") + try: env.ParseConfig('pkg-config --cflags --libs sdl2 SDL2_image') except OSError: diff --git a/core/embed/gfx/jpegdec/unix/jpegdec.c b/core/embed/gfx/jpegdec/unix/jpegdec.c new file mode 100644 index 0000000000..0b28d67ba3 --- /dev/null +++ b/core/embed/gfx/jpegdec/unix/jpegdec.c @@ -0,0 +1,345 @@ +/* + * This file is part of the Trezor project, https://trezor.io/ + * + * Copyright (c) SatoshiLabs + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include + +#include +#include + +#include + +#define MAX_SLICE_HEIGHT 16 +#define MAX_SLICE_WIDTH \ + (JPEGDEC_RGBA8888_BUFFER_SIZE / (MAX_SLICE_HEIGHT * sizeof(uint32_t))) + +// Custom source manager structure +typedef struct { + struct jpeg_source_mgr pub; + uint8_t buffer[4096]; + jpegdec_input_t *input; + +} custom_source_mgr_t; + +// Our internal decoder state. +struct jpegdec { + bool inuse; + + // jpeglib context + struct jpeg_decompress_struct cinfo; + struct jpeg_error_mgr jerr; + + // Our custom source manager + custom_source_mgr_t source_mgr; + + // Last decoder state + jpegdec_state_t state; + // Image info + jpegdec_image_t image; + + // Up to MAX_SLICE_HEIGHT lines of decoded data in RGBA8888 format + JSAMPARRAY slice_buffer; + + // Current slice coordinates + int16_t slice_x; + int16_t slice_y; +}; + +// JPEG decoder instance +jpegdec_t g_jpegdec = { + .inuse = false, +}; + +//--------------------------------------------------------------------- +// Custom source manager functions +//--------------------------------------------------------------------- + +static void init_source(j_decompress_ptr cinfo) { + // No special initialization is needed. + UNUSED(cinfo); +} + +static boolean fill_input_buffer(j_decompress_ptr cinfo) { + custom_source_mgr_t *src = (custom_source_mgr_t *)cinfo->src; + jpegdec_input_t *input = src->input; + + if (input->offset < input->size) { + size_t nbytes = MIN(input->size - input->offset, sizeof(src->buffer)); + memcpy(src->buffer, input->data + input->offset, nbytes); + input->offset += nbytes; + src->pub.next_input_byte = src->buffer; + src->pub.bytes_in_buffer = nbytes; + return TRUE; + } + + // If no more data is available and this is the last chunk, + // supply a fake EOI marker. + + if (input->last_chunk) { + src->buffer[0] = 0xFF; + src->buffer[1] = JPEG_EOI; + src->pub.next_input_byte = src->buffer; + src->pub.bytes_in_buffer = 2; + return TRUE; + } + + // No data available, but not the last chunk: suspend input + return FALSE; +} + +static void skip_input_data(j_decompress_ptr cinfo, long num_bytes) { + custom_source_mgr_t *src = (custom_source_mgr_t *)cinfo->src; + + if (num_bytes > 0) { + while (num_bytes > (long)src->pub.bytes_in_buffer) { + num_bytes -= (long)src->pub.bytes_in_buffer; + fill_input_buffer(cinfo); + } + src->pub.next_input_byte += num_bytes; + src->pub.bytes_in_buffer -= num_bytes; + } +} + +static void term_source(j_decompress_ptr cinfo) { + // No cleanup necessary + UNUSED(cinfo); +} + +//--------------------------------------------------------------------- +// JPEG decoder functions +//--------------------------------------------------------------------- + +bool jpegdec_open(void) { + jpegdec_t *dec = &g_jpegdec; + + if (dec->inuse) { + return false; + } + + memset(dec, 0, sizeof(jpegdec_t)); + dec->inuse = true; + + // Set up the JPEG decompression object with default error handling + dec->cinfo.err = jpeg_std_error(&dec->jerr); + jpeg_create_decompress(&dec->cinfo); + + // Init our custom source manager + custom_source_mgr_t *src = &dec->source_mgr; + src->pub.init_source = init_source; + src->pub.fill_input_buffer = fill_input_buffer; + src->pub.skip_input_data = skip_input_data; + src->pub.resync_to_restart = jpeg_resync_to_restart; + src->pub.term_source = term_source; + src->pub.bytes_in_buffer = 0; + src->pub.next_input_byte = NULL; + src->input = NULL; + dec->cinfo.src = (struct jpeg_source_mgr *)src; + + dec->state = JPEGDEC_STATE_NEED_DATA; + + return true; +} + +jpegdec_state_t jpegdec_process(jpegdec_input_t *input) { + jpegdec_t *dec = &g_jpegdec; + + if (!dec->inuse) { + return JPEGDEC_STATE_ERROR; + } + + dec->source_mgr.input = input; + + if (dec->state == JPEGDEC_STATE_SLICE_READY) { + dec->slice_x += MAX_SLICE_WIDTH; + if (dec->slice_x < dec->image.width) { + // There is more data ready in the slice buffer + return JPEGDEC_STATE_SLICE_READY; + } + dec->slice_x = 0; + dec->slice_y = MIN(dec->image.height, dec->slice_y + MAX_SLICE_HEIGHT); + + if (dec->slice_y >= dec->image.height) { + // The image is fully decoded + dec->state = JPEGDEC_STATE_FINISHED; + return dec->state; + } + } + + if (dec->state == JPEGDEC_STATE_FINISHED || + dec->state == JPEGDEC_STATE_ERROR) { + // Do nothing if the decoder is finished or in error state + } else if (dec->image.width == 0 && dec->image.height == 0) { + // Decode jpeg headers and get image parameters + int ret = jpeg_consume_input(&dec->cinfo); + + switch (ret) { + case JPEG_SUSPENDED: + dec->state = JPEGDEC_STATE_NEED_DATA; + break; + + case JPEG_REACHED_SOS: + dec->image.width = (int16_t)dec->cinfo.image_width; + dec->image.height = (int16_t)dec->cinfo.image_height; + + if (dec->cinfo.num_components == 1) { + dec->image.format = JPEGDEC_IMAGE_GRAYSCALE; + } else if (dec->cinfo.num_components == 3) { + int h = dec->cinfo.comp_info[0].h_samp_factor; + int v = dec->cinfo.comp_info[0].v_samp_factor; + if (h == 2 && v == 2) + dec->image.format = JPEGDEC_IMAGE_YCBCR420; + else if (h == 2 && v == 1) + dec->image.format = JPEGDEC_IMAGE_YCBCR422; + else + dec->image.format = JPEGDEC_IMAGE_YCBCR444; + } else { + dec->state = JPEGDEC_STATE_ERROR; + break; + } + + dec->slice_x = 0; + dec->slice_y = 0; + + // Set the output color space + // (need to be set before jpeg_start_decompress) + dec->cinfo.out_color_space = JCS_EXT_BGRA; + + jpeg_start_decompress(&dec->cinfo); + + // Allocate the output buffer + dec->slice_buffer = (*dec->cinfo.mem->alloc_sarray)( + (j_common_ptr)&dec->cinfo, JPOOL_IMAGE, + dec->cinfo.output_width * dec->cinfo.output_components, + MAX_SLICE_HEIGHT); + + dec->state = JPEGDEC_STATE_INFO_READY; + break; + + default: + dec->state = JPEGDEC_STATE_ERROR; + break; + } + } else { + // Image headers where decoded, now we can decode the image data + // Read scanlines until we have a full slice + + for (;;) { + // Row in slice buffer + size_t row = dec->cinfo.output_scanline - dec->slice_y; + + if (row >= MAX_SLICE_HEIGHT) { + dec->slice_y = dec->cinfo.output_scanline; + row = 0; + } + + int lines_read = + jpeg_read_scanlines(&dec->cinfo, &dec->slice_buffer[row], 1); + + if (lines_read == 0) { + dec->state = JPEGDEC_STATE_NEED_DATA; + break; + } + + if (row == MAX_SLICE_HEIGHT - 1 || + dec->cinfo.output_scanline >= dec->cinfo.output_height) { + dec->state = JPEGDEC_STATE_SLICE_READY; + break; + } + } + } + + return dec->state; +} + +bool jpegdec_get_info(jpegdec_image_t *image) { + jpegdec_t *dec = &g_jpegdec; + + if (!dec->inuse) { + return false; + } + + if (dec->image.width == 0 || dec->image.height == 0) { + return false; + } + + *image = dec->image; + return true; +} + +bool jpegdec_get_slice_rgba8888(uint32_t *rgba8888, jpegdec_slice_t *slice) { + jpegdec_t *dec = &g_jpegdec; + + if (!dec->inuse) { + return false; + } + + if (dec->state != JPEGDEC_STATE_SLICE_READY) { + return false; + } + + slice->x = dec->slice_x; + slice->y = dec->slice_y; + slice->width = MIN(dec->image.width - dec->slice_x, MAX_SLICE_WIDTH); + slice->height = MIN(dec->image.height - dec->slice_y, MAX_SLICE_HEIGHT); + + for (int y = 0; y < slice->height; y++) { + void *src = &((uint32_t *)dec->slice_buffer[y])[slice->x]; + void *dst = rgba8888 + y * slice->width; + memcpy(dst, src, slice->width * sizeof(uint32_t)); + } + + return true; +} + +bool jpegdec_get_slice_mono8(uint32_t *mono8, jpegdec_slice_t *slice) { + jpegdec_t *dec = &g_jpegdec; + + if (!dec->inuse) { + return false; + } + + if (dec->state != JPEGDEC_STATE_SLICE_READY) { + return false; + } + + slice->x = dec->slice_x; + slice->y = dec->slice_y; + slice->width = MIN(dec->image.width - dec->slice_x, MAX_SLICE_WIDTH); + slice->height = MIN(dec->image.height - dec->slice_y, MAX_SLICE_HEIGHT); + + uint8_t *dst = (uint8_t *)mono8; + + for (int y = 0; y < slice->height; y++) { + for (int x = 0; x < slice->width; x++) { + gfx_color32_t color = ((uint32_t *)dec->slice_buffer[y])[slice->x + x]; + dst[y * slice->width + x] = gfx_color32_lum(color); + } + } + + return true; +} + +void jpegdec_close(void) { + jpegdec_t *dec = &g_jpegdec; + + if (dec->inuse) { + jpeg_destroy_decompress(&dec->cinfo); + + memset(dec, 0, sizeof(jpegdec_t)); + } +} diff --git a/core/site_scons/models/T3W1/emulator.py b/core/site_scons/models/T3W1/emulator.py index b679310f28..48e9929667 100644 --- a/core/site_scons/models/T3W1/emulator.py +++ b/core/site_scons/models/T3W1/emulator.py @@ -92,4 +92,10 @@ def configure( sources += ["embed/util/flash/stm32u5/flash_layout.c"] + defines += ["USE_HW_JPEG_DECODER"] + features_available.append("hw_jpeg_decoder") + sources += [ + "embed/gfx/jpegdec/unix/jpegdec.c", + ] + return features_available