mirror of
https://github.com/RetroDECK/Duckstation.git
synced 2024-11-22 05:45:38 +00:00
Common: Add DRM display helper class and GBM GL context
This commit is contained in:
parent
b267020d07
commit
b09da307b5
|
@ -36,6 +36,9 @@ endif()
|
|||
if(LINUX OR ANDROID)
|
||||
option(USE_EGL "Support EGL OpenGL context creation" ON)
|
||||
endif()
|
||||
if(LINUX AND NOT ANDROID)
|
||||
option(USE_DRMKMS "Support DRM/KMS display and contexts" OFF)
|
||||
endif()
|
||||
|
||||
# Force EGL when using Wayland
|
||||
if(USE_WAYLAND)
|
||||
|
@ -112,6 +115,11 @@ if(USE_WAYLAND)
|
|||
find_package(Wayland REQUIRED Egl)
|
||||
message(STATUS "Wayland support enabled")
|
||||
endif()
|
||||
if(USE_DRMKMS)
|
||||
find_package(GBM REQUIRED)
|
||||
find_package(Libdrm REQUIRED)
|
||||
message(STATUS "DRM/KMS support enabled")
|
||||
endif()
|
||||
|
||||
# Set _DEBUG macro for Debug builds.
|
||||
set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -D_DEBUG")
|
||||
|
|
70
CMakeModules/FindGBM.cmake
Normal file
70
CMakeModules/FindGBM.cmake
Normal file
|
@ -0,0 +1,70 @@
|
|||
# https://fossies.org/linux/misc/xbmc-18.9-Leia.tar.gz/xbmc-18.9-Leia/cmake/modules/FindGBM.cmake?m=t
|
||||
|
||||
# FindGBM
|
||||
# ----------
|
||||
# Finds the GBM library
|
||||
#
|
||||
# This will define the following variables::
|
||||
#
|
||||
# GBM_FOUND - system has GBM
|
||||
# GBM_INCLUDE_DIRS - the GBM include directory
|
||||
# GBM_LIBRARIES - the GBM libraries
|
||||
# GBM_DEFINITIONS - the GBM definitions
|
||||
#
|
||||
# and the following imported targets::
|
||||
#
|
||||
# GBM::GBM - The GBM library
|
||||
|
||||
if(PKG_CONFIG_FOUND)
|
||||
pkg_check_modules(PC_GBM gbm QUIET)
|
||||
endif()
|
||||
|
||||
find_path(GBM_INCLUDE_DIR NAMES gbm.h
|
||||
PATHS ${PC_GBM_INCLUDEDIR})
|
||||
find_library(GBM_LIBRARY NAMES gbm
|
||||
PATHS ${PC_GBM_LIBDIR})
|
||||
|
||||
set(GBM_VERSION ${PC_GBM_VERSION})
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
find_package_handle_standard_args(GBM
|
||||
REQUIRED_VARS GBM_LIBRARY GBM_INCLUDE_DIR
|
||||
VERSION_VAR GBM_VERSION)
|
||||
|
||||
include(CheckCSourceCompiles)
|
||||
set(CMAKE_REQUIRED_LIBRARIES ${GBM_LIBRARY})
|
||||
check_c_source_compiles("#include <gbm.h>
|
||||
|
||||
int main()
|
||||
{
|
||||
gbm_bo_map(NULL, 0, 0, 0, 0, GBM_BO_TRANSFER_WRITE, NULL, NULL);
|
||||
}
|
||||
" GBM_HAS_BO_MAP)
|
||||
|
||||
check_c_source_compiles("#include <gbm.h>
|
||||
|
||||
int main()
|
||||
{
|
||||
gbm_surface_create_with_modifiers(NULL, 0, 0, 0, NULL, 0);
|
||||
}
|
||||
" GBM_HAS_MODIFIERS)
|
||||
|
||||
if(GBM_FOUND)
|
||||
set(GBM_LIBRARIES ${GBM_LIBRARY})
|
||||
set(GBM_INCLUDE_DIRS ${GBM_INCLUDE_DIR})
|
||||
set(GBM_DEFINITIONS -DHAVE_GBM=1)
|
||||
if(GBM_HAS_BO_MAP)
|
||||
list(APPEND GBM_DEFINITIONS -DHAS_GBM_BO_MAP=1)
|
||||
endif()
|
||||
if(GBM_HAS_MODIFIERS)
|
||||
list(APPEND GBM_DEFINITIONS -DHAS_GBM_MODIFIERS=1)
|
||||
endif()
|
||||
if(NOT TARGET GBM::GBM)
|
||||
add_library(GBM::GBM UNKNOWN IMPORTED)
|
||||
set_target_properties(GBM::GBM PROPERTIES
|
||||
IMPORTED_LOCATION "${GBM_LIBRARY}"
|
||||
INTERFACE_INCLUDE_DIRECTORIES "${GBM_INCLUDE_DIR}")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
mark_as_advanced(GBM_INCLUDE_DIR GBM_LIBRARY)
|
33
CMakeModules/FindLIBEVDEV.cmake
Normal file
33
CMakeModules/FindLIBEVDEV.cmake
Normal file
|
@ -0,0 +1,33 @@
|
|||
# - Try to find libevdev
|
||||
# Once done this will define
|
||||
# LIBEVDEV_FOUND - System has libevdev
|
||||
# LIBEVDEV_INCLUDE_DIRS - The libevdev include directories
|
||||
# LIBEVDEV_LIBRARIES - The libraries needed to use libevdev
|
||||
|
||||
find_package(PkgConfig)
|
||||
pkg_check_modules(PC_LIBEVDEV QUIET libevdev)
|
||||
|
||||
FIND_PATH(
|
||||
LIBEVDEV_INCLUDE_DIR libevdev/libevdev.h
|
||||
HINTS ${PC_LIBEVDEV_INCLUDEDIR} ${PC_LIBEVDEV_INCLUDE_DIRS}
|
||||
/usr/include
|
||||
/usr/local/include
|
||||
${LIBEVDEV_PATH_INCLUDES}
|
||||
)
|
||||
|
||||
FIND_LIBRARY(
|
||||
LIBEVDEV_LIBRARY
|
||||
NAMES evdev libevdev
|
||||
HINTS ${PC_LIBEVDEV_LIBDIR} ${PC_LIBEVDEV_LIBRARY_DIRS}
|
||||
PATHS ${ADDITIONAL_LIBRARY_PATHS}
|
||||
${LIBEVDEV_PATH_LIB}
|
||||
)
|
||||
|
||||
set(LIBEVDEV_LIBRARIES ${LIBEVDEV_LIBRARY} )
|
||||
set(LIBEVDEV_INCLUDE_DIRS ${LIBEVDEV_INCLUDE_DIR} )
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
find_package_handle_standard_args(LIBEVDEV DEFAULT_MSG
|
||||
LIBEVDEV_LIBRARY LIBEVDEV_INCLUDE_DIR)
|
||||
|
||||
mark_as_advanced(LIBEVDEV_INCLUDE_DIR LIBEVDEV_LIBRARY )
|
107
CMakeModules/FindLibdrm.cmake
Normal file
107
CMakeModules/FindLibdrm.cmake
Normal file
|
@ -0,0 +1,107 @@
|
|||
# https://raw.githubusercontent.com/KDE/kwin/master/cmake/modules/FindLibdrm.cmake
|
||||
|
||||
#.rst:
|
||||
# FindLibdrm
|
||||
# -------
|
||||
#
|
||||
# Try to find libdrm on a Unix system.
|
||||
#
|
||||
# This will define the following variables:
|
||||
#
|
||||
# ``Libdrm_FOUND``
|
||||
# True if (the requested version of) libdrm is available
|
||||
# ``Libdrm_VERSION``
|
||||
# The version of libdrm
|
||||
# ``Libdrm_LIBRARIES``
|
||||
# This can be passed to target_link_libraries() instead of the ``Libdrm::Libdrm``
|
||||
# target
|
||||
# ``Libdrm_INCLUDE_DIRS``
|
||||
# This should be passed to target_include_directories() if the target is not
|
||||
# used for linking
|
||||
# ``Libdrm_DEFINITIONS``
|
||||
# This should be passed to target_compile_options() if the target is not
|
||||
# used for linking
|
||||
#
|
||||
# If ``Libdrm_FOUND`` is TRUE, it will also define the following imported target:
|
||||
#
|
||||
# ``Libdrm::Libdrm``
|
||||
# The libdrm library
|
||||
#
|
||||
# In general we recommend using the imported target, as it is easier to use.
|
||||
# Bear in mind, however, that if the target is in the link interface of an
|
||||
# exported library, it must be made available by the package config file.
|
||||
|
||||
#=============================================================================
|
||||
# SPDX-FileCopyrightText: 2014 Alex Merry <alex.merry@kde.org>
|
||||
# SPDX-FileCopyrightText: 2014 Martin Gräßlin <mgraesslin@kde.org>
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
#=============================================================================
|
||||
|
||||
if(CMAKE_VERSION VERSION_LESS 2.8.12)
|
||||
message(FATAL_ERROR "CMake 2.8.12 is required by FindLibdrm.cmake")
|
||||
endif()
|
||||
if(CMAKE_MINIMUM_REQUIRED_VERSION VERSION_LESS 2.8.12)
|
||||
message(AUTHOR_WARNING "Your project should require at least CMake 2.8.12 to use FindLibdrm.cmake")
|
||||
endif()
|
||||
|
||||
if(NOT WIN32)
|
||||
# Use pkg-config to get the directories and then use these values
|
||||
# in the FIND_PATH() and FIND_LIBRARY() calls
|
||||
find_package(PkgConfig)
|
||||
pkg_check_modules(PKG_Libdrm QUIET libdrm)
|
||||
|
||||
set(Libdrm_DEFINITIONS ${PKG_Libdrm_CFLAGS_OTHER})
|
||||
set(Libdrm_VERSION ${PKG_Libdrm_VERSION})
|
||||
|
||||
find_path(Libdrm_INCLUDE_DIR
|
||||
NAMES
|
||||
xf86drm.h
|
||||
HINTS
|
||||
${PKG_Libdrm_INCLUDE_DIRS}
|
||||
)
|
||||
find_library(Libdrm_LIBRARY
|
||||
NAMES
|
||||
drm
|
||||
HINTS
|
||||
${PKG_Libdrm_LIBRARY_DIRS}
|
||||
)
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
find_package_handle_standard_args(Libdrm
|
||||
FOUND_VAR
|
||||
Libdrm_FOUND
|
||||
REQUIRED_VARS
|
||||
Libdrm_LIBRARY
|
||||
Libdrm_INCLUDE_DIR
|
||||
VERSION_VAR
|
||||
Libdrm_VERSION
|
||||
)
|
||||
|
||||
if(Libdrm_FOUND AND NOT TARGET Libdrm::Libdrm)
|
||||
add_library(Libdrm::Libdrm UNKNOWN IMPORTED)
|
||||
set_target_properties(Libdrm::Libdrm PROPERTIES
|
||||
IMPORTED_LOCATION "${Libdrm_LIBRARY}"
|
||||
INTERFACE_COMPILE_OPTIONS "${Libdrm_DEFINITIONS}"
|
||||
INTERFACE_INCLUDE_DIRECTORIES "${Libdrm_INCLUDE_DIR}"
|
||||
INTERFACE_INCLUDE_DIRECTORIES "${Libdrm_INCLUDE_DIR}/libdrm"
|
||||
)
|
||||
endif()
|
||||
|
||||
mark_as_advanced(Libdrm_LIBRARY Libdrm_INCLUDE_DIR)
|
||||
|
||||
# compatibility variables
|
||||
set(Libdrm_LIBRARIES ${Libdrm_LIBRARY})
|
||||
set(Libdrm_INCLUDE_DIRS ${Libdrm_INCLUDE_DIR} "${Libdrm_INCLUDE_DIR}/libdrm")
|
||||
set(Libdrm_VERSION_STRING ${Libdrm_VERSION})
|
||||
|
||||
else()
|
||||
message(STATUS "FindLibdrm.cmake cannot find libdrm on Windows systems.")
|
||||
set(Libdrm_FOUND FALSE)
|
||||
endif()
|
||||
|
||||
include(FeatureSummary)
|
||||
set_package_properties(Libdrm PROPERTIES
|
||||
URL "https://wiki.freedesktop.org/dri/"
|
||||
DESCRIPTION "Userspace interface to kernel DRM services."
|
||||
)
|
|
@ -142,6 +142,14 @@ if(USE_X11)
|
|||
target_link_libraries(common PRIVATE "${X11_LIBRARIES}")
|
||||
endif()
|
||||
|
||||
if(USE_DRMKMS)
|
||||
target_sources(common PRIVATE
|
||||
drm_display.cpp
|
||||
drm_display.h
|
||||
)
|
||||
target_link_libraries(common PUBLIC Libdrm::Libdrm)
|
||||
endif()
|
||||
|
||||
if(USE_EGL)
|
||||
target_sources(common PRIVATE
|
||||
gl/context_egl.cpp
|
||||
|
@ -161,6 +169,14 @@ if(USE_EGL)
|
|||
gl/context_egl_android.h
|
||||
)
|
||||
endif()
|
||||
if(USE_DRMKMS)
|
||||
target_compile_definitions(common PRIVATE "-DUSE_GBM=1")
|
||||
target_sources(common PRIVATE
|
||||
gl/context_egl_gbm.cpp
|
||||
gl/context_egl_gbm.h
|
||||
)
|
||||
target_link_libraries(common PUBLIC GBM::GBM)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(USE_X11)
|
||||
|
|
249
src/common/drm_display.cpp
Normal file
249
src/common/drm_display.cpp
Normal file
|
@ -0,0 +1,249 @@
|
|||
#include "drm_display.h"
|
||||
#include "common/assert.h"
|
||||
#include "common/log.h"
|
||||
#include "common/string.h"
|
||||
#include "file_system.h"
|
||||
#include <fcntl.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
Log_SetChannel(DRMDisplay);
|
||||
|
||||
DRMDisplay::DRMDisplay(int card /*= 1*/) : m_card_id(card) {}
|
||||
|
||||
DRMDisplay::~DRMDisplay()
|
||||
{
|
||||
if (m_connector)
|
||||
drmModeFreeConnector(m_connector);
|
||||
|
||||
if (m_card_fd >= 0)
|
||||
close(m_card_fd);
|
||||
}
|
||||
|
||||
// https://gist.github.com/Miouyouyou/89e9fe56a2c59bce7d4a18a858f389ef
|
||||
|
||||
static uint32_t find_crtc_for_encoder(const drmModeRes* resources, const drmModeEncoder* encoder)
|
||||
{
|
||||
int i;
|
||||
|
||||
for (i = 0; i < resources->count_crtcs; i++)
|
||||
{
|
||||
/* possible_crtcs is a bitmask as described here:
|
||||
* https://dvdhrm.wordpress.com/2012/09/13/linux-drm-mode-setting-api
|
||||
*/
|
||||
const uint32_t crtc_mask = 1 << i;
|
||||
const uint32_t crtc_id = resources->crtcs[i];
|
||||
if (encoder->possible_crtcs & crtc_mask)
|
||||
{
|
||||
return crtc_id;
|
||||
}
|
||||
}
|
||||
|
||||
/* no match found */
|
||||
return -1;
|
||||
}
|
||||
|
||||
static uint32_t find_crtc_for_connector(int card_fd, const drmModeRes* resources, const drmModeConnector* connector)
|
||||
{
|
||||
int i;
|
||||
|
||||
for (i = 0; i < connector->count_encoders; i++)
|
||||
{
|
||||
const uint32_t encoder_id = connector->encoders[i];
|
||||
drmModeEncoder* encoder = drmModeGetEncoder(card_fd, encoder_id);
|
||||
|
||||
if (encoder)
|
||||
{
|
||||
const uint32_t crtc_id = find_crtc_for_encoder(resources, encoder);
|
||||
|
||||
drmModeFreeEncoder(encoder);
|
||||
if (crtc_id != 0)
|
||||
{
|
||||
return crtc_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* no match found */
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool DRMDisplay::Initialize()
|
||||
{
|
||||
if (m_card_id < 0)
|
||||
{
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
if (TryOpeningCard(i))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return TryOpeningCard(m_card_id);
|
||||
}
|
||||
|
||||
bool DRMDisplay::TryOpeningCard(int card)
|
||||
{
|
||||
if (m_card_fd >= 0)
|
||||
close(m_card_fd);
|
||||
|
||||
m_card_fd = open(TinyString::FromFormat("/dev/dri/card%d", card), O_RDWR);
|
||||
if (m_card_fd < 0)
|
||||
{
|
||||
Log_ErrorPrintf("open(/dev/dri/card%d) failed: %d (%s)", card, errno, strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
drmModeRes* resources = drmModeGetResources(m_card_fd);
|
||||
if (!resources)
|
||||
{
|
||||
Log_ErrorPrintf("drmModeGetResources() failed: %d (%s)", errno, strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
Assert(!m_connector);
|
||||
|
||||
for (int i = 0; i < resources->count_connectors; i++)
|
||||
{
|
||||
drmModeConnector* next_connector = drmModeGetConnector(m_card_fd, resources->connectors[i]);
|
||||
if (next_connector->connection == DRM_MODE_CONNECTED)
|
||||
{
|
||||
m_connector = next_connector;
|
||||
break;
|
||||
}
|
||||
|
||||
drmModeFreeConnector(next_connector);
|
||||
}
|
||||
|
||||
if (!m_connector)
|
||||
{
|
||||
Log_ErrorPrintf("No connector found");
|
||||
drmModeFreeResources(resources);
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < m_connector->count_modes; i++)
|
||||
{
|
||||
drmModeModeInfo* next_mode = &m_connector->modes[i];
|
||||
if (next_mode->type & DRM_MODE_TYPE_PREFERRED)
|
||||
{
|
||||
m_mode = next_mode;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!m_mode || (next_mode->hdisplay * next_mode->vdisplay) > (m_mode->hdisplay * m_mode->vdisplay))
|
||||
{
|
||||
m_mode = next_mode;
|
||||
}
|
||||
}
|
||||
|
||||
if (!m_mode)
|
||||
{
|
||||
Log_ErrorPrintf("No mode found");
|
||||
drmModeFreeResources(resources);
|
||||
return false;
|
||||
}
|
||||
|
||||
drmModeEncoder* encoder = nullptr;
|
||||
for (int i = 0; i < resources->count_encoders; i++)
|
||||
{
|
||||
drmModeEncoder* next_encoder = drmModeGetEncoder(m_card_fd, resources->encoders[i]);
|
||||
if (next_encoder->encoder_id == m_connector->encoder_id)
|
||||
{
|
||||
encoder = next_encoder;
|
||||
m_crtc_id = encoder->crtc_id;
|
||||
break;
|
||||
}
|
||||
|
||||
drmModeFreeEncoder(next_encoder);
|
||||
}
|
||||
|
||||
if (encoder)
|
||||
{
|
||||
drmModeFreeEncoder(encoder);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_crtc_id = find_crtc_for_connector(m_card_fd, resources, m_connector);
|
||||
if (m_crtc_id == 0)
|
||||
{
|
||||
Log_ErrorPrintf("No CRTC found");
|
||||
drmModeFreeResources(resources);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
drmModeFreeResources(resources);
|
||||
|
||||
m_card_id = card;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::optional<u32> DRMDisplay::AddBuffer(u32 width, u32 height, u32 format, u32 handle, u32 pitch, u32 offset)
|
||||
{
|
||||
uint32_t bo_handles[4] = {handle, 0, 0, 0};
|
||||
uint32_t pitches[4] = {pitch, 0, 0, 0};
|
||||
uint32_t offsets[4] = {offset, 0, 0, 0};
|
||||
|
||||
u32 fb_id;
|
||||
int res = drmModeAddFB2(m_card_fd, width, height, format, bo_handles, pitches, offsets, &fb_id, 0);
|
||||
if (res != 0)
|
||||
{
|
||||
Log_ErrorPrintf("drmModeAddFB2() failed: %d", res);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return fb_id;
|
||||
}
|
||||
|
||||
void DRMDisplay::RemoveBuffer(u32 fb_id)
|
||||
{
|
||||
drmModeRmFB(m_card_fd, fb_id);
|
||||
}
|
||||
|
||||
void DRMDisplay::PresentBuffer(u32 fb_id, bool wait_for_vsync)
|
||||
{
|
||||
if (!wait_for_vsync)
|
||||
{
|
||||
u32 connector_id = m_connector->connector_id;
|
||||
int res = drmModeSetCrtc(m_card_fd, m_crtc_id, fb_id, 0, 0, &connector_id, 1, m_mode);
|
||||
if (res != 0)
|
||||
Log_ErrorPrintf("drmModeSetCrtc() failed: %d", res);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
bool waiting_for_flip = true;
|
||||
drmEventContext event_ctx = {};
|
||||
event_ctx.version = DRM_EVENT_CONTEXT_VERSION;
|
||||
event_ctx.page_flip_handler = [](int fd, unsigned int frame, unsigned int sec, unsigned int usec, void* data) {
|
||||
*reinterpret_cast<bool*>(data) = false;
|
||||
};
|
||||
|
||||
int res = drmModePageFlip(m_card_fd, m_crtc_id, fb_id, DRM_MODE_PAGE_FLIP_EVENT, &waiting_for_flip);
|
||||
if (res != 0)
|
||||
{
|
||||
Log_ErrorPrintf("drmModePageFlip() failed: %d", res);
|
||||
return;
|
||||
}
|
||||
|
||||
while (waiting_for_flip)
|
||||
{
|
||||
fd_set fds;
|
||||
FD_ZERO(&fds);
|
||||
FD_SET(m_card_fd, &fds);
|
||||
int res = select(m_card_fd + 1, &fds, nullptr, nullptr, nullptr);
|
||||
if (res < 0)
|
||||
{
|
||||
Log_ErrorPrintf("select() failed: %d", errno);
|
||||
break;
|
||||
}
|
||||
else if (res == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
drmHandleEvent(m_card_fd, &event_ctx);
|
||||
}
|
||||
}
|
38
src/common/drm_display.h
Normal file
38
src/common/drm_display.h
Normal file
|
@ -0,0 +1,38 @@
|
|||
#pragma once
|
||||
#include "core/types.h"
|
||||
#include <array>
|
||||
#include <optional>
|
||||
#include <xf86drm.h>
|
||||
#include <xf86drmMode.h>
|
||||
|
||||
class DRMDisplay
|
||||
{
|
||||
public:
|
||||
DRMDisplay(int card = -1);
|
||||
~DRMDisplay();
|
||||
|
||||
bool Initialize();
|
||||
|
||||
int GetCardID() const { return m_card_id; }
|
||||
int GetCardFD() const { return m_card_fd; }
|
||||
u32 GetWidth() const { return m_mode->hdisplay; }
|
||||
u32 GetHeight() const { return m_mode->vdisplay; }
|
||||
|
||||
std::optional<u32> AddBuffer(u32 width, u32 height, u32 format, u32 handle, u32 pitch, u32 offset);
|
||||
void RemoveBuffer(u32 fb_id);
|
||||
void PresentBuffer(u32 fb_id, bool wait_for_vsync);
|
||||
|
||||
private:
|
||||
enum : u32
|
||||
{
|
||||
MAX_BUFFERS = 5
|
||||
};
|
||||
|
||||
bool TryOpeningCard(int card);
|
||||
|
||||
int m_card_id = 0;
|
||||
int m_card_fd = -1;
|
||||
u32 m_crtc_id = 0;
|
||||
drmModeConnector* m_connector = nullptr;
|
||||
drmModeModeInfo* m_mode = nullptr;
|
||||
};
|
|
@ -11,18 +11,21 @@ Log_SetChannel(GL::Context);
|
|||
|
||||
#if defined(WIN32) && !defined(_M_ARM64)
|
||||
#include "context_wgl.h"
|
||||
#elif defined(__APPLE__)
|
||||
#elif defined(__APPLE__) && !defined(LIBERTRO)
|
||||
#include "context_agl.h"
|
||||
#endif
|
||||
|
||||
#ifdef USE_EGL
|
||||
#if defined(USE_X11) || defined(USE_WAYLAND)
|
||||
#if defined(USE_X11) || defined(USE_WAYLAND) || defined(USE_GBM)
|
||||
#if defined(USE_X11)
|
||||
#include "context_egl_x11.h"
|
||||
#endif
|
||||
#if defined(USE_WAYLAND)
|
||||
#include "context_egl_wayland.h"
|
||||
#endif
|
||||
#if defined(USE_GBM)
|
||||
#include "context_egl_gbm.h"
|
||||
#endif
|
||||
#elif defined(ANDROID)
|
||||
#include "context_egl_android.h"
|
||||
#else
|
||||
|
@ -77,7 +80,7 @@ std::unique_ptr<GL::Context> Context::Create(const WindowInfo& wi, const Version
|
|||
std::unique_ptr<Context> context;
|
||||
#if defined(WIN32) && !defined(_M_ARM64)
|
||||
context = ContextWGL::Create(wi, versions_to_try, num_versions_to_try);
|
||||
#elif defined(__APPLE__)
|
||||
#elif defined(__APPLE__) && !defined(LIBRETRO)
|
||||
context = ContextAGL::Create(wi, versions_to_try, num_versions_to_try);
|
||||
#elif defined(ANDROID)
|
||||
#ifdef USE_EGL
|
||||
|
@ -105,6 +108,11 @@ std::unique_ptr<GL::Context> Context::Create(const WindowInfo& wi, const Version
|
|||
context = ContextEGLWayland::Create(wi, versions_to_try, num_versions_to_try);
|
||||
#endif
|
||||
|
||||
#if defined(USE_GBM)
|
||||
if (wi.type == WindowInfo::Type::DRM)
|
||||
context = ContextEGLGBM::Create(wi, versions_to_try, num_versions_to_try);
|
||||
#endif
|
||||
|
||||
if (!context)
|
||||
return nullptr;
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
#include "context_egl.h"
|
||||
#include "../assert.h"
|
||||
#include "../log.h"
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
Log_SetChannel(GL::ContextEGL);
|
||||
|
||||
namespace GL {
|
||||
|
@ -33,12 +35,8 @@ bool ContextEGL::Initialize(const Version* versions_to_try, size_t num_versions_
|
|||
return false;
|
||||
}
|
||||
|
||||
m_display = eglGetDisplay(static_cast<EGLNativeDisplayType>(m_wi.display_connection));
|
||||
if (!m_display)
|
||||
{
|
||||
Log_ErrorPrintf("eglGetDisplay() failed: %d", eglGetError());
|
||||
if (!SetDisplay())
|
||||
return false;
|
||||
}
|
||||
|
||||
int egl_major, egl_minor;
|
||||
if (!eglInitialize(m_display, &egl_major, &egl_minor))
|
||||
|
@ -66,6 +64,18 @@ bool ContextEGL::Initialize(const Version* versions_to_try, size_t num_versions_
|
|||
return false;
|
||||
}
|
||||
|
||||
bool ContextEGL::SetDisplay()
|
||||
{
|
||||
m_display = eglGetDisplay(static_cast<EGLNativeDisplayType>(m_wi.display_connection));
|
||||
if (!m_display)
|
||||
{
|
||||
Log_ErrorPrintf("eglGetDisplay() failed: %d", eglGetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void* ContextEGL::GetProcAddress(const char* name)
|
||||
{
|
||||
return reinterpret_cast<void*>(eglGetProcAddress(name));
|
||||
|
@ -216,6 +226,36 @@ bool ContextEGL::CreatePBufferSurface()
|
|||
return true;
|
||||
}
|
||||
|
||||
bool ContextEGL::CheckConfigSurfaceFormat(EGLConfig config, WindowInfo::SurfaceFormat format) const
|
||||
{
|
||||
int red_size, green_size, blue_size, alpha_size;
|
||||
if (!eglGetConfigAttrib(m_display, config, EGL_RED_SIZE, &red_size) ||
|
||||
!eglGetConfigAttrib(m_display, config, EGL_GREEN_SIZE, &green_size) ||
|
||||
!eglGetConfigAttrib(m_display, config, EGL_BLUE_SIZE, &blue_size) ||
|
||||
!eglGetConfigAttrib(m_display, config, EGL_ALPHA_SIZE, &alpha_size))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (format)
|
||||
{
|
||||
case WindowInfo::SurfaceFormat::Auto:
|
||||
return true;
|
||||
|
||||
case WindowInfo::SurfaceFormat::RGB8:
|
||||
return (red_size == 8 && green_size == 8 && blue_size == 8);
|
||||
|
||||
case WindowInfo::SurfaceFormat::RGBA8:
|
||||
return (red_size == 8 && green_size == 8 && blue_size == 8 && alpha_size == 8);
|
||||
|
||||
case WindowInfo::SurfaceFormat::RGB565:
|
||||
return (red_size == 5 && green_size == 6 && blue_size == 5);
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool ContextEGL::CreateContext(const Version& version, EGLContext share_context)
|
||||
{
|
||||
Log_DevPrintf(
|
||||
|
@ -263,6 +303,9 @@ bool ContextEGL::CreateContext(const Version& version, EGLContext share_context)
|
|||
surface_attribs[nsurface_attribs++] = 5;
|
||||
break;
|
||||
|
||||
case WindowInfo::SurfaceFormat::Auto:
|
||||
break;
|
||||
|
||||
default:
|
||||
UnreachableCode();
|
||||
break;
|
||||
|
@ -272,13 +315,36 @@ bool ContextEGL::CreateContext(const Version& version, EGLContext share_context)
|
|||
surface_attribs[nsurface_attribs++] = 0;
|
||||
|
||||
EGLint num_configs;
|
||||
EGLConfig config;
|
||||
if (!eglChooseConfig(m_display, surface_attribs, &config, 1, &num_configs) || num_configs == 0)
|
||||
if (!eglChooseConfig(m_display, surface_attribs, nullptr, 0, &num_configs) || num_configs == 0)
|
||||
{
|
||||
Log_ErrorPrintf("eglChooseConfig() failed: %d", eglGetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<EGLConfig> configs(static_cast<u32>(num_configs));
|
||||
if (!eglChooseConfig(m_display, surface_attribs, configs.data(), num_configs, &num_configs))
|
||||
{
|
||||
Log_ErrorPrintf("eglChooseConfig() failed: %d", eglGetError());
|
||||
return false;
|
||||
}
|
||||
configs.resize(static_cast<u32>(num_configs));
|
||||
|
||||
std::optional<EGLConfig> config;
|
||||
for (EGLConfig check_config : configs)
|
||||
{
|
||||
if (CheckConfigSurfaceFormat(check_config, m_wi.surface_format))
|
||||
{
|
||||
config = check_config;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!config.has_value())
|
||||
{
|
||||
Log_WarningPrintf("No EGL configs matched exactly, using first.");
|
||||
config = configs.front();
|
||||
}
|
||||
|
||||
int attribs[8];
|
||||
int nattribs = 0;
|
||||
if (version.profile != Profile::NoProfile)
|
||||
|
@ -297,7 +363,7 @@ bool ContextEGL::CreateContext(const Version& version, EGLContext share_context)
|
|||
return false;
|
||||
}
|
||||
|
||||
m_context = eglCreateContext(m_display, config, share_context, attribs);
|
||||
m_context = eglCreateContext(m_display, config.value(), share_context, attribs);
|
||||
if (!m_context)
|
||||
{
|
||||
Log_ErrorPrintf("eglCreateContext() failed: %d", eglGetError());
|
||||
|
@ -308,7 +374,7 @@ bool ContextEGL::CreateContext(const Version& version, EGLContext share_context)
|
|||
"Got version %u.%u (%s)", version.major_version, version.minor_version,
|
||||
version.profile == Context::Profile::ES ? "ES" : (version.profile == Context::Profile::Core ? "Core" : "None"));
|
||||
|
||||
m_config = config;
|
||||
m_config = config.value();
|
||||
m_version = version;
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ public:
|
|||
virtual std::unique_ptr<Context> CreateSharedContext(const WindowInfo& wi) override;
|
||||
|
||||
protected:
|
||||
virtual bool SetDisplay();
|
||||
virtual EGLNativeWindowType GetNativeWindow(EGLConfig config);
|
||||
|
||||
bool Initialize(const Version* versions_to_try, size_t num_versions_to_try);
|
||||
|
@ -31,6 +32,7 @@ protected:
|
|||
bool CreateContextAndSurface(const Version& version, EGLContext share_context, bool make_current);
|
||||
bool CreateSurface();
|
||||
bool CreatePBufferSurface();
|
||||
bool CheckConfigSurfaceFormat(EGLConfig config, WindowInfo::SurfaceFormat format) const;
|
||||
|
||||
EGLDisplay m_display = EGL_NO_DISPLAY;
|
||||
EGLSurface m_surface = EGL_NO_SURFACE;
|
||||
|
|
253
src/common/gl/context_egl_gbm.cpp
Normal file
253
src/common/gl/context_egl_gbm.cpp
Normal file
|
@ -0,0 +1,253 @@
|
|||
#include "context_egl_gbm.h"
|
||||
#include "../assert.h"
|
||||
#include "../log.h"
|
||||
#include <drm.h>
|
||||
#include <drm_fourcc.h>
|
||||
#include <gbm.h>
|
||||
Log_SetChannel(GL::ContextEGLGBM);
|
||||
|
||||
namespace GL {
|
||||
ContextEGLGBM::ContextEGLGBM(const WindowInfo& wi) : ContextEGL(wi)
|
||||
{
|
||||
#ifdef CONTEXT_EGL_GBM_USE_PRESENT_THREAD
|
||||
StartPresentThread();
|
||||
#endif
|
||||
}
|
||||
|
||||
ContextEGLGBM::~ContextEGLGBM()
|
||||
{
|
||||
#ifdef CONTEXT_EGL_GBM_USE_PRESENT_THREAD
|
||||
StopPresentThread();
|
||||
Assert(!m_current_present_buffer);
|
||||
#endif
|
||||
|
||||
while (m_num_buffers > 0)
|
||||
{
|
||||
Buffer& buffer = m_buffers[--m_num_buffers];
|
||||
GetDisplay()->RemoveBuffer(buffer.fb_id);
|
||||
}
|
||||
|
||||
if (m_fb_surface)
|
||||
gbm_surface_destroy(m_fb_surface);
|
||||
|
||||
if (m_gbm_device)
|
||||
gbm_device_destroy(m_gbm_device);
|
||||
}
|
||||
|
||||
std::unique_ptr<Context> ContextEGLGBM::Create(const WindowInfo& wi, const Version* versions_to_try,
|
||||
size_t num_versions_to_try)
|
||||
{
|
||||
std::unique_ptr<ContextEGLGBM> context = std::make_unique<ContextEGLGBM>(wi);
|
||||
if (!context->CreateGBMDevice() || !context->Initialize(versions_to_try, num_versions_to_try))
|
||||
return nullptr;
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
std::unique_ptr<Context> ContextEGLGBM::CreateSharedContext(const WindowInfo& wi)
|
||||
{
|
||||
std::unique_ptr<ContextEGLGBM> context = std::make_unique<ContextEGLGBM>(wi);
|
||||
context->m_display = m_display;
|
||||
|
||||
if (!context->CreateContextAndSurface(m_version, m_context, false))
|
||||
return nullptr;
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
void ContextEGLGBM::ResizeSurface(u32 new_surface_width, u32 new_surface_height)
|
||||
{
|
||||
ContextEGL::ResizeSurface(new_surface_width, new_surface_height);
|
||||
}
|
||||
|
||||
bool ContextEGLGBM::CreateGBMDevice()
|
||||
{
|
||||
Assert(!m_gbm_device);
|
||||
m_gbm_device = gbm_create_device(GetDisplay()->GetCardFD());
|
||||
if (!m_gbm_device)
|
||||
{
|
||||
Log_ErrorPrintf("gbm_create_device() failed: %d", errno);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ContextEGLGBM::SetDisplay()
|
||||
{
|
||||
if (!eglGetPlatformDisplayEXT)
|
||||
{
|
||||
Log_ErrorPrintf("eglGetPlatformDisplayEXT() not loaded");
|
||||
return false;
|
||||
}
|
||||
|
||||
m_display = eglGetPlatformDisplayEXT(EGL_PLATFORM_GBM_KHR, m_gbm_device, nullptr);
|
||||
if (!m_display)
|
||||
{
|
||||
Log_ErrorPrintf("eglGetPlatformDisplayEXT() failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
EGLNativeWindowType ContextEGLGBM::GetNativeWindow(EGLConfig config)
|
||||
{
|
||||
EGLint visual_id;
|
||||
eglGetConfigAttrib(m_display, config, EGL_NATIVE_VISUAL_ID, &visual_id);
|
||||
|
||||
Assert(!m_fb_surface);
|
||||
m_fb_surface = gbm_surface_create(m_gbm_device, GetDisplay()->GetWidth(), GetDisplay()->GetHeight(),
|
||||
static_cast<u32>(visual_id), GBM_BO_USE_RENDERING | GBM_BO_USE_SCANOUT);
|
||||
if (!m_fb_surface)
|
||||
{
|
||||
Log_ErrorPrintf("gbm_surface_create() failed: %d", errno);
|
||||
return {};
|
||||
}
|
||||
|
||||
return (EGLNativeWindowType)((void*)m_fb_surface);
|
||||
}
|
||||
|
||||
ContextEGLGBM::Buffer* ContextEGLGBM::LockFrontBuffer()
|
||||
{
|
||||
struct gbm_bo* bo = gbm_surface_lock_front_buffer(m_fb_surface);
|
||||
|
||||
Buffer* buffer = nullptr;
|
||||
for (u32 i = 0; i < m_num_buffers; i++)
|
||||
{
|
||||
if (m_buffers[i].bo == bo)
|
||||
{
|
||||
buffer = &m_buffers[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!buffer)
|
||||
{
|
||||
// haven't tracked this buffer yet
|
||||
Assert(m_num_buffers < MAX_BUFFERS);
|
||||
|
||||
const u32 width = gbm_bo_get_width(bo);
|
||||
const u32 height = gbm_bo_get_height(bo);
|
||||
const u32 stride = gbm_bo_get_stride(bo);
|
||||
const u32 format = gbm_bo_get_format(bo);
|
||||
const u32 handle = gbm_bo_get_handle(bo).u32;
|
||||
|
||||
std::optional<u32> fb_id = GetDisplay()->AddBuffer(width, height, format, handle, stride, 0);
|
||||
if (!fb_id.has_value())
|
||||
return nullptr;
|
||||
|
||||
buffer = &m_buffers[m_num_buffers];
|
||||
buffer->bo = bo;
|
||||
buffer->fb_id = fb_id.value();
|
||||
m_num_buffers++;
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
void ContextEGLGBM::ReleaseBuffer(Buffer* buffer)
|
||||
{
|
||||
gbm_surface_release_buffer(m_fb_surface, buffer->bo);
|
||||
}
|
||||
|
||||
void ContextEGLGBM::PresentBuffer(Buffer* buffer, bool wait_for_vsync)
|
||||
{
|
||||
GetDisplay()->PresentBuffer(buffer->fb_id, wait_for_vsync);
|
||||
}
|
||||
|
||||
bool ContextEGLGBM::SwapBuffers()
|
||||
{
|
||||
if (!ContextEGL::SwapBuffers())
|
||||
return false;
|
||||
|
||||
#ifdef CONTEXT_EGL_GBM_USE_PRESENT_THREAD
|
||||
std::unique_lock lock(m_present_mutex);
|
||||
m_present_pending.store(true);
|
||||
m_present_cv.notify_one();
|
||||
if (m_vsync)
|
||||
m_present_done_cv.wait(lock, [this]() { return !m_present_pending.load(); });
|
||||
#else
|
||||
Buffer* front_buffer = LockFrontBuffer();
|
||||
if (!front_buffer)
|
||||
return false;
|
||||
|
||||
PresentSurface(front_buffer, m_vsync && m_last_front_buffer);
|
||||
|
||||
if (m_last_front_buffer)
|
||||
ReleaseBuffer(m_last_front_buffer);
|
||||
|
||||
m_last_front_buffer = front_buffer;
|
||||
#endif
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ContextEGLGBM::SetSwapInterval(s32 interval)
|
||||
{
|
||||
if (interval < 0 || interval > 1)
|
||||
return false;
|
||||
|
||||
std::unique_lock lock(m_present_mutex);
|
||||
m_vsync = (interval > 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
#ifdef CONTEXT_EGL_GBM_USE_PRESENT_THREAD
|
||||
|
||||
void ContextEGLGBM::StartPresentThread()
|
||||
{
|
||||
m_present_thread_shutdown.store(false);
|
||||
m_present_thread = std::thread(&ContextEGLGBM::PresentThread, this);
|
||||
}
|
||||
|
||||
void ContextEGLGBM::StopPresentThread()
|
||||
{
|
||||
if (!m_present_thread.joinable())
|
||||
return;
|
||||
|
||||
{
|
||||
std::unique_lock lock(m_present_mutex);
|
||||
m_present_thread_shutdown.store(true);
|
||||
m_present_cv.notify_one();
|
||||
}
|
||||
|
||||
m_present_thread.join();
|
||||
}
|
||||
|
||||
void ContextEGLGBM::PresentThread()
|
||||
{
|
||||
std::unique_lock lock(m_present_mutex);
|
||||
|
||||
while (!m_present_thread_shutdown.load())
|
||||
{
|
||||
m_present_cv.wait(lock);
|
||||
|
||||
if (!m_present_pending.load())
|
||||
continue;
|
||||
|
||||
Buffer* next_buffer = LockFrontBuffer();
|
||||
const bool wait_for_vsync = m_vsync && m_current_present_buffer;
|
||||
|
||||
lock.unlock();
|
||||
PresentBuffer(next_buffer, wait_for_vsync);
|
||||
lock.lock();
|
||||
|
||||
if (m_current_present_buffer)
|
||||
ReleaseBuffer(m_current_present_buffer);
|
||||
|
||||
m_current_present_buffer = next_buffer;
|
||||
m_present_pending.store(false);
|
||||
m_present_done_cv.notify_one();
|
||||
}
|
||||
|
||||
if (m_current_present_buffer)
|
||||
{
|
||||
ReleaseBuffer(m_current_present_buffer);
|
||||
m_current_present_buffer = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
} // namespace GL
|
76
src/common/gl/context_egl_gbm.h
Normal file
76
src/common/gl/context_egl_gbm.h
Normal file
|
@ -0,0 +1,76 @@
|
|||
#pragma once
|
||||
#include "../drm_display.h"
|
||||
#include "context_egl.h"
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <gbm.h>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
|
||||
#define CONTEXT_EGL_GBM_USE_PRESENT_THREAD 1
|
||||
|
||||
namespace GL {
|
||||
|
||||
class ContextEGLGBM final : public ContextEGL
|
||||
{
|
||||
public:
|
||||
ContextEGLGBM(const WindowInfo& wi);
|
||||
~ContextEGLGBM() override;
|
||||
|
||||
static std::unique_ptr<Context> Create(const WindowInfo& wi, const Version* versions_to_try,
|
||||
size_t num_versions_to_try);
|
||||
|
||||
std::unique_ptr<Context> CreateSharedContext(const WindowInfo& wi) override;
|
||||
void ResizeSurface(u32 new_surface_width = 0, u32 new_surface_height = 0) override;
|
||||
|
||||
bool SwapBuffers() override;
|
||||
bool SetSwapInterval(s32 interval) override;
|
||||
|
||||
protected:
|
||||
bool SetDisplay() override;
|
||||
EGLNativeWindowType GetNativeWindow(EGLConfig config) override;
|
||||
|
||||
private:
|
||||
enum : u32
|
||||
{
|
||||
MAX_BUFFERS = 5
|
||||
};
|
||||
|
||||
struct Buffer
|
||||
{
|
||||
struct gbm_bo* bo;
|
||||
u32 fb_id;
|
||||
};
|
||||
|
||||
DRMDisplay* GetDisplay() { return static_cast<DRMDisplay*>(m_wi.display_connection); }
|
||||
|
||||
bool CreateGBMDevice();
|
||||
Buffer* LockFrontBuffer();
|
||||
void ReleaseBuffer(Buffer* buffer);
|
||||
void PresentBuffer(Buffer* buffer, bool wait_for_vsync);
|
||||
|
||||
void StartPresentThread();
|
||||
void StopPresentThread();
|
||||
void PresentThread();
|
||||
|
||||
bool m_vsync = true;
|
||||
|
||||
struct gbm_device* m_gbm_device = nullptr;
|
||||
struct gbm_surface* m_fb_surface = nullptr;
|
||||
|
||||
#ifdef CONTEXT_EGL_GBM_USE_PRESENT_THREAD
|
||||
std::thread m_present_thread;
|
||||
std::mutex m_present_mutex;
|
||||
std::condition_variable m_present_cv;
|
||||
std::atomic_bool m_present_pending{false};
|
||||
std::atomic_bool m_present_thread_shutdown{false};
|
||||
std::condition_variable m_present_done_cv;
|
||||
|
||||
Buffer* m_current_present_buffer = nullptr;
|
||||
#endif
|
||||
|
||||
u32 m_num_buffers = 0;
|
||||
std::array<Buffer, MAX_BUFFERS> m_buffers{};
|
||||
};
|
||||
|
||||
} // namespace GL
|
|
@ -221,6 +221,9 @@ bool ContextGLX::CreateWindow(int screen)
|
|||
attribs[nattribs++] = 5;
|
||||
break;
|
||||
|
||||
case WindowInfo::SurfaceFormat::Auto:
|
||||
break;
|
||||
|
||||
default:
|
||||
UnreachableCode();
|
||||
break;
|
||||
|
|
|
@ -204,6 +204,9 @@ bool ContextWGL::InitializeDC()
|
|||
pfd.cBlueBits = 5;
|
||||
break;
|
||||
|
||||
case WindowInfo::SurfaceFormat::Auto:
|
||||
break;
|
||||
|
||||
default:
|
||||
UnreachableCode();
|
||||
break;
|
||||
|
|
|
@ -12,11 +12,13 @@ struct WindowInfo
|
|||
Wayland,
|
||||
MacOS,
|
||||
Android,
|
||||
DRM,
|
||||
};
|
||||
|
||||
enum class SurfaceFormat
|
||||
{
|
||||
None,
|
||||
Auto,
|
||||
RGB8,
|
||||
RGBA8,
|
||||
RGB565,
|
||||
|
|
Loading…
Reference in a new issue