mirror of
https://github.com/hyprwm/hyprlock.git
synced 2024-11-16 23:05:58 +01:00
background: add screenshot
This commit is contained in:
parent
3b7e055dcf
commit
2836f02ded
11 changed files with 779 additions and 17 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,3 +1,4 @@
|
|||
.vscode/
|
||||
build/
|
||||
protocols/
|
||||
protocols/*.h
|
||||
protocols/*.c
|
|
@ -33,7 +33,7 @@ message(STATUS "Checking deps...")
|
|||
find_package(Threads REQUIRED)
|
||||
find_package(PkgConfig REQUIRED)
|
||||
find_package(OpenGL REQUIRED)
|
||||
pkg_check_modules(deps REQUIRED IMPORTED_TARGET wayland-client wayland-protocols wayland-egl hyprlang>=0.4.0 egl opengl xkbcommon cairo pangocairo pam)
|
||||
pkg_check_modules(deps REQUIRED IMPORTED_TARGET wayland-client wayland-protocols wayland-egl hyprlang>=0.4.0 egl opengl xkbcommon cairo pangocairo pam libdrm gbm)
|
||||
|
||||
file(GLOB_RECURSE SRCFILES CONFIGURE_DEPENDS "src/*.cpp")
|
||||
add_executable(hyprlock ${SRCFILES})
|
||||
|
@ -75,6 +75,8 @@ protocol("staging/cursor-shape/cursor-shape-v1.xml" "cursor-shape-v1" false)
|
|||
protocol("unstable/tablet/tablet-unstable-v2.xml" "tablet-unstable-v2" false)
|
||||
protocol("staging/fractional-scale/fractional-scale-v1.xml" "fractional-scale-v1" false)
|
||||
protocol("stable/viewporter/viewporter.xml" "viewporter" false)
|
||||
protocol("protocols/wlr-screencopy-unstable-v1.xml" "wlr-screencopy-unstable-v1" true)
|
||||
protocol("unstable/linux-dmabuf/linux-dmabuf-unstable-v1.xml" "linux-dmabuf-unstable-v1" false)
|
||||
|
||||
# Installation
|
||||
install(TARGETS hyprlock)
|
||||
|
|
232
protocols/wlr-screencopy-unstable-v1.xml
Normal file
232
protocols/wlr-screencopy-unstable-v1.xml
Normal file
|
@ -0,0 +1,232 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<protocol name="wlr_screencopy_unstable_v1">
|
||||
<copyright>
|
||||
Copyright © 2018 Simon Ser
|
||||
Copyright © 2019 Andri Yngvason
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice (including the next
|
||||
paragraph) shall be included in all copies or substantial portions of the
|
||||
Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
</copyright>
|
||||
|
||||
<description summary="screen content capturing on client buffers">
|
||||
This protocol allows clients to ask the compositor to copy part of the
|
||||
screen content to a client buffer.
|
||||
|
||||
Warning! The protocol described in this file is experimental and
|
||||
backward incompatible changes may be made. Backward compatible changes
|
||||
may be added together with the corresponding interface version bump.
|
||||
Backward incompatible changes are done by bumping the version number in
|
||||
the protocol and interface names and resetting the interface version.
|
||||
Once the protocol is to be declared stable, the 'z' prefix and the
|
||||
version number in the protocol and interface names are removed and the
|
||||
interface version number is reset.
|
||||
</description>
|
||||
|
||||
<interface name="zwlr_screencopy_manager_v1" version="3">
|
||||
<description summary="manager to inform clients and begin capturing">
|
||||
This object is a manager which offers requests to start capturing from a
|
||||
source.
|
||||
</description>
|
||||
|
||||
<request name="capture_output">
|
||||
<description summary="capture an output">
|
||||
Capture the next frame of an entire output.
|
||||
</description>
|
||||
<arg name="frame" type="new_id" interface="zwlr_screencopy_frame_v1"/>
|
||||
<arg name="overlay_cursor" type="int"
|
||||
summary="composite cursor onto the frame"/>
|
||||
<arg name="output" type="object" interface="wl_output"/>
|
||||
</request>
|
||||
|
||||
<request name="capture_output_region">
|
||||
<description summary="capture an output's region">
|
||||
Capture the next frame of an output's region.
|
||||
|
||||
The region is given in output logical coordinates, see
|
||||
xdg_output.logical_size. The region will be clipped to the output's
|
||||
extents.
|
||||
</description>
|
||||
<arg name="frame" type="new_id" interface="zwlr_screencopy_frame_v1"/>
|
||||
<arg name="overlay_cursor" type="int"
|
||||
summary="composite cursor onto the frame"/>
|
||||
<arg name="output" type="object" interface="wl_output"/>
|
||||
<arg name="x" type="int"/>
|
||||
<arg name="y" type="int"/>
|
||||
<arg name="width" type="int"/>
|
||||
<arg name="height" type="int"/>
|
||||
</request>
|
||||
|
||||
<request name="destroy" type="destructor">
|
||||
<description summary="destroy the manager">
|
||||
All objects created by the manager will still remain valid, until their
|
||||
appropriate destroy request has been called.
|
||||
</description>
|
||||
</request>
|
||||
</interface>
|
||||
|
||||
<interface name="zwlr_screencopy_frame_v1" version="3">
|
||||
<description summary="a frame ready for copy">
|
||||
This object represents a single frame.
|
||||
|
||||
When created, a series of buffer events will be sent, each representing a
|
||||
supported buffer type. The "buffer_done" event is sent afterwards to
|
||||
indicate that all supported buffer types have been enumerated. The client
|
||||
will then be able to send a "copy" request. If the capture is successful,
|
||||
the compositor will send a "flags" followed by a "ready" event.
|
||||
|
||||
For objects version 2 or lower, wl_shm buffers are always supported, ie.
|
||||
the "buffer" event is guaranteed to be sent.
|
||||
|
||||
If the capture failed, the "failed" event is sent. This can happen anytime
|
||||
before the "ready" event.
|
||||
|
||||
Once either a "ready" or a "failed" event is received, the client should
|
||||
destroy the frame.
|
||||
</description>
|
||||
|
||||
<event name="buffer">
|
||||
<description summary="wl_shm buffer information">
|
||||
Provides information about wl_shm buffer parameters that need to be
|
||||
used for this frame. This event is sent once after the frame is created
|
||||
if wl_shm buffers are supported.
|
||||
</description>
|
||||
<arg name="format" type="uint" enum="wl_shm.format" summary="buffer format"/>
|
||||
<arg name="width" type="uint" summary="buffer width"/>
|
||||
<arg name="height" type="uint" summary="buffer height"/>
|
||||
<arg name="stride" type="uint" summary="buffer stride"/>
|
||||
</event>
|
||||
|
||||
<request name="copy">
|
||||
<description summary="copy the frame">
|
||||
Copy the frame to the supplied buffer. The buffer must have a the
|
||||
correct size, see zwlr_screencopy_frame_v1.buffer and
|
||||
zwlr_screencopy_frame_v1.linux_dmabuf. The buffer needs to have a
|
||||
supported format.
|
||||
|
||||
If the frame is successfully copied, a "flags" and a "ready" events are
|
||||
sent. Otherwise, a "failed" event is sent.
|
||||
</description>
|
||||
<arg name="buffer" type="object" interface="wl_buffer"/>
|
||||
</request>
|
||||
|
||||
<enum name="error">
|
||||
<entry name="already_used" value="0"
|
||||
summary="the object has already been used to copy a wl_buffer"/>
|
||||
<entry name="invalid_buffer" value="1"
|
||||
summary="buffer attributes are invalid"/>
|
||||
</enum>
|
||||
|
||||
<enum name="flags" bitfield="true">
|
||||
<entry name="y_invert" value="1" summary="contents are y-inverted"/>
|
||||
</enum>
|
||||
|
||||
<event name="flags">
|
||||
<description summary="frame flags">
|
||||
Provides flags about the frame. This event is sent once before the
|
||||
"ready" event.
|
||||
</description>
|
||||
<arg name="flags" type="uint" enum="flags" summary="frame flags"/>
|
||||
</event>
|
||||
|
||||
<event name="ready">
|
||||
<description summary="indicates frame is available for reading">
|
||||
Called as soon as the frame is copied, indicating it is available
|
||||
for reading. This event includes the time at which presentation happened
|
||||
at.
|
||||
|
||||
The timestamp is expressed as tv_sec_hi, tv_sec_lo, tv_nsec triples,
|
||||
each component being an unsigned 32-bit value. Whole seconds are in
|
||||
tv_sec which is a 64-bit value combined from tv_sec_hi and tv_sec_lo,
|
||||
and the additional fractional part in tv_nsec as nanoseconds. Hence,
|
||||
for valid timestamps tv_nsec must be in [0, 999999999]. The seconds part
|
||||
may have an arbitrary offset at start.
|
||||
|
||||
After receiving this event, the client should destroy the object.
|
||||
</description>
|
||||
<arg name="tv_sec_hi" type="uint"
|
||||
summary="high 32 bits of the seconds part of the timestamp"/>
|
||||
<arg name="tv_sec_lo" type="uint"
|
||||
summary="low 32 bits of the seconds part of the timestamp"/>
|
||||
<arg name="tv_nsec" type="uint"
|
||||
summary="nanoseconds part of the timestamp"/>
|
||||
</event>
|
||||
|
||||
<event name="failed">
|
||||
<description summary="frame copy failed">
|
||||
This event indicates that the attempted frame copy has failed.
|
||||
|
||||
After receiving this event, the client should destroy the object.
|
||||
</description>
|
||||
</event>
|
||||
|
||||
<request name="destroy" type="destructor">
|
||||
<description summary="delete this object, used or not">
|
||||
Destroys the frame. This request can be sent at any time by the client.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<!-- Version 2 additions -->
|
||||
<request name="copy_with_damage" since="2">
|
||||
<description summary="copy the frame when it's damaged">
|
||||
Same as copy, except it waits until there is damage to copy.
|
||||
</description>
|
||||
<arg name="buffer" type="object" interface="wl_buffer"/>
|
||||
</request>
|
||||
|
||||
<event name="damage" since="2">
|
||||
<description summary="carries the coordinates of the damaged region">
|
||||
This event is sent right before the ready event when copy_with_damage is
|
||||
requested. It may be generated multiple times for each copy_with_damage
|
||||
request.
|
||||
|
||||
The arguments describe a box around an area that has changed since the
|
||||
last copy request that was derived from the current screencopy manager
|
||||
instance.
|
||||
|
||||
The union of all regions received between the call to copy_with_damage
|
||||
and a ready event is the total damage since the prior ready event.
|
||||
</description>
|
||||
<arg name="x" type="uint" summary="damaged x coordinates"/>
|
||||
<arg name="y" type="uint" summary="damaged y coordinates"/>
|
||||
<arg name="width" type="uint" summary="current width"/>
|
||||
<arg name="height" type="uint" summary="current height"/>
|
||||
</event>
|
||||
|
||||
<!-- Version 3 additions -->
|
||||
<event name="linux_dmabuf" since="3">
|
||||
<description summary="linux-dmabuf buffer information">
|
||||
Provides information about linux-dmabuf buffer parameters that need to
|
||||
be used for this frame. This event is sent once after the frame is
|
||||
created if linux-dmabuf buffers are supported.
|
||||
</description>
|
||||
<arg name="format" type="uint" summary="fourcc pixel format"/>
|
||||
<arg name="width" type="uint" summary="buffer width"/>
|
||||
<arg name="height" type="uint" summary="buffer height"/>
|
||||
</event>
|
||||
|
||||
<event name="buffer_done" since="3">
|
||||
<description summary="all buffer types reported">
|
||||
This event is sent once after all buffer events have been sent.
|
||||
|
||||
The client should proceed to create a buffer of one of the supported
|
||||
types, and send a "copy" request.
|
||||
</description>
|
||||
</event>
|
||||
</interface>
|
||||
</protocol>
|
|
@ -10,6 +10,9 @@
|
|||
#include <sys/mman.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
#include <xf86drm.h>
|
||||
|
||||
CHyprlock::CHyprlock(const std::string& wlDisplay) {
|
||||
m_sWaylandState.display = wl_display_connect(wlDisplay.empty() ? nullptr : wlDisplay.c_str());
|
||||
|
@ -24,8 +27,6 @@ CHyprlock::CHyprlock(const std::string& wlDisplay) {
|
|||
if (!m_pXKBContext)
|
||||
Debug::log(ERR, "Failed to create xkb context");
|
||||
|
||||
g_pRenderer = std::make_unique<CRenderer>();
|
||||
|
||||
const auto GRACE = (Hyprlang::INT* const*)g_pConfigManager->getValuePtr("general:grace");
|
||||
m_tGraceEnds = **GRACE ? std::chrono::system_clock::now() + std::chrono::seconds(**GRACE) : std::chrono::system_clock::from_time_t(0);
|
||||
}
|
||||
|
@ -42,6 +43,177 @@ inline const wl_seat_listener seatListener = {
|
|||
|
||||
// end wl_seat
|
||||
|
||||
// dmabuf
|
||||
|
||||
static void handleDMABUFFormat(void* data, struct zwp_linux_dmabuf_v1* zwp_linux_dmabuf_v1, uint32_t format) {
|
||||
;
|
||||
}
|
||||
|
||||
static void handleDMABUFModifier(void* data, struct zwp_linux_dmabuf_v1* zwp_linux_dmabuf_v1, uint32_t format, uint32_t modifier_hi, uint32_t modifier_lo) {
|
||||
g_pHyprlock->dma.dmabufMods.push_back({format, (((uint64_t)modifier_hi) << 32) | modifier_lo});
|
||||
}
|
||||
|
||||
inline const zwp_linux_dmabuf_v1_listener dmabufListener = {
|
||||
.format = handleDMABUFFormat,
|
||||
.modifier = handleDMABUFModifier,
|
||||
};
|
||||
|
||||
static void dmabufFeedbackMainDevice(void* data, zwp_linux_dmabuf_feedback_v1* feedback, wl_array* device_arr) {
|
||||
Debug::log(LOG, "[core] dmabufFeedbackMainDevice");
|
||||
|
||||
RASSERT(!g_pHyprlock->dma.gbm, "double dmabuf feedback");
|
||||
|
||||
dev_t device;
|
||||
assert(device_arr->size == sizeof(device));
|
||||
memcpy(&device, device_arr->data, sizeof(device));
|
||||
|
||||
drmDevice* drmDev;
|
||||
if (drmGetDeviceFromDevId(device, /* flags */ 0, &drmDev) != 0) {
|
||||
Debug::log(WARN, "[dmabuf] unable to open main device?");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
g_pHyprlock->dma.gbmDevice = g_pHyprlock->createGBMDevice(drmDev);
|
||||
}
|
||||
|
||||
static void dmabufFeedbackFormatTable(void* data, zwp_linux_dmabuf_feedback_v1* feedback, int fd, uint32_t size) {
|
||||
Debug::log(TRACE, "[core] dmabufFeedbackFormatTable");
|
||||
|
||||
g_pHyprlock->dma.dmabufMods.clear();
|
||||
|
||||
g_pHyprlock->dma.formatTable = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0);
|
||||
|
||||
if (g_pHyprlock->dma.formatTable == MAP_FAILED) {
|
||||
Debug::log(ERR, "[core] format table failed to mmap");
|
||||
g_pHyprlock->dma.formatTable = nullptr;
|
||||
g_pHyprlock->dma.formatTableSize = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
g_pHyprlock->dma.formatTableSize = size;
|
||||
}
|
||||
|
||||
static void dmabufFeedbackDone(void* data, zwp_linux_dmabuf_feedback_v1* feedback) {
|
||||
Debug::log(TRACE, "[core] dmabufFeedbackDone");
|
||||
|
||||
if (g_pHyprlock->dma.formatTable)
|
||||
munmap(g_pHyprlock->dma.formatTable, g_pHyprlock->dma.formatTableSize);
|
||||
|
||||
g_pHyprlock->dma.formatTable = nullptr;
|
||||
g_pHyprlock->dma.formatTableSize = 0;
|
||||
}
|
||||
|
||||
static void dmabufFeedbackTrancheTargetDevice(void* data, zwp_linux_dmabuf_feedback_v1* feedback, wl_array* device_arr) {
|
||||
Debug::log(TRACE, "[core] dmabufFeedbackTrancheTargetDevice");
|
||||
|
||||
dev_t device;
|
||||
assert(device_arr->size == sizeof(device));
|
||||
memcpy(&device, device_arr->data, sizeof(device));
|
||||
|
||||
drmDevice* drmDev;
|
||||
if (drmGetDeviceFromDevId(device, /* flags */ 0, &drmDev) != 0)
|
||||
return;
|
||||
|
||||
if (g_pHyprlock->dma.gbmDevice) {
|
||||
drmDevice* drmDevRenderer = NULL;
|
||||
drmGetDevice2(gbm_device_get_fd(g_pHyprlock->dma.gbmDevice), /* flags */ 0, &drmDevRenderer);
|
||||
g_pHyprlock->dma.deviceUsed = drmDevicesEqual(drmDevRenderer, drmDev);
|
||||
} else {
|
||||
g_pHyprlock->dma.gbmDevice = g_pHyprlock->createGBMDevice(drmDev);
|
||||
g_pHyprlock->dma.deviceUsed = g_pHyprlock->dma.gbm;
|
||||
}
|
||||
}
|
||||
|
||||
static void dmabufFeedbackTrancheFlags(void* data, zwp_linux_dmabuf_feedback_v1* feedback, uint32_t flags) {
|
||||
;
|
||||
}
|
||||
|
||||
static void dmabufFeedbackTrancheFormats(void* data, zwp_linux_dmabuf_feedback_v1* feedback, wl_array* indices) {
|
||||
Debug::log(TRACE, "[core] dmabufFeedbackTrancheFormats");
|
||||
|
||||
if (!g_pHyprlock->dma.deviceUsed || !g_pHyprlock->dma.formatTable)
|
||||
return;
|
||||
|
||||
struct fm_entry {
|
||||
uint32_t format;
|
||||
uint32_t padding;
|
||||
uint64_t modifier;
|
||||
};
|
||||
// An entry in the table has to be 16 bytes long
|
||||
assert(sizeof(fm_entry) == 16);
|
||||
|
||||
uint32_t n_modifiers = g_pHyprlock->dma.formatTableSize / sizeof(fm_entry);
|
||||
fm_entry* fm_entry = (struct fm_entry*)g_pHyprlock->dma.formatTable;
|
||||
uint16_t* idx;
|
||||
|
||||
for (idx = (uint16_t*)indices->data; (const char*)idx < (const char*)indices->data + indices->size; idx++) {
|
||||
if (*idx >= n_modifiers)
|
||||
continue;
|
||||
|
||||
g_pHyprlock->dma.dmabufMods.push_back({(fm_entry + *idx)->format, (fm_entry + *idx)->modifier});
|
||||
}
|
||||
}
|
||||
|
||||
static void dmabufFeedbackTrancheDone(void* data, struct zwp_linux_dmabuf_feedback_v1* zwp_linux_dmabuf_feedback_v1) {
|
||||
Debug::log(TRACE, "[core] dmabufFeedbackTrancheDone");
|
||||
|
||||
g_pHyprlock->dma.deviceUsed = false;
|
||||
}
|
||||
|
||||
inline const zwp_linux_dmabuf_feedback_v1_listener dmabufFeedbackListener = {
|
||||
.done = dmabufFeedbackDone,
|
||||
.format_table = dmabufFeedbackFormatTable,
|
||||
.main_device = dmabufFeedbackMainDevice,
|
||||
.tranche_done = dmabufFeedbackTrancheDone,
|
||||
.tranche_target_device = dmabufFeedbackTrancheTargetDevice,
|
||||
.tranche_formats = dmabufFeedbackTrancheFormats,
|
||||
.tranche_flags = dmabufFeedbackTrancheFlags,
|
||||
};
|
||||
|
||||
static char* gbm_find_render_node(drmDevice* device) {
|
||||
drmDevice* devices[64];
|
||||
char* render_node = NULL;
|
||||
|
||||
int n = drmGetDevices2(0, devices, sizeof(devices) / sizeof(devices[0]));
|
||||
for (int i = 0; i < n; ++i) {
|
||||
drmDevice* dev = devices[i];
|
||||
if (device && !drmDevicesEqual(device, dev)) {
|
||||
continue;
|
||||
}
|
||||
if (!(dev->available_nodes & (1 << DRM_NODE_RENDER)))
|
||||
continue;
|
||||
|
||||
render_node = strdup(dev->nodes[DRM_NODE_RENDER]);
|
||||
break;
|
||||
}
|
||||
|
||||
drmFreeDevices(devices, n);
|
||||
return render_node;
|
||||
}
|
||||
|
||||
gbm_device* CHyprlock::createGBMDevice(drmDevice* dev) {
|
||||
char* renderNode = gbm_find_render_node(dev);
|
||||
|
||||
if (!renderNode) {
|
||||
Debug::log(ERR, "[core] Couldn't find a render node");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Debug::log(TRACE, "[core] createGBMDevice: render node {}", renderNode);
|
||||
|
||||
int fd = open(renderNode, O_RDWR | O_CLOEXEC);
|
||||
if (fd < 0) {
|
||||
Debug::log(ERR, "[core] couldn't open render node");
|
||||
free(renderNode);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
free(renderNode);
|
||||
return gbm_create_device(fd);
|
||||
}
|
||||
|
||||
// end dmabuf
|
||||
|
||||
// wl_registry
|
||||
|
||||
static void handleGlobal(void* data, struct wl_registry* registry, uint32_t name, const char* interface, uint32_t version) {
|
||||
|
@ -75,11 +247,9 @@ void CHyprlock::onGlobal(void* data, struct wl_registry* registry, uint32_t name
|
|||
Debug::log(LOG, " > Bound to {} v{}", IFACE, version);
|
||||
} else if (IFACE == wl_output_interface.name) {
|
||||
m_vOutputs.emplace_back(std::make_unique<COutput>((wl_output*)wl_registry_bind(registry, name, &wl_output_interface, version), name));
|
||||
|
||||
Debug::log(LOG, " > Bound to {} v{}", IFACE, version);
|
||||
} else if (IFACE == wp_cursor_shape_manager_v1_interface.name) {
|
||||
m_pCursorShape = std::make_unique<CCursorShape>((wp_cursor_shape_manager_v1*)wl_registry_bind(registry, name, &wp_cursor_shape_manager_v1_interface, version));
|
||||
|
||||
Debug::log(LOG, " > Bound to {} v{}", IFACE, version);
|
||||
} else if (IFACE == wl_compositor_interface.name) {
|
||||
m_sWaylandState.compositor = (wl_compositor*)wl_registry_bind(registry, name, &wl_compositor_interface, version);
|
||||
|
@ -90,6 +260,19 @@ void CHyprlock::onGlobal(void* data, struct wl_registry* registry, uint32_t name
|
|||
} else if (IFACE == wp_viewporter_interface.name) {
|
||||
m_sWaylandState.viewporter = (wp_viewporter*)wl_registry_bind(registry, name, &wp_viewporter_interface, version);
|
||||
Debug::log(LOG, " > Bound to {} v{}", IFACE, version);
|
||||
} else if (IFACE == zwp_linux_dmabuf_v1_interface.name) {
|
||||
if (version < 4) {
|
||||
Debug::log(ERR, "cannot use linux_dmabuf with ver < 4");
|
||||
return;
|
||||
}
|
||||
|
||||
dma.linuxDmabuf = wl_registry_bind(registry, name, &zwp_linux_dmabuf_v1_interface, version);
|
||||
dma.linuxDmabufFeedback = zwp_linux_dmabuf_v1_get_default_feedback((zwp_linux_dmabuf_v1*)dma.linuxDmabuf);
|
||||
zwp_linux_dmabuf_feedback_v1_add_listener((zwp_linux_dmabuf_feedback_v1*)dma.linuxDmabufFeedback, &dmabufFeedbackListener, nullptr);
|
||||
Debug::log(LOG, " > Bound to {} v{}", IFACE, version);
|
||||
} else if (IFACE == zwlr_screencopy_manager_v1_interface.name) {
|
||||
m_sWaylandState.screencopy = (zwlr_screencopy_manager_v1*)wl_registry_bind(registry, name, &zwlr_screencopy_manager_v1_interface, version);
|
||||
Debug::log(LOG, " > Bound to {} v{}", IFACE, version);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -115,6 +298,8 @@ void CHyprlock::run() {
|
|||
// gather info about monitors
|
||||
wl_display_roundtrip(m_sWaylandState.display);
|
||||
|
||||
g_pRenderer = std::make_unique<CRenderer>();
|
||||
|
||||
lockSession();
|
||||
|
||||
pollfd pollfds[] = {
|
||||
|
@ -244,8 +429,10 @@ void CHyprlock::run() {
|
|||
m_sLoopState.timerEvent = true;
|
||||
m_sLoopState.timerCV.notify_all();
|
||||
g_pRenderer->asyncResourceGatherer->notify();
|
||||
g_pRenderer->asyncResourceGatherer->await();
|
||||
|
||||
m_vOutputs.clear();
|
||||
g_pEGL.reset();
|
||||
|
||||
wl_display_disconnect(m_sWaylandState.display);
|
||||
|
||||
|
@ -491,8 +678,6 @@ void CHyprlock::unlockSession() {
|
|||
ext_session_lock_v1_unlock_and_destroy(m_sLockState.lock);
|
||||
m_sLockState.lock = nullptr;
|
||||
|
||||
m_vOutputs.clear();
|
||||
g_pEGL.reset();
|
||||
Debug::log(LOG, "Unlocked, exiting!");
|
||||
|
||||
m_bTerminate = true;
|
||||
|
@ -619,3 +804,7 @@ std::string CHyprlock::spawnSync(const std::string& cmd) {
|
|||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
zwlr_screencopy_manager_v1* CHyprlock::getScreencopy() {
|
||||
return m_sWaylandState.screencopy;
|
||||
}
|
|
@ -3,6 +3,8 @@
|
|||
#include <wayland-client.h>
|
||||
#include "ext-session-lock-v1-protocol.h"
|
||||
#include "fractional-scale-v1-protocol.h"
|
||||
#include "linux-dmabuf-unstable-v1-protocol.h"
|
||||
#include "wlr-screencopy-unstable-v1-protocol.h"
|
||||
#include "viewporter-protocol.h"
|
||||
#include "Output.hpp"
|
||||
#include "CursorShape.hpp"
|
||||
|
@ -16,6 +18,14 @@
|
|||
|
||||
#include <xkbcommon/xkbcommon.h>
|
||||
|
||||
#include <gbm.h>
|
||||
#include <xf86drm.h>
|
||||
|
||||
struct SDMABUFModifier {
|
||||
uint32_t fourcc = 0;
|
||||
uint64_t mod = 0;
|
||||
};
|
||||
|
||||
class CHyprlock {
|
||||
public:
|
||||
CHyprlock(const std::string& wlDisplay);
|
||||
|
@ -49,6 +59,7 @@ class CHyprlock {
|
|||
wl_display* getDisplay();
|
||||
wp_fractional_scale_manager_v1* getFractionalMgr();
|
||||
wp_viewporter* getViewporter();
|
||||
zwlr_screencopy_manager_v1* getScreencopy();
|
||||
|
||||
wl_pointer* m_pPointer = nullptr;
|
||||
wl_keyboard* m_pKeeb = nullptr;
|
||||
|
@ -65,6 +76,23 @@ class CHyprlock {
|
|||
std::chrono::system_clock::time_point m_tGraceEnds;
|
||||
Vector2D m_vLastEnterCoords = {};
|
||||
|
||||
std::vector<std::unique_ptr<COutput>> m_vOutputs;
|
||||
|
||||
struct {
|
||||
void* linuxDmabuf = nullptr;
|
||||
void* linuxDmabufFeedback = nullptr;
|
||||
|
||||
gbm_bo* gbm = nullptr;
|
||||
gbm_device* gbmDevice = nullptr;
|
||||
|
||||
void* formatTable = nullptr;
|
||||
size_t formatTableSize = 0;
|
||||
bool deviceUsed = false;
|
||||
|
||||
std::vector<SDMABUFModifier> dmabufMods;
|
||||
} dma;
|
||||
gbm_device* createGBMDevice(drmDevice* dev);
|
||||
|
||||
private:
|
||||
struct {
|
||||
wl_display* display = nullptr;
|
||||
|
@ -74,6 +102,7 @@ class CHyprlock {
|
|||
wl_compositor* compositor = nullptr;
|
||||
wp_fractional_scale_manager_v1* fractional = nullptr;
|
||||
wp_viewporter* viewporter = nullptr;
|
||||
zwlr_screencopy_manager_v1* screencopy = nullptr;
|
||||
} m_sWaylandState;
|
||||
|
||||
struct {
|
||||
|
@ -98,9 +127,7 @@ class CHyprlock {
|
|||
bool timerEvent = false;
|
||||
} m_sLoopState;
|
||||
|
||||
std::vector<std::unique_ptr<COutput>> m_vOutputs;
|
||||
|
||||
std::vector<std::shared_ptr<CTimer>> m_vTimers;
|
||||
std::vector<std::shared_ptr<CTimer>> m_vTimers;
|
||||
};
|
||||
|
||||
inline std::unique_ptr<CHyprlock> g_pHyprlock;
|
|
@ -15,6 +15,42 @@ CAsyncResourceGatherer::CAsyncResourceGatherer() {
|
|||
this->asyncLoopThread.detach();
|
||||
});
|
||||
initThread.detach();
|
||||
|
||||
// some things can't be done async :(
|
||||
// gather background textures when needed
|
||||
|
||||
const auto CWIDGETS = g_pConfigManager->getWidgetConfigs();
|
||||
|
||||
std::vector<std::string> mons;
|
||||
|
||||
for (auto& c : CWIDGETS) {
|
||||
if (c.type != "background")
|
||||
continue;
|
||||
|
||||
if (std::string{std::any_cast<Hyprlang::STRING>(c.values.at("path"))} != "screenshot")
|
||||
continue;
|
||||
|
||||
// mamma mia
|
||||
if (c.monitor.empty()) {
|
||||
mons.clear();
|
||||
for (auto& m : g_pHyprlock->m_vOutputs) {
|
||||
mons.push_back(m->stringPort);
|
||||
}
|
||||
break;
|
||||
} else
|
||||
mons.push_back(c.monitor);
|
||||
}
|
||||
|
||||
for (auto& mon : mons) {
|
||||
const auto MON = std::find_if(g_pHyprlock->m_vOutputs.begin(), g_pHyprlock->m_vOutputs.end(), [mon](const auto& other) { return other->stringPort == mon; });
|
||||
|
||||
if (MON == g_pHyprlock->m_vOutputs.end())
|
||||
continue;
|
||||
|
||||
const auto PMONITOR = MON->get();
|
||||
|
||||
dmas.emplace_back(std::make_unique<CDMAFrame>(PMONITOR));
|
||||
}
|
||||
}
|
||||
|
||||
SPreloadedAsset* CAsyncResourceGatherer::getAssetByID(const std::string& id) {
|
||||
|
@ -36,6 +72,11 @@ SPreloadedAsset* CAsyncResourceGatherer::getAssetByID(const std::string& id) {
|
|||
}
|
||||
}
|
||||
|
||||
for (auto& dma : dmas) {
|
||||
if (id == "dma:" + dma->name)
|
||||
return dma->asset.ready ? &dma->asset : nullptr;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
@ -228,6 +269,8 @@ void CAsyncResourceGatherer::asyncAssetSpinLock() {
|
|||
|
||||
asyncLoopState.busy = false;
|
||||
}
|
||||
|
||||
dmas.clear();
|
||||
}
|
||||
|
||||
void CAsyncResourceGatherer::requestAsyncAssetPreload(const SPreloadRequest& request) {
|
||||
|
@ -247,3 +290,8 @@ void CAsyncResourceGatherer::notify() {
|
|||
asyncLoopState.pending = true;
|
||||
asyncLoopState.loopGuard.notify_all();
|
||||
}
|
||||
|
||||
void CAsyncResourceGatherer::await() {
|
||||
if (asyncLoopThread.joinable())
|
||||
asyncLoopThread.join();
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
#include "Shader.hpp"
|
||||
#include "../helpers/Box.hpp"
|
||||
#include "../helpers/Color.hpp"
|
||||
#include "DMAFrame.hpp"
|
||||
#include "Texture.hpp"
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
|
@ -10,10 +11,7 @@
|
|||
#include <unordered_map>
|
||||
#include <condition_variable>
|
||||
#include <any>
|
||||
|
||||
struct SPreloadedAsset {
|
||||
CTexture texture;
|
||||
};
|
||||
#include "Shared.hpp"
|
||||
|
||||
class CAsyncResourceGatherer {
|
||||
public:
|
||||
|
@ -44,6 +42,7 @@ class CAsyncResourceGatherer {
|
|||
void requestAsyncAssetPreload(const SPreloadRequest& request);
|
||||
void unloadAsset(SPreloadedAsset* asset);
|
||||
void notify();
|
||||
void await();
|
||||
|
||||
private:
|
||||
std::thread initThread;
|
||||
|
@ -77,6 +76,8 @@ class CAsyncResourceGatherer {
|
|||
Vector2D size;
|
||||
};
|
||||
|
||||
std::vector<std::unique_ptr<CDMAFrame>> dmas;
|
||||
|
||||
std::vector<SPreloadTarget> preloadTargets;
|
||||
std::unordered_map<std::string, SPreloadedAsset> assets;
|
||||
|
||||
|
|
203
src/renderer/DMAFrame.cpp
Normal file
203
src/renderer/DMAFrame.cpp
Normal file
|
@ -0,0 +1,203 @@
|
|||
#include "DMAFrame.hpp"
|
||||
#include "wlr-screencopy-unstable-v1-protocol.h"
|
||||
#include "../helpers/Log.hpp"
|
||||
#include "../core/hyprlock.hpp"
|
||||
#include "../core/Egl.hpp"
|
||||
|
||||
#include <EGL/eglext.h>
|
||||
#include <libdrm/drm_fourcc.h>
|
||||
#include <GLES3/gl32.h>
|
||||
#include <GLES3/gl3ext.h>
|
||||
#include <GLES2/gl2ext.h>
|
||||
|
||||
static PFNGLEGLIMAGETARGETTEXTURE2DOESPROC glEGLImageTargetTexture2DOES = nullptr;
|
||||
|
||||
static void wlrOnBuffer(void* data, zwlr_screencopy_frame_v1* frame, uint32_t format, uint32_t width, uint32_t height, uint32_t stride) {
|
||||
const auto PDATA = (SScreencopyData*)data;
|
||||
|
||||
Debug::log(TRACE, "[sc] wlrOnBuffer for {}", (void*)PDATA);
|
||||
|
||||
PDATA->size = stride * height;
|
||||
PDATA->stride = stride;
|
||||
}
|
||||
|
||||
static void wlrOnFlags(void* data, zwlr_screencopy_frame_v1* frame, uint32_t flags) {
|
||||
;
|
||||
}
|
||||
|
||||
static void wlrOnReady(void* data, zwlr_screencopy_frame_v1* frame, uint32_t tv_sec_hi, uint32_t tv_sec_lo, uint32_t tv_nsec) {
|
||||
const auto PDATA = (SScreencopyData*)data;
|
||||
|
||||
Debug::log(TRACE, "[sc] wlrOnReady for {}", (void*)PDATA);
|
||||
|
||||
if (!PDATA->frame->onBufferReady()) {
|
||||
Debug::log(ERR, "onBufferReady failed");
|
||||
return;
|
||||
}
|
||||
|
||||
zwlr_screencopy_frame_v1_destroy(frame);
|
||||
}
|
||||
|
||||
static void wlrOnFailed(void* data, zwlr_screencopy_frame_v1* frame) {
|
||||
;
|
||||
}
|
||||
|
||||
static void wlrOnDamage(void* data, zwlr_screencopy_frame_v1* frame, uint32_t x, uint32_t y, uint32_t width, uint32_t height) {
|
||||
;
|
||||
}
|
||||
|
||||
static void wlrOnDmabuf(void* data, zwlr_screencopy_frame_v1* frame, uint32_t format, uint32_t width, uint32_t height) {
|
||||
const auto PDATA = (SScreencopyData*)data;
|
||||
|
||||
Debug::log(TRACE, "[sc] wlrOnDmabuf for {}", (void*)PDATA);
|
||||
|
||||
PDATA->w = width;
|
||||
PDATA->h = height;
|
||||
PDATA->fmt = format;
|
||||
}
|
||||
|
||||
static void wlrOnBufferDone(void* data, zwlr_screencopy_frame_v1* frame) {
|
||||
const auto PDATA = (SScreencopyData*)data;
|
||||
|
||||
Debug::log(TRACE, "[sc] wlrOnBufferDone for {}", (void*)PDATA);
|
||||
|
||||
if (!PDATA->frame->onBufferDone()) {
|
||||
Debug::log(ERR, "onBufferDone failed");
|
||||
return;
|
||||
}
|
||||
|
||||
zwlr_screencopy_frame_v1_copy(frame, PDATA->frame->wlBuffer);
|
||||
|
||||
Debug::log(TRACE, "[sc] wlr frame copied");
|
||||
}
|
||||
|
||||
static const zwlr_screencopy_frame_v1_listener wlrFrameListener = {
|
||||
.buffer = wlrOnBuffer,
|
||||
.flags = wlrOnFlags,
|
||||
.ready = wlrOnReady,
|
||||
.failed = wlrOnFailed,
|
||||
.damage = wlrOnDamage,
|
||||
.linux_dmabuf = wlrOnDmabuf,
|
||||
.buffer_done = wlrOnBufferDone,
|
||||
};
|
||||
|
||||
CDMAFrame::CDMAFrame(COutput* output) {
|
||||
|
||||
if (!glEGLImageTargetTexture2DOES) {
|
||||
glEGLImageTargetTexture2DOES = (PFNGLEGLIMAGETARGETTEXTURE2DOESPROC)eglGetProcAddress("glEGLImageTargetTexture2DOES");
|
||||
if (!glEGLImageTargetTexture2DOES) {
|
||||
Debug::log(ERR, "No glEGLImageTargetTexture2DOES??");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// firstly, plant a listener for the frame
|
||||
frameCb = zwlr_screencopy_manager_v1_capture_output(g_pHyprlock->getScreencopy(), false, output->output);
|
||||
|
||||
scdata.frame = this;
|
||||
|
||||
zwlr_screencopy_frame_v1_add_listener(frameCb, &wlrFrameListener, &scdata);
|
||||
|
||||
name = output->stringPort;
|
||||
}
|
||||
|
||||
CDMAFrame::~CDMAFrame() {
|
||||
if (g_pEGL)
|
||||
eglDestroyImage(g_pEGL->eglDisplay, image);
|
||||
}
|
||||
|
||||
bool CDMAFrame::onBufferDone() {
|
||||
uint32_t flags = GBM_BO_USE_RENDERING;
|
||||
|
||||
bo = gbm_bo_create(g_pHyprlock->dma.gbmDevice, scdata.w, scdata.h, scdata.fmt, flags);
|
||||
|
||||
if (!bo) {
|
||||
Debug::log(ERR, "Couldn't create a drm buffer");
|
||||
return false;
|
||||
}
|
||||
|
||||
planes = gbm_bo_get_plane_count(bo);
|
||||
|
||||
zwp_linux_buffer_params_v1* params = zwp_linux_dmabuf_v1_create_params((zwp_linux_dmabuf_v1*)g_pHyprlock->dma.linuxDmabuf);
|
||||
if (!params) {
|
||||
Debug::log(ERR, "zwp_linux_dmabuf_v1_create_params failed");
|
||||
gbm_bo_destroy(bo);
|
||||
return false;
|
||||
}
|
||||
|
||||
for (size_t plane = 0; plane < (size_t)planes; plane++) {
|
||||
size[plane] = 0;
|
||||
stride[plane] = gbm_bo_get_stride_for_plane(bo, plane);
|
||||
offset[plane] = gbm_bo_get_offset(bo, plane);
|
||||
uint64_t mod = gbm_bo_get_modifier(bo);
|
||||
fd[plane] = gbm_bo_get_fd_for_plane(bo, plane);
|
||||
|
||||
if (fd[plane] < 0) {
|
||||
Debug::log(ERR, "gbm_bo_get_fd_for_plane failed");
|
||||
zwp_linux_buffer_params_v1_destroy(params);
|
||||
gbm_bo_destroy(bo);
|
||||
for (size_t plane_tmp = 0; plane_tmp < plane; plane_tmp++) {
|
||||
close(fd[plane_tmp]);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
zwp_linux_buffer_params_v1_add(params, fd[plane], plane, offset[plane], stride[plane], mod >> 32, mod & 0xffffffff);
|
||||
}
|
||||
|
||||
wlBuffer = zwp_linux_buffer_params_v1_create_immed(params, scdata.w, scdata.h, scdata.fmt, 0);
|
||||
zwp_linux_buffer_params_v1_destroy(params);
|
||||
|
||||
if (!wlBuffer) {
|
||||
Debug::log(ERR, "[pw] zwp_linux_buffer_params_v1_create_immed failed");
|
||||
gbm_bo_destroy(bo);
|
||||
for (size_t plane = 0; plane < (size_t)planes; plane++)
|
||||
close(fd[plane]);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CDMAFrame::onBufferReady() {
|
||||
static const int general_attribs = 3;
|
||||
static const int plane_attribs = 5;
|
||||
static const int entries_per_attrib = 2;
|
||||
EGLAttrib attribs[(general_attribs + plane_attribs * 4) * entries_per_attrib + 1];
|
||||
int attr = 0;
|
||||
attribs[attr++] = EGL_WIDTH;
|
||||
attribs[attr++] = scdata.w;
|
||||
attribs[attr++] = EGL_HEIGHT;
|
||||
attribs[attr++] = scdata.h;
|
||||
attribs[attr++] = EGL_LINUX_DRM_FOURCC_EXT;
|
||||
attribs[attr++] = scdata.fmt;
|
||||
attribs[attr++] = EGL_DMA_BUF_PLANE0_FD_EXT;
|
||||
attribs[attr++] = fd[0];
|
||||
attribs[attr++] = EGL_DMA_BUF_PLANE0_OFFSET_EXT;
|
||||
attribs[attr++] = offset[0];
|
||||
attribs[attr++] = EGL_DMA_BUF_PLANE0_PITCH_EXT;
|
||||
attribs[attr++] = stride[0];
|
||||
attribs[attr] = EGL_NONE;
|
||||
|
||||
image = eglCreateImage(g_pEGL->eglDisplay, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, NULL, attribs);
|
||||
|
||||
if (image == EGL_NO_IMAGE) {
|
||||
Debug::log(ERR, "failed creating an egl image");
|
||||
return false;
|
||||
}
|
||||
|
||||
asset.texture.allocate();
|
||||
asset.texture.m_vSize = {scdata.w, scdata.h};
|
||||
glBindTexture(GL_TEXTURE_2D, asset.texture.m_iTexID);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, image);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
|
||||
asset.ready = true;
|
||||
|
||||
return true;
|
||||
}
|
46
src/renderer/DMAFrame.hpp
Normal file
46
src/renderer/DMAFrame.hpp
Normal file
|
@ -0,0 +1,46 @@
|
|||
#pragma once
|
||||
|
||||
#include "../core/Output.hpp"
|
||||
#include <gbm.h>
|
||||
#include "Texture.hpp"
|
||||
#include "Shared.hpp"
|
||||
|
||||
struct zwlr_screencopy_frame_v1;
|
||||
|
||||
class CDMAFrame;
|
||||
|
||||
struct SScreencopyData {
|
||||
int w = 0, h = 0;
|
||||
uint32_t fmt;
|
||||
size_t size;
|
||||
size_t stride;
|
||||
CDMAFrame* frame = nullptr;
|
||||
};
|
||||
|
||||
class CDMAFrame {
|
||||
public:
|
||||
CDMAFrame(COutput* mon);
|
||||
~CDMAFrame();
|
||||
|
||||
bool onBufferDone();
|
||||
bool onBufferReady();
|
||||
|
||||
wl_buffer* wlBuffer = nullptr;
|
||||
|
||||
std::string name;
|
||||
|
||||
SPreloadedAsset asset;
|
||||
|
||||
private:
|
||||
gbm_bo* bo = nullptr;
|
||||
|
||||
int planes = 0;
|
||||
|
||||
int fd[4];
|
||||
uint32_t size[4], stride[4], offset[4];
|
||||
|
||||
zwlr_screencopy_frame_v1* frameCb = nullptr;
|
||||
SScreencopyData scdata;
|
||||
|
||||
EGLImage image = nullptr;
|
||||
};
|
|
@ -255,8 +255,14 @@ std::vector<std::unique_ptr<IWidget>>* CRenderer::getOrCreateWidgetsFor(const CS
|
|||
// by type
|
||||
if (c.type == "background") {
|
||||
const std::string PATH = std::any_cast<Hyprlang::STRING>(c.values.at("path"));
|
||||
widgets[surf].emplace_back(
|
||||
std::make_unique<CBackground>(surf->size, PATH.empty() ? "" : std::string{"background:"} + PATH, std::any_cast<Hyprlang::INT>(c.values.at("color"))));
|
||||
|
||||
std::string resourceID = "";
|
||||
if (PATH == "screenshot")
|
||||
resourceID = "dma:" + surf->output->stringPort;
|
||||
else if (!PATH.empty())
|
||||
resourceID = "background:" + PATH;
|
||||
|
||||
widgets[surf].emplace_back(std::make_unique<CBackground>(surf->size, resourceID, std::any_cast<Hyprlang::INT>(c.values.at("color"))));
|
||||
} else if (c.type == "input-field") {
|
||||
widgets[surf].emplace_back(std::make_unique<CPasswordInputField>(surf->size, c.values));
|
||||
} else if (c.type == "label") {
|
||||
|
|
7
src/renderer/Shared.hpp
Normal file
7
src/renderer/Shared.hpp
Normal file
|
@ -0,0 +1,7 @@
|
|||
#pragma once
|
||||
#include "Texture.hpp"
|
||||
|
||||
struct SPreloadedAsset {
|
||||
CTexture texture;
|
||||
bool ready = false;
|
||||
};
|
Loading…
Reference in a new issue