#include "vdpau.h" #include #include #define BAIL_ON_FAIL(status, something) if ((status) != VDP_STATUS_OK) { \ SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, \ #something " failed: %d", (status)); \ return false; \ } #define GET_PROC_ADDRESS(id, func) status = vdpauCtx->get_proc_address(m_Device, id, (void**)func); \ BAIL_ON_FAIL(status, id) const VdpRGBAFormat VDPAURenderer::k_OutputFormats[] = { VDP_RGBA_FORMAT_B8G8R8A8, VDP_RGBA_FORMAT_R8G8B8A8 }; VDPAURenderer::VDPAURenderer() : m_HwContext(nullptr), m_PresentationQueueTarget(0), m_PresentationQueue(0), m_VideoMixer(0), m_NextSurfaceIndex(0) { SDL_zero(m_OutputSurface); } VDPAURenderer::~VDPAURenderer() { if (m_PresentationQueue != 0) { m_VdpPresentationQueueDestroy(m_PresentationQueue); } if (m_VideoMixer != 0) { m_VdpVideoMixerDestroy(m_VideoMixer); } if (m_PresentationQueueTarget != 0) { m_VdpPresentationQueueTargetDestroy(m_PresentationQueueTarget); } for (int i = 0; i < OUTPUT_SURFACE_COUNT; i++) { if (m_OutputSurface[i] != 0) { m_VdpOutputSurfaceDestroy(m_OutputSurface[i]); } } // This must be done last as it frees VDPAU context required to call // the functions above. if (m_HwContext != nullptr) { av_buffer_unref(&m_HwContext); } } bool VDPAURenderer::initialize(SDL_Window* window, int, int width, int height, int, bool) { int err; VdpStatus status; SDL_SysWMinfo info; m_VideoWidth = width; m_VideoHeight = height; err = av_hwdevice_ctx_create(&m_HwContext, AV_HWDEVICE_TYPE_VDPAU, nullptr, nullptr, 0); if (err < 0) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to create VDPAU context: %d", err); return false; } AVHWDeviceContext* devCtx = (AVHWDeviceContext*)m_HwContext->data; AVVDPAUDeviceContext* vdpauCtx = (AVVDPAUDeviceContext*)devCtx->hwctx; m_Device = vdpauCtx->device; GET_PROC_ADDRESS(VDP_FUNC_ID_GET_ERROR_STRING, &m_VdpGetErrorString); GET_PROC_ADDRESS(VDP_FUNC_ID_PRESENTATION_QUEUE_TARGET_DESTROY, &m_VdpPresentationQueueTargetDestroy); GET_PROC_ADDRESS(VDP_FUNC_ID_VIDEO_MIXER_CREATE, &m_VdpVideoMixerCreate); GET_PROC_ADDRESS(VDP_FUNC_ID_VIDEO_MIXER_DESTROY, &m_VdpVideoMixerDestroy); GET_PROC_ADDRESS(VDP_FUNC_ID_VIDEO_MIXER_RENDER, &m_VdpVideoMixerRender); GET_PROC_ADDRESS(VDP_FUNC_ID_PRESENTATION_QUEUE_CREATE, &m_VdpPresentationQueueCreate); GET_PROC_ADDRESS(VDP_FUNC_ID_PRESENTATION_QUEUE_DESTROY, &m_VdpPresentationQueueDestroy); GET_PROC_ADDRESS(VDP_FUNC_ID_PRESENTATION_QUEUE_DISPLAY, &m_VdpPresentationQueueDisplay); GET_PROC_ADDRESS(VDP_FUNC_ID_PRESENTATION_QUEUE_SET_BACKGROUND_COLOR, &m_VdpPresentationQueueSetBackgroundColor); GET_PROC_ADDRESS(VDP_FUNC_ID_PRESENTATION_QUEUE_BLOCK_UNTIL_SURFACE_IDLE, &m_VdpPresentationQueueBlockUntilSurfaceIdle); GET_PROC_ADDRESS(VDP_FUNC_ID_OUTPUT_SURFACE_CREATE, &m_VdpOutputSurfaceCreate); GET_PROC_ADDRESS(VDP_FUNC_ID_OUTPUT_SURFACE_DESTROY, &m_VdpOutputSurfaceDestroy); GET_PROC_ADDRESS(VDP_FUNC_ID_OUTPUT_SURFACE_QUERY_CAPABILITIES, &m_VdpOutputSurfaceQueryCapabilities); GET_PROC_ADDRESS(VDP_FUNC_ID_VIDEO_SURFACE_GET_PARAMETERS, &m_VdpVideoSurfaceGetParameters); SDL_GetWindowSize(window, (int*)&m_DisplayWidth, (int*)&m_DisplayHeight); SDL_VERSION(&info.version); if (!SDL_GetWindowWMInfo(window, &info)) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_GetWindowWMInfo() failed: %s", SDL_GetError()); return false; } SDL_assert(info.subsystem == SDL_SYSWM_X11); if (info.subsystem == SDL_SYSWM_X11) { GET_PROC_ADDRESS(VDP_FUNC_ID_PRESENTATION_QUEUE_TARGET_CREATE_X11, &m_VdpPresentationQueueTargetCreateX11); status = m_VdpPresentationQueueTargetCreateX11(m_Device, info.info.x11.window, &m_PresentationQueueTarget); if (status != VDP_STATUS_OK) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "VdpPresentationQueueTargetCreateX11() failed: %s", m_VdpGetErrorString(status)); return false; } } else if (info.subsystem == SDL_SYSWM_WAYLAND) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "VDPAU backend does not currently support Wayland"); return false; } else { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Unsupported VDPAU rendering subsystem: %d", info.subsystem); return false; } // Try our available output formats to find something the GPU supports bool foundFormat = false; for (int i = 0; i < OUTPUT_SURFACE_FORMAT_COUNT; i++) { VdpBool supported; uint32_t maxWidth, maxHeight; status = m_VdpOutputSurfaceQueryCapabilities(m_Device, k_OutputFormats[i], &supported, &maxWidth, &maxHeight); if (status != VDP_STATUS_OK) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "VdpOutputSurfaceQueryCapabilities() failed: %s", m_VdpGetErrorString(status)); return false; } if (supported) { if (m_DisplayWidth <= maxWidth && m_DisplayHeight <= maxHeight) { m_OutputSurfaceFormat = k_OutputFormats[i]; foundFormat = true; break; } else { SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Display size not within capabilities %dx%d vs %dx%d", m_DisplayWidth, m_DisplayWidth, maxWidth, maxHeight); } } } if (!foundFormat) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "No compatible output surface format found!"); return false; } // Create the output surfaces for (int i = 0; i < OUTPUT_SURFACE_COUNT; i++) { // It seems there's some lazy freeing going on or something in VDPAU // because we can get VDP_STATUS_RESOURCES, then wait a bit and it'll // complete without a problem. int tries = 1; do { status = m_VdpOutputSurfaceCreate(m_Device, m_OutputSurfaceFormat, m_DisplayWidth, m_DisplayHeight, &m_OutputSurface[i]); if (status != VDP_STATUS_OK) { SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "VdpOutputSurfaceCreate() try #%d: %s", tries, m_VdpGetErrorString(status)); SDL_Delay(250); } } while (status == VDP_STATUS_RESOURCES && ++tries <= 10); if (status != VDP_STATUS_OK) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "VdpOutputSurfaceCreate() failed: %s", m_VdpGetErrorString(status)); return false; } } status = m_VdpPresentationQueueCreate(m_Device, m_PresentationQueueTarget, &m_PresentationQueue); if (status != VDP_STATUS_OK) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "VdpPresentationQueueCreate() failed: %s", m_VdpGetErrorString(status)); return false; } // Set the background to opaque black VdpColor color = {0.0, 0.0, 0.0, 1.0}; m_VdpPresentationQueueSetBackgroundColor(m_PresentationQueue, &color); return true; } bool VDPAURenderer::prepareDecoderContext(AVCodecContext* context) { context->hw_device_ctx = av_buffer_ref(m_HwContext); // Allow HEVC usage on VDPAU. This was disabled by FFmpeg due to // GL interop issues, but we use VDPAU for rendering so it's no issue. // https://github.com/FFmpeg/FFmpeg/commit/64ecb78b7179cab2dbdf835463104679dbb7c895 context->hwaccel_flags |= AV_HWACCEL_FLAG_ALLOW_PROFILE_MISMATCH; SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Using VDPAU accelerated renderer"); return true; } bool VDPAURenderer::needsTestFrame() { // We need a test frame to see if this VDPAU driver // supports the profile used for streaming return true; } void VDPAURenderer::renderFrameAtVsync(AVFrame* frame) { VdpStatus status; VdpVideoSurface videoSurface = (VdpVideoSurface)(uintptr_t)frame->data[3]; // This is safe without locking because this is always called on the main thread VdpOutputSurface chosenSurface = m_OutputSurface[m_NextSurfaceIndex]; m_NextSurfaceIndex = (m_NextSurfaceIndex + 1) % OUTPUT_SURFACE_COUNT; // We need to create the mixer on the fly, because we don't know the dimensions // of our video surfaces in advance of decoding if (m_VideoMixer == 0) { VdpChromaType videoSurfaceChroma; uint32_t videoSurfaceWidth, videoSurfaceHeight; status = m_VdpVideoSurfaceGetParameters(videoSurface, &videoSurfaceChroma, &videoSurfaceWidth, &videoSurfaceHeight); if (status != VDP_STATUS_OK) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "VdpVideoSurfaceGetParameters() failed: %s", m_VdpGetErrorString(status)); return; } SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "VDPAU surface size: %dx%d", videoSurfaceWidth, videoSurfaceHeight); #define PARAM_COUNT 3 const VdpVideoMixerParameter params[PARAM_COUNT] = { VDP_VIDEO_MIXER_PARAMETER_VIDEO_SURFACE_WIDTH, VDP_VIDEO_MIXER_PARAMETER_VIDEO_SURFACE_HEIGHT, VDP_VIDEO_MIXER_PARAMETER_CHROMA_TYPE, }; const void* const paramValues[PARAM_COUNT] = { &videoSurfaceWidth, &videoSurfaceHeight, &videoSurfaceChroma, }; status = m_VdpVideoMixerCreate(m_Device, 0, nullptr, PARAM_COUNT, params, paramValues, &m_VideoMixer); if (status != VDP_STATUS_OK) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "VdpVideoMixerCreate() failed: %s", m_VdpGetErrorString(status)); return; } } // Wait for this frame to be off the screen VdpTime pts; m_VdpPresentationQueueBlockUntilSurfaceIdle(m_PresentationQueue, chosenSurface, &pts); VdpRect sourceRect, outputRect; SDL_Rect src, dst; src.x = src.y = 0; src.w = m_VideoWidth; src.h = m_VideoHeight; dst.x = dst.y = 0; dst.w = m_DisplayWidth; dst.h = m_DisplayHeight; StreamUtils::scaleSourceToDestinationSurface(&src, &dst); outputRect.x0 = dst.x; outputRect.x1 = dst.x + dst.w; outputRect.y0 = dst.y; outputRect.y1 = dst.y + dst.h; sourceRect.x0 = sourceRect.y0 = 0; sourceRect.x1 = m_VideoWidth; sourceRect.y1 = m_VideoHeight; // Render the next frame into the output surface status = m_VdpVideoMixerRender(m_VideoMixer, VDP_INVALID_HANDLE, nullptr, VDP_VIDEO_MIXER_PICTURE_STRUCTURE_FRAME, 0, nullptr, videoSurface, 0, nullptr, &sourceRect, chosenSurface, &outputRect, nullptr, 0, nullptr); if (status != VDP_STATUS_OK) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "VdpVideoMixerRender() failed: %s", m_VdpGetErrorString(status)); return; } // Queue the frame for display immediately status = m_VdpPresentationQueueDisplay(m_PresentationQueue, chosenSurface, 0, 0, 0); if (status != VDP_STATUS_OK) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "VdpPresentationQueueDisplay() failed: %s", m_VdpGetErrorString(status)); return; } }