init
This commit is contained in:
commit
75fbf8809d
489 files changed
+398278
No files matched your search
@@ -0,0 +1,934 @@
|
||||
/**
|
||||
******************************************************************************
|
||||
* File Name : HardwareMJPEGDecoder.cpp
|
||||
******************************************************************************
|
||||
* This file is generated by TouchGFX Generator 4.26.1. Please, do not edit!
|
||||
******************************************************************************
|
||||
* @attention
|
||||
*
|
||||
* Copyright (c) 2026 STMicroelectronics.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This software is licensed under terms that can be found in the LICENSE file
|
||||
* in the root directory of this software component.
|
||||
* If no LICENSE file comes with this software, it is provided AS-IS.
|
||||
*
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#include <HardwareMJPEGDecoder.hpp>
|
||||
#include <touchgfx/hal/BlitOp.hpp>
|
||||
|
||||
extern "C"
|
||||
{
|
||||
#include <string.h>
|
||||
#include <stm32h7rsxx_hal.h>
|
||||
|
||||
uint32_t JPEG_Decode_DMA(JPEG_HandleTypeDef* hjpeg, uint8_t* input, uint32_t chunkSizeIn, uint8_t* output);
|
||||
uint32_t JPEG_OutputHandler(JPEG_HandleTypeDef* hjpeg);
|
||||
void HAL_JPEG_DecodeCpltCallback(JPEG_HandleTypeDef* hjpeg);
|
||||
void HAL_JPEG_ErrorCallback(JPEG_HandleTypeDef* hjpeg);
|
||||
void HAL_JPEG_DataReadyCallback(JPEG_HandleTypeDef* hjpeg, uint8_t* pDataOut, uint32_t OutDataLength);
|
||||
void DMA2D_CropBuffer(JPEG_Data_BufferTypeDef& job);
|
||||
void DMA2D_CopyBuffer(JPEG_Data_BufferTypeDef& job);
|
||||
void DMA2D_ExternalJobCompleted(JPEG_Data_BufferTypeDef& job);
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
uint8_t* FrameBufferAddress;
|
||||
uint32_t JPEG_InputImageIndex;
|
||||
uint32_t JPEG_InputImageSize_Bytes;
|
||||
uint32_t JPEG_InputImageAddress;
|
||||
volatile uint32_t Jpeg_HWDecodingEnd = 0;
|
||||
volatile uint32_t JPEG_output_is_paused = 0;
|
||||
volatile uint32_t JpegProcessing_End = 0;
|
||||
uint32_t MCU_TotalNb = 0;
|
||||
touchgfx::DMA_Interface* DMA2D_reference;
|
||||
volatile uint32_t JPEG_OUT_Write_BufferIndex = 0;
|
||||
uint32_t FrameBufferWidth;
|
||||
volatile uint32_t line_count = 0;
|
||||
}
|
||||
|
||||
#define MCU_WIDTH_PIXELS ((uint32_t)16)
|
||||
#define MCU_HEIGHT_PIXELS ((uint32_t)16)
|
||||
#define MCU_CHROMA_420_SIZE_BYTES ((uint32_t)384)
|
||||
|
||||
#define CHUNK_SIZE_IN ((uint32_t)(1024*52)) /* Max block size */
|
||||
#define CHUNK_SIZE_OUT ((uint32_t)(MCU_CHROMA_420_SIZE_BYTES * ((800 + MCU_WIDTH_PIXELS - 1) / MCU_WIDTH_PIXELS)))
|
||||
|
||||
LOCATION_PRAGMA_32("TouchGFX_MCU_Data_OutBuffer0")
|
||||
uint8_t MCU_Data_OutBuffer0[CHUNK_SIZE_OUT] LOCATION_ATTRIBUTE_32("TouchGFX_MCU_Data_OutBuffer0");
|
||||
LOCATION_PRAGMA_32("TouchGFX_MCU_Data_OutBuffer1")
|
||||
uint8_t MCU_Data_OutBuffer1[CHUNK_SIZE_OUT] LOCATION_ATTRIBUTE_32("TouchGFX_MCU_Data_OutBuffer1");
|
||||
|
||||
uint8_t MCU_Cropping_Buffer[MCU_WIDTH_PIXELS * MCU_HEIGHT_PIXELS * 2];
|
||||
|
||||
__IO uint32_t MCU_BlockIndex = 0;
|
||||
|
||||
SEM_TYPE semDecodingDone;
|
||||
|
||||
extern JPEG_ConfTypeDef* JPEG_Info;
|
||||
extern JPEG_HandleTypeDef hjpeg;
|
||||
|
||||
volatile uint32_t JPEG_OUT_Read_BufferIndex = 0;
|
||||
volatile uint32_t DMA2D_CopyBufferEnd = 0;
|
||||
|
||||
JPEG_Data_BufferTypeDef Jpeg_OUT_BufferTab[NB_OUTPUT_DATA_BUFFERS] =
|
||||
{
|
||||
{JPEG_BUFFER_EMPTY, MCU_Data_OutBuffer0, 0, 0, NULL, false, false, 0, false},
|
||||
{JPEG_BUFFER_EMPTY, MCU_Data_OutBuffer1, 0, 0, NULL, false, false, 0, false},
|
||||
};
|
||||
|
||||
static struct JPEG_MCU_RGB_Converter
|
||||
{
|
||||
uint32_t WidthExtend;
|
||||
uint32_t ScaledWidth;
|
||||
uint32_t LastLineHeight;
|
||||
uint32_t MCU_pr_line;
|
||||
uint32_t bytes_pr_pixel;
|
||||
uint32_t startY;
|
||||
uint32_t endY;
|
||||
uint32_t startX;
|
||||
uint32_t endX;
|
||||
uint32_t MCUStart;
|
||||
uint32_t MCUEnd;
|
||||
uint32_t MCU_pr_job;
|
||||
uint32_t firstColOffset;
|
||||
uint32_t firstRowOffset;
|
||||
uint32_t lastColOffset;
|
||||
uint32_t lastRowOffset;
|
||||
} JPEG_ConvertorParams;
|
||||
|
||||
HardwareMJPEGDecoder::HardwareMJPEGDecoder()
|
||||
: frameNumber(0), currentMovieOffset(0), indexOffset(0), firstFrameOffset(0), lastFrameEnd(0), movieLength(0), movieData(0),
|
||||
reader(0), aviBuffer(0), aviBufferLength(0), aviBufferStartOffset(0), lastError(AVI_NO_ERROR)
|
||||
{
|
||||
/* Clear video info */
|
||||
videoInfo.frame_height = 0;
|
||||
videoInfo.frame_width = 0;
|
||||
videoInfo.ms_between_frames = 0;
|
||||
videoInfo.number_of_frames = 0;
|
||||
|
||||
/* Create decoding semaphore */
|
||||
semDecodingDone = SEM_CREATE();
|
||||
}
|
||||
|
||||
int HardwareMJPEGDecoder::compare(const uint32_t offset, const char* str, uint32_t num)
|
||||
{
|
||||
const char* src;
|
||||
if (reader != 0)
|
||||
{
|
||||
/* Assuming data is in buffer! */
|
||||
src = reinterpret_cast<const char*>(aviBuffer + (offset - aviBufferStartOffset));
|
||||
}
|
||||
else
|
||||
{
|
||||
src = (const char*)movieData + offset;
|
||||
}
|
||||
return strncmp(src, str, num);
|
||||
}
|
||||
|
||||
inline uint32_t HardwareMJPEGDecoder::getU32(const uint32_t offset)
|
||||
{
|
||||
if (reader != 0)
|
||||
{
|
||||
/* Assuming data is in buffer! */
|
||||
const uint32_t index = offset - aviBufferStartOffset;
|
||||
return aviBuffer[index + 0] | (aviBuffer[index + 1] << 8) | (aviBuffer[index + 2] << 16) | (aviBuffer[index + 3] << 24);
|
||||
}
|
||||
else
|
||||
{
|
||||
volatile const uint8_t* const d = movieData + offset;
|
||||
uint32_t val = 0U;
|
||||
val |= d[0];
|
||||
val |= d[1] << 8;
|
||||
val |= d[2] << 16;
|
||||
val |= d[3] << 24;
|
||||
return val;
|
||||
}
|
||||
}
|
||||
|
||||
inline uint32_t HardwareMJPEGDecoder::getU16(const uint32_t offset)
|
||||
{
|
||||
if (reader != 0)
|
||||
{
|
||||
/* Assuming data is in buffer! */
|
||||
const uint32_t index = offset - aviBufferStartOffset;
|
||||
return aviBuffer[index + 0] | (aviBuffer[index + 1] << 8);
|
||||
}
|
||||
else
|
||||
{
|
||||
volatile const uint8_t* const d = movieData + offset;
|
||||
uint32_t val = 0U;
|
||||
val |= d[0];
|
||||
val |= d[1] << 8;
|
||||
return val;
|
||||
}
|
||||
}
|
||||
|
||||
const uint8_t* HardwareMJPEGDecoder::readData(uint32_t offset, uint32_t length)
|
||||
{
|
||||
if (reader != 0)
|
||||
{
|
||||
if (length > aviBufferLength)
|
||||
{
|
||||
lastError = AVI_ERROR_FILE_BUFFER_TO_SMALL;
|
||||
assert(!"Buffer to small");
|
||||
}
|
||||
|
||||
reader->seek(offset);
|
||||
if (!reader->readData(aviBuffer, length))
|
||||
{
|
||||
lastError = AVI_ERROR_EOF_REACHED;
|
||||
}
|
||||
|
||||
aviBufferStartOffset = offset;
|
||||
return aviBuffer;
|
||||
}
|
||||
|
||||
return movieData + offset;
|
||||
}
|
||||
|
||||
bool HardwareMJPEGDecoder::decodeNextFrame(uint8_t* buffer, uint16_t buffer_width, uint16_t buffer_height, uint32_t buffer_stride)
|
||||
{
|
||||
assert((frameNumber > 0) && "HardwareMJPEGDecoder decoding without frame data!");
|
||||
|
||||
/* find next frame and decode it */
|
||||
readData(currentMovieOffset, 8);
|
||||
uint32_t streamNo = getU16(currentMovieOffset);
|
||||
uint32_t chunkType = getU16(currentMovieOffset + 2);
|
||||
uint32_t chunkSize = getU32(currentMovieOffset + 4);
|
||||
|
||||
const uint16_t STREAM0 = 0x3030;
|
||||
const uint16_t TYPEDC = 0x6364;
|
||||
|
||||
bool isCurrentFrameLast;
|
||||
/* play frame if we have it all */
|
||||
if (currentMovieOffset + 8 + chunkSize < movieLength)
|
||||
{
|
||||
if (streamNo == STREAM0 && chunkType == TYPEDC && chunkSize > 0)
|
||||
{
|
||||
currentMovieOffset += 8;
|
||||
/* decode frame */
|
||||
const uint8_t* chunk = readData(currentMovieOffset, chunkSize);
|
||||
decodeMJPEGFrame(chunk, chunkSize, buffer, buffer_width, buffer_height, buffer_stride);
|
||||
frameNumber++;
|
||||
}
|
||||
|
||||
isCurrentFrameLast = false;
|
||||
|
||||
/* Advance to next frame */
|
||||
currentMovieOffset += chunkSize;
|
||||
if (chunkSize == 0) /* Empty frame - Skip */
|
||||
{
|
||||
currentMovieOffset += 8;
|
||||
}
|
||||
currentMovieOffset = (currentMovieOffset + 1) & 0xFFFFFFFE; /* pad to next word */
|
||||
|
||||
if (currentMovieOffset == lastFrameEnd)
|
||||
{
|
||||
frameNumber = 1;
|
||||
currentMovieOffset = firstFrameOffset; /* start over */
|
||||
isCurrentFrameLast = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
frameNumber = 1;
|
||||
currentMovieOffset = firstFrameOffset; /* start over */
|
||||
isCurrentFrameLast = true;
|
||||
}
|
||||
return !isCurrentFrameLast;
|
||||
}
|
||||
|
||||
bool HardwareMJPEGDecoder::gotoNextFrame()
|
||||
{
|
||||
assert((frameNumber > 0) && "HardwareMJPEGDecoder decoding without frame data!");
|
||||
|
||||
readData(currentMovieOffset, 8);
|
||||
uint32_t chunkSize = getU32(currentMovieOffset + 4);
|
||||
|
||||
/* increment until next video frame */
|
||||
while (currentMovieOffset + 8 + chunkSize < movieLength)
|
||||
{
|
||||
/* increment one frame */
|
||||
currentMovieOffset += chunkSize + 8;
|
||||
currentMovieOffset = (currentMovieOffset + 1) & 0xFFFFFFFE; /* pad to next word */
|
||||
frameNumber++;
|
||||
|
||||
/* next chunk */
|
||||
readData(currentMovieOffset, 8);
|
||||
/* check it is a video frame */
|
||||
uint32_t streamNo = getU16(currentMovieOffset);
|
||||
uint32_t chunkType = getU16(currentMovieOffset + 2);
|
||||
chunkSize = getU32(currentMovieOffset + 4);
|
||||
const uint16_t STREAM0 = 0x3030;
|
||||
const uint16_t TYPEDC = 0x6364;
|
||||
|
||||
if (streamNo == STREAM0 && chunkType == TYPEDC)
|
||||
{
|
||||
/* Found next frame */
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/* skip back to first frame */
|
||||
frameNumber = 1;
|
||||
currentMovieOffset = firstFrameOffset; /* start over */
|
||||
return false;
|
||||
}
|
||||
|
||||
void HardwareMJPEGDecoder::setVideoData(const uint8_t* movie, const uint32_t length)
|
||||
{
|
||||
movieData = movie;
|
||||
movieLength = length;
|
||||
reader = 0; /* not using reader */
|
||||
|
||||
readVideoHeader();
|
||||
}
|
||||
|
||||
void HardwareMJPEGDecoder::setVideoData(touchgfx::VideoDataReader& reader)
|
||||
{
|
||||
this->reader = &reader;
|
||||
movieData = 0;
|
||||
movieLength = reader.getDataLength();
|
||||
|
||||
readVideoHeader();
|
||||
}
|
||||
|
||||
bool HardwareMJPEGDecoder::hasVideo()
|
||||
{
|
||||
return (reader != 0) || (movieData != 0);
|
||||
}
|
||||
|
||||
void HardwareMJPEGDecoder::readVideoHeader()
|
||||
{
|
||||
/* Start from the start */
|
||||
currentMovieOffset = 0;
|
||||
lastError = AVI_NO_ERROR;
|
||||
|
||||
/* Make header available in buffer */
|
||||
readData(0, 72);
|
||||
|
||||
/* Decode the movie header to find first frame */
|
||||
/* Must be RIFF file */
|
||||
if (compare(currentMovieOffset, "RIFF", 4))
|
||||
{
|
||||
lastError = AVI_ERROR_NOT_RIFF;
|
||||
assert(!"RIFF header not found");
|
||||
}
|
||||
|
||||
/* skip fourcc and length */
|
||||
currentMovieOffset += 8;
|
||||
if (compare(currentMovieOffset, "AVI ", 4))
|
||||
{
|
||||
lastError = AVI_ERROR_AVI_HEADER_NOT_FOUND;
|
||||
assert(!"AVI header not found");
|
||||
}
|
||||
|
||||
currentMovieOffset += 4;
|
||||
if (compare(currentMovieOffset, "LIST", 4))
|
||||
{
|
||||
lastError = AVI_ERROR_AVI_LIST_NOT_FOUND;
|
||||
assert(!"AVI LIST not found");
|
||||
}
|
||||
|
||||
/* save AVI List info */
|
||||
const uint32_t aviListSize = getU32(currentMovieOffset + 4);
|
||||
const uint32_t aviListOffset = currentMovieOffset;
|
||||
assert(aviListSize);
|
||||
|
||||
/* look into header to find frame rate */
|
||||
bool foundFrame = true;
|
||||
uint32_t offset = currentMovieOffset + 8;
|
||||
if (compare(offset, "hdrl", 4))
|
||||
{
|
||||
lastError = AVI_ERROR_AVI_HDRL_NOT_FOUND;
|
||||
foundFrame = false;
|
||||
}
|
||||
|
||||
offset += 4;
|
||||
if (compare(offset, "avih", 4))
|
||||
{
|
||||
lastError = AVI_ERROR_AVI_AVIH_NOT_FOUND;
|
||||
foundFrame = false;
|
||||
}
|
||||
|
||||
if (foundFrame)
|
||||
{
|
||||
offset += 8; /* skip fourcc and cb in AVIMAINHEADER */
|
||||
videoInfo.ms_between_frames = getU32(offset) / 1000;
|
||||
videoInfo.number_of_frames = getU32(offset + 16);
|
||||
videoInfo.frame_width = getU32(offset + 32);
|
||||
videoInfo.frame_height = getU32(offset + 36);
|
||||
}
|
||||
/* skip rest of AVI header, start from end of AVI List */
|
||||
|
||||
/* look for list with 'movi' header */
|
||||
uint32_t listOffset = aviListOffset + aviListSize + 8;
|
||||
readData(listOffset, 12);
|
||||
while (compare(listOffset + 8, "movi", 4) && (lastError == AVI_NO_ERROR) && listOffset < movieLength)
|
||||
{
|
||||
const uint32_t listSize = getU32(listOffset + 4) + 8;
|
||||
listOffset += listSize;
|
||||
readData(listOffset, 12);
|
||||
}
|
||||
|
||||
if (lastError != AVI_NO_ERROR)
|
||||
{
|
||||
lastError = AVI_ERROR_MOVI_NOT_FOUND;
|
||||
return;
|
||||
}
|
||||
|
||||
/* save first frame and end of last frame */
|
||||
currentMovieOffset = listOffset + 8 + 4; /* skip LIST and 'movi' */
|
||||
lastFrameEnd = listOffset + 8 + getU32(listOffset + 4);
|
||||
|
||||
/* find idx */
|
||||
const uint32_t listSize = getU32(listOffset + 4) + 8;
|
||||
listOffset += listSize;
|
||||
readData(listOffset, 4);
|
||||
if (!compare(listOffset, "idx1", 4))
|
||||
{
|
||||
indexOffset = listOffset;
|
||||
}
|
||||
else
|
||||
{
|
||||
lastError = AVI_ERROR_IDX1_NOT_FOUND;
|
||||
return;
|
||||
}
|
||||
|
||||
/* start on first frame */
|
||||
frameNumber = 1; /* next frame number is 1 */
|
||||
firstFrameOffset = currentMovieOffset;
|
||||
}
|
||||
|
||||
void HardwareMJPEGDecoder::decodeMJPEGFrame(const uint8_t* const mjpgdata, const uint32_t length, uint8_t* outputBuffer, uint16_t bufferWidth, uint16_t bufferHeight, uint32_t bufferStride)
|
||||
{
|
||||
if (length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (outputBuffer) /* only decode if buffers are assigned. */
|
||||
{
|
||||
/* Update JPEG conversion parameters */
|
||||
JPEG_ConvertorParams.bytes_pr_pixel = 2;
|
||||
JPEG_ConvertorParams.WidthExtend = videoInfo.frame_width;
|
||||
if ((JPEG_ConvertorParams.WidthExtend % 16) != 0)
|
||||
{
|
||||
JPEG_ConvertorParams.WidthExtend += 16 - (JPEG_ConvertorParams.WidthExtend % 16);
|
||||
}
|
||||
JPEG_ConvertorParams.ScaledWidth = 800 * JPEG_ConvertorParams.bytes_pr_pixel;
|
||||
JPEG_ConvertorParams.MCU_pr_line = JPEG_ConvertorParams.WidthExtend / MCU_WIDTH_PIXELS;
|
||||
JPEG_ConvertorParams.LastLineHeight = (videoInfo.frame_height % MCU_HEIGHT_PIXELS) == 0 ? 0 : MCU_HEIGHT_PIXELS - (videoInfo.frame_height % MCU_HEIGHT_PIXELS);
|
||||
|
||||
FrameBufferWidth = bufferStride / 2;
|
||||
FrameBufferAddress = outputBuffer;
|
||||
|
||||
JPEG_Decode_DMA(&hjpeg, const_cast<uint8_t*>(mjpgdata), length, outputBuffer);
|
||||
DMA2D_reference = dma;
|
||||
do
|
||||
{
|
||||
JpegProcessing_End = JPEG_OutputHandler(&hjpeg);
|
||||
|
||||
/* If nothing to do, allow other tasks */
|
||||
if (JpegProcessing_End == 2)
|
||||
{
|
||||
SEM_WAIT(semDecodingDone);
|
||||
}
|
||||
} while (JpegProcessing_End != 1);
|
||||
|
||||
/* reset flag */
|
||||
Jpeg_HWDecodingEnd = 0;
|
||||
DMA2D_CopyBufferEnd = 0;
|
||||
}
|
||||
}
|
||||
|
||||
bool HardwareMJPEGDecoder::decodeFrame(const touchgfx::Rect& area, uint8_t* frameBuffer, uint32_t framebuffer_width)
|
||||
{
|
||||
/* Assuming that chunk is available and streamNo and chunkType is correct. */
|
||||
/* Check by gotoNextFrame */
|
||||
readData(currentMovieOffset, 8);
|
||||
const uint32_t length = getU32(currentMovieOffset + 4);
|
||||
|
||||
/* Ensure whole frame is read */
|
||||
const uint8_t* mjpgdata = readData(currentMovieOffset + 8, length);
|
||||
|
||||
/* Update JPEG conversion parameters */
|
||||
JPEG_ConvertorParams.bytes_pr_pixel = 2;
|
||||
JPEG_ConvertorParams.WidthExtend = videoInfo.frame_width;
|
||||
if ((JPEG_ConvertorParams.WidthExtend % 16) != 0)
|
||||
{
|
||||
JPEG_ConvertorParams.WidthExtend += 16 - (JPEG_ConvertorParams.WidthExtend % 16);
|
||||
}
|
||||
JPEG_ConvertorParams.ScaledWidth = 800 * JPEG_ConvertorParams.bytes_pr_pixel;
|
||||
JPEG_ConvertorParams.MCU_pr_line = JPEG_ConvertorParams.WidthExtend / MCU_WIDTH_PIXELS;
|
||||
JPEG_ConvertorParams.startY = area.y;
|
||||
JPEG_ConvertorParams.endY = MIN((uint32_t)area.bottom(), videoInfo.frame_height);
|
||||
JPEG_ConvertorParams.startX = area.x;
|
||||
JPEG_ConvertorParams.endX = MIN((uint32_t)area.right(), videoInfo.frame_width);
|
||||
JPEG_ConvertorParams.MCUStart = JPEG_ConvertorParams.startX / MCU_WIDTH_PIXELS;
|
||||
JPEG_ConvertorParams.MCUEnd = (JPEG_ConvertorParams.endX + MCU_WIDTH_PIXELS - 1) / MCU_WIDTH_PIXELS; // Ceil division
|
||||
JPEG_ConvertorParams.MCU_pr_job = JPEG_ConvertorParams.MCUEnd - JPEG_ConvertorParams.MCUStart;
|
||||
JPEG_ConvertorParams.firstColOffset = JPEG_ConvertorParams.startX % MCU_WIDTH_PIXELS;
|
||||
JPEG_ConvertorParams.firstRowOffset = JPEG_ConvertorParams.startY % MCU_HEIGHT_PIXELS;
|
||||
JPEG_ConvertorParams.lastColOffset = (JPEG_ConvertorParams.endX % MCU_WIDTH_PIXELS) == 0 ? 0 : MCU_WIDTH_PIXELS - (JPEG_ConvertorParams.endX % MCU_WIDTH_PIXELS);
|
||||
JPEG_ConvertorParams.lastRowOffset = (JPEG_ConvertorParams.endY % MCU_HEIGHT_PIXELS) == 0 ? 0 : MCU_HEIGHT_PIXELS - (JPEG_ConvertorParams.endY % MCU_HEIGHT_PIXELS);
|
||||
|
||||
FrameBufferWidth = framebuffer_width;
|
||||
FrameBufferAddress = frameBuffer;
|
||||
|
||||
JPEG_Decode_DMA(&hjpeg, const_cast<uint8_t*>(mjpgdata), length, frameBuffer);
|
||||
DMA2D_reference = dma;
|
||||
do
|
||||
{
|
||||
JpegProcessing_End = JPEG_OutputHandler(&hjpeg);
|
||||
|
||||
/* If nothing to do, wait */
|
||||
if (JpegProcessing_End == 2)
|
||||
{
|
||||
SEM_WAIT(semDecodingDone);
|
||||
}
|
||||
} while (JpegProcessing_End != 1);
|
||||
|
||||
/* reset flag */
|
||||
Jpeg_HWDecodingEnd = 0;
|
||||
DMA2D_CopyBufferEnd = 0;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HardwareMJPEGDecoder::decodeThumbnail(uint32_t frameno, uint8_t* buffer, uint16_t width, uint16_t height)
|
||||
{
|
||||
assert(0);
|
||||
return false;
|
||||
}
|
||||
|
||||
void HardwareMJPEGDecoder::gotoFrame(uint32_t frameNumber)
|
||||
{
|
||||
if (frameNumber == 0)
|
||||
{
|
||||
frameNumber = 1;
|
||||
}
|
||||
|
||||
if (frameNumber > getNumberOfFrames())
|
||||
{
|
||||
frameNumber = getNumberOfFrames();
|
||||
}
|
||||
|
||||
uint32_t offset = indexOffset + 8 + (frameNumber - 1) * 16;
|
||||
|
||||
readData(offset, 16);
|
||||
|
||||
currentMovieOffset = getU32(offset + 8) + firstFrameOffset - 4;
|
||||
this->frameNumber = frameNumber;
|
||||
}
|
||||
|
||||
uint32_t HardwareMJPEGDecoder::getNumberOfFrames()
|
||||
{
|
||||
return videoInfo.number_of_frames;
|
||||
}
|
||||
|
||||
void HardwareMJPEGDecoder::setRepeatVideo(bool repeat)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
void HardwareMJPEGDecoder::getVideoInfo(touchgfx::VideoInformation* data)
|
||||
{
|
||||
*data = videoInfo;
|
||||
}
|
||||
|
||||
/* C HELPER FUNCTIONS */
|
||||
|
||||
extern "C"
|
||||
{
|
||||
/**
|
||||
* @brief Decode_DMA
|
||||
* @param hjpeg: JPEG handle pointer
|
||||
* @param JPEGImageBufferAddress : jpg image buffer Address.
|
||||
* @param JPEGImageSize_Bytes : jpg image size in bytes.
|
||||
* @param DestAddress : ARGB8888 destination Frame Buffer Address.
|
||||
* @retval None
|
||||
*/
|
||||
uint32_t JPEG_Decode_DMA(JPEG_HandleTypeDef* hjpeg, uint8_t* input, uint32_t chunkSizeIn /* length */, uint8_t* output)
|
||||
{
|
||||
JPEG_output_is_paused = 0;
|
||||
JPEG_OUT_Read_BufferIndex = 0;
|
||||
JPEG_OUT_Write_BufferIndex = 0;
|
||||
JPEG_InputImageIndex = 0;
|
||||
JPEG_InputImageAddress = (uint32_t)input;
|
||||
JPEG_InputImageSize_Bytes = chunkSizeIn;
|
||||
MCU_BlockIndex = 0;
|
||||
line_count = 0;
|
||||
|
||||
/* Init buffers */
|
||||
for (uint32_t i = 0; i < NB_OUTPUT_DATA_BUFFERS; ++i)
|
||||
{
|
||||
Jpeg_OUT_BufferTab[i].State = JPEG_BUFFER_EMPTY;
|
||||
Jpeg_OUT_BufferTab[i].DataBufferSize = 0;
|
||||
Jpeg_OUT_BufferTab[i].MCU_index = 0;
|
||||
Jpeg_OUT_BufferTab[i].OutputBuffer = NULL;
|
||||
Jpeg_OUT_BufferTab[i].MCU_drawn = 0;
|
||||
Jpeg_OUT_BufferTab[i].DoCropping = false;
|
||||
Jpeg_OUT_BufferTab[i].FirstJob = false;
|
||||
Jpeg_OUT_BufferTab[i].LastJob = false;
|
||||
}
|
||||
Jpeg_OUT_BufferTab[0].FirstJob = true;
|
||||
if (JPEG_ConvertorParams.firstRowOffset != 0)
|
||||
{
|
||||
Jpeg_OUT_BufferTab[0].DoCropping = true;
|
||||
}
|
||||
|
||||
/* Do not return from this function until done with decoding all chunks. */
|
||||
HAL_JPEG_Decode_DMA(hjpeg, input, CHUNK_SIZE_IN, Jpeg_OUT_BufferTab[JPEG_OUT_Write_BufferIndex].DataBuffer, MCU_CHROMA_420_SIZE_BYTES * JPEG_ConvertorParams.MCU_pr_line);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief JPEG Info ready callback
|
||||
* @param hjpeg: JPEG handle pointer
|
||||
* @param pInfo: JPEG Info Struct pointer
|
||||
* @retval None
|
||||
*/
|
||||
void HAL_JPEG_InfoReadyCallback(JPEG_HandleTypeDef* hjpeg, JPEG_ConfTypeDef* pInfo)
|
||||
{
|
||||
uint32_t hMCU, vMCU;
|
||||
|
||||
if (pInfo->ChromaSubsampling == JPEG_420_SUBSAMPLING)
|
||||
{
|
||||
if ((pInfo->ImageWidth % 16) != 0)
|
||||
{
|
||||
pInfo->ImageWidth += (16 - (pInfo->ImageWidth % 16));
|
||||
}
|
||||
|
||||
if ((pInfo->ImageHeight % 16) != 0)
|
||||
{
|
||||
pInfo->ImageHeight += (16 - (pInfo->ImageHeight % 16));
|
||||
}
|
||||
|
||||
hMCU = (pInfo->ImageWidth / MCU_WIDTH_PIXELS);
|
||||
vMCU = (pInfo->ImageHeight / MCU_HEIGHT_PIXELS);
|
||||
MCU_TotalNb = (hMCU * vMCU);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (pInfo->ChromaSubsampling == JPEG_422_SUBSAMPLING)
|
||||
{
|
||||
if ((pInfo->ImageWidth % 16) != 0)
|
||||
{
|
||||
pInfo->ImageWidth += (16 - (pInfo->ImageWidth % 16));
|
||||
}
|
||||
|
||||
if ((pInfo->ImageHeight % 8) != 0)
|
||||
{
|
||||
pInfo->ImageHeight += (8 - (pInfo->ImageHeight % 8));
|
||||
}
|
||||
}
|
||||
|
||||
if (pInfo->ChromaSubsampling == JPEG_444_SUBSAMPLING)
|
||||
{
|
||||
if ((pInfo->ImageWidth % 8) != 0)
|
||||
{
|
||||
pInfo->ImageWidth += (8 - (pInfo->ImageWidth % 8));
|
||||
}
|
||||
|
||||
if ((pInfo->ImageHeight % 8) != 0)
|
||||
{
|
||||
pInfo->ImageHeight += (8 - (pInfo->ImageHeight % 8));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief JPEG Get Data callback.
|
||||
* @param hjpeg: JPEG handle pointer
|
||||
* @param NbDecodedData: Number of decoded (consummed) bytes from input buffer
|
||||
* @retval None
|
||||
*/
|
||||
void HAL_JPEG_GetDataCallback(JPEG_HandleTypeDef* hjpeg, uint32_t NbDecodedData)
|
||||
{
|
||||
/* Input buffer has been consumed by the peripheral and to ask for a new data chunk if the operation (encoding/decoding) has not been complete yet. */
|
||||
JPEG_InputImageIndex += NbDecodedData;
|
||||
if (JPEG_InputImageIndex < JPEG_InputImageSize_Bytes)
|
||||
{
|
||||
JPEG_InputImageAddress = JPEG_InputImageAddress + NbDecodedData;
|
||||
uint32_t inDataLength = JPEG_InputImageSize_Bytes - JPEG_InputImageIndex;
|
||||
HAL_JPEG_ConfigInputBuffer(hjpeg, (uint8_t*)JPEG_InputImageAddress, inDataLength >= CHUNK_SIZE_IN ? CHUNK_SIZE_IN : inDataLength);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief JPEG Data Ready callback. Data has been converted from JPEG to YCbCr.
|
||||
* @param hjpeg: JPEG handle pointer
|
||||
* @param pDataOut: pointer to the output data buffer
|
||||
* @param OutDataLength: length of output buffer in bytes
|
||||
* @retval None
|
||||
*/
|
||||
void HAL_JPEG_DataReadyCallback(JPEG_HandleTypeDef* hjpeg, uint8_t* pDataOut, uint32_t OutDataLength)
|
||||
{
|
||||
line_count += MCU_HEIGHT_PIXELS;
|
||||
|
||||
Jpeg_OUT_BufferTab[JPEG_OUT_Write_BufferIndex].OutputBuffer = FrameBufferAddress;
|
||||
|
||||
/* Increment framebuffer */
|
||||
FrameBufferAddress += FrameBufferWidth * MCU_HEIGHT_PIXELS * JPEG_ConvertorParams.bytes_pr_pixel;
|
||||
|
||||
/* Decode until we reach area to draw */
|
||||
if (line_count <= JPEG_ConvertorParams.startY)
|
||||
{
|
||||
HAL_JPEG_ConfigOutputBuffer(hjpeg, Jpeg_OUT_BufferTab[JPEG_OUT_Write_BufferIndex].DataBuffer, MCU_CHROMA_420_SIZE_BYTES * JPEG_ConvertorParams.MCU_pr_line);
|
||||
return;
|
||||
}
|
||||
|
||||
Jpeg_OUT_BufferTab[JPEG_OUT_Write_BufferIndex].State = JPEG_BUFFER_FULL;
|
||||
Jpeg_OUT_BufferTab[JPEG_OUT_Write_BufferIndex].DataBufferSize = OutDataLength;
|
||||
Jpeg_OUT_BufferTab[JPEG_OUT_Write_BufferIndex].MCU_drawn = 0;
|
||||
|
||||
/* Left column requires cropping */
|
||||
if (JPEG_ConvertorParams.firstColOffset != 0)
|
||||
{
|
||||
Jpeg_OUT_BufferTab[JPEG_OUT_Write_BufferIndex].DoCropping = true;
|
||||
}
|
||||
|
||||
if (line_count < JPEG_ConvertorParams.endY)
|
||||
{
|
||||
Jpeg_OUT_BufferTab[JPEG_OUT_Write_BufferIndex].LastJob = false;
|
||||
|
||||
JPEG_OUT_Write_BufferIndex++;
|
||||
if (JPEG_OUT_Write_BufferIndex >= NB_OUTPUT_DATA_BUFFERS)
|
||||
{
|
||||
JPEG_OUT_Write_BufferIndex = 0;
|
||||
}
|
||||
|
||||
/* if the other buffer is full, then ui thread might be converting it */
|
||||
if (Jpeg_OUT_BufferTab[JPEG_OUT_Write_BufferIndex].State != JPEG_BUFFER_EMPTY)
|
||||
{
|
||||
HAL_JPEG_Pause(hjpeg, JPEG_PAUSE_RESUME_OUTPUT);
|
||||
JPEG_output_is_paused = 1;
|
||||
}
|
||||
|
||||
HAL_JPEG_ConfigOutputBuffer(hjpeg, Jpeg_OUT_BufferTab[JPEG_OUT_Write_BufferIndex].DataBuffer, MCU_CHROMA_420_SIZE_BYTES * JPEG_ConvertorParams.MCU_pr_line);
|
||||
}
|
||||
|
||||
/* Stop decoding when we exit area to draw */
|
||||
if (line_count >= JPEG_ConvertorParams.endY)
|
||||
{
|
||||
Jpeg_OUT_BufferTab[JPEG_OUT_Write_BufferIndex].LastJob = true;
|
||||
Jpeg_HWDecodingEnd = 1;
|
||||
|
||||
HAL_JPEG_Pause(hjpeg, JPEG_PAUSE_RESUME_OUTPUT);
|
||||
}
|
||||
|
||||
/* Signal Hardware Decoding to wake up */
|
||||
if (!DMA2D_reference->isDMARunning())
|
||||
{
|
||||
SEM_POST(semDecodingDone);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief JPEG Error callback
|
||||
* @param hjpeg: JPEG handle pointer
|
||||
* @retval None
|
||||
*/
|
||||
void HAL_JPEG_ErrorCallback(JPEG_HandleTypeDef* hjpeg)
|
||||
{
|
||||
__disable_irq();
|
||||
while (1)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief JPEG Decode complete callback
|
||||
* @param hjpeg: JPEG handle pointer
|
||||
* @retval None
|
||||
*/
|
||||
void HAL_JPEG_DecodeCpltCallback(JPEG_HandleTypeDef* hjpeg)
|
||||
{
|
||||
Jpeg_HWDecodingEnd = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief JPEG Ouput Data BackGround Postprocessing .
|
||||
* @param hjpeg: JPEG handle pointer
|
||||
* @retval 1 : if JPEG processing has finished, 0 : if JPEG processing still ongoing
|
||||
*/
|
||||
uint32_t JPEG_OutputHandler(JPEG_HandleTypeDef* hjpeg)
|
||||
{
|
||||
/* Decode frame complete */
|
||||
if (Jpeg_HWDecodingEnd && DMA2D_CopyBufferEnd)
|
||||
{
|
||||
/* Abort any ongoing operations */
|
||||
if (HAL_JPEG_GetState(hjpeg) == HAL_JPEG_STATE_BUSY_DECODING)
|
||||
{
|
||||
HAL_JPEG_Abort(hjpeg);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Try to start DMA2D video transfer if next buffer if full */
|
||||
if (!DMA2D_reference->isDMARunning() && (Jpeg_OUT_BufferTab[JPEG_OUT_Read_BufferIndex].State == JPEG_BUFFER_FULL) && (DMA2D_CopyBufferEnd == 0))
|
||||
{
|
||||
DMA2D_reference->start();
|
||||
}
|
||||
|
||||
/* Start JPEG IP if paused and next buffer is empty */
|
||||
if ((JPEG_output_is_paused == 1) && (Jpeg_OUT_BufferTab[JPEG_OUT_Write_BufferIndex].State == JPEG_BUFFER_EMPTY) && (Jpeg_HWDecodingEnd == 0))
|
||||
{
|
||||
JPEG_output_is_paused = 0;
|
||||
HAL_JPEG_Resume(hjpeg, JPEG_PAUSE_RESUME_OUTPUT);
|
||||
}
|
||||
|
||||
return 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Configures external DMA2D job to copy YCbCr data to RGB buffer(s)
|
||||
* @param job: External job reference
|
||||
* @retval None
|
||||
*/
|
||||
void DMA2D_CopyBuffer(JPEG_Data_BufferTypeDef& job)
|
||||
{
|
||||
const uint32_t width = JPEG_ConvertorParams.MCU_pr_job * MCU_WIDTH_PIXELS - job.MCU_drawn * MCU_WIDTH_PIXELS - JPEG_ConvertorParams.lastColOffset;
|
||||
const uint32_t scaledWidth = (width % MCU_WIDTH_PIXELS) == 0 ? 0 : MCU_WIDTH_PIXELS - (width % MCU_WIDTH_PIXELS);
|
||||
const uint32_t srcOffset = (JPEG_ConvertorParams.MCUStart + job.MCU_drawn) * MCU_CHROMA_420_SIZE_BYTES;
|
||||
const uint32_t dstOffset = JPEG_ConvertorParams.MCUStart * MCU_WIDTH_PIXELS * JPEG_ConvertorParams.bytes_pr_pixel
|
||||
+ job.MCU_drawn * MCU_WIDTH_PIXELS * JPEG_ConvertorParams.bytes_pr_pixel;
|
||||
|
||||
/* Mark job as fully drawn */
|
||||
job.MCU_drawn = JPEG_ConvertorParams.MCU_pr_job;
|
||||
|
||||
/* DMA2D OPFCCR register configuration */
|
||||
WRITE_REG(DMA2D->OPFCCR, DMA2D_OUTPUT_RGB565);
|
||||
|
||||
/* Configure DMA2D data size */
|
||||
if (job.LastJob) /* Last line of frame */
|
||||
{
|
||||
WRITE_REG(DMA2D->NLR, (MCU_HEIGHT_PIXELS - JPEG_ConvertorParams.lastRowOffset) | (width << DMA2D_NLR_PL_Pos));
|
||||
}
|
||||
else
|
||||
{
|
||||
WRITE_REG(DMA2D->NLR, MCU_HEIGHT_PIXELS | (width << DMA2D_NLR_PL_Pos));
|
||||
}
|
||||
|
||||
/* Configure DMA2D destination address */
|
||||
WRITE_REG(DMA2D->OMAR, reinterpret_cast<uint32_t>(job.OutputBuffer + dstOffset));
|
||||
|
||||
/* DMA2D OOR register configuration */
|
||||
WRITE_REG(DMA2D->OOR, FrameBufferWidth - width);
|
||||
|
||||
/* DMA2D FGOR register configuration */
|
||||
WRITE_REG(DMA2D->FGOR, scaledWidth);
|
||||
|
||||
/* DMA2D FGPFCCR register configuration */
|
||||
WRITE_REG(DMA2D->FGPFCCR, DMA2D_INPUT_YCBCR | (DMA2D_CSS_420 << DMA2D_FGPFCCR_CSS_Pos) | (DMA2D_REPLACE_ALPHA << DMA2D_FGPFCCR_AM_Pos) | (0xFFU << DMA2D_FGPFCCR_ALPHA_Pos));
|
||||
|
||||
/* Configure DMA2D source address */
|
||||
WRITE_REG(DMA2D->FGMAR, reinterpret_cast<uint32_t>(job.DataBuffer + srcOffset));
|
||||
|
||||
/* Configure DMA2D contol register */
|
||||
WRITE_REG(DMA2D->CR, DMA2D_M2M_PFC | DMA2D_IT_TC | DMA2D_CR_START | DMA2D_IT_CE | DMA2D_IT_TE);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Configures external DMA2D job to copy and crop YCbCr data to an RGB cropping buffer
|
||||
* @param job: External job reference
|
||||
* @retval None
|
||||
*/
|
||||
void DMA2D_CropBuffer(JPEG_Data_BufferTypeDef& job)
|
||||
{
|
||||
const uint32_t colLeftOffset = job.MCU_drawn == 0 ? JPEG_ConvertorParams.firstColOffset : 0;
|
||||
const uint32_t colRightOffset = job.MCU_drawn == JPEG_ConvertorParams.MCU_pr_job - 1 ? JPEG_ConvertorParams.lastColOffset : 0;
|
||||
const uint32_t rowTopOffset = job.FirstJob ? JPEG_ConvertorParams.firstRowOffset : 0;
|
||||
const uint32_t rowBottomOffset = job.LastJob ? JPEG_ConvertorParams.lastRowOffset : 0;
|
||||
const uint32_t srcOffset = (JPEG_ConvertorParams.MCUStart + job.MCU_drawn) * MCU_CHROMA_420_SIZE_BYTES;
|
||||
const uint32_t dstOffset = JPEG_ConvertorParams.MCUStart * MCU_WIDTH_PIXELS * JPEG_ConvertorParams.bytes_pr_pixel
|
||||
+ job.MCU_drawn * MCU_WIDTH_PIXELS * JPEG_ConvertorParams.bytes_pr_pixel
|
||||
+ rowTopOffset * JPEG_ConvertorParams.bytes_pr_pixel * FrameBufferWidth
|
||||
+ colLeftOffset * JPEG_ConvertorParams.bytes_pr_pixel;
|
||||
const uint32_t cropSrcOffset = colLeftOffset * JPEG_ConvertorParams.bytes_pr_pixel
|
||||
+ rowTopOffset * JPEG_ConvertorParams.bytes_pr_pixel * MCU_HEIGHT_PIXELS;
|
||||
|
||||
/* Update job and assert if more cropping is needed */
|
||||
job.MCU_drawn++;
|
||||
if ((JPEG_ConvertorParams.firstRowOffset == 0) || !job.FirstJob)
|
||||
{
|
||||
job.DoCropping = false;
|
||||
}
|
||||
|
||||
/* Configure BlitOp */
|
||||
touchgfx::BlitOp blitOp;
|
||||
blitOp.operation = touchgfx::BLIT_OP_COPY;
|
||||
blitOp.pSrc = reinterpret_cast<uint16_t*>(MCU_Cropping_Buffer + cropSrcOffset);
|
||||
blitOp.nSteps = MCU_WIDTH_PIXELS - colLeftOffset - colRightOffset;
|
||||
blitOp.nLoops = MCU_HEIGHT_PIXELS - rowTopOffset - rowBottomOffset;
|
||||
blitOp.srcLoopStride = MCU_WIDTH_PIXELS;
|
||||
blitOp.dstLoopStride = FrameBufferWidth;
|
||||
blitOp.pDst = reinterpret_cast<uint16_t*>(job.OutputBuffer + dstOffset);
|
||||
blitOp.srcFormat = touchgfx::Bitmap::RGB565;
|
||||
blitOp.dstFormat = touchgfx::Bitmap::RGB565;
|
||||
DMA2D_reference->addToQueue(blitOp);
|
||||
|
||||
/* DMA2D OPFCCR register configuration */
|
||||
WRITE_REG(DMA2D->OPFCCR, DMA2D_OUTPUT_RGB565);
|
||||
|
||||
/* Configure DMA2D data size */
|
||||
WRITE_REG(DMA2D->NLR, MCU_HEIGHT_PIXELS | (MCU_WIDTH_PIXELS << DMA2D_NLR_PL_Pos));
|
||||
|
||||
/* Configure DMA2D destination address */
|
||||
WRITE_REG(DMA2D->OMAR, reinterpret_cast<uint32_t>(MCU_Cropping_Buffer));
|
||||
|
||||
/* DMA2D OOR register configuration */
|
||||
WRITE_REG(DMA2D->OOR, 0);
|
||||
|
||||
/* DMA2D FGOR register configuration */
|
||||
WRITE_REG(DMA2D->FGOR, 0);
|
||||
|
||||
/* DMA2D FGPFCCR register configuration */
|
||||
WRITE_REG(DMA2D->FGPFCCR, DMA2D_INPUT_YCBCR | (DMA2D_CSS_420 << DMA2D_FGPFCCR_CSS_Pos) | (DMA2D_REPLACE_ALPHA << DMA2D_FGPFCCR_AM_Pos) | (0xFFU << DMA2D_FGPFCCR_ALPHA_Pos));
|
||||
|
||||
/* Configure DMA2D source address */
|
||||
WRITE_REG(DMA2D->FGMAR, reinterpret_cast<uint32_t>(job.DataBuffer + srcOffset));
|
||||
|
||||
/* Configure DMA2D contol register */
|
||||
WRITE_REG(DMA2D->CR, DMA2D_M2M_PFC | DMA2D_IT_TC | DMA2D_CR_START | DMA2D_IT_CE | DMA2D_IT_TE);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief External DMA2D job complete callback
|
||||
* @param job: External job reference
|
||||
* @retval None
|
||||
*/
|
||||
void DMA2D_ExternalJobCompleted(JPEG_Data_BufferTypeDef& job)
|
||||
{
|
||||
/* Mark job done if all MCUs are drawn */
|
||||
if (job.MCU_drawn == JPEG_ConvertorParams.MCU_pr_job)
|
||||
{
|
||||
job.State = JPEG_BUFFER_EMPTY;
|
||||
job.DataBufferSize = 0;
|
||||
job.DoCropping = false;
|
||||
job.FirstJob = false;
|
||||
|
||||
JPEG_OUT_Read_BufferIndex++;
|
||||
if (JPEG_OUT_Read_BufferIndex >= NB_OUTPUT_DATA_BUFFERS)
|
||||
{
|
||||
JPEG_OUT_Read_BufferIndex = 0;
|
||||
}
|
||||
|
||||
/* Check if last line */
|
||||
if (job.LastJob)
|
||||
{
|
||||
DMA2D_CopyBufferEnd = 1;
|
||||
}
|
||||
|
||||
/* Signal decoder thread to wake up and continue decoding */
|
||||
SEM_POST(semDecodingDone);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user