【精通SDL之----SDL_RenderReadPixels截屏】

news2024/12/22 17:08:13

SDL_RenderReadPixels截屏

  • 前言
  • 一、SDL_RenderReadPixels简介
  • 二、问题现象
  • 三、规避方案
    • 1. 离屏纹理
    • 2. `ding!` *==灵光一现==*

前言

  最近使用SDL2在鸿蒙系统(Harmoney OS)上截取视频播放过程中的数据,发现捕获的数据为空,然在windows上却可以正常捕获,欲定位其中问题并解决之。

一、SDL_RenderReadPixels简介

  SDL_RenderReadPixels用于从当前渲染目标(render target)读取像素数据,以便进行后续处理、保存图像或者其他用途。其参数定义及实现如下:
  renderer:指向用于渲染的 SDL_Renderer。
  rect:指向定义读取区域的 SDL_Rect。如果为 NULL,则读取整个渲染目标。
  format:希望得到的像素数据格式。使用 SDL 的像素格式,如 SDL_PIXELFORMAT_ARGB8888。
  pixels:指向用于存储读取到的像素数据的缓冲区。
  pitch:一行像素数据的字节数,即每行像素数据的步幅(宽度)。

SDL_RenderReadPixels(SDL_Renderer * renderer, const SDL_Rect * rect,
                     Uint32 format, void * pixels, int pitch)
{
    SDL_Rect real_rect;

    CHECK_RENDERER_MAGIC(renderer, -1);

    if (!renderer->RenderReadPixels) {
        return SDL_Unsupported();
    }

    FlushRenderCommands(renderer);  /* we need to render before we read the results. */

    if (!format) {
        format = SDL_GetWindowPixelFormat(renderer->window);
    }

    real_rect.x = renderer->viewport.x;
    real_rect.y = renderer->viewport.y;
    real_rect.w = renderer->viewport.w;
    real_rect.h = renderer->viewport.h;
    if (rect) {
        if (!SDL_IntersectRect(rect, &real_rect, &real_rect)) {
            return 0;
        }
        if (real_rect.y > rect->y) {
            pixels = (Uint8 *)pixels + pitch * (real_rect.y - rect->y);
        }
        if (real_rect.x > rect->x) {
            int bpp = SDL_BYTESPERPIXEL(format);
            pixels = (Uint8 *)pixels + bpp * (real_rect.x - rect->x);
        }
    }

    return renderer->RenderReadPixels(renderer, &real_rect,
                                      format, pixels, pitch);
}
  1. 上面先确定当前系统有无适配渲染器的RenderReadPixels函数,在每个平台下都有自己的渲染器实现,如windowsD3D11苹果metal索尼psp、嵌入式系统如安卓和鸿蒙opengles,及其他平台的opengl

  2. 使用FlushRenderCommands将当前渲染命令队列中的命令发送给GPU执行渲染,这里可以看到官方的注释说一定要在渲染之后才能读取到像素数据。【这里同时我也在github上问了下SDL的维护人员进行了确认:】
    在这里插入图片描述

  3. 如果传入的rect比实际渲染视口real_rect的大小要小,,则将更新pixels指向的起始捕获地址,这里如果是rgba8888格式,那么pitch就是1920(视频宽) * 4 = 7680

二、问题现象

  从上面的renderer->RenderReadPixels调到具体各平台的实现:
  在Open GLES上直接调用渲染器的driverdata的glReadPixels获取数据,但是此处得到的数据是个空值,具体也无法再往下跟了。

static int
GLES2_RenderReadPixels(SDL_Renderer * renderer, const SDL_Rect * rect,
                    Uint32 pixel_format, void * pixels, int pitch)
{
    GLES2_RenderData *data = (GLES2_RenderData *)renderer->driverdata;
    Uint32 temp_format = renderer->target ? renderer->target->format : SDL_PIXELFORMAT_ABGR8888;
    size_t buflen;
    void *temp_pixels;
    int temp_pitch;
    Uint8 *src, *dst, *tmp;
    int w, h, length, rows;
    int status;

    temp_pitch = rect->w * SDL_BYTESPERPIXEL(temp_format);
    buflen = rect->h * temp_pitch;
    if (buflen == 0) {
        return 0;  /* nothing to do. */
    }

    temp_pixels = SDL_malloc(buflen);
    if (!temp_pixels) {
        return SDL_OutOfMemory();
    }

    SDL_GetRendererOutputSize(renderer, &w, &h);

    data->glReadPixels(rect->x, renderer->target ? rect->y : (h-rect->y)-rect->h,
                       rect->w, rect->h, GL_RGBA, GL_UNSIGNED_BYTE, temp_pixels);
    if (GL_CheckError("glReadPixels()", renderer) < 0) {
        return -1;
    }

    /* Flip the rows to be top-down if necessary */
    if (!renderer->target) {
        SDL_bool isstack;
        length = rect->w * SDL_BYTESPERPIXEL(temp_format);
        src = (Uint8*)temp_pixels + (rect->h-1)*temp_pitch;
        dst = (Uint8*)temp_pixels;
        tmp = SDL_small_alloc(Uint8, length, &isstack);
        rows = rect->h / 2;
        while (rows--) {
            SDL_memcpy(tmp, dst, length);
            SDL_memcpy(dst, src, length);
            SDL_memcpy(src, tmp, length);
            dst += temp_pitch;
            src -= temp_pitch;
        }
        SDL_small_free(tmp, isstack);
    }

    status = SDL_ConvertPixels(rect->w, rect->h,
                               temp_format, temp_pixels, temp_pitch,
                               pixel_format, pixels, pitch);
    SDL_free(temp_pixels);

    return status;
}

  在windows下,同样的流程使用D3D11,确能正常获取到数据:

static int
D3D11_RenderReadPixels(SDL_Renderer * renderer, const SDL_Rect * rect,
                       Uint32 format, void * pixels, int pitch)
{
    D3D11_RenderData * data = (D3D11_RenderData *) renderer->driverdata;
    ID3D11Texture2D *backBuffer = NULL;
    ID3D11Texture2D *stagingTexture = NULL;
    HRESULT result;
    int status = -1;
    D3D11_TEXTURE2D_DESC stagingTextureDesc;
    D3D11_RECT srcRect = {0, 0, 0, 0};
    D3D11_BOX srcBox;
    D3D11_MAPPED_SUBRESOURCE textureMemory;

    /* Retrieve a pointer to the back buffer: */
    result = IDXGISwapChain_GetBuffer(data->swapChain,
        0,
        &SDL_IID_ID3D11Texture2D,
        (void **)&backBuffer
        );
    if (FAILED(result)) {
        WIN_SetErrorFromHRESULT(SDL_COMPOSE_ERROR("IDXGISwapChain1::GetBuffer [get back buffer]"), result);
        goto done;
    }

    /* Create a staging texture to copy the screen's data to: */
    ID3D11Texture2D_GetDesc(backBuffer, &stagingTextureDesc);
    stagingTextureDesc.Width = rect->w;
    stagingTextureDesc.Height = rect->h;
    stagingTextureDesc.BindFlags = 0;
    stagingTextureDesc.MiscFlags = 0;
    stagingTextureDesc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
    stagingTextureDesc.Usage = D3D11_USAGE_STAGING;
    result = ID3D11Device_CreateTexture2D(data->d3dDevice,
        &stagingTextureDesc,
        NULL,
        &stagingTexture);
    if (FAILED(result)) {
        WIN_SetErrorFromHRESULT(SDL_COMPOSE_ERROR("ID3D11Device1::CreateTexture2D [create staging texture]"), result);
        goto done;
    }

    /* Copy the desired portion of the back buffer to the staging texture: */
    if (D3D11_GetViewportAlignedD3DRect(renderer, rect, &srcRect, FALSE) != 0) {
        /* D3D11_GetViewportAlignedD3DRect will have set the SDL error */
        goto done;
    }

    srcBox.left = srcRect.left;
    srcBox.right = srcRect.right;
    srcBox.top = srcRect.top;
    srcBox.bottom = srcRect.bottom;
    srcBox.front = 0;
    srcBox.back = 1;
    ID3D11DeviceContext_CopySubresourceRegion(data->d3dContext,
        (ID3D11Resource *)stagingTexture,
        0,
        0, 0, 0,
        (ID3D11Resource *)backBuffer,
        0,
        &srcBox);

    /* Map the staging texture's data to CPU-accessible memory: */
    result = ID3D11DeviceContext_Map(data->d3dContext,
        (ID3D11Resource *)stagingTexture,
        0,
        D3D11_MAP_READ,
        0,
        &textureMemory);
    if (FAILED(result)) {
        WIN_SetErrorFromHRESULT(SDL_COMPOSE_ERROR("ID3D11DeviceContext1::Map [map staging texture]"), result);
        goto done;
    }

    /* Copy the data into the desired buffer, converting pixels to the
     * desired format at the same time:
     */
    if (SDL_ConvertPixels(
        rect->w, rect->h,
        D3D11_DXGIFormatToSDLPixelFormat(stagingTextureDesc.Format),
        textureMemory.pData,
        textureMemory.RowPitch,
        format,
        pixels,
        pitch) != 0) {
        /* When SDL_ConvertPixels fails, it'll have already set the format.
         * Get the error message, and attach some extra data to it.
         */
        char errorMessage[1024];
        SDL_snprintf(errorMessage, sizeof(errorMessage), "%s, Convert Pixels failed: %s", __FUNCTION__, SDL_GetError());
        SDL_SetError("%s", errorMessage);
        goto done;
    }

    /* Unmap the texture: */
    ID3D11DeviceContext_Unmap(data->d3dContext,
        (ID3D11Resource *)stagingTexture,
        0);

    status = 0;

done:
    SAFE_RELEASE(backBuffer);
    SAFE_RELEASE(stagingTexture);
    return status;
}

  可以看出ID3D11Device_CreateTexture2D首先创建了一个纹理,然后复制后缓冲区的一部分到纹理中,然后通过ID3D11DeviceContext_Map将暂存纹理的数据映射到 CPU 可访问的内存,最后将数据复制到所需的缓冲区,同时将像素转换为所需格式。
  从上面不难看出,两者的主要不同在于D3D11中创建了一个离屏的纹理,通过将这个纹理映射到内存中来读取数据,这样即使视频渲染和截屏操作不在同一个线程中,也可以捕获到正常的数据。

三、规避方案

1. 离屏纹理

  于是乎,想参照D3D11一样也搞个离屏纹理,然后使用离屏纹理的数据来生成图片,代码如下:

	SDL_Rect renderRect = { 0, 0, viewSize.width(), viewSize.height() };
	// 1.先创建纹理
	SDL_Texture* targetTexture = _createSdlTexture(m_sdlRender, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, renderRect.w, renderRect.h, m_videoControl);
	if (!targetTexture)
		return false;
	// 2.设置渲染目标
	SDL_SetRenderTarget(m_sdlRender, targetTexture);
	// 3.拷贝纹理
	SDL_RenderCopyEx(m_sdlRender, m_videoYUVTexture, &m_videoOriginRect, &renderRect, 0, nullptr, SDL_FLIP_NONE);

	*ppImg = new QImage(renderRect.w, renderRect.h, QImage::Format_ARGB32);
	(*ppImg)->fill(0);

	uchar* data = (*ppImg)->bits();
	// 4.将拷贝的纹理中像素数据读到QImage中去
	SDL_RenderReadPixels(m_sdlRender, &renderRect, SDL_PIXELFORMAT_ARGB8888, (void*)data, (*ppImg)->bytesPerLine());
	// 5.销毁渲染目标
	SDL_SetRenderTarget(m_sdlRender, nullptr);

	_safeDestroy(targetTexture, &SDL_DestroyTexture, m_videoControl);

  这样可是可以,只不过只能适用于一张纹理的情况(欲哭无泪.jpg),如果同时还要捕获视频的前景和背景,实现是可以实现,就太繁琐了。

2. ding! 灵光一现

  既然github上SDL的维护人员说SDL_RenderReadPixels一定要在SDL_RenderPresent之前调用才有数据,那SDL_RenderPresent里面是不是有方法让它不去交换前后缓冲区呢?如果不交换缓冲区,就可以捕获到当前前景+背景+视频帧的所有纹理数据了!
  带着这个疑问,去看了下SDL_RenderPresent的实现:

void
SDL_RenderPresent(SDL_Renderer * renderer)
{
    CHECK_RENDERER_MAGIC(renderer, );

    FlushRenderCommands(renderer);  /* time to send everything to the GPU! */

    /* Don't present while we're hidden */
    if (renderer->hidden) {
        return;
    }
    renderer->RenderPresent(renderer);
}

  嘿嘿,在交换缓冲区之前有个判断渲染器是否隐藏,如果隐藏就直接return了,好家伙,可以用这个来控制。又由于渲染器SDL_Render和SDL_Window是绑定的,因此就直接在SDL_RenderReadPixels前隐藏窗口就OK了:

	SDL_HideWindow(m_sdlWindow);
	SDL_RenderReadPixels(m_sdlRenderer, &destRect, SDL_PIXELFORMAT_ARGB8888, (void*)data, image.bytesPerLine());
	SDL_RaiseWindow(m_sdlWindow);

真是踏破铁鞋无觅处,得来全不费工夫
XD

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2036734.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux 内核源码分析---网络层分析

版本(version)&#xff1a;指定所用 IP 协议的版本&#xff0c;该字段有效值为 4 或 6&#xff1b; IP首部长度(IHL)&#xff1a;定义首部的长度&#xff0c;由于选项数量可变&#xff1b; 服务区分&#xff1a;用于更复杂协议选项&#xff1b; 长度&#xff1a;指定分组的总长…

AI技术重塑招聘流程

一、引言 随着科技的飞速发展&#xff0c;人工智能&#xff08;AI&#xff09;技术正以前所未有的速度渗透到社会的各个领域&#xff0c;其中&#xff0c;人力资源管理领域也不例外。在全员招聘这一关键环节中&#xff0c;AI技术的应用不仅极大地提高了招聘效率&#xff0c;还…

【Java数据结构】---Queue

乐观学习&#xff0c;乐观生活&#xff0c;才能不断前进啊&#xff01;&#xff01;&#xff01; 我的主页&#xff1a;optimistic_chen 我的专栏&#xff1a;c语言 &#xff0c;Java 欢迎大家访问~ 创作不易&#xff0c;大佬们点赞鼓励下吧~ 文章目录 前言队列Queue队列的模拟…

fme处理空间数据入门v0

概述 本教程是最最最基本的fme处理空间数据内容&#xff0c;给新同事介绍我以前是怎么干活用的&#xff0c;基本啥有用的东西。 本教程主要说如何在 FME Workbench中创建模板文件&#xff08;一般我们把fme写的工作空间就叫模板&#xff09;&#xff0c;教程里面主要是利用GI…

Linux2.6内核进程调度队列详细讲解

上图是 Linux2.6 内核中进程队列的数据结构&#xff0c;之间关系也已经给大家画出来&#xff0c;方便大家理解。 一个 CPU 拥有一个 runqueue。 Linux真正的调度方式是通过runqueue进行调度的&#xff0c;我们知道进程的优先级范围是根据nice值确定的&#xff0c;而nice值的范围…

怎么配置Datagrip中字体的大小,修改注释的颜色呢

新手刚安装Datagrip时&#xff0c;都会遇到怎么调字体大小的问题&#xff0c;接下来解决一下吧 具体步骤&#xff1a; 1.进入Datagrip的操作窗口后&#xff0c;点击左上方的菜单栏中file(文件)。 2.在文件中点击设置setting。 3.在设置窗口点击Editor,再点击Font。 4.在Fo…

.[[Hoeosi@airmail.cc]].rntc勒索病毒数据怎么处理|数据解密恢复

导言&#xff1a; 近年来&#xff0c;勒索病毒&#xff08;也称为勒索软件&#xff09;已成为网络安全领域的一大威胁。其中&#xff0c;.[[Hoeosiairmail.cc]].rntc勒索病毒作为一种新型恶意软件&#xff0c;通过加密用户的重要文件并要求支付赎金来解锁&#xff0c;给个人和…

深度学习 —— 个人学习笔记20(转置卷积、全卷积网络)

声明 本文章为个人学习使用&#xff0c;版面观感若有不适请谅解&#xff0c;文中知识仅代表个人观点&#xff0c;若出现错误&#xff0c;欢迎各位批评指正。 三十九、转置卷积 import torch from torch import nndef trans_conv(X, K):h, w K.shapeY torch.zeros((X.shape[…

Datawhale AI夏令营第四期魔搭- AIGC文生图方向 task02笔记

1 前言 本次是学习内容是Datawhale AI夏令营第四期-AIGC文生图方向的学习笔记。 2 AIGC简介 AIGC&#xff08;Artificial Intelligence Generated Content&#xff09;即人工智能生成内容&#xff0c;即人工智能通过学习大量的数据&#xff0c;来实现自动生成各种内容&#xf…

仿RabbitMQ实现消息队列

前言&#xff1a;本项目是仿照RabbitMQ并基于SpringBoot Mybatis SQLite3实现的消息队列&#xff0c;该项目实现了MQ的核心功能&#xff1a;生产者、消费者、中间人、发布、订阅等。 源码链接&#xff1a;仿Rabbit MQ实现消息队列 目录 前言&#xff1a;本项目是仿照Rabbi…

JVM运行时数据区之虚拟机栈

【1】概述 Java虚拟机栈&#xff08;Java Virtual Machine Stack&#xff09;&#xff0c;早期也叫Java栈。每个线程在创建时都会创建一个虚拟机栈&#xff0c;其内部保存一个个的栈帧&#xff08;Stack Frame&#xff09;&#xff0c;对应着一次次的Java方法调用。 栈是运行…

World of Warcraft [CLASSIC] 80 WLK [Gundrak] BUG

World of Warcraft [CLASSIC] 80 WLK [Gundrak] BUG 魔兽世界怀旧版&#xff0c;80级&#xff0c;5人副本古达克&#xff0c;科技队伍&#xff08;BUG队伍&#xff09; 副本有两个门口 这样看&#xff0c;是不是觉得很怪。是的&#xff0c;和图1刚好相反的。 因此应该翻转180…

24电赛H题总结

一、题目 题目链接&#xff1a;自动行驶小车&#xff08;H题&#xff09; 我们截取一些重要信息 1. 小车行驶场地示意图 2.要求 二、赛题分析 技术挑战与准备 MCU熟悉度&#xff1a;尽管TI MSPM0系列MCU在使用上类似于STM32CUBEIDEKeil&#xff0c;但其开发环境也需要熟悉。因…

数据结构入门——04栈

1.栈 栈是限制在一端进行插入操作和删除操作的线性表&#xff08;俗称堆栈&#xff09; 允许进行操作的一端称为“栈顶”&#xff0c;另一固定端称为“栈底”&#xff0c;当栈中没有元素时称为“空栈”。 栈的特点 &#xff1a;后进先出LIFO&#xff08;Last In First Out&a…

支持I2C接口、抗干扰性强、14通道触摸按键的电容式触摸芯片-GTX314L

电容式触摸芯片 - GTX314L是具有多通道触发传感器的14位触摸传感器系列&#xff0c;它是通过持续模式提供中断功能和唤醒功能&#xff0c;广泛适用于各种控制面板应用&#xff0c;可直接兼容原机械式轻触按键的处理信号。 GTX314L芯片内部采用特殊的集成电路&#xff0c;具有高…

C++进阶-智能指针

1. 为什么需要智能指针&#xff1f; 下面我们先分析一下下面这段程序有没有什么内存方面的问题&#xff1f;提示一下&#xff1a;注意分析MergeSort函数中的问题。 int div() {int a, b;cin >> a >> b;if (b 0)throw invalid_argument("除0错误");retur…

【C语言】内存管理

C语言-内存管理 一、C进程内存布局二、栈内存1、存储在栈内存中的参数有哪些&#xff1f;2、栈内存的特点&#xff1f; 三、静态数据四、数据段与代码段五、堆内存 一、C进程内存布局 \qquad 任何一个程序&#xff0c;正常运行都需要内存资源&#xff0c;用来存放诸如变量、常量…

第九届“创客中国”武汉区域赛正式启幕 灵途科技勇夺前三,晋级决赛!

8月8日&#xff0c;第九届“创客中国”武汉区域赛正式启幕&#xff0c;首场聚焦先进制造领域。灵途科技勇夺先进制造领域专场企业组前三名&#xff0c;成功晋级决赛。 “创客中国”大赛是工业和信息化部组织开展的双创赛事活动&#xff0c;以构建产业链协同发展为出发点&#…

Win10 VisualStudio 2022编译ollvm 13.x

VisualStudio配置 1&#xff0c;正常配置C桌面环境 2&#xff0c;在单个组件中选择用于Windows得C Cmake工具 下载OLLVM13.x https://github.com/heroims/obfuscator/tree/llvm-13.x 解压后进入文件夹&#xff0c;命令行输入 cmake -G “Visual Studio 17 2022” -DLLVM_EN…

Java面试--设计模式

设计模式 目录 设计模式1.单例模式&#xff1f;2.代理模式&#xff1f;3.策略模式&#xff1f;4.工厂模式&#xff1f; 1.单例模式&#xff1f; 单例模式是Java的一种设计思想&#xff0c;用此模式下&#xff0c;某个对象在jvm只允许有一个实例&#xff0c;防止这个对象多次引…