diff --git a/src/frame_colorcorrect.cpp b/src/frame_colorcorrect.cpp index 73ccf84..54bda16 100644 --- a/src/frame_colorcorrect.cpp +++ b/src/frame_colorcorrect.cpp @@ -5,9 +5,16 @@ */ #include "frame_colorcorrect.h" +#include "mem_info.h" +#include "rockchip_bo.h" +#include +#include +#include #include +#include #include +#include #include #include #include @@ -204,22 +211,73 @@ bool FrameColorCorrect::create_targets() { // GBM_BO_USE_SCANOUT hints the allocator toward a scanout-optimized // (potentially tiled/compressed) layout; GBM_BO_USE_LINEAR forces the // layout RGA actually expects. - t.bo = gbm_bo_create(gbm_, width_, height_, GBM_FORMAT_ARGB8888, - GBM_BO_USE_RENDERING | GBM_BO_USE_LINEAR); - if (!t.bo) { - spdlog::error("FrameCC: gbm_bo_create failed for target {}", i); - return false; + // + // On >=4GB board variants, this buffer is one of the two RGA reads + // from/writes to during imcvtcolor() below, and GBM has no way to + // request the CMA-backed (<4GB) allocation RGA2's MMU requires (see + // mem_info.h). Bypass GBM there and allocate a plain KMS dumb buffer + // with the Rockchip CONTIG flag set directly; on the 1GB variant this + // can't matter (no memory exists above 4GB), so GBM is left as-is. + uint32_t stride_bytes = 0; + bool want_contig = platform_has_large_ram(); + if (want_contig) { + struct drm_mode_create_dumb dmcd; + memset(&dmcd, 0, sizeof(dmcd)); + dmcd.width = width_; + dmcd.height = height_; + dmcd.bpp = 32; + dmcd.flags = ROCKCHIP_BO_CONTIG; + int ret; + do { + ret = ioctl(drm_fd_, DRM_IOCTL_MODE_CREATE_DUMB, &dmcd); + } while (ret == -1 && (errno == EINTR || errno == EAGAIN)); + if (ret == -1) { + spdlog::error("FrameCC: CONTIG dumb-buffer create failed for " + "target {} ({}), falling back to GBM", i, strerror(errno)); + want_contig = false; + } else { + struct drm_prime_handle dph; + memset(&dph, 0, sizeof(dph)); + dph.handle = dmcd.handle; + dph.flags = DRM_CLOEXEC | DRM_RDWR; + dph.fd = -1; + do { + ret = ioctl(drm_fd_, DRM_IOCTL_PRIME_HANDLE_TO_FD, &dph); + } while (ret == -1 && (errno == EINTR || errno == EAGAIN)); + if (ret == -1) { + spdlog::error("FrameCC: prime export failed for CONTIG " + "target {} ({})", i, strerror(errno)); + struct drm_mode_destroy_dumb dmd; + memset(&dmd, 0, sizeof(dmd)); + dmd.handle = dmcd.handle; + ioctl(drm_fd_, DRM_IOCTL_MODE_DESTROY_DUMB, &dmd); + return false; + } + t.gem_handle = dmcd.handle; + t.prime_fd = dph.fd; + stride_bytes = dmcd.pitch; + spdlog::info("FrameCC: target {} CONTIG dumb bo stride={}", i, stride_bytes); + } } - spdlog::info("FrameCC: target {} bo stride={} modifier=0x{:x}", i, - gbm_bo_get_stride(t.bo), gbm_bo_get_modifier(t.bo)); - - // Export once; keep fd open for RGA use in process() - t.prime_fd = gbm_bo_get_fd(t.bo); - if (t.prime_fd < 0) { - spdlog::error("FrameCC: gbm_bo_get_fd failed for target {}", i); - return false; + if (!want_contig) { + t.bo = gbm_bo_create(gbm_, width_, height_, GBM_FORMAT_ARGB8888, + GBM_BO_USE_RENDERING | GBM_BO_USE_LINEAR); + if (!t.bo) { + spdlog::error("FrameCC: gbm_bo_create failed for target {}", i); + return false; + } + stride_bytes = gbm_bo_get_stride(t.bo); + spdlog::info("FrameCC: target {} bo stride={} modifier=0x{:x}", i, + stride_bytes, gbm_bo_get_modifier(t.bo)); + + // Export once; keep fd open for RGA use in process() + t.prime_fd = gbm_bo_get_fd(t.bo); + if (t.prime_fd < 0) { + spdlog::error("FrameCC: gbm_bo_get_fd failed for target {}", i); + return false; + } } - t.stride_px = gbm_bo_get_stride(t.bo) / 4; // bytes/row ÷ 4 bytes/px + t.stride_px = stride_bytes / 4; // bytes/row ÷ 4 bytes/px // Import as EGLImage → bind as texture → attach as FBO const EGLint img_attrs[] = { @@ -228,7 +286,7 @@ bool FrameColorCorrect::create_targets() { EGL_LINUX_DRM_FOURCC_EXT, (EGLint)DRM_FORMAT_ARGB8888, EGL_DMA_BUF_PLANE0_FD_EXT, t.prime_fd, EGL_DMA_BUF_PLANE0_OFFSET_EXT, 0, - EGL_DMA_BUF_PLANE0_PITCH_EXT, (EGLint)gbm_bo_get_stride(t.bo), + EGL_DMA_BUF_PLANE0_PITCH_EXT, (EGLint)stride_bytes, EGL_NONE }; t.img = eglCreateImageKHR_(dpy_, EGL_NO_CONTEXT, @@ -451,7 +509,15 @@ void FrameColorCorrect::destroy_targets() { eglDestroyImageKHR_(dpy_, t.img); t.img = EGL_NO_IMAGE_KHR; } if (t.prime_fd >= 0) { close(t.prime_fd); t.prime_fd = -1; } - if (t.bo) { gbm_bo_destroy(t.bo); t.bo = nullptr; } + if (t.bo) { + gbm_bo_destroy(t.bo); t.bo = nullptr; + } else if (t.gem_handle) { + struct drm_mode_destroy_dumb dmd; + memset(&dmd, 0, sizeof(dmd)); + dmd.handle = t.gem_handle; + ioctl(drm_fd_, DRM_IOCTL_MODE_DESTROY_DUMB, &dmd); + t.gem_handle = 0; + } } } diff --git a/src/frame_colorcorrect.h b/src/frame_colorcorrect.h index 682bb5c..3ee6715 100644 --- a/src/frame_colorcorrect.h +++ b/src/frame_colorcorrect.h @@ -89,6 +89,9 @@ class FrameColorCorrect { static constexpr int kTargets = 2; struct Target { gbm_bo* bo{nullptr}; + uint32_t gem_handle{0}; // set instead of bo on the CONTIG (raw + // dumb-buffer) allocation path -- see + // create_targets() in the .cpp EGLImageKHR img{EGL_NO_IMAGE_KHR}; GLuint tex{0}; GLuint fbo{0}; diff --git a/src/frame_processor.cpp b/src/frame_processor.cpp index 425a708..2a4c02e 100644 --- a/src/frame_processor.cpp +++ b/src/frame_processor.cpp @@ -1,15 +1,22 @@ #include #include +#include #include #include +#include +#include +#include #include "spdlog/spdlog.h" #include #include +#include #include "dvr.h" #include "frame_processor.h" +#include "mem_info.h" +#include "rockchip_bo.h" // Map MPP pixel format to the corresponding RGA format for im2d DMA copies. static int mpp_fmt_to_rga(MppFrameFormat fmt) @@ -25,6 +32,67 @@ static inline uint32_t align_up(uint32_t v, uint32_t a) { return (v + a - 1) & ~(a - 1); } +bool FrameProcessor::alloc_contig_proc_copy(size_t size) { + struct drm_mode_create_dumb dmcd; + memset(&dmcd, 0, sizeof(dmcd)); + // Opaque byte blob: width=size, height=1, bpp=8 gives exactly `size` + // bytes with pitch==size -- no image format semantics needed here, + // proc_copy_ is addressed by mpp_buffer_get_fd()/get_ptr() only. + dmcd.width = (uint32_t)size; + dmcd.height = 1; + dmcd.bpp = 8; + dmcd.flags = ROCKCHIP_BO_CONTIG; + int ret; + do { + ret = ioctl(drm_fd_, DRM_IOCTL_MODE_CREATE_DUMB, &dmcd); + } while (ret == -1 && (errno == EINTR || errno == EAGAIN)); + if (ret == -1) { + spdlog::error("FrameProcessor: CONTIG dumb-buffer create failed ({})", strerror(errno)); + return false; + } + + struct drm_prime_handle dph; + memset(&dph, 0, sizeof(dph)); + dph.handle = dmcd.handle; + dph.flags = DRM_CLOEXEC | DRM_RDWR; + dph.fd = -1; + do { + ret = ioctl(drm_fd_, DRM_IOCTL_PRIME_HANDLE_TO_FD, &dph); + } while (ret == -1 && (errno == EINTR || errno == EAGAIN)); + + // The dma-buf fd (once exported) holds its own reference to the + // backing memory, so the GEM handle can be closed immediately -- + // standard DRM/dma-buf pattern, same as done for the FrameColorCorrect + // render targets. + struct drm_mode_destroy_dumb dmd; + memset(&dmd, 0, sizeof(dmd)); + dmd.handle = dmcd.handle; + ioctl(drm_fd_, DRM_IOCTL_MODE_DESTROY_DUMB, &dmd); + + if (ret == -1) { + spdlog::error("FrameProcessor: CONTIG prime export failed ({})", strerror(errno)); + return false; + } + + MppBufferInfo info; + memset(&info, 0, sizeof(info)); + info.type = MPP_BUFFER_TYPE_DRM; + info.size = size; + info.fd = dph.fd; + MPP_RET mret = mpp_buffer_import(&proc_copy_, &info); + // mpp_buffer_import() dups whatever fd it's handed rather than taking + // ownership of it -- close our copy unconditionally, on both success + // and failure, or a failed import leaks dph.fd (info.fd is only + // touched on the success path). + close(dph.fd); + if (mret != MPP_OK) { + spdlog::error("FrameProcessor: CONTIG buffer import failed ({})", (int)mret); + proc_copy_ = nullptr; + return false; + } + return true; +} + FrameProcessor::FrameProcessor(MppEncoder *enc, int fps, EncResolution res, int drm_fd, std::function on_fatal_error) : encoder(enc), interval_ns(1000000000L / fps), target_res_((int)res), @@ -164,9 +232,20 @@ void FrameProcessor::process_loop() { size_t dst_sz = (size_t)dst_hs * dst_vs * 3 / 2; // NV12 - if (hold_grp && (!proc_copy_ || mpp_buffer_get_size(proc_copy_) < dst_sz)) { + if (!proc_copy_ || mpp_buffer_get_size(proc_copy_) < dst_sz) { if (proc_copy_) { mpp_buffer_put(proc_copy_); proc_copy_ = nullptr; } - mpp_buffer_get(hold_grp, &proc_copy_, dst_sz); + + if (platform_has_large_ram() && drm_fd_ >= 0) { + alloc_contig_proc_copy(dst_sz); + // On failure, deliberately do NOT fall back to hold_grp + // here: that would risk landing this buffer >=4GB again, + // exactly the corruption this workaround exists to + // prevent (see mem_info.h). proc_copy_ staying null is + // handled as fatal below, same as any other + // unrecoverable conversion failure. + } else if (hold_grp) { + mpp_buffer_get(hold_grp, &proc_copy_, dst_sz); + } } if (proc_copy_) { // ── GL path: colour-correct + OSD in one GPU pass ─────── @@ -274,6 +353,9 @@ void FrameProcessor::process_loop() { proc_meta_.fmt = fresh.fmt; proc_meta_.buffer = nullptr; } + } else { + spdlog::error("FrameProcessor: no reencode working buffer available, stopping DVR reencode"); + fatal = true; } fresh.release(); // decoder buffer is free again } diff --git a/src/frame_processor.h b/src/frame_processor.h index 17afc11..1252b23 100644 --- a/src/frame_processor.h +++ b/src/frame_processor.h @@ -92,6 +92,14 @@ class FrameProcessor { void process_loop(); void timer_loop(); + // Attempts CMA-backed (<4GB-guaranteed) allocation of proc_copy_ via a + // raw DRM dumb buffer, bypassing MPP's buffer-group CONTIG/DMA32 flags + // (see mem_info.h -- they don't reliably route to CMA on this + // platform). Only called on large-RAM boards. On failure, proc_copy_ + // is left null; the caller does not fall back to hold_grp for this -- + // see process_loop(). + bool alloc_contig_proc_copy(size_t size); + MppEncoder *encoder; std::atomic interval_ns; std::atomic target_res_{1}; // 0=720p, 1=1080p diff --git a/src/mem_info.h b/src/mem_info.h new file mode 100644 index 0000000..eb17abe --- /dev/null +++ b/src/mem_info.h @@ -0,0 +1,33 @@ +#pragma once +/** + * mem_info.h + * + * Gate for the DVR-reencode CONTIG/CMA buffer workaround (see + * frame_processor.cpp and frame_colorcorrect.cpp): RGA2's MMU can only + * address physical memory below 4GB, and a kernel bug in the Rockchip GEM + * allocator (the __GFP_DMA32 safety flag is only ever applied under + * CONFIG_ARM_LPAE, a 32-bit-only Kconfig symbol never set on this arm64 + * kernel) means a buffer can silently land above that boundary on board + * variants with >=4GB RAM. On the 1GB variant no buffer can ever + * physically be placed above 4GB, so forcing CMA allocation there would + * only add memory pressure for zero benefit -- gate on actual installed + * RAM rather than a compile-time board flag so one binary is correct on + * both variants. + */ + +#include + +inline bool platform_has_large_ram() { + FILE *f = fopen("/proc/meminfo", "r"); + if (!f) return false; + char line[256]; + unsigned long kb = 0; + bool found = false; + while (fgets(line, sizeof(line), f)) { + if (sscanf(line, "MemTotal: %lu kB", &kb) == 1) { found = true; break; } + } + fclose(f); + // Wide margin between the ~1GB and ~4GB board variants -- this only + // needs to land on the right side, not find a precise boundary. + return found && kb > 2ul * 1024 * 1024; +} diff --git a/src/rockchip_bo.h b/src/rockchip_bo.h new file mode 100644 index 0000000..5b5258f --- /dev/null +++ b/src/rockchip_bo.h @@ -0,0 +1,14 @@ +#pragma once +/** + * rockchip_bo.h + * + * Rockchip vendor extension to struct drm_mode_create_dumb.flags (normally + * "must be zero"), from include/uapi/drm/rockchip_drm.h -- not shipped in + * this build's sysroot, so mirrored here. ROCKCHIP_BO_CONTIG requests + * CMA-backed allocation, guaranteed to sit below the 4GB physical boundary + * RGA2's MMU can address; see mem_info.h for why that matters. + */ + +#include + +static constexpr uint32_t ROCKCHIP_BO_CONTIG = 1u << 0;