aquamarine/src/backend/drm/DRM.cpp

1984 lines
67 KiB
C++

#include "aquamarine/output/Output.hpp"
#include <aquamarine/backend/DRM.hpp>
#include <aquamarine/backend/drm/Legacy.hpp>
#include <aquamarine/backend/drm/Atomic.hpp>
#include <aquamarine/allocator/GBM.hpp>
#include <hyprutils/string/VarList.hpp>
#include <chrono>
#include <thread>
#include <deque>
#include <cstring>
#include <filesystem>
#include <system_error>
#include <sys/mman.h>
#include <fcntl.h>
extern "C" {
#include <libseat.h>
#include <libudev.h>
#include <xf86drm.h>
#include <xf86drmMode.h>
#include <libdisplay-info/cvt.h>
#include <libdisplay-info/info.h>
#include <libdisplay-info/edid.h>
}
#include "Props.hpp"
#include "FormatUtils.hpp"
#include "Shared.hpp"
#include "hwdata.hpp"
#include "Renderer.hpp"
using namespace Aquamarine;
using namespace Hyprutils::Memory;
using namespace Hyprutils::Math;
#define SP CSharedPointer
Aquamarine::CDRMBackend::CDRMBackend(SP<CBackend> backend_) : backend(backend_) {
listeners.sessionActivate = backend->session->events.changeActive.registerListener([this](std::any d) {
if (backend->session->active) {
// session got activated, we need to restore
restoreAfterVT();
}
});
}
static udev_enumerate* enumDRMCards(udev* udev) {
auto enumerate = udev_enumerate_new(udev);
if (!enumerate)
return nullptr;
udev_enumerate_add_match_subsystem(enumerate, "drm");
udev_enumerate_add_match_sysname(enumerate, DRM_PRIMARY_MINOR_NAME "[0-9]*");
if (udev_enumerate_scan_devices(enumerate)) {
udev_enumerate_unref(enumerate);
return nullptr;
}
return enumerate;
}
static std::vector<SP<CSessionDevice>> scanGPUs(SP<CBackend> backend) {
auto enumerate = enumDRMCards(backend->session->udevHandle);
if (!enumerate) {
backend->log(AQ_LOG_ERROR, "drm: couldn't enumerate gpus with udev");
return {};
}
if (!udev_enumerate_get_list_entry(enumerate)) {
// TODO: wait for them.
backend->log(AQ_LOG_ERROR, "drm: No gpus in scanGPUs.");
return {};
}
udev_list_entry* entry = nullptr;
size_t i = 0;
std::deque<SP<CSessionDevice>> devices;
udev_list_entry_foreach(entry, udev_enumerate_get_list_entry(enumerate)) {
auto path = udev_list_entry_get_name(entry);
auto device = udev_device_new_from_syspath(backend->session->udevHandle, path);
if (!device) {
backend->log(AQ_LOG_WARNING, std::format("drm: Skipping device {}", path ? path : "unknown"));
continue;
}
backend->log(AQ_LOG_DEBUG, std::format("drm: Enumerated device {}", path ? path : "unknown"));
auto seat = udev_device_get_property_value(device, "ID_SEAT");
if (!seat)
seat = "seat0";
if (!backend->session->seatName.empty() && backend->session->seatName != seat) {
backend->log(AQ_LOG_WARNING, std::format("drm: Skipping device {} because seat {} doesn't match our {}", path ? path : "unknown", seat, backend->session->seatName));
udev_device_unref(device);
continue;
}
auto pciDevice = udev_device_get_parent_with_subsystem_devtype(device, "pci", nullptr);
bool isBootVGA = false;
if (pciDevice) {
auto id = udev_device_get_sysattr_value(pciDevice, "boot_vga");
isBootVGA = id && id == std::string{"1"};
}
if (!udev_device_get_devnode(device)) {
backend->log(AQ_LOG_ERROR, std::format("drm: Skipping device {}, no devnode", path ? path : "unknown"));
udev_device_unref(device);
continue;
}
auto sessionDevice = CSessionDevice::openIfKMS(backend->session, udev_device_get_devnode(device));
if (!sessionDevice) {
backend->log(AQ_LOG_ERROR, std::format("drm: Skipping device {}, not a KMS device", path ? path : "unknown"));
udev_device_unref(device);
continue;
}
udev_device_unref(device);
if (isBootVGA)
devices.push_front(sessionDevice);
else
devices.push_back(sessionDevice);
++i;
}
udev_enumerate_unref(enumerate);
std::vector<SP<CSessionDevice>> vecDevices;
auto explicitGpus = getenv("AQ_DRM_DEVICES");
if (explicitGpus) {
backend->log(AQ_LOG_DEBUG, std::format("drm: Explicit device list {}", explicitGpus));
Hyprutils::String::CVarList explicitDevices(explicitGpus, 0, ':', true);
for (auto& d : explicitDevices) {
std::error_code ec;
auto temp = std::filesystem::canonical(d, ec);
if (ec) {
backend->log(AQ_LOG_ERROR, std::format("drm: Failed to canonicalize path {}", d));
continue;
}
d = temp.string();
}
for (auto& d : explicitDevices) {
bool found = false;
for (auto& vd : devices) {
if (vd->path == d) {
vecDevices.emplace_back(vd);
found = true;
break;
}
}
if (found)
backend->log(AQ_LOG_DEBUG, std::format("drm: Explicit device {} found", d));
else
backend->log(AQ_LOG_ERROR, std::format("drm: Explicit device {} not found", d));
}
} else {
for (auto& d : devices) {
vecDevices.push_back(d);
}
}
return vecDevices;
}
std::vector<SP<CDRMBackend>> Aquamarine::CDRMBackend::attempt(SP<CBackend> backend) {
if (!backend->session)
backend->session = CSession::attempt(backend);
if (!backend->session) {
backend->log(AQ_LOG_ERROR, "Failed to open a session");
return {};
}
if (!backend->session->active) {
backend->log(AQ_LOG_DEBUG, "Session is not active, waiting for 5s");
auto started = std::chrono::system_clock::now();
while (!backend->session->active) {
std::this_thread::sleep_for(std::chrono::milliseconds(250));
backend->session->dispatchPendingEventsAsync();
if (std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now() - started).count() >= 5000) {
backend->log(AQ_LOG_DEBUG, "Session timeout reached");
break;
}
}
if (!backend->session->active) {
backend->log(AQ_LOG_DEBUG, "Session could not be activated in time");
return {};
}
}
auto gpus = scanGPUs(backend);
if (gpus.empty()) {
backend->log(AQ_LOG_ERROR, "drm: Found no gpus to use, cannot continue");
return {};
}
backend->log(AQ_LOG_DEBUG, std::format("drm: Found {} GPUs", gpus.size()));
std::vector<SP<CDRMBackend>> backends;
SP<CDRMBackend> newPrimary;
for (auto& gpu : gpus) {
auto drmBackend = SP<CDRMBackend>(new CDRMBackend(backend));
drmBackend->self = drmBackend;
if (!drmBackend->registerGPU(gpu, newPrimary)) {
backend->log(AQ_LOG_ERROR, std::format("drm: Failed to register gpu {}", gpu->path));
continue;
} else
backend->log(AQ_LOG_DEBUG, std::format("drm: Registered gpu {}", gpu->path));
// TODO: consider listening for new devices
// But if you expect me to handle gpu hotswaps you are probably insane LOL
if (!drmBackend->checkFeatures()) {
backend->log(AQ_LOG_ERROR, "drm: Failed checking features");
continue;
}
if (!drmBackend->initResources()) {
backend->log(AQ_LOG_ERROR, "drm: Failed initializing resources");
continue;
}
backend->log(AQ_LOG_DEBUG, std::format("drm: Basic init pass for gpu {}", gpu->path));
drmBackend->grabFormats();
drmBackend->scanConnectors();
drmBackend->recheckCRTCs();
if (!newPrimary) {
backend->log(AQ_LOG_DEBUG, std::format("drm: gpu {} becomes primary drm", gpu->path));
newPrimary = drmBackend;
}
backends.emplace_back(drmBackend);
// so that session can handle udev change/remove events for this gpu
backend->session->sessionDevices.push_back(gpu);
}
return backends;
}
Aquamarine::CDRMBackend::~CDRMBackend() {
;
}
void Aquamarine::CDRMBackend::log(eBackendLogLevel l, const std::string& s) {
backend->log(l, s);
}
bool Aquamarine::CDRMBackend::sessionActive() {
return backend->session->active;
}
void Aquamarine::CDRMBackend::restoreAfterVT() {
backend->log(AQ_LOG_DEBUG, "drm: Restoring after VT switch");
scanConnectors();
recheckCRTCs();
backend->log(AQ_LOG_DEBUG, "drm: Rescanned connectors");
if (!impl->reset())
backend->log(AQ_LOG_ERROR, "drm: failed reset");
std::vector<SP<SDRMConnector>> noMode;
for (auto& c : connectors) {
if (!c->crtc || !c->output)
continue;
SDRMConnectorCommitData data = {
.mainFB = nullptr,
.modeset = true,
.blocking = true,
.flags = 0,
.test = false,
};
auto& STATE = c->output->state->state();
if (!STATE.customMode && !STATE.mode) {
backend->log(AQ_LOG_WARNING, "drm: Connector {} has output but state has no mode, will send a reset state event later.");
noMode.emplace_back(c);
continue;
}
if (STATE.mode && STATE.mode->modeInfo.has_value())
data.modeInfo = *STATE.mode->modeInfo;
else
data.calculateMode(c);
if (STATE.buffer) {
SP<CDRMFB> drmFB;
auto buf = STATE.buffer;
bool isNew = false;
drmFB = CDRMFB::create(buf, self, &isNew);
if (!drmFB)
backend->log(AQ_LOG_ERROR, "drm: Buffer failed to import to KMS");
data.mainFB = drmFB;
}
if (c->crtc->pendingCursor)
data.cursorFB = c->crtc->pendingCursor;
if (data.cursorFB && data.cursorFB->buffer->dmabuf().modifier == DRM_FORMAT_MOD_INVALID)
data.cursorFB = nullptr;
backend->log(AQ_LOG_DEBUG,
std::format("drm: Restoring crtc {} with clock {} hdisplay {} vdisplay {} vrefresh {}", c->crtc->id, data.modeInfo.clock, data.modeInfo.hdisplay,
data.modeInfo.vdisplay, data.modeInfo.vrefresh));
if (!impl->commit(c, data))
backend->log(AQ_LOG_ERROR, std::format("drm: crtc {} failed restore", c->crtc->id));
}
for (auto& c : noMode) {
if (!c->output)
continue;
// tell the consumer to re-set a state because we had no mode
c->output->events.state.emit(IOutput::SStateEvent{});
}
}
bool Aquamarine::CDRMBackend::checkFeatures() {
uint64_t curW = 0, curH = 0;
if (drmGetCap(gpu->fd, DRM_CAP_CURSOR_WIDTH, &curW))
curW = 64;
if (drmGetCap(gpu->fd, DRM_CAP_CURSOR_HEIGHT, &curH))
curH = 64;
drmProps.cursorSize = Hyprutils::Math::Vector2D{(double)curW, (double)curH};
uint64_t cap = 0;
if (drmGetCap(gpu->fd, DRM_CAP_PRIME, &cap) || !(cap & DRM_PRIME_CAP_IMPORT)) {
backend->log(AQ_LOG_ERROR, std::format("drm: DRM_PRIME_CAP_IMPORT unsupported"));
return false;
}
if (drmGetCap(gpu->fd, DRM_CAP_CRTC_IN_VBLANK_EVENT, &cap) || !cap) {
backend->log(AQ_LOG_ERROR, std::format("drm: DRM_CAP_CRTC_IN_VBLANK_EVENT unsupported"));
return false;
}
if (drmGetCap(gpu->fd, DRM_CAP_TIMESTAMP_MONOTONIC, &cap) || !cap) {
backend->log(AQ_LOG_ERROR, std::format("drm: DRM_PRIME_CAP_IMPORT unsupported"));
return false;
}
if (drmSetClientCap(gpu->fd, DRM_CLIENT_CAP_UNIVERSAL_PLANES, 1)) {
backend->log(AQ_LOG_ERROR, std::format("drm: DRM_CLIENT_CAP_UNIVERSAL_PLANES unsupported"));
return false;
}
drmProps.supportsAsyncCommit = drmGetCap(gpu->fd, DRM_CAP_ASYNC_PAGE_FLIP, &cap) == 0 && cap == 1;
drmProps.supportsAddFb2Modifiers = drmGetCap(gpu->fd, DRM_CAP_ADDFB2_MODIFIERS, &cap) == 0 && cap == 1;
drmProps.supportsTimelines = drmGetCap(gpu->fd, DRM_CAP_SYNCOBJ_TIMELINE, &cap) == 0 && cap == 1;
if (envEnabled("AQ_NO_ATOMIC")) {
backend->log(AQ_LOG_WARNING, "drm: AQ_NO_ATOMIC enabled, using the legacy drm iface");
impl = makeShared<CDRMLegacyImpl>(self.lock());
} else if (drmSetClientCap(gpu->fd, DRM_CLIENT_CAP_ATOMIC, 1)) {
backend->log(AQ_LOG_WARNING, "drm: failed to set DRM_CLIENT_CAP_ATOMIC, falling back to legacy");
impl = makeShared<CDRMLegacyImpl>(self.lock());
} else {
backend->log(AQ_LOG_DEBUG, "drm: Atomic supported, using atomic for modesetting");
impl = makeShared<CDRMAtomicImpl>(self.lock());
drmProps.supportsAsyncCommit = drmGetCap(gpu->fd, DRM_CAP_ATOMIC_ASYNC_PAGE_FLIP, &cap) == 0 && cap == 1;
atomic = true;
}
backend->log(AQ_LOG_DEBUG, std::format("drm: drmProps.supportsAsyncCommit: {}", drmProps.supportsAsyncCommit));
backend->log(AQ_LOG_DEBUG, std::format("drm: drmProps.supportsAddFb2Modifiers: {}", drmProps.supportsAddFb2Modifiers));
backend->log(AQ_LOG_DEBUG, std::format("drm: drmProps.supportsTimelines: {}", drmProps.supportsTimelines));
// TODO: allow no-modifiers?
return true;
}
bool Aquamarine::CDRMBackend::initResources() {
auto resources = drmModeGetResources(gpu->fd);
if (!resources) {
backend->log(AQ_LOG_ERROR, std::format("drm: drmModeGetResources failed"));
return false;
}
backend->log(AQ_LOG_DEBUG, std::format("drm: found {} CRTCs", resources->count_crtcs));
for (size_t i = 0; i < resources->count_crtcs; ++i) {
auto CRTC = makeShared<SDRMCRTC>();
CRTC->id = resources->crtcs[i];
CRTC->backend = self;
auto drmCRTC = drmModeGetCrtc(gpu->fd, CRTC->id);
if (!drmCRTC) {
backend->log(AQ_LOG_ERROR, std::format("drm: drmModeGetCrtc for crtc {} failed", CRTC->id));
drmModeFreeResources(resources);
crtcs.clear();
return false;
}
CRTC->legacy.gammaSize = drmCRTC->gamma_size;
drmModeFreeCrtc(drmCRTC);
if (!getDRMCRTCProps(gpu->fd, CRTC->id, &CRTC->props)) {
backend->log(AQ_LOG_ERROR, std::format("drm: getDRMCRTCProps for crtc {} failed", CRTC->id));
drmModeFreeResources(resources);
crtcs.clear();
return false;
}
crtcs.emplace_back(CRTC);
}
if (crtcs.size() > 32) {
backend->log(AQ_LOG_CRITICAL, "drm: Cannot support more than 32 CRTCs");
return false;
}
// initialize planes
auto planeResources = drmModeGetPlaneResources(gpu->fd);
if (!planeResources) {
backend->log(AQ_LOG_ERROR, std::format("drm: drmModeGetPlaneResources failed"));
return false;
}
backend->log(AQ_LOG_DEBUG, std::format("drm: found {} planes", planeResources->count_planes));
for (uint32_t i = 0; i < planeResources->count_planes; ++i) {
auto id = planeResources->planes[i];
auto plane = drmModeGetPlane(gpu->fd, id);
if (!plane) {
backend->log(AQ_LOG_ERROR, std::format("drm: drmModeGetPlane for plane {} failed", id));
drmModeFreeResources(resources);
crtcs.clear();
planes.clear();
return false;
}
auto aqPlane = makeShared<SDRMPlane>();
aqPlane->backend = self;
aqPlane->self = aqPlane;
if (!aqPlane->init((drmModePlane*)plane)) {
backend->log(AQ_LOG_ERROR, std::format("drm: aqPlane->init for plane {} failed", id));
drmModeFreeResources(resources);
crtcs.clear();
planes.clear();
return false;
}
planes.emplace_back(aqPlane);
drmModeFreePlane(plane);
}
drmModeFreePlaneResources(planeResources);
drmModeFreeResources(resources);
return true;
}
bool Aquamarine::CDRMBackend::shouldBlit() {
return primary;
}
bool Aquamarine::CDRMBackend::initMgpu() {
if (!primary)
return true;
auto newAllocator = CGBMAllocator::create(backend->reopenDRMNode(gpu->fd), backend);
mgpu.allocator = newAllocator;
if (!mgpu.allocator) {
backend->log(AQ_LOG_ERROR, "drm: initMgpu: no allocator");
return false;
}
mgpu.renderer = CDRMRenderer::attempt(newAllocator, backend.lock());
if (!mgpu.renderer) {
backend->log(AQ_LOG_ERROR, "drm: initMgpu: no renderer");
return false;
}
mgpu.renderer->self = mgpu.renderer;
buildGlFormats(mgpu.renderer->formats);
return true;
}
void Aquamarine::CDRMBackend::buildGlFormats(const std::vector<SGLFormat>& fmts) {
std::vector<SDRMFormat> result;
for (auto& fmt : fmts) {
if (fmt.external)
continue;
if (auto it = std::find_if(result.begin(), result.end(), [fmt](const auto& e) { return fmt.drmFormat == e.drmFormat; }); it != result.end()) {
it->modifiers.emplace_back(fmt.modifier);
continue;
}
result.emplace_back(SDRMFormat{
fmt.drmFormat,
{fmt.modifier},
});
}
glFormats = result;
}
void Aquamarine::CDRMBackend::recheckCRTCs() {
if (connectors.empty() || crtcs.empty())
return;
backend->log(AQ_LOG_DEBUG, "drm: Rechecking CRTCs");
std::vector<SP<SDRMConnector>> recheck, changed;
for (auto& c : connectors) {
if (c->crtc && c->status == DRM_MODE_CONNECTED) {
backend->log(AQ_LOG_DEBUG, std::format("drm: Skipping connector {}, has crtc {} and is connected", c->szName, c->crtc->id));
continue;
}
recheck.emplace_back(c);
backend->log(AQ_LOG_DEBUG, std::format("drm: connector {}, has crtc {}, will be rechecked", c->szName, c->crtc ? (int)c->crtc->id : -1));
}
for (size_t i = 0; i < crtcs.size(); ++i) {
bool taken = false;
for (auto& c : connectors) {
if (c->crtc != crtcs.at(i))
continue;
if (c->status != DRM_MODE_CONNECTED)
continue;
backend->log(AQ_LOG_DEBUG, std::format("drm: slot {} crtc {} taken by {}, skipping", i, c->crtc->id, c->szName));
taken = true;
break;
}
if (taken)
continue;
bool assigned = false;
// try to use a connected connector
for (auto& c : recheck) {
if (!(c->possibleCrtcs & (1 << i)))
continue;
if (c->status != DRM_MODE_CONNECTED)
continue;
// deactivate old output
if (c->output && c->output->state && c->output->state->state().enabled) {
c->output->state->setEnabled(false);
c->output->commit();
}
backend->log(AQ_LOG_DEBUG,
std::format("drm: connected slot {} crtc {} assigned to {}{}", i, crtcs.at(i)->id, c->szName, c->crtc ? std::format(" (old {})", c->crtc->id) : ""));
c->crtc = crtcs.at(i);
assigned = true;
changed.emplace_back(c);
std::erase(recheck, c);
break;
}
if (!assigned)
backend->log(AQ_LOG_DEBUG, std::format("drm: slot {} crtc {} unassigned", i, crtcs.at(i)->id));
}
for (auto& c : connectors) {
if (c->status == DRM_MODE_CONNECTED)
continue;
backend->log(AQ_LOG_DEBUG, std::format("drm: Connector {} is not connected{}", c->szName, c->crtc ? std::format(", removing old crtc {}", c->crtc->id) : ""));
}
// if any connectors get a crtc and are connected, we need to rescan to assign them outputs.
bool rescan = false;
for (auto& c : changed) {
if (!c->output && c->status == DRM_MODE_CONNECTED) {
rescan = true;
continue;
}
// tell the user to re-assign a valid mode etc
if (c->output)
c->output->events.state.emit(IOutput::SStateEvent{});
}
backend->log(AQ_LOG_DEBUG, "drm: rescanning after realloc");
scanConnectors();
}
bool Aquamarine::CDRMBackend::grabFormats() {
// FIXME: do this properly maybe?
return true;
}
bool Aquamarine::CDRMBackend::registerGPU(SP<CSessionDevice> gpu_, SP<CDRMBackend> primary_) {
gpu = gpu_;
primary = primary_;
auto drmName = drmGetDeviceNameFromFd2(gpu->fd);
auto drmVer = drmGetVersion(gpu->fd);
gpuName = drmName;
auto drmVerName = drmVer->name ? drmVer->name : "unknown";
if (std::string_view(drmVerName) == "evdi")
primary = {};
backend->log(AQ_LOG_DEBUG,
std::format("drm: Starting backend for {}, with driver {}{}", drmName ? drmName : "unknown", drmVerName,
(primary ? std::format(" with primary {}", primary->gpu->path) : "")));
drmFreeVersion(drmVer);
listeners.gpuChange = gpu->events.change.registerListener([this](std::any d) {
auto E = std::any_cast<CSessionDevice::SChangeEvent>(d);
if (E.type == CSessionDevice::AQ_SESSION_EVENT_CHANGE_HOTPLUG) {
backend->log(AQ_LOG_DEBUG, std::format("drm: Got a hotplug event for {}", gpuName));
scanConnectors();
recheckCRTCs();
} else if (E.type == CSessionDevice::AQ_SESSION_EVENT_CHANGE_LEASE) {
backend->log(AQ_LOG_DEBUG, std::format("drm: Got a lease event for {}", gpuName));
scanLeases();
}
});
listeners.gpuRemove = gpu->events.remove.registerListener(
[this](std::any d) { backend->log(AQ_LOG_ERROR, std::format("drm: !!!!FIXME: Got a remove event for {}, this is not handled properly!!!!!", gpuName)); });
return true;
}
eBackendType Aquamarine::CDRMBackend::type() {
return eBackendType::AQ_BACKEND_DRM;
}
void Aquamarine::CDRMBackend::scanConnectors() {
backend->log(AQ_LOG_DEBUG, std::format("drm: Scanning connectors for {}", gpu->path));
auto resources = drmModeGetResources(gpu->fd);
if (!resources) {
backend->log(AQ_LOG_ERROR, std::format("drm: Scanning connectors for {} failed", gpu->path));
return;
}
for (size_t i = 0; i < resources->count_connectors; ++i) {
uint32_t connectorID = resources->connectors[i];
SP<SDRMConnector> conn;
auto drmConn = drmModeGetConnector(gpu->fd, connectorID);
backend->log(AQ_LOG_DEBUG, std::format("drm: Scanning connector id {}", connectorID));
if (!drmConn) {
backend->log(AQ_LOG_ERROR, std::format("drm: Failed to get connector id {}", connectorID));
continue;
}
auto it = std::find_if(connectors.begin(), connectors.end(), [connectorID](const auto& e) { return e->id == connectorID; });
if (it == connectors.end()) {
backend->log(AQ_LOG_DEBUG, std::format("drm: Initializing connector id {}", connectorID));
conn = connectors.emplace_back(SP<SDRMConnector>(new SDRMConnector()));
conn->self = conn;
conn->backend = self;
conn->id = connectorID;
if (!conn->init(drmConn)) {
backend->log(AQ_LOG_ERROR, std::format("drm: Connector id {} failed initializing", connectorID));
connectors.pop_back();
continue;
}
} else {
backend->log(AQ_LOG_DEBUG, std::format("drm: Connector id {} already initialized", connectorID));
conn = *it;
}
conn->status = drmConn->connection;
if (!conn->crtc) {
backend->log(AQ_LOG_DEBUG, std::format("drm: Ignoring connector {} because it has no CRTC", connectorID));
continue;
}
backend->log(AQ_LOG_DEBUG, std::format("drm: Connector {} connection state: {}", connectorID, (int)drmConn->connection));
if (conn->status == DRM_MODE_CONNECTED && !conn->output) {
backend->log(AQ_LOG_DEBUG, std::format("drm: Connector {} connected", conn->szName));
conn->connect(drmConn);
} else if (conn->status != DRM_MODE_CONNECTED && conn->output) {
backend->log(AQ_LOG_DEBUG, std::format("drm: Connector {} disconnected", conn->szName));
conn->disconnect();
}
drmModeFreeConnector(drmConn);
}
drmModeFreeResources(resources);
}
void Aquamarine::CDRMBackend::scanLeases() {
auto lessees = drmModeListLessees(gpu->fd);
if (!lessees) {
backend->log(AQ_LOG_ERROR, "drmModeListLessees failed");
return;
}
for (auto& c : connectors) {
if (!c->output || !c->output->lease)
continue;
bool has = false;
for (size_t i = 0; i < lessees->count; ++i) {
if (lessees->lessees[i] == c->output->lease->lesseeID) {
has = true;
break;
}
}
if (has)
continue;
backend->log(AQ_LOG_DEBUG, std::format("lessee {} gone, removing", c->output->lease->lesseeID));
// don't terminate
c->output->lease->active = false;
auto l = c->output->lease;
for (auto& c2 : connectors) {
if (!c2->output || c2->output->lease != c->output->lease)
continue;
c2->output->lease.reset();
}
l->destroy();
}
drmFree(lessees);
}
bool Aquamarine::CDRMBackend::start() {
impl->reset();
return true;
}
std::vector<Hyprutils::Memory::CSharedPointer<SPollFD>> Aquamarine::CDRMBackend::pollFDs() {
return {makeShared<SPollFD>(gpu->fd, [this]() { dispatchEvents(); })};
}
int Aquamarine::CDRMBackend::drmFD() {
return gpu->fd;
}
static void handlePF(int fd, unsigned seq, unsigned tv_sec, unsigned tv_usec, unsigned crtc_id, void* data) {
auto pageFlip = (SDRMPageFlip*)data;
if (!pageFlip->connector)
return;
pageFlip->connector->isPageFlipPending = false;
const auto& BACKEND = pageFlip->connector->backend;
TRACE(BACKEND->log(AQ_LOG_TRACE, std::format("drm: pf event seq {} sec {} usec {} crtc {}", seq, tv_sec, tv_usec, crtc_id)));
if (pageFlip->connector->status != DRM_MODE_CONNECTED || !pageFlip->connector->crtc) {
BACKEND->log(AQ_LOG_DEBUG, "drm: Ignoring a pf event from a disabled crtc / connector");
return;
}
pageFlip->connector->onPresent();
uint32_t flags = IOutput::AQ_OUTPUT_PRESENT_VSYNC | IOutput::AQ_OUTPUT_PRESENT_HW_CLOCK | IOutput::AQ_OUTPUT_PRESENT_HW_COMPLETION | IOutput::AQ_OUTPUT_PRESENT_ZEROCOPY;
timespec presented = {.tv_sec = (time_t)tv_sec, .tv_nsec = (long)(tv_usec * 1000)};
pageFlip->connector->output->events.present.emit(IOutput::SPresentEvent{
.presented = BACKEND->sessionActive(),
.when = &presented,
.seq = seq,
.refresh = (int)(pageFlip->connector->refresh ? (1000000000000LL / pageFlip->connector->refresh) : 0),
.flags = flags,
});
if (BACKEND->sessionActive() && !pageFlip->connector->frameEventScheduled)
pageFlip->connector->output->events.frame.emit();
}
bool Aquamarine::CDRMBackend::dispatchEvents() {
drmEventContext event = {
.version = 3,
.page_flip_handler2 = ::handlePF,
};
if (drmHandleEvent(gpu->fd, &event) != 0)
backend->log(AQ_LOG_ERROR, std::format("drm: Failed to handle event on fd {}", gpu->fd));
return true;
}
uint32_t Aquamarine::CDRMBackend::capabilities() {
return eBackendCapabilities::AQ_BACKEND_CAPABILITY_POINTER;
}
bool Aquamarine::CDRMBackend::setCursor(SP<IBuffer> buffer, const Hyprutils::Math::Vector2D& hotspot) {
return false;
}
void Aquamarine::CDRMBackend::onReady() {
backend->log(AQ_LOG_DEBUG, std::format("drm: Connectors size2 {}", connectors.size()));
// init a drm renderer to gather gl formats.
// if we are secondary, initMgpu will have done that
if (!primary) {
auto a = CGBMAllocator::create(backend->reopenDRMNode(gpu->fd), backend);
if (!a)
backend->log(AQ_LOG_ERROR, "drm: onReady: no renderer for gl formats");
else {
auto r = CDRMRenderer::attempt(a, backend.lock());
if (!r)
backend->log(AQ_LOG_ERROR, "drm: onReady: no renderer for gl formats");
else {
TRACE(backend->log(AQ_LOG_TRACE, std::format("drm: onReady: gathered {} gl formats", r->formats.size())));
buildGlFormats(r->formats);
r.reset();
a.reset();
}
}
}
for (auto& c : connectors) {
backend->log(AQ_LOG_DEBUG, std::format("drm: onReady: connector {}", c->id));
if (!c->output)
continue;
backend->log(AQ_LOG_DEBUG, std::format("drm: onReady: connector {} has output name {}", c->id, c->output->name));
// swapchain has to be created here because allocator is absent in connect if not ready
c->output->swapchain = CSwapchain::create(backend->primaryAllocator, self.lock());
c->output->swapchain->reconfigure(SSwapchainOptions{.length = 0, .scanout = true, .multigpu = !!primary}); // mark the swapchain for scanout
c->output->needsFrame = true;
backend->events.newOutput.emit(SP<IOutput>(c->output));
}
if (!initMgpu()) {
backend->log(AQ_LOG_ERROR, "drm: Failed initializing mgpu");
return;
}
}
std::vector<SDRMFormat> Aquamarine::CDRMBackend::getRenderFormats() {
for (auto& p : planes) {
if (p->type != DRM_PLANE_TYPE_PRIMARY)
continue;
return p->formats;
}
return {};
}
std::vector<SDRMFormat> Aquamarine::CDRMBackend::getRenderableFormats() {
return glFormats;
}
std::vector<SDRMFormat> Aquamarine::CDRMBackend::getCursorFormats() {
for (auto& p : planes) {
if (p->type != DRM_PLANE_TYPE_CURSOR)
continue;
if (primary) {
TRACE(backend->log(AQ_LOG_TRACE, std::format("drm: getCursorFormats on secondary {}", gpu->path)));
// this is a secondary GPU renderer. In order to receive buffers,
// we'll force linear modifiers.
// TODO: don't. Find a common maybe?
auto fmts = p->formats;
for (auto& fmt : fmts) {
fmt.modifiers = {DRM_FORMAT_MOD_LINEAR};
}
return fmts;
}
return p->formats;
}
return {};
}
bool Aquamarine::CDRMBackend::createOutput(const std::string&) {
return false;
}
int Aquamarine::CDRMBackend::getNonMasterFD() {
int fd = open(gpuName.c_str(), O_RDWR | O_CLOEXEC);
if (fd < 0) {
backend->log(AQ_LOG_ERROR, "drm: couldn't dupe fd for non master");
return -1;
}
if (drmIsMaster(fd) && drmDropMaster(fd) < 0) {
backend->log(AQ_LOG_ERROR, "drm: couldn't drop master from duped fd");
return -1;
}
return fd;
}
SP<IAllocator> Aquamarine::CDRMBackend::preferredAllocator() {
return backend->primaryAllocator;
}
bool Aquamarine::SDRMPlane::init(drmModePlane* plane) {
id = plane->plane_id;
if (!getDRMPlaneProps(backend->gpu->fd, id, &props))
return false;
if (!getDRMProp(backend->gpu->fd, id, props.type, &type))
return false;
initialID = id;
backend->backend->log(AQ_LOG_DEBUG, std::format("drm: Plane {} has type {}", id, (int)type));
backend->backend->log(AQ_LOG_DEBUG, std::format("drm: Plane {} has {} formats", id, plane->count_formats));
for (size_t i = 0; i < plane->count_formats; ++i) {
if (type != DRM_PLANE_TYPE_CURSOR)
formats.emplace_back(SDRMFormat{.drmFormat = plane->formats[i], .modifiers = {DRM_FORMAT_MOD_LINEAR, DRM_FORMAT_MOD_INVALID}});
else
formats.emplace_back(SDRMFormat{.drmFormat = plane->formats[i], .modifiers = {DRM_FORMAT_MOD_LINEAR}});
TRACE(backend->backend->log(AQ_LOG_TRACE, std::format("drm: | Format {}", fourccToName(plane->formats[i]))));
}
if (props.in_formats && backend->drmProps.supportsAddFb2Modifiers) {
backend->backend->log(AQ_LOG_DEBUG, "drm: Plane: checking for modifiers");
uint64_t blobID = 0;
if (!getDRMProp(backend->gpu->fd, id, props.in_formats, &blobID)) {
backend->backend->log(AQ_LOG_ERROR, "drm: Plane: No blob id");
return false;
}
auto blob = drmModeGetPropertyBlob(backend->gpu->fd, blobID);
if (!blob) {
backend->backend->log(AQ_LOG_ERROR, "drm: Plane: No property");
return false;
}
drmModeFormatModifierIterator iter = {0};
while (drmModeFormatModifierBlobIterNext(blob, &iter)) {
auto it = std::find_if(formats.begin(), formats.end(), [iter](const auto& e) { return e.drmFormat == iter.fmt; });
TRACE(backend->backend->log(AQ_LOG_TRACE, std::format("drm: | Modifier {} with format {}", iter.mod, fourccToName(iter.fmt))));
if (it == formats.end())
formats.emplace_back(SDRMFormat{.drmFormat = iter.fmt, .modifiers = {iter.mod}});
else
it->modifiers.emplace_back(iter.mod);
}
drmModeFreePropertyBlob(blob);
}
for (size_t i = 0; i < backend->crtcs.size(); ++i) {
uint32_t crtcBit = (1 << i);
if (!(plane->possible_crtcs & crtcBit))
continue;
auto CRTC = backend->crtcs.at(i);
if (type == DRM_PLANE_TYPE_PRIMARY && !CRTC->primary) {
CRTC->primary = self.lock();
break;
}
if (type == DRM_PLANE_TYPE_CURSOR && !CRTC->cursor) {
CRTC->cursor = self.lock();
break;
}
}
return true;
}
SP<SDRMCRTC> Aquamarine::SDRMConnector::getCurrentCRTC(const drmModeConnector* connector) {
uint32_t crtcID = 0;
if (props.crtc_id) {
TRACE(backend->backend->log(AQ_LOG_TRACE, "drm: Using crtc_id for finding crtc"));
uint64_t value = 0;
if (!getDRMProp(backend->gpu->fd, id, props.crtc_id, &value)) {
backend->backend->log(AQ_LOG_ERROR, "drm: Failed to get CRTC_ID");
return nullptr;
}
crtcID = static_cast<uint32_t>(value);
} else if (connector->encoder_id) {
TRACE(backend->backend->log(AQ_LOG_TRACE, "drm: Using encoder_id for finding crtc"));
auto encoder = drmModeGetEncoder(backend->gpu->fd, connector->encoder_id);
if (!encoder) {
backend->backend->log(AQ_LOG_ERROR, "drm: drmModeGetEncoder failed");
return nullptr;
}
crtcID = encoder->crtc_id;
drmModeFreeEncoder(encoder);
} else {
backend->backend->log(AQ_LOG_ERROR, "drm: Connector has neither crtc_id nor encoder_id");
return nullptr;
}
if (crtcID == 0) {
backend->backend->log(AQ_LOG_ERROR, "drm: getCurrentCRTC: No CRTC 0");
return nullptr;
}
auto it = std::find_if(backend->crtcs.begin(), backend->crtcs.end(), [crtcID](const auto& e) { return e->id == crtcID; });
if (it == backend->crtcs.end()) {
backend->backend->log(AQ_LOG_ERROR, std::format("drm: Failed to find a CRTC with ID {}", crtcID));
return nullptr;
}
return *it;
}
bool Aquamarine::SDRMConnector::init(drmModeConnector* connector) {
pendingPageFlip.connector = self.lock();
if (!getDRMConnectorProps(backend->gpu->fd, id, &props))
return false;
auto name = drmModeGetConnectorTypeName(connector->connector_type);
if (!name)
name = "ERROR";
szName = std::format("{}-{}", name, connector->connector_type_id);
backend->backend->log(AQ_LOG_DEBUG, std::format("drm: Connector gets name {}", szName));
possibleCrtcs = drmModeConnectorGetPossibleCrtcs(backend->gpu->fd, connector);
if (!possibleCrtcs)
backend->backend->log(AQ_LOG_ERROR, "drm: No CRTCs possible");
crtc = getCurrentCRTC(connector);
return true;
}
Aquamarine::SDRMConnector::~SDRMConnector() {
disconnect();
}
static int32_t calculateRefresh(const drmModeModeInfo& mode) {
int32_t refresh = (mode.clock * 1000000LL / mode.htotal + mode.vtotal / 2) / mode.vtotal;
if (mode.flags & DRM_MODE_FLAG_INTERLACE)
refresh *= 2;
if (mode.flags & DRM_MODE_FLAG_DBLSCAN)
refresh /= 2;
if (mode.vscan > 1)
refresh /= mode.vscan;
return refresh;
}
drmModeModeInfo* Aquamarine::SDRMConnector::getCurrentMode() {
if (!crtc)
return nullptr;
if (crtc->props.mode_id) {
size_t size = 0;
return (drmModeModeInfo*)getDRMPropBlob(backend->gpu->fd, crtc->id, crtc->props.mode_id, &size);
}
auto drmCrtc = drmModeGetCrtc(backend->gpu->fd, crtc->id);
if (!drmCrtc)
return nullptr;
if (!drmCrtc->mode_valid) {
drmModeFreeCrtc(drmCrtc);
return nullptr;
}
drmModeModeInfo* modeInfo = (drmModeModeInfo*)malloc(sizeof(drmModeModeInfo));
if (!modeInfo) {
drmModeFreeCrtc(drmCrtc);
return nullptr;
}
*modeInfo = drmCrtc->mode;
drmModeFreeCrtc(drmCrtc);
return modeInfo;
}
void Aquamarine::SDRMConnector::parseEDID(std::vector<uint8_t> data) {
auto info = di_info_parse_edid(data.data(), data.size());
if (!info) {
backend->backend->log(AQ_LOG_ERROR, "drm: failed to parse edid");
return;
}
auto edid = di_info_get_edid(info);
auto venProduct = di_edid_get_vendor_product(edid);
auto pnpID = std::string{venProduct->manufacturer, 3};
if (PNPIDS.contains(pnpID))
make = PNPIDS.at(pnpID);
else
make = pnpID;
auto mod = di_info_get_model(info);
auto ser = di_info_get_serial(info);
model = mod ? mod : "";
serial = ser ? ser : "";
di_info_destroy(info);
}
void Aquamarine::SDRMConnector::connect(drmModeConnector* connector) {
if (output) {
backend->backend->log(AQ_LOG_DEBUG, std::format("drm: Not connecting connector {} because it's already connected", szName));
return;
}
backend->backend->log(AQ_LOG_DEBUG, std::format("drm: Connecting connector {}, CRTC ID {}", szName, crtc ? crtc->id : -1));
output = SP<CDRMOutput>(new CDRMOutput(szName, backend, self.lock()));
output->self = output;
output->connector = self.lock();
backend->backend->log(AQ_LOG_DEBUG, "drm: Dumping detected modes:");
auto currentModeInfo = getCurrentMode();
for (int i = 0; i < connector->count_modes; ++i) {
auto& drmMode = connector->modes[i];
if (drmMode.flags & DRM_MODE_FLAG_INTERLACE) {
backend->backend->log(AQ_LOG_DEBUG, std::format("drm: Skipping mode {} because it's interlaced", i));
continue;
}
auto aqMode = makeShared<SOutputMode>();
aqMode->pixelSize = {drmMode.hdisplay, drmMode.vdisplay};
aqMode->refreshRate = calculateRefresh(drmMode);
aqMode->preferred = (drmMode.type & DRM_MODE_TYPE_PREFERRED);
aqMode->modeInfo = drmMode;
if (i == 1)
fallbackMode = aqMode;
output->modes.emplace_back(aqMode);
if (currentModeInfo && std::memcmp(&drmMode, currentModeInfo, sizeof(drmModeModeInfo))) {
output->state->setMode(aqMode);
//uint64_t modeID = 0;
// getDRMProp(backend->gpu->fd, crtc->id, crtc->props.mode_id, &modeID);
crtc->refresh = calculateRefresh(drmMode);
}
backend->backend->log(AQ_LOG_DEBUG,
std::format("drm: Mode {}: {}x{}@{:.2f}Hz {}", i, (int)aqMode->pixelSize.x, (int)aqMode->pixelSize.y, aqMode->refreshRate / 1000.0,
aqMode->preferred ? " (preferred)" : ""));
}
if (!currentModeInfo && fallbackMode) {
output->state->setMode(fallbackMode);
crtc->refresh = calculateRefresh(fallbackMode->modeInfo.value());
}
output->physicalSize = {(double)connector->mmWidth, (double)connector->mmHeight};
backend->backend->log(AQ_LOG_DEBUG, std::format("drm: Physical size {} (mm)", output->physicalSize));
switch (connector->subpixel) {
case DRM_MODE_SUBPIXEL_NONE: output->subpixel = eSubpixelMode::AQ_SUBPIXEL_NONE; break;
case DRM_MODE_SUBPIXEL_UNKNOWN: output->subpixel = eSubpixelMode::AQ_SUBPIXEL_UNKNOWN; break;
case DRM_MODE_SUBPIXEL_HORIZONTAL_RGB: output->subpixel = eSubpixelMode::AQ_SUBPIXEL_HORIZONTAL_RGB; break;
case DRM_MODE_SUBPIXEL_HORIZONTAL_BGR: output->subpixel = eSubpixelMode::AQ_SUBPIXEL_HORIZONTAL_BGR; break;
case DRM_MODE_SUBPIXEL_VERTICAL_RGB: output->subpixel = eSubpixelMode::AQ_SUBPIXEL_VERTICAL_RGB; break;
case DRM_MODE_SUBPIXEL_VERTICAL_BGR: output->subpixel = eSubpixelMode::AQ_SUBPIXEL_VERTICAL_BGR; break;
default: output->subpixel = eSubpixelMode::AQ_SUBPIXEL_UNKNOWN;
}
uint64_t prop = 0;
if (getDRMProp(backend->gpu->fd, id, props.non_desktop, &prop)) {
if (prop == 1)
backend->backend->log(AQ_LOG_DEBUG, "drm: Non-desktop connector");
output->nonDesktop = prop;
}
canDoVrr = props.vrr_capable && crtc->props.vrr_enabled && getDRMProp(backend->gpu->fd, id, props.vrr_capable, &prop) && prop;
output->vrrCapable = canDoVrr;
backend->backend->log(AQ_LOG_DEBUG,
std::format("drm: crtc is {} of vrr: props.vrr_capable -> {}, crtc->props.vrr_enabled -> {}", (canDoVrr ? "capable" : "incapable"), props.vrr_capable,
crtc->props.vrr_enabled));
maxBpcBounds.fill(0);
if (props.max_bpc && !introspectDRMPropRange(backend->gpu->fd, props.max_bpc, maxBpcBounds.data(), &maxBpcBounds[1]))
backend->backend->log(AQ_LOG_ERROR, "drm: Failed to check max_bpc");
size_t edidLen = 0;
uint8_t* edidData = (uint8_t*)getDRMPropBlob(backend->gpu->fd, id, props.edid, &edidLen);
std::vector<uint8_t> edid{edidData, edidData + edidLen};
parseEDID(edid);
free(edidData);
edid = {};
// TODO: subconnectors
output->make = make;
output->model = model;
output->serial = serial;
output->description = std::format("{} {} {} ({})", make, model, serial, szName);
output->needsFrame = true;
output->supportsExplicit = backend->drmProps.supportsTimelines && crtc->props.out_fence_ptr && crtc->primary->props.in_fence_fd;
backend->backend->log(AQ_LOG_DEBUG, std::format("drm: Explicit sync {}", output->supportsExplicit ? "supported" : "unsupported"));
backend->backend->log(AQ_LOG_DEBUG, std::format("drm: Description {}", output->description));
status = DRM_MODE_CONNECTED;
if (!backend->backend->ready)
return;
output->swapchain = CSwapchain::create(backend->backend->primaryAllocator, backend->self.lock());
backend->backend->events.newOutput.emit(SP<IOutput>(output));
output->scheduleFrame(IOutput::AQ_SCHEDULE_NEW_CONNECTOR);
}
void Aquamarine::SDRMConnector::disconnect() {
if (!output) {
backend->backend->log(AQ_LOG_DEBUG, std::format("drm: Not disconnecting connector {} because it's already disconnected", szName));
return;
}
output->events.destroy.emit();
output.reset();
status = DRM_MODE_DISCONNECTED;
}
bool Aquamarine::SDRMConnector::commitState(SDRMConnectorCommitData& data) {
const bool ok = backend->impl->commit(self.lock(), data);
if (ok && !data.test)
applyCommit(data);
else
rollbackCommit(data);
return ok;
}
void Aquamarine::SDRMConnector::applyCommit(const SDRMConnectorCommitData& data) {
crtc->primary->back = data.mainFB;
if (crtc->cursor && data.cursorFB)
crtc->cursor->back = data.cursorFB;
if (data.mainFB)
data.mainFB->buffer->lockedByBackend = true;
if (crtc->cursor && data.cursorFB)
data.cursorFB->buffer->lockedByBackend = true;
pendingCursorFB.reset();
if (output->state->state().committed & COutputState::AQ_OUTPUT_STATE_MODE)
refresh = calculateRefresh(data.modeInfo);
}
void Aquamarine::SDRMConnector::rollbackCommit(const SDRMConnectorCommitData& data) {
// cursors are applied regardless.
if (crtc->cursor && data.cursorFB)
crtc->cursor->back = data.cursorFB;
crtc->pendingCursor.reset();
}
void Aquamarine::SDRMConnector::onPresent() {
crtc->primary->last = crtc->primary->front;
crtc->primary->front = crtc->primary->back;
if (crtc->primary->last && crtc->primary->last->buffer) {
crtc->primary->last->buffer->lockedByBackend = false;
crtc->primary->last->buffer->events.backendRelease.emit();
}
if (crtc->cursor) {
crtc->cursor->last = crtc->cursor->front;
crtc->cursor->front = crtc->cursor->back;
if (crtc->cursor->last && crtc->cursor->last->buffer) {
crtc->cursor->last->buffer->lockedByBackend = false;
crtc->cursor->last->buffer->events.backendRelease.emit();
}
}
}
Aquamarine::CDRMOutput::~CDRMOutput() {
backend->backend->removeIdleEvent(frameIdle);
connector->isPageFlipPending = false;
connector->frameEventScheduled = false;
}
bool Aquamarine::CDRMOutput::commit() {
return commitState();
}
bool Aquamarine::CDRMOutput::test() {
return commitState(true);
}
void Aquamarine::CDRMOutput::setCursorVisible(bool visible) {
cursorVisible = visible;
scheduleFrame(AQ_SCHEDULE_CURSOR_VISIBLE);
}
bool Aquamarine::CDRMOutput::commitState(bool onlyTest) {
if (!backend->backend->session->active) {
backend->backend->log(AQ_LOG_ERROR, "drm: Session inactive");
return false;
}
if (!connector->crtc) {
backend->backend->log(AQ_LOG_ERROR, "drm: No CRTC attached to output");
return false;
}
const auto& STATE = state->state();
const uint32_t COMMITTED = STATE.committed;
if ((COMMITTED & COutputState::eOutputStateProperties::AQ_OUTPUT_STATE_ENABLED) && STATE.enabled) {
if (!STATE.mode && STATE.customMode) {
backend->backend->log(AQ_LOG_ERROR, "drm: No mode on enable commit");
return false;
}
}
if (STATE.adaptiveSync && !connector->canDoVrr) {
backend->backend->log(AQ_LOG_ERROR, "drm: No Adaptive sync support for output");
return false;
}
if (STATE.presentationMode == AQ_OUTPUT_PRESENTATION_IMMEDIATE && !backend->drmProps.supportsAsyncCommit) {
backend->backend->log(AQ_LOG_ERROR, "drm: No Immediate presentation support in the backend");
return false;
}
if ((COMMITTED & COutputState::eOutputStateProperties::AQ_OUTPUT_STATE_BUFFER) && !STATE.buffer) {
backend->backend->log(AQ_LOG_ERROR, "drm: No buffer committed");
return false;
}
if ((COMMITTED & COutputState::eOutputStateProperties::AQ_OUTPUT_STATE_BUFFER) && STATE.buffer->attachments.has(AQ_ATTACHMENT_DRM_KMS_UNIMPORTABLE)) {
TRACE(backend->backend->log(AQ_LOG_TRACE, "drm: Cannot commit a KMS-unimportable buffer."));
return false;
}
// If we are changing the rendering format, we may need to reconfigure the output (aka modeset)
// which may result in some glitches
const bool NEEDS_RECONFIG = COMMITTED &
(COutputState::eOutputStateProperties::AQ_OUTPUT_STATE_ENABLED | COutputState::eOutputStateProperties::AQ_OUTPUT_STATE_FORMAT |
COutputState::eOutputStateProperties::AQ_OUTPUT_STATE_MODE);
const bool BLOCKING = NEEDS_RECONFIG || !(COMMITTED & COutputState::eOutputStateProperties::AQ_OUTPUT_STATE_BUFFER);
const auto MODE = STATE.mode ? STATE.mode : STATE.customMode;
if (!MODE) // modeless commits are invalid
return false;
uint32_t flags = 0;
if (!onlyTest) {
if (NEEDS_RECONFIG) {
if (STATE.enabled)
backend->backend->log(AQ_LOG_DEBUG,
std::format("drm: Modesetting {} with {}x{}@{:.2f}Hz", name, (int)MODE->pixelSize.x, (int)MODE->pixelSize.y, MODE->refreshRate / 1000.F));
else
backend->backend->log(AQ_LOG_DEBUG, std::format("drm: Disabling output {}", name));
}
if ((NEEDS_RECONFIG || (COMMITTED & COutputState::eOutputStateProperties::AQ_OUTPUT_STATE_BUFFER)) && connector->isPageFlipPending) {
backend->backend->log(AQ_LOG_ERROR, "drm: Cannot commit when a page-flip is awaiting");
return false;
}
if (STATE.enabled && (COMMITTED & COutputState::eOutputStateProperties::AQ_OUTPUT_STATE_BUFFER))
flags |= DRM_MODE_PAGE_FLIP_EVENT;
if (STATE.presentationMode == AQ_OUTPUT_PRESENTATION_IMMEDIATE && (COMMITTED & COutputState::eOutputStateProperties::AQ_OUTPUT_STATE_BUFFER))
flags |= DRM_MODE_PAGE_FLIP_ASYNC;
}
// we can't go further without a blit
if (backend->primary && onlyTest)
return true;
SDRMConnectorCommitData data;
if (STATE.buffer) {
TRACE(backend->backend->log(AQ_LOG_TRACE, "drm: Committed a buffer, updating state"));
SP<CDRMFB> drmFB;
if (backend->shouldBlit()) {
TRACE(backend->backend->log(AQ_LOG_TRACE, "drm: Backend requires blit, blitting"));
if (!mgpu.swapchain) {
TRACE(backend->backend->log(AQ_LOG_TRACE, "drm: No swapchain for blit, creating"));
mgpu.swapchain = CSwapchain::create(backend->mgpu.allocator, backend.lock());
}
auto OPTIONS = swapchain->currentOptions();
auto bufDma = STATE.buffer->dmabuf();
OPTIONS.size = STATE.buffer->size;
if (OPTIONS.format == DRM_FORMAT_INVALID)
OPTIONS.format = bufDma.format;
OPTIONS.multigpu = false; // this is not a shared swapchain, and additionally, don't make it linear, nvidia would be mad
OPTIONS.cursor = false;
OPTIONS.scanout = true;
if (!mgpu.swapchain->reconfigure(OPTIONS)) {
backend->backend->log(AQ_LOG_ERROR, "drm: Backend requires blit, but the mgpu swapchain failed reconfiguring");
return false;
}
auto NEWAQBUF = mgpu.swapchain->next(nullptr);
if (!backend->mgpu.renderer->blit(STATE.buffer, NEWAQBUF)) {
backend->backend->log(AQ_LOG_ERROR, "drm: Backend requires blit, but blit failed");
return false;
}
drmFB = CDRMFB::create(NEWAQBUF, backend, nullptr); // will return attachment if present
} else
drmFB = CDRMFB::create(STATE.buffer, backend, nullptr); // will return attachment if present
if (!drmFB) {
backend->backend->log(AQ_LOG_ERROR, "drm: Buffer failed to import to KMS");
return false;
}
if (drmFB->dead) {
backend->backend->log(AQ_LOG_ERROR, "drm: KMS buffer is dead?!");
return false;
}
data.mainFB = drmFB;
}
// sometimes, our consumer could f up the swapchain format and change it without the state changing
bool formatMismatch = false;
if (data.mainFB) {
if (const auto params = data.mainFB->buffer->dmabuf(); params.success && params.format != STATE.drmFormat) {
// formats mismatch. Update the state format and roll with it
backend->backend->log(AQ_LOG_WARNING,
std::format("drm: Formats mismatch in commit, buffer is {} but output is set to {}. Modesetting to {}", fourccToName(params.format),
fourccToName(STATE.drmFormat), fourccToName(params.format)));
state->setFormat(params.format);
formatMismatch = true;
flags &= ~DRM_MODE_PAGE_FLIP_ASYNC; // we cannot modeset with async pf
}
}
if (connector->crtc->pendingCursor)
data.cursorFB = connector->crtc->pendingCursor;
else if (connector->crtc->cursor)
data.cursorFB = connector->crtc->cursor->front;
if (data.cursorFB) {
// verify cursor format. This might be wrong on NVIDIA where linear buffers
// fail to be created from gbm
// TODO: add an API to detect this and request drm_dumb linear buffers. Or do something,
// idk
if (data.cursorFB->dead || data.cursorFB->buffer->dmabuf().modifier == DRM_FORMAT_MOD_INVALID) {
TRACE(backend->backend->log(AQ_LOG_TRACE, "drm: Dropping invalid buffer for cursor plane"));
data.cursorFB = nullptr;
}
}
data.blocking = BLOCKING || formatMismatch;
data.modeset = NEEDS_RECONFIG || lastCommitNoBuffer || formatMismatch;
data.flags = flags;
data.test = onlyTest;
if (MODE->modeInfo.has_value())
data.modeInfo = *MODE->modeInfo;
else
data.calculateMode(connector);
bool ok = connector->commitState(data);
if (!ok && !data.modeset && !connector->commitTainted) {
// attempt to re-modeset, however, flip a tainted flag if the modesetting fails
// to avoid doing this over and over.
data.modeset = true;
data.blocking = true;
data.flags = DRM_MODE_PAGE_FLIP_EVENT;
ok = connector->commitState(data);
if (!ok)
connector->commitTainted = true;
}
if (onlyTest || !ok)
return ok;
events.commit.emit();
state->onCommit();
lastCommitNoBuffer = !data.mainFB;
needsFrame = false;
if (ok)
connector->commitTainted = false;
return ok;
}
SP<IBackendImplementation> Aquamarine::CDRMOutput::getBackend() {
return backend.lock();
}
bool Aquamarine::CDRMOutput::setCursor(SP<IBuffer> buffer, const Vector2D& hotspot) {
if (buffer && !buffer->dmabuf().success) {
backend->backend->log(AQ_LOG_ERROR, "drm: Cursor buffer has to be a dmabuf");
return false;
}
if (!buffer)
setCursorVisible(false);
else {
SP<CDRMFB> fb;
if (backend->primary) {
TRACE(backend->backend->log(AQ_LOG_TRACE, "drm: Backend requires cursor blit, blitting"));
if (!mgpu.cursorSwapchain) {
TRACE(backend->backend->log(AQ_LOG_TRACE, "drm: No cursorSwapchain for blit, creating"));
mgpu.cursorSwapchain = CSwapchain::create(backend->mgpu.allocator, backend.lock());
}
auto OPTIONS = mgpu.cursorSwapchain->currentOptions();
OPTIONS.multigpu = false;
OPTIONS.scanout = true;
OPTIONS.cursor = true;
OPTIONS.format = buffer->dmabuf().format;
OPTIONS.size = buffer->dmabuf().size;
OPTIONS.length = 2;
if (!mgpu.cursorSwapchain->reconfigure(OPTIONS)) {
backend->backend->log(AQ_LOG_ERROR, "drm: Backend requires blit, but the mgpu cursorSwapchain failed reconfiguring");
return false;
}
auto NEWAQBUF = mgpu.cursorSwapchain->next(nullptr);
if (!backend->mgpu.renderer->blit(buffer, NEWAQBUF)) {
backend->backend->log(AQ_LOG_ERROR, "drm: Backend requires blit, but cursor blit failed");
return false;
}
fb = CDRMFB::create(NEWAQBUF, backend, nullptr); // will return attachment if present
} else
fb = CDRMFB::create(buffer, backend, nullptr);
if (!fb) {
backend->backend->log(AQ_LOG_ERROR, "drm: Cursor buffer failed to import to KMS");
return false;
}
cursorHotspot = hotspot;
backend->backend->log(AQ_LOG_DEBUG, std::format("drm: Cursor buffer imported into KMS with id {}", fb->id));
connector->crtc->pendingCursor = fb;
cursorVisible = true;
}
scheduleFrame(AQ_SCHEDULE_CURSOR_SHAPE);
return true;
}
void Aquamarine::CDRMOutput::moveCursor(const Vector2D& coord, bool skipShedule) {
cursorPos = coord;
// cursorVisible = true;
backend->impl->moveCursor(connector, skipShedule);
}
void Aquamarine::CDRMOutput::scheduleFrame(const scheduleFrameReason reason) {
TRACE(backend->backend->log(AQ_LOG_TRACE,
std::format("CDRMOutput::scheduleFrame: reason {}, needsFrame {}, isPageFlipPending {}, frameEventScheduled {}", (uint32_t)reason, needsFrame,
connector->isPageFlipPending, connector->frameEventScheduled)));
needsFrame = true;
if (connector->isPageFlipPending || connector->frameEventScheduled)
return;
connector->frameEventScheduled = true;
backend->backend->addIdleEvent(frameIdle);
}
Vector2D Aquamarine::CDRMOutput::cursorPlaneSize() {
return backend->drmProps.cursorSize;
}
size_t Aquamarine::CDRMOutput::getGammaSize() {
if (!backend->atomic) {
backend->log(AQ_LOG_ERROR, "No support for gamma on the legacy iface");
return 0;
}
uint64_t size = 0;
if (!getDRMProp(backend->gpu->fd, connector->crtc->id, connector->crtc->props.gamma_lut_size, &size)) {
backend->log(AQ_LOG_ERROR, "Couldn't get the gamma_size prop");
return 0;
}
return size;
}
std::vector<SDRMFormat> Aquamarine::CDRMOutput::getRenderFormats() {
return connector->crtc->primary->formats;
}
int Aquamarine::CDRMOutput::getConnectorID() {
return connector->id;
}
Aquamarine::CDRMOutput::CDRMOutput(const std::string& name_, Hyprutils::Memory::CWeakPointer<CDRMBackend> backend_, SP<SDRMConnector> connector_) :
backend(backend_), connector(connector_) {
name = name_;
frameIdle = makeShared<std::function<void(void)>>([this]() {
connector->frameEventScheduled = false;
if (connector->isPageFlipPending)
return;
events.frame.emit();
});
}
SP<CDRMFB> Aquamarine::CDRMFB::create(SP<IBuffer> buffer_, Hyprutils::Memory::CWeakPointer<CDRMBackend> backend_, bool* isNew) {
SP<CDRMFB> fb;
if (isNew)
*isNew = true;
if (buffer_->attachments.has(AQ_ATTACHMENT_DRM_BUFFER)) {
auto at = (CDRMBufferAttachment*)buffer_->attachments.get(AQ_ATTACHMENT_DRM_BUFFER).get();
fb = at->fb;
TRACE(backend_->log(AQ_LOG_TRACE, std::format("drm: CDRMFB: buffer has drmfb attachment with fb {:x}", (uintptr_t)fb.get())));
}
if (fb) {
if (isNew)
*isNew = false;
return fb;
}
fb = SP<CDRMFB>(new CDRMFB(buffer_, backend_));
if (!fb->id)
return nullptr;
buffer_->attachments.add(makeShared<CDRMBufferAttachment>(fb));
return fb;
}
Aquamarine::CDRMFB::CDRMFB(SP<IBuffer> buffer_, Hyprutils::Memory::CWeakPointer<CDRMBackend> backend_) : buffer(buffer_), backend(backend_) {
import();
}
void Aquamarine::CDRMFB::import() {
auto attrs = buffer->dmabuf();
if (!attrs.success) {
backend->backend->log(AQ_LOG_ERROR, "drm: Buffer submitted has no dmabuf");
return;
}
if (buffer->attachments.has(AQ_ATTACHMENT_DRM_KMS_UNIMPORTABLE)) {
backend->backend->log(AQ_LOG_ERROR, "drm: Buffer submitted is unimportable");
return;
}
// TODO: check format
for (int i = 0; i < attrs.planes; ++i) {
int ret = drmPrimeFDToHandle(backend->gpu->fd, attrs.fds.at(i), &boHandles[i]);
if (ret) {
backend->backend->log(AQ_LOG_ERROR, "drm: drmPrimeFDToHandle failed");
drop();
return;
}
TRACE(backend->backend->log(AQ_LOG_TRACE, std::format("drm: CDRMFB: plane {} has fd {}, got handle {}", i, attrs.fds.at(i), boHandles.at(i))));
}
id = submitBuffer();
if (!id) {
backend->backend->log(AQ_LOG_ERROR, "drm: Failed to submit a buffer to KMS");
buffer->attachments.add(makeShared<CDRMBufferUnimportable>());
drop();
return;
}
TRACE(backend->backend->log(AQ_LOG_TRACE, std::format("drm: new buffer {}", id)));
// FIXME: why does this implode when it doesnt on wlroots or kwin?
closeHandles();
listeners.destroyBuffer = buffer->events.destroy.registerListener([this](std::any d) {
drop();
dead = true;
id = 0;
boHandles = {0, 0, 0, 0};
});
}
void Aquamarine::CDRMFB::reimport() {
drop();
dropped = false;
handlesClosed = false;
boHandles = {0, 0, 0, 0};
import();
}
Aquamarine::CDRMFB::~CDRMFB() {
drop();
}
void Aquamarine::CDRMFB::closeHandles() {
if (handlesClosed)
return;
handlesClosed = true;
std::vector<uint32_t> closed;
for (size_t i = 0; i < 4; ++i) {
if (boHandles.at(i) == 0)
continue;
bool exists = false;
for (size_t j = 0; j < i; ++j) {
if (boHandles.at(i) == boHandles.at(j)) {
exists = true;
break;
}
}
if (exists)
continue;
if (drmCloseBufferHandle(backend->gpu->fd, boHandles.at(i)))
backend->backend->log(AQ_LOG_ERROR, "drm: drmCloseBufferHandle failed");
}
boHandles = {0, 0, 0, 0};
}
void Aquamarine::CDRMFB::drop() {
if (dropped)
return;
dropped = true;
if (!id)
return;
closeHandles();
TRACE(backend->backend->log(AQ_LOG_TRACE, std::format("drm: dropping buffer {}", id)));
int ret = drmModeCloseFB(backend->gpu->fd, id);
if (ret == -EINVAL)
ret = drmModeRmFB(backend->gpu->fd, id);
if (ret)
backend->backend->log(AQ_LOG_ERROR, std::format("drm: Failed to close a buffer: {}", strerror(-ret)));
}
uint32_t Aquamarine::CDRMFB::submitBuffer() {
auto attrs = buffer->dmabuf();
uint32_t newID = 0;
std::array<uint64_t, 4> mods = {0, 0, 0, 0};
for (size_t i = 0; i < attrs.planes; ++i) {
mods[i] = attrs.modifier;
}
if (backend->drmProps.supportsAddFb2Modifiers && attrs.modifier != DRM_FORMAT_MOD_INVALID) {
TRACE(backend->backend->log(AQ_LOG_TRACE,
std::format("drm: Using drmModeAddFB2WithModifiers to import buffer into KMS: Size {} with format {} and mod {}", attrs.size,
fourccToName(attrs.format), attrs.modifier)));
if (drmModeAddFB2WithModifiers(backend->gpu->fd, attrs.size.x, attrs.size.y, attrs.format, boHandles.data(), attrs.strides.data(), attrs.offsets.data(), mods.data(),
&newID, DRM_MODE_FB_MODIFIERS)) {
backend->backend->log(AQ_LOG_ERROR, "drm: Failed to submit a buffer with drmModeAddFB2WithModifiers");
return 0;
}
} else {
if (attrs.modifier != DRM_FORMAT_MOD_INVALID && attrs.modifier != DRM_FORMAT_MOD_LINEAR) {
backend->backend->log(AQ_LOG_ERROR, "drm: drmModeAddFB2WithModifiers unsupported and buffer has explicit modifiers");
return 0;
}
TRACE(backend->backend->log(
AQ_LOG_TRACE,
std::format("drm: Using drmModeAddFB2 to import buffer into KMS: Size {} with format {} and mod {}", attrs.size, fourccToName(attrs.format), attrs.modifier)));
if (drmModeAddFB2(backend->gpu->fd, attrs.size.x, attrs.size.y, attrs.format, boHandles.data(), attrs.strides.data(), attrs.offsets.data(), &newID, 0)) {
backend->backend->log(AQ_LOG_ERROR, "drm: Failed to submit a buffer with drmModeAddFB2");
return 0;
}
}
return newID;
}
void Aquamarine::SDRMConnectorCommitData::calculateMode(Hyprutils::Memory::CSharedPointer<SDRMConnector> connector) {
if (!connector || !connector->output || !connector->output->state)
return;
const auto& STATE = connector->output->state->state();
const auto MODE = STATE.mode ? STATE.mode : STATE.customMode;
if (!MODE) {
connector->backend->log(AQ_LOG_ERROR, "drm: no mode in calculateMode??");
return;
}
di_cvt_options options = {
.red_blank_ver = DI_CVT_REDUCED_BLANKING_NONE,
.h_pixels = (int)MODE->pixelSize.x,
.v_lines = (int)MODE->pixelSize.y,
.ip_freq_rqd = MODE->refreshRate ? MODE->refreshRate / 1000.0 : 60.0,
};
di_cvt_timing timing;
di_cvt_compute(&timing, &options);
uint16_t hsync_start = (int)MODE->pixelSize.y + timing.h_front_porch;
uint16_t vsync_start = timing.v_lines_rnd + timing.v_front_porch;
uint16_t hsync_end = hsync_start + timing.h_sync;
uint16_t vsync_end = vsync_start + timing.v_sync;
modeInfo = (drmModeModeInfo){
.clock = (uint32_t)std::round(timing.act_pixel_freq * 1000),
.hdisplay = (uint16_t)MODE->pixelSize.y,
.hsync_start = hsync_start,
.hsync_end = hsync_end,
.htotal = (uint16_t)(hsync_end + timing.h_back_porch),
.vdisplay = (uint16_t)timing.v_lines_rnd,
.vsync_start = vsync_start,
.vsync_end = vsync_end,
.vtotal = (uint16_t)(vsync_end + timing.v_back_porch),
.vrefresh = (uint32_t)std::round(timing.act_frame_rate),
.flags = DRM_MODE_FLAG_NHSYNC | DRM_MODE_FLAG_PVSYNC,
};
snprintf(modeInfo.name, sizeof(modeInfo.name), "%dx%d", (int)MODE->pixelSize.x, (int)MODE->pixelSize.y);
}
Aquamarine::CDRMBufferAttachment::CDRMBufferAttachment(SP<CDRMFB> fb_) : fb(fb_) {
;
}
SP<CDRMLease> Aquamarine::CDRMLease::create(std::vector<SP<IOutput>> outputs) {
if (outputs.empty())
return nullptr;
if (outputs.at(0)->getBackend()->type() != AQ_BACKEND_DRM)
return nullptr;
auto backend = ((CDRMBackend*)outputs.at(0)->getBackend().get())->self.lock();
for (auto& o : outputs) {
if (o->getBackend() != backend) {
backend->log(AQ_LOG_ERROR, "drm lease: Mismatched backends");
return nullptr;
}
}
std::vector<uint32_t> objects;
auto lease = SP<CDRMLease>(new CDRMLease);
for (auto& o : outputs) {
auto drmo = ((CDRMOutput*)o.get())->self.lock();
backend->log(AQ_LOG_DEBUG, std::format("drm lease: output {}, connector {}", drmo->name, drmo->connector->id));
// FIXME: do we have to alloc a crtc here?
if (!drmo->connector->crtc) {
backend->log(AQ_LOG_ERROR, std::format("drm lease: output {} has no crtc", drmo->name));
return nullptr;
}
backend->log(AQ_LOG_DEBUG, std::format("drm lease: crtc {}, primary {}", drmo->connector->crtc->id, drmo->connector->crtc->primary->id));
objects.push_back(drmo->connector->id);
objects.push_back(drmo->connector->crtc->id);
objects.push_back(drmo->connector->crtc->primary->id);
if (drmo->connector->crtc->cursor)
objects.push_back(drmo->connector->crtc->cursor->id);
lease->outputs.emplace_back(drmo);
}
backend->log(AQ_LOG_DEBUG, "drm lease: issuing a lease");
int leaseFD = drmModeCreateLease(backend->gpu->fd, objects.data(), objects.size(), O_CLOEXEC, &lease->lesseeID);
if (leaseFD < 0) {
backend->log(AQ_LOG_ERROR, "drm lease: drm rejected a lease");
return nullptr;
}
for (auto& o : lease->outputs) {
o->lease = lease;
}
lease->leaseFD = leaseFD;
backend->log(AQ_LOG_DEBUG, std::format("drm lease: lease granted with lessee id {}", lease->lesseeID));
return lease;
}
Aquamarine::CDRMLease::~CDRMLease() {
if (active)
terminate();
else
destroy();
}
void Aquamarine::CDRMLease::terminate() {
active = false;
if (drmModeRevokeLease(backend->gpu->fd, lesseeID) < 0)
backend->log(AQ_LOG_ERROR, "drm lease: Failed to revoke lease");
destroy();
}
void Aquamarine::CDRMLease::destroy() {
events.destroy.emit();
}