Skip to content

Commit 21872fd

Browse files
authored
Add color correction and dvr-reencode (#115)
* drm colortrans * upside down * correct stranger things mode * add librga dependency * fix wrong color correct * drop cpu colortrans * fix second recording start * drop LutCache * fix h265 * add frame pacer * cleanup * rec colortrans * record osd * fix res change segfault * gsmenu integration * readme update * fix actions * fix pipeline 2 * fix wrong framerate while toggleing re-enocdeing * fix dvr in mixed code scenarios * fix reenc jitter * debug log frame pacer grace period * strict encoding modes * rework dvr options * add dvr split * change dvr_reenc_bitrate defaults
1 parent cc8c4fd commit 21872fd

24 files changed

Lines changed: 3379 additions & 118 deletions

CMakeLists.txt

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,8 +146,15 @@ set(LIB_SOURCE_FILES
146146
src/osd.cpp
147147
src/os_mon.hpp
148148
src/os_mon.cpp
149+
src/osd_gl.cpp
149150
src/dvr.h
150151
src/dvr.cpp
152+
src/mpp_encoder.h
153+
src/mpp_encoder.cpp
154+
src/frame_processor.h
155+
src/frame_processor.cpp
156+
src/frame_colorcorrect.h
157+
src/frame_colorcorrect.cpp
151158
src/mavlink.h
152159
src/mavlink.c
153160
src/wfbcli.hpp
@@ -177,6 +184,9 @@ else()
177184
find_package(yaml-cpp REQUIRED)
178185
pkg_check_modules(LIBGPIOD REQUIRED libgpiod)
179186
include_directories(${LIBGPIOD_INCLUDE_DIR})
187+
188+
find_path(RGA_INCLUDE_DIRS NAMES rga.h rga/rga.h)
189+
find_library(RGA_LIBRARIES NAMES rga librga)
180190

181191
# GStreamer setup
182192
find_package(PkgConfig REQUIRED)
@@ -196,6 +206,7 @@ else()
196206
${PROJECT_SOURCE_DIR}
197207
${PROJECT_SOURCE_DIR}/lvgl
198208
${LIBDRM_INCLUDE_DIRS}
209+
${RGA_INCLUDE_DIRS}
199210
)
200211

201212
target_link_libraries(${PROJECT_NAME}
@@ -216,6 +227,8 @@ else()
216227
PkgConfig::gstreamer-app
217228
PkgConfig::gstnet
218229
PkgConfig::gio
230+
${RGA_LIBRARIES}
231+
EGL GLESv2 gbm
219232
)
220233
endif()
221234

@@ -263,7 +276,7 @@ if(BUILD_TESTS)
263276
pkg_search_module(gstreamer-app REQUIRED IMPORTED_TARGET gstreamer-app-1.0>=1.4)
264277
pkg_search_module(gstnet REQUIRED IMPORTED_TARGET gstreamer-net-1.0>=1.4)
265278
pkg_search_module(gio REQUIRED IMPORTED_TARGET gio-2.0)
266-
target_link_libraries(pixelpilot_tests PkgConfig::gstreamer PkgConfig::gstreamer-app PkgConfig::gstnet PkgConfig::gio)
279+
target_link_libraries(pixelpilot_tests PkgConfig::gstreamer PkgConfig::gstreamer-app PkgConfig::gstnet PkgConfig::gio ${RGA_LIBRARIES} EGL GLESv2 gbm)
267280

268281
target_link_options(pixelpilot_tests
269282
BEFORE PUBLIC -fsanitize=undefined PUBLIC -fsanitize=address

README.md

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@
99
WFB-ng client (Video Decoder) for Rockchip platform powered by the [Rockchip MPP library](https://github.com/rockchip-linux/mpp).
1010
It also displays a simple cairo based OSD that shows the bandwidth, decoding latency, and framerate of the decoded video, and wfb-ng link statistics.
1111

12+
Additional features:
13+
- **GPU color correction** — live linear color transform applied to video and OSD via EGL/GLES2 (configurable gain/offset, e.g. for FPV color grading)
14+
- **DVR re-encoding with OSD overlay** — records video via Rockchip MPP hardware encoder with the OSD blended in; supports live bitrate, FPS and codec changes
15+
- **Frame pacer** — feeds the re-encoder at a steady target FPS, dropping excess frames or repeating the last frame as needed
16+
- **Image flip** — upside-down display support via DRM
17+
- **GSMenu** — on-screen ground station control menu for live air-unit and link settings
18+
1219
This project is based on a unique frozen development [FPVue_rk](https://github.com/gehee/FPVue_rk) by [Gee He](https://github.com/gehee).
1320

1421
Tested on RK3566 (Radxa Zero 3W) and RK3588s (Orange Pi 5).
@@ -78,6 +85,12 @@ Build on the Rockchip linux system directly.
7885
sudo apt install libdrm-dev libcairo-dev librockchip-mpp-dev libspdlog-dev nlohmann-json3-dev libmsgpack-dev libgpiod-dev libyaml-cpp-dev
7986
```
8087

88+
- EGL/GLES2 and RGA (required for GPU color correction and DVR re-encoding)
89+
90+
```
91+
sudo apt install libegl-dev libgles2-mesa-dev libgbm-dev librga-dev
92+
```
93+
8194
- gstreamer
8295

8396
```
@@ -341,6 +354,24 @@ Specific widgets expect quite concrete facts as input:
341354
* `{"type": "IconSelectorWidget", "ranges_and_icons": [{"range": [0, 10], "icon_path": "0_10.png"}, {"range": [11, 20], ...}]}` - shows
342355
different icon depending on the range where the value lands to.
343356

357+
## Color Correction
358+
359+
PixelPilot supports a real-time GPU-accelerated color transform applied to the live video feed.
360+
The transform formula is:
361+
362+
```
363+
output = clamp((input + offset) * gain, 0, 1)
364+
```
365+
366+
Default values are `offset = -0.15` and `gain = 2.5`, which produce a high-contrast FPV look.
367+
The transform is applied via an EGL/GLES2 shader directly on the DRM buffer (NV12), so there is no CPU copy overhead.
368+
369+
When color correction is active, the OSD overlay is also GPU-processed with the **inverse** transform so that OSD elements remain visually correct.
370+
371+
If DVR re-encoding is enabled alongside color correction, the recorded video uses the same color-corrected frames.
372+
373+
Parameters can be adjusted live through the GSMenu.
374+
344375
## GSMenu
345376

346377
The gsmenu provides a ui to modify air and ground settings.
@@ -391,8 +422,11 @@ It uses [Direct Rendering Manager (DRM)](https://en.wikipedia.org/wiki/Direct_Re
391422
display video on the screen, see `drm.c`.
392423
It uses `mavlink` decoder to read Mavlink telemetry from telemetry UDP (if enabled), see `mavlink.c`
393424
It uses `cairo` library to draw OSD elements (if enabled), see `osd.c`.
394-
It uses `lvgl`to draw the gsmenu.
425+
It uses `lvgl` to draw the gsmenu.
395426
It writes non-decoded MPEG stream to file as DVR (if enabled) using `minimp4.h` library.
427+
It uses EGL/GLES2 (via `frame_colorcorrect.cpp`) to apply a GPU color transform to decoded frames before display and recording.
428+
It uses Rockchip MPP hardware encoder (`mpp_encoder.cpp`) to optionally re-encode the color-corrected video with OSD blended in for DVR recording.
429+
The `EncoderPacer` (`encoder_pacer.cpp`) decouples the decoder from the re-encoder, ensuring the encoder receives frames at a steady target FPS.
396430

397431
Pixelpilot starts several threads:
398432

@@ -425,6 +459,12 @@ Pixelpilot starts several threads:
425459
periodically draws widgets on a buffer inside `output_list` using Cairo library.
426460
There exists legacy OSD, is based on `osd_vars`, draws using Cairo library, to be removed.
427461
The loop yields on queue's mutex with timeout (timeout in order to re-draw OSD at fixed intervals).
462+
* ENCODER_PACER_THREAD (if DVR re-encoding is enabled):
463+
wakes at the target FPS interval and submits the most recent decoded frame to the MPP re-encoder.
464+
Drops frames when the source is faster than the target FPS; repeats the last frame when slower.
465+
* MPP_ENCODER_THREAD (if DVR re-encoding is enabled):
466+
receives frames via an RPC queue from the pacer and encodes them with the Rockchip MPP hardware
467+
encoder. Handles live bitrate, FPS, and codec changes without full re-initialisation where possible.
428468

429469
## Release
430470

gsmenu.sh

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -871,6 +871,51 @@ case "$@" in
871871
: #noop
872872
fi
873873
;;
874+
"values gs system dvr_mode"*)
875+
echo -n -e "raw\nreencode\nboth"
876+
;;
877+
"get gs system dvr_mode"*)
878+
echo "raw"
879+
;;
880+
"set gs system dvr_mode"*)
881+
: # noop
882+
;;
883+
"values gs system dvr_max_size"*)
884+
echo -n "1 40" # will be multiplied by 100
885+
;;
886+
"get gs system dvr_max_size"*)
887+
echo -n "40" # will be multiplied by 100
888+
;;
889+
"set gs system dvr_max_size"*)
890+
: # noop needs division by 100
891+
;;
892+
"values gs system dvr_reenc_codec"*)
893+
echo -n -e "h264\nh265"
894+
;;
895+
"values gs system dvr_reenc_resolution"*)
896+
echo -n -e "720p\n1080p"
897+
;;
898+
"values gs system dvr_reenc_fps"*)
899+
echo -n -e "30\n60"
900+
;;
901+
"values gs system dvr_reenc_bitrate"*)
902+
echo -n -e "5000\n10000\n15000\n20000\n25000\n30000\n35000\n40000\n45000\n50000"
903+
;;
904+
"set gs system dvr_reenc_codec"*)
905+
: # noop
906+
;;
907+
"set gs system dvr_reenc_resolution"*)
908+
: # noop
909+
;;
910+
"set gs system dvr_reenc_fps"*)
911+
: # noop
912+
;;
913+
"set gs system dvr_reenc_bitrate"*)
914+
: # noop
915+
;;
916+
"set gs system dvr_osd"*)
917+
: # noop
918+
;;
874919
"get gs system video_scale")
875920
grep "^video_scale =" /config/setup.txt | cut -d '=' -f2 | xargs
876921
;;

src/drm.c

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#include <pthread.h>
1818
#include <rockchip/rk_mpi.h>
1919
#include <assert.h>
20+
#include <math.h>
2021

2122
int modeset_open(int *out, const char *node)
2223
{
@@ -338,6 +339,7 @@ int modeset_create_fb(int fd, struct modeset_buf *buf)
338339
struct drm_mode_create_dumb creq;
339340
struct drm_mode_destroy_dumb dreq;
340341
struct drm_mode_map_dumb mreq;
342+
struct drm_prime_handle ph;
341343
int ret;
342344
uint32_t handles[4] = {0}, pitches[4] = {0}, offsets[4] = {0};
343345

@@ -355,6 +357,18 @@ int modeset_create_fb(int fd, struct modeset_buf *buf)
355357
buf->size = creq.size;
356358
buf->handle = creq.handle;
357359

360+
/* Export as DMA-buf prime fd for RGA. */
361+
buf->prime_fd = -1;
362+
buf->gl_fb_id = 0;
363+
memset(&ph, 0, sizeof(ph));
364+
ph.handle = buf->handle;
365+
ph.flags = DRM_CLOEXEC | DRM_RDWR;
366+
if (drmIoctl(fd, DRM_IOCTL_PRIME_HANDLE_TO_FD, &ph) == 0) {
367+
buf->prime_fd = ph.fd;
368+
} else {
369+
fprintf(stderr, "warning: PRIME_HANDLE_TO_FD failed (%m); RGA path unavailable\n");
370+
}
371+
358372
handles[0] = buf->handle;
359373
pitches[0] = buf->stride;
360374

@@ -393,6 +407,7 @@ int modeset_create_fb(int fd, struct modeset_buf *buf)
393407
err_fb:
394408
drmModeRmFB(fd, buf->fb);
395409
err_destroy:
410+
if (buf->prime_fd >= 0) { close(buf->prime_fd); buf->prime_fd = -1; }
396411
memset(&dreq, 0, sizeof(dreq));
397412
dreq.handle = buf->handle;
398413
drmIoctl(fd, DRM_IOCTL_MODE_DESTROY_DUMB, &dreq);
@@ -404,6 +419,8 @@ void modeset_destroy_fb(int fd, struct modeset_buf *buf)
404419
{
405420
struct drm_mode_destroy_dumb dreq;
406421

422+
if (buf->prime_fd >= 0) { close(buf->prime_fd); buf->prime_fd = -1; }
423+
407424
munmap(buf->map, buf->size);
408425

409426
drmModeRmFB(fd, buf->fb);
@@ -735,3 +752,145 @@ void modeset_cleanup(int fd, struct modeset_output *output_list)
735752
{
736753
modeset_output_destroy(fd, output_list);
737754
}
755+
756+
// Initialize the controller
757+
void gamma_lut_controller_init(gamma_lut_controller* ctrl, int drm_fd, struct modeset_output* output_list) {
758+
ctrl->drm_fd = drm_fd;
759+
ctrl->output_list = output_list;
760+
ctrl->gamma_lut_blob_id = 0;
761+
ctrl->last_offset = 0.0f;
762+
ctrl->last_gain = 1.0f;
763+
ctrl->is_enabled = false;
764+
}
765+
766+
// Get LUT size from DRM properties
767+
static uint64_t get_lut_size(gamma_lut_controller* ctrl) {
768+
uint64_t lut_size = get_property_value(ctrl->drm_fd, ctrl->output_list->crtc.props, "GAMMA_LUT_SIZE");
769+
if (lut_size == (uint64_t)-1) {
770+
lut_size = get_property_value(ctrl->drm_fd, ctrl->output_list->crtc.props, "gamma_lut_size");
771+
}
772+
return lut_size;
773+
}
774+
775+
// Enable gamma LUT with specified offset and gain
776+
bool gamma_lut_enable(gamma_lut_controller* ctrl, float offset, float gain) {
777+
// Store parameters for potential re-enable
778+
ctrl->last_offset = offset;
779+
ctrl->last_gain = gain;
780+
781+
// Get LUT size
782+
uint64_t lut_size = get_lut_size(ctrl);
783+
if (lut_size == 0 || lut_size == (uint64_t)-1) {
784+
// Fallback to a reasonable default if size not available
785+
lut_size = 4096; // Common LUT size
786+
}
787+
788+
// Create the LUT data
789+
struct drm_color_lut* lut = malloc(lut_size * sizeof(struct drm_color_lut));
790+
if (!lut) {
791+
return false;
792+
}
793+
794+
for (uint64_t i = 0; i < lut_size; i++) {
795+
float x = (lut_size > 1) ? (float)i / (float)(lut_size - 1) : 0.0f;
796+
float y = (x + offset) * gain;
797+
798+
// Clamp to [0, 1]
799+
if (y < 0.0f) y = 0.0f;
800+
if (y > 1.0f) y = 1.0f;
801+
802+
uint16_t v = (uint16_t)lroundf(y * 65535.0f);
803+
lut[i].red = v;
804+
lut[i].green = v;
805+
lut[i].blue = v;
806+
lut[i].reserved = 0;
807+
}
808+
809+
// Destroy previous blob if exists
810+
if (ctrl->gamma_lut_blob_id > 0) {
811+
drmModeDestroyPropertyBlob(ctrl->drm_fd, ctrl->gamma_lut_blob_id);
812+
ctrl->gamma_lut_blob_id = 0;
813+
}
814+
815+
// Create new property blob
816+
int ret = drmModeCreatePropertyBlob(ctrl->drm_fd, lut,
817+
lut_size * sizeof(struct drm_color_lut),
818+
&ctrl->gamma_lut_blob_id);
819+
free(lut);
820+
821+
if (ret != 0) {
822+
return false;
823+
}
824+
825+
// Apply the LUT via atomic commit
826+
drmModeAtomicReq *req = drmModeAtomicAlloc();
827+
if (!req) {
828+
drmModeDestroyPropertyBlob(ctrl->drm_fd, ctrl->gamma_lut_blob_id);
829+
ctrl->gamma_lut_blob_id = 0;
830+
return false;
831+
}
832+
833+
// Try both property names
834+
int set_ret = set_drm_object_property(req, &ctrl->output_list->crtc, "GAMMA_LUT", ctrl->gamma_lut_blob_id);
835+
if (set_ret < 0) {
836+
set_ret = set_drm_object_property(req, &ctrl->output_list->crtc, "gamma_lut", ctrl->gamma_lut_blob_id);
837+
}
838+
839+
if (set_ret < 0) {
840+
drmModeAtomicFree(req);
841+
drmModeDestroyPropertyBlob(ctrl->drm_fd, ctrl->gamma_lut_blob_id);
842+
ctrl->gamma_lut_blob_id = 0;
843+
return false;
844+
}
845+
846+
int commit_ret = drmModeAtomicCommit(ctrl->drm_fd, req, 0, NULL);
847+
drmModeAtomicFree(req);
848+
849+
if (commit_ret != 0) {
850+
drmModeDestroyPropertyBlob(ctrl->drm_fd, ctrl->gamma_lut_blob_id);
851+
ctrl->gamma_lut_blob_id = 0;
852+
return false;
853+
}
854+
855+
ctrl->is_enabled = true;
856+
return true;
857+
}
858+
859+
// Disable gamma LUT (revert to defaults)
860+
bool gamma_lut_disable(gamma_lut_controller* ctrl) {
861+
if (!ctrl->is_enabled && ctrl->gamma_lut_blob_id == 0) {
862+
return true; // Already disabled
863+
}
864+
return gamma_lut_enable(ctrl, 0, 1);
865+
}
866+
867+
868+
// Re-enable with last used parameters
869+
bool gamma_lut_reenable(gamma_lut_controller* ctrl) {
870+
return gamma_lut_enable(ctrl, ctrl->last_offset, ctrl->last_gain);
871+
}
872+
873+
// Toggle between enabled and disabled states
874+
bool gamma_lut_toggle(gamma_lut_controller* ctrl) {
875+
if (ctrl->is_enabled) {
876+
return gamma_lut_disable(ctrl);
877+
} else {
878+
return gamma_lut_reenable(ctrl);
879+
}
880+
}
881+
882+
// Check if gamma LUT is currently enabled
883+
bool gamma_lut_is_enabled(gamma_lut_controller* ctrl) {
884+
return ctrl->is_enabled;
885+
}
886+
887+
// Get current parameters
888+
void gamma_lut_get_params(gamma_lut_controller* ctrl, float* offset, float* gain) {
889+
*offset = ctrl->last_offset;
890+
*gain = ctrl->last_gain;
891+
}
892+
893+
// Clean up and disable
894+
void gamma_lut_cleanup(gamma_lut_controller* ctrl) {
895+
gamma_lut_disable(ctrl);
896+
}

0 commit comments

Comments
 (0)