// Needed to ensure we get a 64-bit offset to mmap when mapping BOs
#undef _FILE_OFFSET_BITS
#define _FILE_OFFSET_BITS 64

#include "drmu.h"
#include "drmu_fmts.h"
#include "drmu_log.h"

#include <pthread.h>

#include "pollqueue.h"

#include <assert.h>
#include <errno.h>
#include <inttypes.h>
#include <limits.h>
#include <stdatomic.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/mman.h>

#include <libdrm/drm.h>
#include <libdrm/drm_mode.h>
#include <libdrm/drm_fourcc.h>
#include <xf86drm.h>

#define TRACE_PROP_NEW 0

#ifndef DRM_FORMAT_P030
#define DRM_FORMAT_P030 fourcc_code('P', '0', '3', '0')
#endif

struct drmu_bo_env_s;
struct drmu_atomic_q_s;
static struct drmu_bo_env_s * env_boe(drmu_env_t * const du);
static struct pollqueue * env_pollqueue(const drmu_env_t * const du);
static struct drmu_atomic_q_s * env_atomic_q(drmu_env_t * const du);
static int env_object_state_save(drmu_env_t * const du, const uint32_t obj_id, const uint32_t obj_type);

// Update return value with a new one for cases where we don't stop on error
static inline int rvup(int rv1, int rv2)
{
    return rv2 ? rv2 : rv1;
}

// Alloc retry helper
static inline int
retry_alloc_u32(uint32_t ** const pp, uint32_t * const palloc_count, uint32_t const new_count)
{
    if (new_count <= *palloc_count)
        return 0;
    free(*pp);
    *palloc_count = 0;
    if ((*pp = malloc(sizeof(**pp) * new_count)) == NULL)
        return -ENOMEM;
    *palloc_count = new_count;
    return 1;
}

drmu_ufrac_t
drmu_ufrac_reduce(drmu_ufrac_t x)
{
    static const unsigned int primes[] = {2,3,5,7,11,13,17,19,23,29,31,UINT_MAX};
    const unsigned int * p;

    // Deal with specials
    if (x.den == 0) {
        x.num = 0;
        return x;
    }
    if (x.num == 0) {
        x.den = 1;
        return x;
    }

    // Shortcut the 1:1 common case - also ensures the default loop terminates
    if (x.num == x.den) {
        x.num = 1;
        x.den = 1;
        return x;
    }

    // As num != den, (num/UINT_MAX == 0 || den/UINT_MAX == 0) must be true
    // so loop will terminate
    for (p = primes;; ++p) {
        const unsigned int n = *p;
        for (;;) {
            const unsigned int xd = x.den / n;
            const unsigned int xn = x.num / n;
            if (xn == 0 || xd == 0)
                return x;
            if (xn * n != x.num || xd * n != x.den)
                break;
            x.num = xn;
            x.den = xd;
        }
    }
}

//----------------------------------------------------------------------------
//
// propinfo

typedef struct drmu_propinfo_s {
    uint64_t val;
    struct drm_mode_get_property prop;
} drmu_propinfo_t;

static uint64_t
propinfo_val(const drmu_propinfo_t * const pi)
{
    return pi == NULL ? 0 : pi->val;
}

static uint32_t
propinfo_prop_id(const drmu_propinfo_t * const pi)
{
    return pi == NULL ? 0 : pi->prop.prop_id;
}


//----------------------------------------------------------------------------
//
// Blob fns

typedef struct drmu_blob_s {
    atomic_int ref_count;  // 0 == 1 ref for ease of init
    struct drmu_env_s * du;
    uint32_t blob_id;
    // Copy of blob data as we nearly always want to keep a copy to compare
    size_t len;
    void * data;
} drmu_blob_t;

static void
blob_free(drmu_blob_t * const blob)
{
    drmu_env_t * const du = blob->du;

    if (blob->blob_id != 0) {
        struct drm_mode_destroy_blob dblob = {
            .blob_id = blob->blob_id
        };
        if (drmu_ioctl(du, DRM_IOCTL_MODE_DESTROYPROPBLOB, &dblob) != 0)
            drmu_err(du, "%s: Failed to destroy blob: %s", __func__, strerror(errno));
    }
    free(blob->data);
    free(blob);
}

void
drmu_blob_unref(drmu_blob_t ** const ppBlob)
{
    drmu_blob_t * const blob = *ppBlob;

    if (blob == NULL)
        return;
    *ppBlob = NULL;

    if (atomic_fetch_sub(&blob->ref_count, 1) != 0)
        return;

    blob_free(blob);
}

uint32_t
drmu_blob_id(const drmu_blob_t * const blob)
{
    return blob == NULL ? 0 : blob->blob_id;
}

drmu_blob_t *
drmu_blob_ref(drmu_blob_t * const blob)
{
    if (blob != NULL)
        atomic_fetch_add(&blob->ref_count, 1);
    return blob;
}

const void *
drmu_blob_data(const drmu_blob_t * const blob)
{
    return blob->data;
}

size_t
drmu_blob_len(const drmu_blob_t * const blob)
{
    return blob->len;
}

drmu_blob_t *
drmu_blob_new(drmu_env_t * const du, const void * const data, const size_t len)
{
    int rv;
    drmu_blob_t * blob = calloc(1, sizeof(*blob));
    struct drm_mode_create_blob cblob = {
        .data = (uintptr_t)data,
        .length = (uint32_t)len,
        .blob_id = 0
    };

    if (blob == NULL) {
        drmu_err(du, "%s: Unable to alloc blob", __func__);
        return NULL;
    }
    blob->du = du;

    if ((blob->data = malloc(len)) == NULL) {
        drmu_err(du, "%s: Unable to alloc blob data", __func__);
        goto fail;
    }
    blob->len = len;
    memcpy(blob->data, data, len);

    if ((rv = drmu_ioctl(du, DRM_IOCTL_MODE_CREATEPROPBLOB, &cblob)) != 0) {
        drmu_err(du, "%s: Unable to create blob: data=%p, len=%zu: %s", __func__,
                 data, len, strerror(-rv));
        goto fail;
    }

    atomic_init(&blob->ref_count, 0);
    blob->blob_id = cblob.blob_id;
    return blob;

fail:
    blob_free(blob);
    return NULL;
}

int
drmu_blob_update(drmu_env_t * const du, drmu_blob_t ** const ppblob, const void * const data, const size_t len)
{
    drmu_blob_t * blob = *ppblob;

    if (data == NULL || len == 0) {
        drmu_blob_unref(ppblob);
        return 0;
    }

    if (blob && len == blob->len && memcmp(data, blob->data, len) == 0)
        return 0;

    if ((blob = drmu_blob_new(du, data, len)) == NULL)
        return -ENOMEM;
    drmu_blob_unref(ppblob);
    *ppblob = blob;
    return 0;
}

// Data alloced here needs freeing later
static int
blob_data_read(drmu_env_t * const du, uint32_t blob_id, void ** const ppdata, size_t * plen)
{
    void * data;
    struct drm_mode_get_blob gblob = {.blob_id = blob_id};
    int rv;

    *ppdata = NULL;
    *plen = 0;

    if (blob_id == 0)
        return 0;

    if ((rv = drmu_ioctl(du, DRM_IOCTL_MODE_GETPROPBLOB, &gblob)) != 0)
        return rv;

    if (gblob.length == 0)
        return 0;

    if ((data = malloc(gblob.length)) == NULL)
        return -ENOMEM;

    gblob.data = (uintptr_t)data;
    if ((rv = drmu_ioctl(du, DRM_IOCTL_MODE_GETPROPBLOB, &gblob)) != 0) {
        free(data);
        return rv;
    }

    *ppdata = data;
    *plen = gblob.length;
    return 0;
}

// Copy existing blob into a new one
// Useful when saving preexisiting values
drmu_blob_t *
drmu_blob_copy_id(drmu_env_t * const du, uint32_t blob_id)
{
    void * data;
    size_t len;
    drmu_blob_t * blob = NULL;

    if (blob_data_read(du, blob_id, &data, &len) == 0)
        blob = drmu_blob_new(du, data, len);  // * This copies data - could just get it to take the malloc

    free(data);
    return blob;
}

static void
atomic_prop_blob_unref(void * v)
{
    drmu_blob_t * blob = v;
    drmu_blob_unref(&blob);
}

static void
atomic_prop_blob_ref(void * v)
{
    drmu_blob_ref(v);
}

int
drmu_atomic_add_prop_blob(drmu_atomic_t * const da, const uint32_t obj_id, const uint32_t prop_id, drmu_blob_t * const blob)
{
    int rv;
    static const drmu_atomic_prop_fns_t fns = {
        .ref = atomic_prop_blob_ref,
        .unref = atomic_prop_blob_unref,
        .commit = drmu_prop_fn_null_commit
    };

    if (blob == NULL)
        return drmu_atomic_add_prop_value(da, obj_id, prop_id, 0);

    rv = drmu_atomic_add_prop_generic(da, obj_id, prop_id, drmu_blob_id(blob), &fns, blob);
    if (rv != 0)
        drmu_warn(drmu_atomic_env(da), "%s: Failed to add blob obj_id=%#x, prop_id=%#x: %s", __func__, obj_id, prop_id, strerror(-rv));

    return rv;
}

//----------------------------------------------------------------------------
//
// Enum fns

typedef struct drmu_prop_enum_s {
    uint32_t id;
    uint32_t flags;
    unsigned int n;
    const struct drm_mode_property_enum * enums;
    char name[DRM_PROP_NAME_LEN];
} drmu_prop_enum_t;

static void
prop_enum_free(drmu_prop_enum_t * const pen)
{
    free((void*)pen->enums);  // Cast away const
    free(pen);
}

static int
prop_enum_qsort_cb(const void * va, const void * vb)
{
    const struct drm_mode_property_enum * a = va;
    const struct drm_mode_property_enum * b = vb;
    return strcmp(a->name, b->name);
}

// NULL if not found
const uint64_t *
drmu_prop_enum_value(const drmu_prop_enum_t * const pen, const char * const name)
{
    if (pen != NULL && name != NULL) {
        unsigned int i = pen->n / 2;
        unsigned int a = 0;
        unsigned int b = pen->n;

        if (name == NULL)
            return NULL;

        while (a < b) {
            const int r = strcmp(name, pen->enums[i].name);

            if (r == 0)
                return (const uint64_t *)&pen->enums[i].value;  // __u64 defn != uint64_t defn always :-(

            if (r < 0) {
                b = i;
                i = (i + a) / 2;
            } else {
                a = i + 1;
                i = (i + b) / 2;
            }
        }
    }
    return NULL;
}

uint64_t
drmu_prop_bitmask_value(const drmu_prop_enum_t * const pen, const char * const name)
{
    const uint64_t *const p = drmu_prop_enum_value(pen, name);
    return p == NULL || *p >= 64 || (pen->flags & DRM_MODE_PROP_BITMASK) == 0 ?
        (uint64_t)0 : (uint64_t)1 << *p;
}

uint32_t
drmu_prop_enum_id(const drmu_prop_enum_t * const pen)
{
    return pen == NULL ? 0 : pen->id;
}

void
drmu_prop_enum_delete(drmu_prop_enum_t ** const pppen)
{
    drmu_prop_enum_t * const pen = *pppen;
    if (pen == NULL)
        return;
    *pppen = NULL;

    prop_enum_free(pen);
}

drmu_prop_enum_t *
drmu_prop_enum_new(drmu_env_t * const du, const uint32_t id)
{
    drmu_prop_enum_t * pen;
    struct drm_mode_property_enum * enums = NULL;
    unsigned int retries;
    int rv;

    // If id 0 return without warning for ease of getting props on init
    if (id == 0 || (pen = calloc(1, sizeof(*pen))) == NULL)
        return NULL;
    pen->id = id;

    // Docn says we must loop till stable as there may be hotplug races
    for (retries = 0; retries < 8; ++retries) {
        struct drm_mode_get_property prop = {
            .prop_id = id,
            .count_enum_blobs = pen->n,
            .enum_blob_ptr = (uintptr_t)enums
        };

        if ((rv = drmu_ioctl(du, DRM_IOCTL_MODE_GETPROPERTY, &prop)) != 0) {
            drmu_err(du, "%s: get property failed: %s", __func__, strerror(-rv));
            goto fail;
        }

        if (prop.count_enum_blobs == 0 ||
            (prop.flags & (DRM_MODE_PROP_ENUM | DRM_MODE_PROP_BITMASK)) == 0) {
            drmu_err(du, "%s: not an enum: flags=%#x, enums=%d", __func__, prop.flags, prop.count_enum_blobs);
            goto fail;
        }

        if (pen->n >= prop.count_enum_blobs) {
            pen->flags = prop.flags;
            pen->n = prop.count_enum_blobs;
            memcpy(pen->name, prop.name, sizeof(pen->name));
            break;
        }

        free(enums);

        pen->n = prop.count_enum_blobs;
        if ((enums = malloc(pen->n * sizeof(*enums))) == NULL)
            goto fail;
    }
    if (retries >= 8) {
        drmu_err(du, "%s: Too many retries", __func__);
        goto fail;
    }

    qsort(enums, pen->n, sizeof(*enums), prop_enum_qsort_cb);
    pen->enums = enums;

#if TRACE_PROP_NEW
    if (!pen->n) {
        drmu_info(du, "%32s %2d: no properties");
    }
    else {
        unsigned int i;
        for (i = 0; i != pen->n; ++i) {
            drmu_info(du, "%32s %2d:%02d: %32s %#"PRIx64, pen->name, pen->id, i, pen->enums[i].name, pen->enums[i].value);
        }
    }
#endif

    return pen;

fail:
    free(enums);
    prop_enum_free(pen);
    return NULL;
}

int
drmu_atomic_add_prop_enum(drmu_atomic_t * const da, const uint32_t obj_id, const drmu_prop_enum_t * const pen, const char * const name)
{
    const uint64_t * const pval = drmu_prop_enum_value(pen, name);
    int rv;

    rv = (pval == NULL) ? -EINVAL :
        drmu_atomic_add_prop_generic(da, obj_id, drmu_prop_enum_id(pen), *pval, NULL, NULL);

    if (rv != 0 && name != NULL)
        drmu_warn(drmu_atomic_env(da), "%s: Failed to add enum obj_id=%#x, prop_id=%#x, name='%s': %s", __func__,
                  obj_id, drmu_prop_enum_id(pen), name, strerror(-rv));

    return rv;
}

int
drmu_atomic_add_prop_bitmask(struct drmu_atomic_s * const da, const uint32_t obj_id, const drmu_prop_enum_t * const pen, const uint64_t val)
{
    int rv;

    rv = !pen ? -ENOENT :
        ((pen->flags & DRM_MODE_PROP_BITMASK) == 0) ? -EINVAL :
            drmu_atomic_add_prop_generic(da, obj_id, drmu_prop_enum_id(pen), val, NULL, NULL);

    if (rv != 0)
        drmu_warn(drmu_atomic_env(da), "%s: Failed to add bitmask obj_id=%#x, prop_id=%#x, val=%#"PRIx64": %s", __func__,
                  obj_id, drmu_prop_enum_id(pen), val, strerror(-rv));

    return rv;
}

//----------------------------------------------------------------------------
//
// Range

typedef struct drmu_prop_range_s {
    uint32_t id;
    uint32_t flags;
    uint64_t range[2];
    char name[DRM_PROP_NAME_LEN];
} drmu_prop_range_t;

static void
prop_range_free(drmu_prop_range_t * const pra)
{
    free(pra);
}

void
drmu_prop_range_delete(drmu_prop_range_t ** pppra)
{
    drmu_prop_range_t * const pra = *pppra;

    if (pra == NULL)
        return;
    *pppra = NULL;

    prop_range_free(pra);
}

bool
drmu_prop_range_validate(const drmu_prop_range_t * const pra, const uint64_t x)
{
    if (pra == NULL)
        return false;
    if ((pra->flags & DRM_MODE_PROP_EXTENDED_TYPE) == DRM_MODE_PROP_SIGNED_RANGE) {
        return (int64_t)pra->range[0] <= (int64_t)x && (int64_t)pra->range[1] >= (int64_t)x;
    }
    return pra->range[0] <= x && pra->range[1] >= x;
}

bool
drmu_prop_range_immutable(const drmu_prop_range_t * const pra)
{
    return !pra || (pra->flags & DRM_MODE_PROP_IMMUTABLE) != 0;
}

uint64_t
drmu_prop_range_max(const drmu_prop_range_t * const pra)
{
    return pra == NULL ? 0 : pra->range[1];
}

uint64_t
drmu_prop_range_min(const drmu_prop_range_t * const pra)
{
    return pra == NULL ? 0 : pra->range[0];
}

uint32_t
drmu_prop_range_id(const drmu_prop_range_t * const pra)
{
    return pra == NULL ? 0 : pra->id;
}

const char *
drmu_prop_range_name(const drmu_prop_range_t * const pra)
{
    return pra == NULL ? "{norange}" : pra->name;
}

drmu_prop_range_t *
drmu_prop_range_new(drmu_env_t * const du, const uint32_t id)
{
    drmu_prop_range_t * pra;
    int rv;

    // If id 0 return without warning for ease of getting props on init
    if (id == 0 || (pra = calloc(1, sizeof(*pra))) == NULL)
        return NULL;
    pra->id = id;

    // We are expecting exactly 2 values so no need to loop
    {
        struct drm_mode_get_property prop = {
            .prop_id = id,
            .count_values = 2,
            .values_ptr = (uintptr_t)pra->range
        };

        if ((rv = drmu_ioctl(du, DRM_IOCTL_MODE_GETPROPERTY, &prop)) != 0) {
            drmu_err(du, "%s: get property failed: %s", __func__, strerror(-rv));
            goto fail;
        }

        if ((prop.flags & DRM_MODE_PROP_RANGE) == 0 &&
            (prop.flags & DRM_MODE_PROP_EXTENDED_TYPE) != DRM_MODE_PROP_SIGNED_RANGE) {
            drmu_err(du, "%s: not an signed range: flags=%#x", __func__, prop.flags);
            goto fail;
        }
        if ((prop.count_values != 2)) {
            drmu_err(du, "%s: unexpected count values: %d", __func__, prop.count_values);
            goto fail;
        }

        pra->flags = prop.flags;
        memcpy(pra->name, prop.name, sizeof(pra->name));
    }

#if TRACE_PROP_NEW
    drmu_info(du, "%32s %2d: %"PRId64"->%"PRId64, pra->name, pra->id, pra->range[0], pra->range[1]);
#endif

    return pra;

fail:
    prop_range_free(pra);
    return NULL;
}

int
drmu_atomic_add_prop_range(drmu_atomic_t * const da, const uint32_t obj_id, const drmu_prop_range_t * const pra, const uint64_t x)
{
    int rv;

    rv = !pra ? -ENOENT :
        !drmu_prop_range_validate(pra, x) ? -EINVAL :
        drmu_prop_range_immutable(pra) ? -EPERM :
        drmu_atomic_add_prop_generic(da, obj_id, drmu_prop_range_id(pra), x, NULL, NULL);

    if (rv != 0)
        drmu_warn(drmu_atomic_env(da),
                  "%s: Failed to add range %s obj_id=%#x, prop_id=%#x, val=%"PRId64", range=%"PRId64"->%"PRId64": %s",
                  __func__, drmu_prop_range_name(pra),
                  obj_id, drmu_prop_range_id(pra), x, drmu_prop_range_min(pra), drmu_prop_range_max(pra), strerror(-rv));

    return rv;
}

//----------------------------------------------------------------------------
//
// Object ID (tracked)

typedef struct drmu_prop_object_s {
    atomic_int ref_count;
    uint32_t obj_id;
    uint32_t prop_id;
    uint32_t value;
} drmu_prop_object_t;

uint32_t
drmu_prop_object_value(const drmu_prop_object_t * const obj)
{
    return !obj ? 0 : obj->value;
}

void
drmu_prop_object_unref(drmu_prop_object_t ** ppobj)
{
    drmu_prop_object_t * const obj = *ppobj;

    if (!obj)
        return;
    *ppobj = NULL;

    if (atomic_fetch_sub(&obj->ref_count, 1) != 0)
        return;

    free(obj);
}

drmu_prop_object_t *
drmu_prop_object_new_propinfo(drmu_env_t * const du, const uint32_t obj_id, const drmu_propinfo_t * const pi)
{
    const uint64_t val = propinfo_val(pi);
    const uint32_t prop_id = propinfo_prop_id(pi);

    if (obj_id == 0 || prop_id == 0)
        return NULL;

    if ((val >> 32) != 0) {  // We expect 32-bit values
        drmu_err(du, "Bad object id value: %#"PRIx64, val);
        return NULL;
    }
    else {
        drmu_prop_object_t *const obj = calloc(1, sizeof(*obj));

        if (obj == NULL)
            return obj;

        obj->obj_id = obj_id;
        obj->prop_id = prop_id;
        obj->value = (uint32_t)val;
        return obj;
    }
}

static void
atomic_prop_object_unref(void * v)
{
    drmu_prop_object_t * obj = v;
    drmu_prop_object_unref(&obj);
}
static void
atomic_prop_object_ref(void * v)
{
    drmu_prop_object_t * obj = v;
    atomic_fetch_add(&obj->ref_count, 1);
}
static void
atomic_prop_object_commit(void * v, uint64_t val)
{
    drmu_prop_object_t * obj = v;
    obj->value = (uint32_t)val;
}

int
drmu_atomic_add_prop_object(drmu_atomic_t * const da, drmu_prop_object_t * obj, uint32_t val)
{
    static const drmu_atomic_prop_fns_t fns = {
        .ref = atomic_prop_object_ref,
        .unref = atomic_prop_object_unref,
        .commit = atomic_prop_object_commit,
    };

    return drmu_atomic_add_prop_generic(da, obj->obj_id, obj->prop_id, val, &fns, obj);
}

//----------------------------------------------------------------------------
//
// BO fns

enum drmu_bo_type_e {
    BO_TYPE_NONE = 0,
    BO_TYPE_FD,
    BO_TYPE_DUMB
};

// BO handles come in 2 very distinct types: DUMB and FD
// They need very different alloc & free but BO usage is the same for both
// so it is better to have a single type.
typedef struct drmu_bo_s {
    // Arguably could be non-atomic for FD as then it is always protected by mutex
    atomic_int ref_count;
    struct drmu_env_s * du;
    enum drmu_bo_type_e bo_type;
    uint32_t handle;

    // FD only els - FD BOs need to be tracked globally
    struct drmu_bo_s * next;
    struct drmu_bo_s * prev;
} drmu_bo_t;

typedef struct drmu_bo_env_s {
    pthread_mutex_t lock;
    drmu_bo_t * fd_head;
} drmu_bo_env_t;

static int
bo_close(drmu_env_t * const du, uint32_t * const ph)
{
    struct drm_gem_close gem_close = {.handle = *ph};

    if (gem_close.handle == 0)
        return 0;
    *ph = 0;

    return drmu_ioctl(du, DRM_IOCTL_GEM_CLOSE, &gem_close);
}

// BOE lock expected
static void
bo_free_dumb(drmu_bo_t * const bo)
{
    if (bo->handle != 0) {
        drmu_env_t * const du = bo->du;
        struct drm_mode_destroy_dumb destroy_env = {.handle = bo->handle};

        if (drmu_ioctl(du, DRM_IOCTL_MODE_DESTROY_DUMB, &destroy_env) != 0)
            drmu_warn(du, "%s: Failed to destroy dumb handle %d", __func__, bo->handle);
    }
    free(bo);
}

static void
bo_free_fd(drmu_bo_t * const bo)
{
    if (bo->handle != 0) {
        drmu_env_t * const du = bo->du;
        drmu_bo_env_t *const boe = env_boe(du);
        const uint32_t h = bo->handle;

        if (bo_close(du, &bo->handle) != 0)
            drmu_warn(du, "%s: Failed to close BO handle %d", __func__, h);
        if (bo->next != NULL)
            bo->next->prev = bo->prev;
        if (bo->prev != NULL)
            bo->prev->next = bo->next;
        else
            boe->fd_head = bo->next;
    }
    free(bo);
}


void
drmu_bo_unref(drmu_bo_t ** const ppbo)
{
    drmu_bo_t * const bo = *ppbo;

    if (bo == NULL)
        return;
    *ppbo = NULL;

    switch (bo->bo_type) {
        case BO_TYPE_FD:
        {
            drmu_bo_env_t * const boe = env_boe(bo->du);

            pthread_mutex_lock(&boe->lock);
            if (atomic_fetch_sub(&bo->ref_count, 1) == 0)
                bo_free_fd(bo);
            pthread_mutex_unlock(&boe->lock);
            break;
        }
        case BO_TYPE_DUMB:
            if (atomic_fetch_sub(&bo->ref_count, 1) == 0)
                bo_free_dumb(bo);
            break;
        case BO_TYPE_NONE:
        default:
            free(bo);
            break;
    }
}


drmu_bo_t *
drmu_bo_ref(drmu_bo_t * const bo)
{
    if (bo != NULL)
        atomic_fetch_add(&bo->ref_count, 1);
    return bo;
}

static drmu_bo_t *
bo_alloc(drmu_env_t *const du, enum drmu_bo_type_e bo_type)
{
    drmu_bo_t * const bo = calloc(1, sizeof(*bo));
    if (bo == NULL) {
        drmu_err(du, "Failed to alloc BO");
        return NULL;
    }

    bo->du = du;
    bo->bo_type = bo_type;
    atomic_init(&bo->ref_count, 0);
    return bo;
}

drmu_bo_t *
drmu_bo_new_fd(drmu_env_t *const du, const int fd)
{
    drmu_bo_env_t * const boe = env_boe(du);
    drmu_bo_t * bo = NULL;
    struct drm_prime_handle ph = { .fd = fd };
    int rv;

    pthread_mutex_lock(&boe->lock);

    if ((rv = drmu_ioctl(du, DRM_IOCTL_PRIME_FD_TO_HANDLE, &ph)) != 0) {
        drmu_err(du, "Failed to convert fd %d to BO: %s", __func__, fd, strerror(-rv));
        goto unlock;
    }

    bo = boe->fd_head;
    while (bo != NULL && bo->handle != ph.handle)
        bo = bo->next;

    if (bo != NULL) {
        drmu_bo_ref(bo);
    }
    else {
        if ((bo = bo_alloc(du, BO_TYPE_FD)) == NULL) {
            bo_close(du, &ph.handle);
        }
        else {
            bo->handle = ph.handle;

            if ((bo->next = boe->fd_head) != NULL)
                bo->next->prev = bo;
            boe->fd_head = bo;
        }
    }

unlock:
    pthread_mutex_unlock(&boe->lock);
    return bo;
}

// Updates the passed dumb structure with the results of creation
drmu_bo_t *
drmu_bo_new_dumb(drmu_env_t *const du, struct drm_mode_create_dumb * const d)
{
    drmu_bo_t *bo = bo_alloc(du, BO_TYPE_DUMB);
    int rv;

    if (bo == NULL)
        return NULL;

    if ((rv = drmu_ioctl(du, DRM_IOCTL_MODE_CREATE_DUMB, d)) != 0)
    {
        drmu_err(du, "%s: Create dumb %dx%dx%d failed: %s", __func__,
                 d->width, d->height, d->bpp, strerror(-rv));
        drmu_bo_unref(&bo);  // After this point aux is bound to dfb and gets freed with it
        return NULL;
    }

    bo->handle = d->handle;
    return bo;
}

void
drmu_bo_env_uninit(drmu_bo_env_t * const boe)
{
    if (boe->fd_head != NULL)
        drmu_warn(boe->fd_head->du, "%s: fd chain not null", __func__);
    boe->fd_head = NULL;
    pthread_mutex_destroy(&boe->lock);
}

void
drmu_bo_env_init(drmu_bo_env_t * boe)
{
    boe->fd_head = NULL;
    pthread_mutex_init(&boe->lock, NULL);
}

//----------------------------------------------------------------------------
//
// FB fns

typedef struct drmu_fb_s {
    atomic_int ref_count;  // 0 == 1 ref for ease of init
    struct drmu_fb_s * prev;
    struct drmu_fb_s * next;

    struct drmu_env_s * du;

    const struct drmu_fmt_info_s * fmt_info;

    struct drm_mode_fb_cmd2 fb;

    drmu_rect_t active;     // Area that was asked for inside the buffer; pixels
    drmu_rect_t crop;       // Cropping inside that; fractional pels (16.16, 16.16)

    void * map_ptr;
    size_t map_size;
    size_t map_pitch;

    drmu_bo_t * bo_list[4];

    drmu_color_encoding_t color_encoding; // Assumed to be constant strings that don't need freeing
    drmu_color_range_t    color_range;
    drmu_colorspace_t     colorspace;
    const char * pixel_blend_mode;
    drmu_chroma_siting_t chroma_siting;
    drmu_isset_t hdr_metadata_isset;
    struct hdr_output_metadata hdr_metadata;

    void * pre_delete_v;
    drmu_fb_pre_delete_fn pre_delete_fn;

    void * on_delete_v;
    drmu_fb_on_delete_fn on_delete_fn;

    // We pass a pointer to this to DRM which defines it as s32 so do not use
    // int that might be s64.
    int32_t fence_fd;
} drmu_fb_t;

int
drmu_fb_out_fence_wait(drmu_fb_t * const fb, const int timeout_ms)
{
    struct pollfd pf;
    int rv;

    if (fb->fence_fd == -1)
        return -EINVAL;

    do {
        pf.fd = fb->fence_fd;
        pf.events = POLLIN;
        pf.revents = 0;

        rv = poll(&pf, 1, timeout_ms);
        if (rv >= 0)
            break;

        rv = -errno;
    } while (rv == -EINTR);

    if (rv == 0)
        return 0;

    // Both on error & success close the fd
    close(fb->fence_fd);
    fb->fence_fd = -1;
    return rv;
}

void
drmu_fb_int_free(drmu_fb_t * const dfb)
{
    drmu_env_t * const du = dfb->du;
    unsigned int i;

    if (dfb->pre_delete_fn && dfb->pre_delete_fn(dfb, dfb->pre_delete_v) != 0)
        return;

    // * If we implement callbacks this logic will want revision
    if (dfb->fence_fd != -1) {
        drmu_warn(du, "Out fence still set on FB on delete");
        if (drmu_fb_out_fence_wait(dfb, 500) == 0) {
            drmu_err(du, "Out fence stuck in FB free");
            close(dfb->fence_fd);
        }
    }

    if (dfb->fb.fb_id != 0)
        drmu_ioctl(du, DRM_IOCTL_MODE_RMFB, &dfb->fb.fb_id);

    if (dfb->map_ptr != NULL && dfb->map_ptr != MAP_FAILED)
        munmap(dfb->map_ptr, dfb->map_size);

    for (i = 0; i != 4; ++i)
        drmu_bo_unref(dfb->bo_list + i);

    // Call on_delete last so we have stopped using anything that might be
    // freed by it
    {
        void * const v = dfb->on_delete_v;
        const drmu_fb_on_delete_fn fn = dfb->on_delete_fn;

        free(dfb);

        if (fn)
            fn(v);
    }
}

void
drmu_fb_unref(drmu_fb_t ** const ppdfb)
{
    drmu_fb_t * const dfb = *ppdfb;

    if (dfb == NULL)
        return;
    *ppdfb = NULL;

    if (atomic_fetch_sub(&dfb->ref_count, 1) > 0)
        return;

    drmu_fb_int_free(dfb);
}

static void
atomic_prop_fb_unref_cb(void * v)
{
    drmu_fb_t * dfb = v;
    drmu_fb_unref(&dfb);
}

drmu_fb_t *
drmu_fb_ref(drmu_fb_t * const dfb)
{
    if (dfb != NULL)
        atomic_fetch_add(&dfb->ref_count, 1);
    return dfb;
}

static void
atomic_prop_fb_ref_cb(void * v)
{
    drmu_fb_ref(v);
}

// Beware: used by pool fns
void
drmu_fb_pre_delete_set(drmu_fb_t *const dfb, drmu_fb_pre_delete_fn fn, void * v)
{
    dfb->pre_delete_fn = fn;
    dfb->pre_delete_v  = v;
}

void
drmu_fb_pre_delete_unset(drmu_fb_t *const dfb)
{
    dfb->pre_delete_fn = (drmu_fb_pre_delete_fn)0;
    dfb->pre_delete_v  = NULL;
}

int
drmu_fb_pixel_blend_mode_set(drmu_fb_t *const dfb, const char * const mode)
{
    dfb->pixel_blend_mode = mode;
    return 0;
}

uint32_t
drmu_fb_pitch(const drmu_fb_t *const dfb, const unsigned int layer)
{
    return layer >= 4 ? 0 : dfb->fb.pitches[layer];
}

uint32_t
drmu_fb_pitch2(const drmu_fb_t *const dfb, const unsigned int layer)
{
    if (layer < 4){
        const uint64_t m = dfb->fb.modifier[layer];
        const uint64_t s2 = fourcc_mod_broadcom_param(m);

        // No good masks to check modifier so check if we convert back it matches
        if (m != 0 && m != DRM_FORMAT_MOD_INVALID &&
            DRM_FORMAT_MOD_BROADCOM_SAND128_COL_HEIGHT(s2) == m)
            return (uint32_t)s2;
    }
    return 0;
}

void *
drmu_fb_data(const drmu_fb_t *const dfb, const unsigned int layer)
{
    return (layer >= 4 || dfb->map_ptr == NULL) ? NULL : (uint8_t * )dfb->map_ptr + dfb->fb.offsets[layer];
}

uint32_t
drmu_fb_width(const drmu_fb_t *const dfb)
{
    return dfb->fb.width;
}

uint32_t
drmu_fb_height(const drmu_fb_t *const dfb)
{
    return dfb->fb.height;
}

// Set cropping (fractional) - x, y, relative to active x, y (and must be +ve)
int
drmu_fb_crop_frac_set(drmu_fb_t *const dfb, drmu_rect_t crop_frac)
{
    // Sanity check
    if (crop_frac.x + crop_frac.w > (dfb->active.w << 16) ||
        crop_frac.y + crop_frac.h > (dfb->active.h << 16))
        return -EINVAL;

    dfb->crop = (drmu_rect_t){
        .x = crop_frac.x,
        .y = crop_frac.y,
        .w = crop_frac.w,
        .h = crop_frac.h
    };
    return 0;
}

drmu_rect_t
drmu_fb_crop_frac(const drmu_fb_t *const dfb)
{
    return dfb->crop;
}

drmu_rect_t
drmu_fb_active(const drmu_fb_t *const dfb)
{
    return dfb->active;
}


// active is in pixels
void
drmu_fb_int_fmt_size_set(drmu_fb_t *const dfb, uint32_t fmt, uint32_t w, uint32_t h, const drmu_rect_t active)
{
    dfb->fmt_info        = drmu_fmt_info_find_fmt(fmt);
    dfb->fb.pixel_format = fmt;
    dfb->fb.width        = w;
    dfb->fb.height       = h;
    dfb->active          = active;
    dfb->crop            = drmu_rect_shl16(active);
    dfb->chroma_siting   = drmu_fmt_info_chroma_siting(dfb->fmt_info);
}

void
drmu_fb_int_color_set(drmu_fb_t *const dfb, const drmu_color_encoding_t enc, const drmu_color_range_t range, const drmu_colorspace_t space)
{
    dfb->color_encoding = enc;
    dfb->color_range    = range;
    dfb->colorspace     = space;
}

void
drmu_fb_int_chroma_siting_set(drmu_fb_t *const dfb, const drmu_chroma_siting_t siting)
{
    dfb->chroma_siting   = siting;
}

void
drmu_fb_int_on_delete_set(drmu_fb_t *const dfb, drmu_fb_on_delete_fn fn, void * v)
{
    dfb->on_delete_fn = fn;
    dfb->on_delete_v  = v;
}

void
drmu_fb_int_bo_set(drmu_fb_t *const dfb, unsigned int i, drmu_bo_t * const bo)
{
    dfb->bo_list[i] = bo;
}

void
drmu_fb_int_layer_mod_set(drmu_fb_t *const dfb, unsigned int i, unsigned int obj_idx, uint32_t pitch, uint32_t offset, uint64_t modifier)
{
    dfb->fb.handles[i] = dfb->bo_list[obj_idx]->handle;
    dfb->fb.pitches[i] = pitch;
    dfb->fb.offsets[i] = offset;
    // We should be able to have "invalid" modifiers and not set the flag
    // but that produces EINVAL - so don't do that
    dfb->fb.modifier[i] = (modifier == DRM_FORMAT_MOD_INVALID) ? 0 : modifier;
}

void
drmu_fb_int_layer_set(drmu_fb_t *const dfb, unsigned int i, unsigned int obj_idx, uint32_t pitch, uint32_t offset)
{
    drmu_fb_int_layer_mod_set(dfb, i, obj_idx, pitch, offset, DRM_FORMAT_MOD_INVALID);
}

int
drmu_fb_int_make(drmu_fb_t *const dfb)
{
    drmu_env_t * du = dfb->du;
    int rv;

    dfb->fb.flags = (dfb->fb.modifier[0] == DRM_FORMAT_MOD_INVALID || dfb->fb.modifier[0] == 0) ? 0 : DRM_MODE_FB_MODIFIERS;

    if ((rv = drmu_ioctl(du, DRM_IOCTL_MODE_ADDFB2, &dfb->fb)) != 0)
        drmu_err(du, "AddFB2 failed: %s", strerror(-rv));
    return rv;
}

void
drmu_fb_hdr_metadata_set(drmu_fb_t *const dfb, const struct hdr_output_metadata * meta)
{
    if (meta == NULL) {
        dfb->hdr_metadata_isset = DRMU_ISSET_NULL;
    }
    else {
        dfb->hdr_metadata_isset = DRMU_ISSET_SET;
        dfb->hdr_metadata = *meta;
    }
}

drmu_isset_t
drmu_fb_hdr_metadata_isset(const drmu_fb_t *const dfb)
{
    return dfb->hdr_metadata_isset;
}

const struct hdr_output_metadata *
drmu_fb_hdr_metadata_get(const drmu_fb_t *const dfb)
{
    return dfb->hdr_metadata_isset == DRMU_ISSET_SET ? &dfb->hdr_metadata : NULL;
}

drmu_colorspace_t
drmu_fb_colorspace_get(const drmu_fb_t * const dfb)
{
    return dfb->colorspace;
}

const char *
drmu_color_range_to_broadcast_rgb(const drmu_color_range_t range)
{
    if (!drmu_color_range_is_set(range))
        return DRMU_BROADCAST_RGB_UNSET;
    else if (strcmp(range, DRMU_COLOR_RANGE_YCBCR_FULL_RANGE) == 0)
        return DRMU_BROADCAST_RGB_FULL;
    else if (strcmp(range, DRMU_COLOR_RANGE_YCBCR_LIMITED_RANGE) == 0)
        return DRMU_BROADCAST_RGB_LIMITED_16_235;
    return NULL;
}

drmu_color_range_t
drmu_fb_color_range_get(const drmu_fb_t * const dfb)
{
    return dfb->color_range;
}

const struct drmu_fmt_info_s *
drmu_fb_format_info_get(const drmu_fb_t * const dfb)
{
    return dfb->fmt_info;
}

drmu_fb_t *
drmu_fb_int_alloc(drmu_env_t * const du)
{
    drmu_fb_t * const dfb = calloc(1, sizeof(*dfb));
    if (dfb == NULL)
        return NULL;

    dfb->du = du;
    dfb->chroma_siting = DRMU_CHROMA_SITING_UNSPECIFIED;
    dfb->fence_fd = -1;
    return dfb;
}

// Bits per pixel on plane 0
unsigned int
drmu_fb_pixel_bits(const drmu_fb_t * const dfb)
{
    return drmu_fmt_info_pixel_bits(dfb->fmt_info);
}

uint32_t
drmu_fb_pixel_format(const drmu_fb_t * const dfb)
{
    return dfb->fb.pixel_format;
}

uint64_t
drmu_fb_modifier(const drmu_fb_t * const dfb, const unsigned int plane)
{
    return plane >= 4 ? DRM_FORMAT_MOD_INVALID : dfb->fb.modifier[plane];
}


// Writeback fence
// Must be unset before set again
// (This is as a handy hint that you must wait for the previous fence
// to go ready before you set a new one)
static int
atomic_fb_add_out_fence(drmu_atomic_t * const da, const uint32_t obj_id, const uint32_t prop_id, drmu_fb_t * const dfb)
{
    static const drmu_atomic_prop_fns_t fns = {
        .ref    = atomic_prop_fb_ref_cb,
        .unref  = atomic_prop_fb_unref_cb,
        .commit = drmu_prop_fn_null_commit,
    };

    if (!dfb)
        return -EINVAL;
    if (dfb->fence_fd != -1)
        return -EBUSY;

    return drmu_atomic_add_prop_generic(da, obj_id, prop_id, (uintptr_t)&dfb->fence_fd, &fns, dfb);
}

// For allocation purposes given fb_pixel bits how tall
// does the frame have to be to fit all planes if constant width
static unsigned int
fb_total_height(const drmu_fb_t * const dfb, const unsigned int h)
{
    unsigned int i;
    const drmu_fmt_info_t *const f = dfb->fmt_info;
    unsigned int t = 0;
    unsigned int h0 = h * drmu_fmt_info_wdiv(f, 0);
    const unsigned int c = drmu_fmt_info_plane_count(f);

    for (i = 0; i != c; ++i)
        t += h0 / (drmu_fmt_info_hdiv(f, i) * drmu_fmt_info_wdiv(f, i));

    return t;
}

static void
fb_pitches_set_mod(drmu_fb_t * const dfb, uint64_t mod)
{
    const drmu_fmt_info_t *const f = dfb->fmt_info;
    const uint32_t pitch0 = dfb->map_pitch * drmu_fmt_info_wdiv(f, 0);
    const uint32_t h = drmu_fb_height(dfb);
    const unsigned int c = drmu_fmt_info_plane_count(f);
    uint32_t t = 0;
    unsigned int i;

    // This should be true for anything we've allocated
    if (mod == DRM_FORMAT_MOD_BROADCOM_SAND128_COL_HEIGHT(0)) {
        // Cope with the joy that is sand
        mod = DRM_FORMAT_MOD_BROADCOM_SAND128_COL_HEIGHT(h * 3/2);
        drmu_fb_int_layer_mod_set(dfb, 0, 0, dfb->map_pitch, 0, mod);
        drmu_fb_int_layer_mod_set(dfb, 1, 0, dfb->map_pitch, h * 128, mod);
        return;
    }

    for (i = 0; i != c; ++i) {
        const unsigned int wdiv = drmu_fmt_info_wdiv(f, i);
        drmu_fb_int_layer_mod_set(dfb, i, 0, pitch0 / wdiv, t, mod);
        t += (pitch0 * h) / (drmu_fmt_info_hdiv(f, i) * wdiv);
    }
}

drmu_fb_t *
drmu_fb_new_dumb_mod(drmu_env_t * const du, uint32_t w, uint32_t h,
                     const uint32_t format, const uint64_t mod)
{
    drmu_fb_t * const dfb = drmu_fb_int_alloc(du);
    uint32_t bpp;
    int rv;
    uint32_t w2;
    const uint32_t s30_cw = 128 / 4 * 3;

    if (dfb == NULL) {
        drmu_err(du, "%s: Alloc failure", __func__);
        return NULL;
    }

    if (mod != DRM_FORMAT_MOD_BROADCOM_SAND128_COL_HEIGHT(0))
        w2 = (w + 15) & ~15;
    else if (format == DRM_FORMAT_NV12)
        w2 = (w + 127) & ~127;
    else if (format == DRM_FORMAT_P030)
        w2 = ((w + s30_cw - 1) / s30_cw) * s30_cw;
    else {
        // Unknown form of sand128
        drmu_err(du, "Sand modifier on unexpected format");
        goto fail;
    }

    drmu_fb_int_fmt_size_set(dfb, format, w2, (h + 15) & ~15, drmu_rect_wh(w, h));

    if ((bpp = drmu_fb_pixel_bits(dfb)) == 0) {
        drmu_err(du, "%s: Unexpected format %#x", __func__, format);
        goto fail;
    }

    {
        struct drm_mode_create_dumb dumb = {
            .height = fb_total_height(dfb, dfb->fb.height),
            .width = dfb->fb.width / drmu_fmt_info_wdiv(dfb->fmt_info, 0),
            .bpp = bpp
        };

        drmu_bo_t * bo = drmu_bo_new_dumb(du, &dumb);
        if (bo == NULL)
            goto fail;
        drmu_fb_int_bo_set(dfb, 0, bo);

        dfb->map_pitch = dumb.pitch;
        dfb->map_size = (size_t)dumb.size;
    }

    {
        struct drm_mode_map_dumb map_dumb = {
            .handle = dfb->bo_list[0]->handle
        };
        if ((rv = drmu_ioctl(du, DRM_IOCTL_MODE_MAP_DUMB, &map_dumb)) != 0)
        {
            drmu_err(du, "%s: map dumb failed: %s", __func__, strerror(-rv));
            goto fail;
        }

        if ((dfb->map_ptr = mmap(NULL, dfb->map_size,
                                 PROT_READ | PROT_WRITE, MAP_SHARED | MAP_POPULATE,
                                 drmu_fd(du), map_dumb.offset)) == MAP_FAILED) {
            drmu_err(du, "%s: mmap failed (size=%zd, fd=%d, off=%#"PRIx64"): %s", __func__,
                     dfb->map_size, drmu_fd(du), map_dumb.offset, strerror(errno));
            goto fail;
        }
    }

    fb_pitches_set_mod(dfb, mod);

    if (drmu_fb_int_make(dfb))
        goto fail;

    drmu_debug(du, "Create dumb %p %s %dx%d / %dx%d size: %zd", dfb,
               drmu_log_fourcc(format), dfb->fb.width, dfb->fb.height, dfb->active.w, dfb->active.h, dfb->map_size);
    return dfb;

fail:
    drmu_fb_int_free(dfb);
    return NULL;
}

drmu_fb_t *
drmu_fb_new_dumb(drmu_env_t * const du, uint32_t w, uint32_t h, const uint32_t format)
{
    return drmu_fb_new_dumb_mod(du, w, h, format, DRM_FORMAT_MOD_INVALID);
}

static int
fb_try_reuse(drmu_fb_t * dfb, uint32_t w, uint32_t h, const uint32_t format)
{
    if (w > dfb->fb.width || h > dfb->fb.height || format != dfb->fb.pixel_format)
        return 0;

    dfb->active = drmu_rect_wh(w, h);
    dfb->crop   = drmu_rect_shl16(dfb->active);
    return 1;
}

drmu_fb_t *
drmu_fb_realloc_dumb(drmu_env_t * const du, drmu_fb_t * dfb, uint32_t w, uint32_t h, const uint32_t format)
{
    if (dfb == NULL)
        return drmu_fb_new_dumb(du, w, h, format);

    if (fb_try_reuse(dfb, w, h, format))
        return dfb;

    drmu_fb_unref(&dfb);
    return drmu_fb_new_dumb(du, w, h, format);
}

static void
atomic_prop_fb_unref(void * v)
{
    drmu_fb_t * fb = v;
    drmu_fb_unref(&fb);
}

static void
atomic_prop_fb_ref(void * v)
{
    drmu_fb_ref(v);
}

int
drmu_atomic_add_prop_fb(drmu_atomic_t * const da, const uint32_t obj_id, const uint32_t prop_id, drmu_fb_t * const dfb)
{
    int rv;
    static const drmu_atomic_prop_fns_t fns = {
        .ref    = atomic_prop_fb_ref,
        .unref  = atomic_prop_fb_unref,
        .commit = drmu_prop_fn_null_commit,
    };

    if (dfb == NULL)
        return drmu_atomic_add_prop_value(da, obj_id, prop_id, 0);

    rv = drmu_atomic_add_prop_generic(da, obj_id, prop_id, dfb->fb.fb_id, &fns, dfb);
    if (rv != 0)
        drmu_warn(drmu_atomic_env(da), "%s: Failed to add fb obj_id=%#x, prop_id=%#x: %s", __func__, obj_id, prop_id, strerror(-rv));

    return rv;
}

//----------------------------------------------------------------------------
//
// props fns (internal)

typedef struct drmu_props_s {
    struct drmu_env_s * du;
    unsigned int n;
    drmu_propinfo_t * info;
    const drmu_propinfo_t ** by_name;
} drmu_props_t;

static void
props_free(drmu_props_t * const props)
{
    free(props->info);  // As yet nothing else is allocated off this
    free(props->by_name);
    free(props);
}

static const drmu_propinfo_t *
props_name_to_propinfo(const drmu_props_t * const props, const char * const name)
{
    unsigned int i = props->n / 2;
    unsigned int a = 0;
    unsigned int b = props->n;

    while (a < b) {
        const int r = strcmp(name, props->by_name[i]->prop.name);

        if (r == 0)
            return props->by_name[i];

        if (r < 0) {
            b = i;
            i = (i + a) / 2;
        } else {
            a = i + 1;
            i = (i + b) / 2;
        }
    }
    return NULL;
}

static uint32_t
props_name_to_id(const drmu_props_t * const props, const char * const name)
{
    return propinfo_prop_id(props_name_to_propinfo(props, name));
}

// Data must be freed later
static int
props_name_get_blob(const drmu_props_t * const props, const char * const name, void ** const ppdata, size_t * const plen)
{
    const drmu_propinfo_t * const pinfo = props_name_to_propinfo(props, name);

    *ppdata = 0;
    *plen = 0;

    if (!pinfo)
        return -ENOENT;
    if ((pinfo->prop.flags & DRM_MODE_PROP_BLOB) == 0)
        return -EINVAL;

    return blob_data_read(props->du, (uint32_t)pinfo->val, ppdata, plen);
}

#if TRACE_PROP_NEW
static void
props_dump(const drmu_props_t * const props)
{
    if (props != NULL) {
        unsigned int i;
        drmu_env_t * const du = props->du;

        for (i = 0; i != props->n; ++i) {
            const drmu_propinfo_t * const inf = props->info + i;
            const struct drm_mode_get_property * const p = &inf->prop;
            char flagbuf[256];

            flagbuf[0] = 0;
            if (p->flags & DRM_MODE_PROP_PENDING)
                strcat(flagbuf, ":pending");
            if (p->flags & DRM_MODE_PROP_RANGE)
                strcat(flagbuf, ":urange");
            if (p->flags & DRM_MODE_PROP_IMMUTABLE)
                strcat(flagbuf, ":immutable");
            if (p->flags & DRM_MODE_PROP_ENUM)
                strcat(flagbuf, ":enum");
            if (p->flags & DRM_MODE_PROP_BLOB)
                strcat(flagbuf, ":blob");
            if (p->flags & DRM_MODE_PROP_BITMASK)
                strcat(flagbuf, ":bitmask");
            if ((p->flags & DRM_MODE_PROP_EXTENDED_TYPE) == DRM_MODE_PROP_OBJECT)
                strcat(flagbuf, ":object");
            else if ((p->flags & DRM_MODE_PROP_EXTENDED_TYPE) == DRM_MODE_PROP_SIGNED_RANGE)
                strcat(flagbuf, ":srange");
            else if ((p->flags & DRM_MODE_PROP_EXTENDED_TYPE) != 0)
                strcat(flagbuf, ":?xtype?");
            if (p->flags & ~(DRM_MODE_PROP_LEGACY_TYPE |
                             DRM_MODE_PROP_EXTENDED_TYPE |
                             DRM_MODE_PROP_PENDING |
                             DRM_MODE_PROP_IMMUTABLE |
                             DRM_MODE_PROP_ATOMIC))
                strcat(flagbuf, ":?other?");
            if (p->flags & DRM_MODE_PROP_ATOMIC)
                strcat(flagbuf, ":atomic");


            drmu_info(du, "Prop%02d/%02d: %#-4x %-16s val=%#-4"PRIx64" flags=%#x%s, values=%d, blobs=%d",
                      i, props->n, p->prop_id,
                      p->name, inf->val,
                      p->flags, flagbuf,
                      p->count_values,
                      p->count_enum_blobs);
        }
    }
}
#endif

static int
props_qsort_by_name_cb(const void * va, const void * vb)
{
    const drmu_propinfo_t * const a = *(drmu_propinfo_t **)va;
    const drmu_propinfo_t * const b = *(drmu_propinfo_t **)vb;
    return strcmp(a->prop.name, b->prop.name);
}

// At the moment we don't need / want to fill in the values / blob arrays
// we just want the name - will get the extra info if we need it
static int
propinfo_fill(drmu_env_t * const du, drmu_propinfo_t * const inf, uint32_t propid, uint64_t val)
{
    int rv;

    inf->val = val;
    inf->prop.prop_id = propid;
    if ((rv = drmu_ioctl(du, DRM_IOCTL_MODE_GETPROPERTY, &inf->prop)) != 0)
        drmu_err(du, "Failed to get property %d: %s", propid, strerror(-rv));
    return rv;
}

static int
props_get_properties(drmu_env_t * const du, const uint32_t objid, const uint32_t objtype,
                     uint32_t ** const ppPropids, uint64_t ** const ppValues)
{
    struct drm_mode_obj_get_properties obj_props = {
        .obj_id = objid,
        .obj_type = objtype,
    };
    uint64_t * values = NULL;
    uint32_t * propids = NULL;
    unsigned int n = 0;
    int rv;

    for (;;) {
        if ((rv = drmu_ioctl(du, DRM_IOCTL_MODE_OBJ_GETPROPERTIES, &obj_props)) != 0) {
            drmu_err(du, "drmModeObjectGetProperties failed: %s", strerror(-rv));
            goto fail;
        }

        if (obj_props.count_props <= n)
            break;

        free(values);
        values = NULL;
        free(propids);
        propids = NULL;
        n = obj_props.count_props;
        if ((values = malloc(n * sizeof(*values))) == NULL ||
            (propids = malloc(n * sizeof(*propids))) == NULL) {
            drmu_err(du, "obj/value array alloc failed");
            rv = -ENOMEM;
            goto fail;
        }
        obj_props.prop_values_ptr = (uintptr_t)values;
        obj_props.props_ptr = (uintptr_t)propids;
    }

    *ppValues = values;
    *ppPropids = propids;
    return (int)n;

fail:
    free(values);
    free(propids);
    *ppPropids = NULL;
    *ppValues = NULL;
    return rv;
}

static drmu_props_t *
props_new(drmu_env_t * const du, const uint32_t objid, const uint32_t objtype)
{
    drmu_props_t * const props = calloc(1, sizeof(*props));
    int rv;
    uint64_t * values;
    uint32_t * propids;

    if (props == NULL) {
        drmu_err(du, "%s: Failed struct alloc", __func__);
        return NULL;
    }
    props->du = du;

    if ((rv = props_get_properties(du, objid, objtype, &propids, &values)) < 0)
        goto fail;

    props->n = rv;
    if ((props->info = calloc(props->n, sizeof(*props->info))) == NULL ||
        (props->by_name = malloc(props->n * sizeof(*props->by_name))) == NULL) {
        drmu_err(du, "info/name array alloc failed");
        goto fail;
    }

    for (unsigned int i = 0; i < props->n; ++i) {
        drmu_propinfo_t * const inf = props->info + i;

        props->by_name[i] = inf;
        if ((rv = propinfo_fill(du, inf, propids[i], values[i])) != 0)
            goto fail;
    }

    // Sort into name order for faster lookup
    qsort(props->by_name, props->n, sizeof(*props->by_name), props_qsort_by_name_cb);

    free(values);
    free(propids);
    return props;

fail:
    props_free(props);
    free(values);
    free(propids);
    return NULL;
}

int
drmu_atomic_obj_add_snapshot(drmu_atomic_t * const da, const uint32_t objid, const uint32_t objtype)
{
#if 0
    drmu_env_t * const du = drmu_atomic_env(da);
    drmu_props_t * props = NULL;
    unsigned int i;
    int rv;

    if (!du)
        return -EINVAL;

    if ((props = props_new(du, objid, objtype)) == NULL)
        return -ENOENT;

    for (i = 0; i != props->n; ++i) {
        if ((props->info[i].prop.flags & (DRM_MODE_PROP_IMMUTABLE | DRM_MODE_PROP_ATOMIC)) != 0 || props->info[i].prop.prop_id == 2)
            continue;
        if ((rv = drmu_atomic_add_prop_generic(da, objid, props->info[i].prop.prop_id, props->info[i].val, 0, 0, NULL)) != 0)
            goto fail;
    }
    rv = 0;

fail:
    props_free(props);
    return rv;
#else
    uint64_t * values;
    uint32_t * propids;
    int rv;
    unsigned int i, n;
    drmu_env_t * const du = drmu_atomic_env(da);

    if (!du)
        return -EINVAL;

    if ((rv = props_get_properties(du, objid, objtype, &propids, &values)) < 0)
        goto fail;
    n = rv;

    for (i = 0; i != n; ++i) {
        if ((rv = drmu_atomic_add_prop_value(da, objid, propids[i], values[i])) != 0)
            goto fail;
    }

fail:
    free(values);
    free(propids);
    return rv;
#endif
}

static int
drmu_atomic_props_add_save(drmu_atomic_t * const da, const uint32_t objid, const drmu_props_t * props)
{
    unsigned int i;
    int rv;
    drmu_env_t * const du = drmu_atomic_env(da);

    if (props == NULL)
        return 0;
    if (du == NULL)
        return -EINVAL;

    for (i = 0; i != props->n; ++i) {
        if ((props->info[i].prop.flags & DRM_MODE_PROP_IMMUTABLE) != 0)
            continue;

        // Blobs, if set, are prone to running out of refs and vanishing, so we
        // must copy. If we fail to copy the blob for any reason drop through
        // to the generic add and hope that that will do
        if ((props->info[i].prop.flags & DRM_MODE_PROP_BLOB) != 0 && props->info[i].val != 0) {
            drmu_blob_t * b = drmu_blob_copy_id(du, (uint32_t)props->info[i].val);
            if (b != NULL) {
                rv = drmu_atomic_add_prop_blob(da, objid, props->info[i].prop.prop_id, b);
                drmu_blob_unref(&b);
                if (rv == 0)
                    continue;
            }
        }

        // Generic value
        if ((rv = drmu_atomic_add_prop_value(da, objid, props->info[i].prop.prop_id, props->info[i].val)) != 0)
            return rv;
    }
    return 0;
}

//----------------------------------------------------------------------------
//
// CRTC fns

typedef struct drmu_crtc_s {
    struct drmu_env_s * du;
    int crtc_idx;

    atomic_int ref_count;
    bool saved;

    struct drm_mode_crtc crtc;

    struct {
        drmu_prop_range_t * active;
        uint32_t mode_id;
    } pid;

    drmu_blob_t * mode_id_blob;

} drmu_crtc_t;

static void
free_crtc(drmu_crtc_t * const dc)
{
    drmu_blob_unref(&dc->mode_id_blob);
    free(dc);
}

static void
crtc_uninit(drmu_crtc_t * const dc)
{
    (void)dc;
}


#if 0
// Set misc derived vars from mode
static void
crtc_mode_set_vars(drmu_crtc_t * const dc)
{
    switch (dc->crtc.mode.flags & DRM_MODE_FLAG_PIC_AR_MASK) {
        case DRM_MODE_FLAG_PIC_AR_4_3:
            dc->par = (drmu_ufrac_t){4,3};
            break;
        case DRM_MODE_FLAG_PIC_AR_16_9:
            dc->par = (drmu_ufrac_t){16,9};
            break;
        case DRM_MODE_FLAG_PIC_AR_64_27:
            dc->par = (drmu_ufrac_t){64,27};
            break;
        case DRM_MODE_FLAG_PIC_AR_256_135:
            dc->par = (drmu_ufrac_t){256,135};
            break;
        default:
        case DRM_MODE_FLAG_PIC_AR_NONE:
            dc->par = (drmu_ufrac_t){0,0};
            break;
    }

    if (dc->par.den == 0) {
        // Assume 1:1
        dc->sar = (drmu_ufrac_t){1,1};
    }
    else {
        dc->sar = drmu_ufrac_reduce((drmu_ufrac_t) {dc->par.num * dc->crtc.mode.vdisplay, dc->par.den * dc->crtc.mode.hdisplay});
    }
}

static int
atomic_crtc_bpc_set(drmu_atomic_t * const da, drmu_crtc_t * const dc,
                    const char * const colorspace,
                    const unsigned int max_bpc)
{
    const uint32_t con_id = dc->con->connector_id;
    int rv = 0;

    if (!dc->du->modeset_allow)
        return 0;

    if ((dc->pid.colorspace &&
         (rv = drmu_atomic_add_prop_enum(da, con_id, dc->pid.colorspace, colorspace)) != 0) ||
        (dc->pid.max_bpc &&
         (rv = drmu_atomic_add_prop_range(da, con_id, dc->pid.max_bpc, max_bpc)) != 0))
        return rv;
    return 0;
}


static int
atomic_crtc_hi_bpc_set(drmu_atomic_t * const da, drmu_crtc_t * const dc)
{
    return atomic_crtc_bpc_set(da, dc, "BT2020_YCC", 12);
}
#endif

void
drmu_crtc_delete(drmu_crtc_t ** ppdc)
{
    drmu_crtc_t * const dc = * ppdc;

    if (dc == NULL)
        return;
    *ppdc = NULL;

    free_crtc(dc);
}

drmu_env_t *
drmu_crtc_env(const drmu_crtc_t * const dc)
{
    return dc == NULL ? NULL : dc->du;
}

uint32_t
drmu_crtc_id(const drmu_crtc_t * const dc)
{
    return dc->crtc.crtc_id;
}

int
drmu_crtc_idx(const drmu_crtc_t * const dc)
{
    return dc->crtc_idx;
}

static int
crtc_init(drmu_env_t * const du, drmu_crtc_t * const dc, const unsigned int idx, const uint32_t crtc_id)
{
    int rv;
    drmu_props_t * props;

    memset(dc, 0, sizeof(*dc));
    dc->du = du;
    dc->crtc_idx = idx;
    dc->crtc.crtc_id = crtc_id;

    if ((rv = drmu_ioctl(du, DRM_IOCTL_MODE_GETCRTC, &dc->crtc)) != 0) {
        drmu_err(du, "Failed to get crtc id %d: %s", crtc_id, strerror(-rv));
        return rv;
    }

    props = props_new(du, dc->crtc.crtc_id, DRM_MODE_OBJECT_CRTC);

    if (props != NULL) {
#if TRACE_PROP_NEW
        drmu_info(du, "CRTC id=%#x, idx=%d:", dc->crtc.crtc_id, dc->crtc_idx);
        props_dump(props);
#endif
        dc->pid.mode_id = props_name_to_id(props, "MODE_ID");
        dc->pid.active = drmu_prop_range_new(du, props_name_to_id(props, "ACTIVE"));

        props_free(props);
    }

    return 0;
}

static drmu_ufrac_t
modeinfo_par(const struct drm_mode_modeinfo * const mode)
{
    switch (mode->flags & DRM_MODE_FLAG_PIC_AR_MASK) {
        case DRM_MODE_FLAG_PIC_AR_4_3:
            return (drmu_ufrac_t){4,3};
        case DRM_MODE_FLAG_PIC_AR_16_9:
            return (drmu_ufrac_t){16,9};
        case DRM_MODE_FLAG_PIC_AR_64_27:
            return (drmu_ufrac_t){64,27};
        case DRM_MODE_FLAG_PIC_AR_256_135:
            return (drmu_ufrac_t){256,135};
        default:
        case DRM_MODE_FLAG_PIC_AR_NONE:
            break;
    }
    return (drmu_ufrac_t){0,0};
}

static drmu_mode_simple_params_t
modeinfo_simple_params(const struct drm_mode_modeinfo * const mode)
{
    if (!mode)
        return (drmu_mode_simple_params_t){ 0 };
    else {
        drmu_mode_simple_params_t rv = {
            .width = mode->hdisplay,
            .height = mode->vdisplay,
            .hz_x_1000 = (uint32_t)(((uint64_t)mode->clock * 1000000) / (mode->htotal * mode->vtotal)),
            .par = modeinfo_par(mode),
            .sar = {1, 1},
            .type = mode->type,
            .flags = mode->flags,
        };

        if (rv.par.den != 0)
            rv.sar = drmu_ufrac_reduce((drmu_ufrac_t) {rv.par.num * rv.height, rv.par.den * rv.width});

        return rv;
    }
}

drmu_crtc_t *
drmu_env_crtc_find_id(drmu_env_t * const du, const uint32_t crtc_id)
{
    unsigned int i;
    drmu_crtc_t * dc;

    for (i = 0; (dc = drmu_env_crtc_find_n(du, i)) != NULL; ++i) {
        if (dc->crtc.crtc_id == crtc_id)
            return dc;
    }
    return NULL;
}

const struct drm_mode_modeinfo *
drmu_crtc_modeinfo(const drmu_crtc_t * const dc)
{
    if (!dc || !dc->crtc.mode_valid)
        return NULL;
    return &dc->crtc.mode;
}

drmu_mode_simple_params_t
drmu_crtc_mode_simple_params(const drmu_crtc_t * const dc)
{
    return modeinfo_simple_params(drmu_crtc_modeinfo(dc));
}

int
drmu_atomic_crtc_add_modeinfo(struct drmu_atomic_s * const da, drmu_crtc_t * const dc, const struct drm_mode_modeinfo * const modeinfo)
{
    drmu_env_t * const du = drmu_atomic_env(da);
    int rv;

    if (!modeinfo || dc->pid.mode_id == 0)
        return 0;

    if ((rv = drmu_blob_update(du, &dc->mode_id_blob, modeinfo, sizeof(*modeinfo))) != 0)
        return rv;

    return drmu_atomic_add_prop_blob(da, dc->crtc.crtc_id, dc->pid.mode_id, dc->mode_id_blob);
}

int
drmu_atomic_crtc_add_active(struct drmu_atomic_s * const da, drmu_crtc_t * const dc, unsigned int val)
{
    return drmu_atomic_add_prop_range(da, dc->crtc.crtc_id, dc->pid.active, val);
}

// Use the same claim logic as we do for planes
// As it stands we don't do anything much on final unref so the logic
// isn't really needed but it doesn't cost us much so do this way against
// future need

bool
drmu_crtc_is_claimed(const drmu_crtc_t * const dc)
{
    return atomic_load(&dc->ref_count) != 0;
}

void
drmu_crtc_unref(drmu_crtc_t ** const ppdc)
{
    drmu_crtc_t * const dc = *ppdc;

    if (dc == NULL)
        return;
    *ppdc = NULL;

    if (atomic_fetch_sub(&dc->ref_count, 1) != 2)
        return;
    atomic_store(&dc->ref_count, 0);
}

drmu_crtc_t *
drmu_crtc_ref(drmu_crtc_t * const dc)
{
    if (!dc)
        return NULL;
    atomic_fetch_add(&dc->ref_count, 1);
    return dc;
}

// A Conn should be claimed before any op that might change its state
int
drmu_crtc_claim_ref(drmu_crtc_t * const dc)
{
    drmu_env_t * const du = dc->du;
    static const int ref0 = 0;
    if (!atomic_compare_exchange_strong(&dc->ref_count, &ref0, 2))
        return -EBUSY;

    // 1st time through save state
    if (!dc->saved && env_object_state_save(du, dc->crtc.crtc_id, DRM_MODE_OBJECT_CRTC) == 0)
        dc->saved = true;

    return 0;
}

//----------------------------------------------------------------------------
//
// CONN functions

static const char * conn_type_names[32] = {
    [DRM_MODE_CONNECTOR_Unknown]     = "Unknown",
    [DRM_MODE_CONNECTOR_VGA]         = "VGA",
    [DRM_MODE_CONNECTOR_DVII]        = "DVI-I",
    [DRM_MODE_CONNECTOR_DVID]        = "DVI-D",
    [DRM_MODE_CONNECTOR_DVIA]        = "DVI-A",
    [DRM_MODE_CONNECTOR_Composite]   = "Composite",
    [DRM_MODE_CONNECTOR_SVIDEO]      = "SVIDEO",
    [DRM_MODE_CONNECTOR_LVDS]        = "LVDS",
    [DRM_MODE_CONNECTOR_Component]   = "Component",
    [DRM_MODE_CONNECTOR_9PinDIN]     = "9PinDIN",
    [DRM_MODE_CONNECTOR_DisplayPort] = "DisplayPort",
    [DRM_MODE_CONNECTOR_HDMIA]       = "HDMI-A",
    [DRM_MODE_CONNECTOR_HDMIB]       = "HDMI-B",
    [DRM_MODE_CONNECTOR_TV]          = "TV",
    [DRM_MODE_CONNECTOR_eDP]         = "eDP",
    [DRM_MODE_CONNECTOR_VIRTUAL]     = "VIRTUAL",
    [DRM_MODE_CONNECTOR_DSI]         = "DSI",
    [DRM_MODE_CONNECTOR_DPI]         = "DPI",
    [DRM_MODE_CONNECTOR_WRITEBACK]   = "WRITEBACK",
    [DRM_MODE_CONNECTOR_SPI]         = "SPI",
#ifdef DRM_MODE_CONNECTOR_USB
    [DRM_MODE_CONNECTOR_USB]         = "USB",
#endif
};

struct drmu_conn_s {
    drmu_env_t * du;
    unsigned int conn_idx;

    atomic_int ref_count;
    bool saved;

    struct drm_mode_get_connector conn;
    bool probed;
    unsigned int modes_size;
    unsigned int enc_ids_size;
    struct drm_mode_modeinfo * modes;
    uint32_t * enc_ids;

    uint32_t avail_crtc_mask;

    struct {
        drmu_prop_object_t * crtc_id;
        drmu_prop_range_t * max_bpc;
        drmu_prop_enum_t * colorspace;
        drmu_prop_enum_t * broadcast_rgb;
        uint32_t hdr_output_metadata;
        uint32_t writeback_out_fence_ptr;
        uint32_t writeback_fb_id;
        uint32_t writeback_pixel_formats;
    } pid;

    drmu_blob_t * hdr_metadata_blob;

    char name[32];
};


int
drmu_atomic_conn_add_hdr_metadata(drmu_atomic_t * const da, drmu_conn_t * const dn, const struct hdr_output_metadata * const m)
{
    drmu_env_t * const du = drmu_atomic_env(da);
    int rv;

    if (!du || !dn)  // du will be null if da is null
        return -ENOENT;

    if (dn->pid.hdr_output_metadata == 0)
        return 0;

    if ((rv = drmu_blob_update(du, &dn->hdr_metadata_blob, m, sizeof(*m))) != 0)
        return rv;

    rv = drmu_atomic_add_prop_blob(da, dn->conn.connector_id, dn->pid.hdr_output_metadata, dn->hdr_metadata_blob);
    if (rv != 0)
        drmu_err(du, "Set property fail: %s", strerror(errno));

    return rv;
}

int
drmu_atomic_conn_add_hi_bpc(drmu_atomic_t * const da, drmu_conn_t * const dn, bool hi_bpc)
{
    return drmu_atomic_add_prop_range(da, dn->conn.connector_id, dn->pid.max_bpc, !hi_bpc ? 8 :
                                      drmu_prop_range_max(dn->pid.max_bpc));
}

int
drmu_atomic_conn_add_colorspace(drmu_atomic_t * const da, drmu_conn_t * const dn, const drmu_colorspace_t colorspace)
{
    if (!dn->pid.colorspace)
        return 0;

    return drmu_atomic_add_prop_enum(da, dn->conn.connector_id, dn->pid.colorspace, colorspace);
}

int
drmu_atomic_conn_add_broadcast_rgb(drmu_atomic_t * const da, drmu_conn_t * const dn, const drmu_broadcast_rgb_t bcrgb)
{
    if (!dn->pid.broadcast_rgb)
        return 0;

    return drmu_atomic_add_prop_enum(da, dn->conn.connector_id, dn->pid.broadcast_rgb, bcrgb);
}

int
drmu_atomic_conn_add_crtc(drmu_atomic_t * const da, drmu_conn_t * const dn, drmu_crtc_t * const dc)
{
    return drmu_atomic_add_prop_object(da, dn->pid.crtc_id, drmu_crtc_id(dc));
}

int
drmu_atomic_conn_add_writeback_fb(drmu_atomic_t * const da_out, drmu_conn_t * const dn,
                                  drmu_fb_t * const dfb)
{
    // Add both or neither, so build a temp atomic to store the intermediate result
    drmu_atomic_t * da = drmu_atomic_new(drmu_atomic_env(da_out));
    int rv;

    if (!da)
        return -ENOMEM;

    if ((rv = atomic_fb_add_out_fence(da, dn->conn.connector_id, dn->pid.writeback_out_fence_ptr, dfb)) != 0)
        goto fail;

    if ((rv = drmu_atomic_add_prop_fb(da, dn->conn.connector_id, dn->pid.writeback_fb_id, dfb)) != 0)
        goto fail;

    return drmu_atomic_merge(da_out, &da);

fail:
    drmu_atomic_unref(&da);
    return rv;
}

const struct drm_mode_modeinfo *
drmu_conn_modeinfo(const drmu_conn_t * const dn, const int mode_id)
{
    return !dn || mode_id < 0 || (unsigned int)mode_id >= dn->conn.count_modes ? NULL :
        dn->modes + mode_id;
}

drmu_mode_simple_params_t
drmu_conn_mode_simple_params(const drmu_conn_t * const dn, const int mode_id)
{
    return modeinfo_simple_params(drmu_conn_modeinfo(dn, mode_id));
}

bool
drmu_conn_is_output(const drmu_conn_t * const dn)
{
    return dn->conn.connector_type != DRM_MODE_CONNECTOR_WRITEBACK;
}

bool
drmu_conn_is_writeback(const drmu_conn_t * const dn)
{
    return dn->conn.connector_type == DRM_MODE_CONNECTOR_WRITEBACK;
}

const char *
drmu_conn_name(const drmu_conn_t * const dn)
{
    return dn->name;
}

uint32_t
drmu_conn_crtc_id_get(const drmu_conn_t * const dn)
{
    return drmu_prop_object_value(dn->pid.crtc_id);
}

uint32_t
drmu_conn_possible_crtcs(const drmu_conn_t * const dn)
{
    return dn->avail_crtc_mask;
}

unsigned int
drmu_conn_idx_get(const drmu_conn_t * const dn)
{
    return dn->conn_idx;
}

static void
conn_uninit(drmu_conn_t * const dn)
{
    drmu_prop_object_unref(&dn->pid.crtc_id);
    drmu_prop_range_delete(&dn->pid.max_bpc);
    drmu_prop_enum_delete(&dn->pid.colorspace);
    drmu_prop_enum_delete(&dn->pid.broadcast_rgb);

    drmu_blob_unref(&dn->hdr_metadata_blob);

    free(dn->modes);
    free(dn->enc_ids);
    dn->modes = NULL;
    dn->enc_ids = NULL;
    dn->modes_size = 0;
    dn->enc_ids_size = 0;
}

// Assumes zeroed before entry
static int
conn_init(drmu_env_t * const du, drmu_conn_t * const dn, unsigned int conn_idx, const uint32_t conn_id)
{
    int rv;
    drmu_props_t * props;
    uint32_t modes_req = 0;
    uint32_t encs_req = 0;

    dn->du = du;
    dn->conn_idx = conn_idx;
    // * As count_modes == 0 this probes - do we really want this?

    do {
        memset(&dn->conn, 0, sizeof(dn->conn));
        dn->conn.connector_id = conn_id;

        if (modes_req > dn->modes_size) {
            free(dn->modes);
            if ((dn->modes = malloc(modes_req * sizeof(*dn->modes))) == NULL) {
                drmu_err(du, "Failed to alloc modes array");
                goto fail;
            }
            dn->modes_size = modes_req;
        }
        dn->conn.modes_ptr = (uintptr_t)dn->modes;
        dn->conn.count_modes = modes_req;

        if (encs_req > dn->enc_ids_size) {
            free(dn->enc_ids);
            if ((dn->enc_ids = malloc(encs_req * sizeof(*dn->enc_ids))) == NULL) {
                drmu_err(du, "Failed to alloc encs array");
                goto fail;
            }
            dn->enc_ids_size = encs_req;
        }
        dn->conn.encoders_ptr = (uintptr_t)dn->enc_ids;
        dn->conn.count_encoders = encs_req;

        if ((rv = drmu_ioctl(du, DRM_IOCTL_MODE_GETCONNECTOR, &dn->conn)) != 0) {
            drmu_err(du, "Get connector id %d failed: %s", dn->conn.connector_id, strerror(-rv));
            goto fail;
        }
        modes_req = dn->conn.count_modes;
        encs_req = dn->conn.count_encoders;

    } while (dn->modes_size < modes_req || dn->enc_ids_size < encs_req);

    dn->probed = true;

    if (dn->conn.connector_type >= sizeof(conn_type_names) / sizeof(conn_type_names[0]))
        snprintf(dn->name, sizeof(dn->name)-1, "CT%"PRIu32"-%"PRIu32,
                 dn->conn.connector_type, dn->conn.connector_type_id);
    else
        snprintf(dn->name, sizeof(dn->name)-1, "%s-%"PRIu32,
                 conn_type_names[dn->conn.connector_type], dn->conn.connector_type_id);

    props = props_new(du, dn->conn.connector_id, DRM_MODE_OBJECT_CONNECTOR);

    // Spin over encoders to create a crtc mask
    dn->avail_crtc_mask = 0;
    for (unsigned int i = 0; i != dn->conn.count_encoders; ++i) {
        struct drm_mode_get_encoder enc = {.encoder_id = dn->enc_ids[i]};
        if ((rv = drmu_ioctl(du, DRM_IOCTL_MODE_GETENCODER, &enc)) != 0) {
            drmu_warn(du, "Failed to get encoder: id: %#x", enc.encoder_id);
            continue;
        }
        dn->avail_crtc_mask |= enc.possible_crtcs;
    }

    if (props != NULL) {
#if TRACE_PROP_NEW
        drmu_info(du, "Connector id=%d, type=%d.%d (%s), crtc_mask=%#x:",
                  dn->conn.connector_id, dn->conn.connector_type, dn->conn.connector_type_id, drmu_conn_name(dn),
                  dn->avail_crtc_mask);
        props_dump(props);
#endif
        dn->pid.crtc_id             = drmu_prop_object_new_propinfo(du, dn->conn.connector_id, props_name_to_propinfo(props, "CRTC_ID"));
        dn->pid.max_bpc             = drmu_prop_range_new(du, props_name_to_id(props, "max bpc"));
        dn->pid.colorspace          = drmu_prop_enum_new(du, props_name_to_id(props, "Colorspace"));
        dn->pid.broadcast_rgb       = drmu_prop_enum_new(du, props_name_to_id(props, "Broadcast RGB"));
        dn->pid.hdr_output_metadata = props_name_to_id(props, "HDR_OUTPUT_METADATA");
        dn->pid.writeback_fb_id     = props_name_to_id(props, "WRITEBACK_FB_ID");
        dn->pid.writeback_out_fence_ptr = props_name_to_id(props, "WRITEBACK_OUT_FENCE_PTR");
        dn->pid.writeback_pixel_formats = props_name_to_id(props, "WRITEBACK_PIXEL_FORMATS");  // Blob of fourccs (no modifier info)

        props_free(props);
    }

    return 0;

fail:
    conn_uninit(dn);
    return rv;
}

// Use the same claim logic as we do for planes
// As it stands we don't do anything much on final unref so the logic
// isn't really needed but it doesn't cost us much so do this way against
// future need

bool
drmu_conn_is_claimed(const drmu_conn_t * const dn)
{
    return atomic_load(&dn->ref_count) != 0;
}

void
drmu_conn_unref(drmu_conn_t ** const ppdn)
{
    drmu_conn_t * const dn = *ppdn;

    if (dn == NULL)
        return;
    *ppdn = NULL;

    if (atomic_fetch_sub(&dn->ref_count, 1) != 2)
        return;
    atomic_store(&dn->ref_count, 0);
}

drmu_conn_t *
drmu_conn_ref(drmu_conn_t * const dn)
{
    if (!dn)
        return NULL;
    atomic_fetch_add(&dn->ref_count, 1);
    return dn;
}

// A Conn should be claimed before any op that might change its state
int
drmu_conn_claim_ref(drmu_conn_t * const dn)
{
    drmu_env_t * const du = dn->du;
    static const int ref0 = 0;
    if (!atomic_compare_exchange_strong(&dn->ref_count, &ref0, 2))
        return -EBUSY;

    // 1st time through save state
    if (!dn->saved && env_object_state_save(du, dn->conn.connector_id, DRM_MODE_OBJECT_CONNECTOR) == 0)
        dn->saved = true;

    return 0;
}

//----------------------------------------------------------------------------
//
// Atomic Q fns (internal)

typedef struct drmu_atomic_q_s {
    pthread_mutex_t lock;
    pthread_cond_t cond;
    drmu_atomic_t * next_flip;
    drmu_atomic_t * cur_flip;
    drmu_atomic_t * last_flip;
    unsigned int retry_count;
    struct polltask * retry_task;
} drmu_atomic_q_t;

static void atomic_q_retry(drmu_atomic_q_t * const aq, drmu_env_t * const du);

// Needs locked
static int
atomic_q_attempt_commit_next(drmu_atomic_q_t * const aq)
{
    drmu_env_t * const du = drmu_atomic_env(aq->next_flip);
    uint32_t flags = DRM_MODE_ATOMIC_NONBLOCK | DRM_MODE_PAGE_FLIP_EVENT | DRM_MODE_ATOMIC_ALLOW_MODESET;
    int rv;

    if ((rv = drmu_atomic_commit(aq->next_flip, flags)) == 0) {
        if (aq->retry_count != 0)
            drmu_warn(du, "%s: Atomic commit OK", __func__);
        aq->cur_flip = aq->next_flip;
        aq->next_flip = NULL;
        aq->retry_count = 0;
    }
    else if (rv == -EBUSY && ++aq->retry_count < 16) {
        // This really shouldn't happen but we observe that the 1st commit after
        // a modeset often fails with BUSY.  It seems to be fine on a 10ms retry
        // but allow some more in case ww need a bit longer in some cases
        drmu_warn(du, "%s: Atomic commit BUSY", __func__);
        atomic_q_retry(aq, du);
        rv = 0;
    }
    else {
        drmu_err(du, "%s: Atomic commit failed: %s", __func__, strerror(-rv));
        drmu_atomic_dump(aq->next_flip);
        drmu_atomic_unref(&aq->next_flip);
        aq->retry_count = 0;
    }

    return rv;
}

static void
atomic_q_retry_cb(void * v, short revents)
{
    drmu_atomic_q_t * const aq = v;
    (void)revents;

    pthread_mutex_lock(&aq->lock);

    // If we need a retry then next != NULL && cur == NULL
    // if not that then we've fixed ourselves elsewhere

    if (aq->next_flip != NULL && aq->cur_flip == NULL)
        atomic_q_attempt_commit_next(aq);

    pthread_mutex_unlock(&aq->lock);
}

static void
atomic_q_retry(drmu_atomic_q_t * const aq, drmu_env_t * const du)
{
    if (aq->retry_task == NULL)
        aq->retry_task = polltask_new_timer(env_pollqueue(du), atomic_q_retry_cb, aq);
    pollqueue_add_task(aq->retry_task, 20);
}

// Called after an atomic commit has completed
// not called on every vsync, so if we haven't committed anything this won't be called
static void
drmu_atomic_page_flip_cb(int fd, unsigned int sequence, unsigned int tv_sec, unsigned int tv_usec, unsigned int crtc_id, void *user_data)
{
    drmu_atomic_t * const da = user_data;
    drmu_env_t * const du = drmu_atomic_env(da);
    drmu_atomic_q_t * const aq = env_atomic_q(du);

    (void)fd;
    (void)sequence;
    (void)tv_sec;
    (void)tv_usec;
    (void)crtc_id;

    // At this point:
    //  next   The atomic we are about to commit
    //  cur    The last atomic we committed, now in use (must be != NULL)
    //  last   The atomic that has just become obsolete

    pthread_mutex_lock(&aq->lock);

    if (da != aq->cur_flip) {
        drmu_err(du, "%s: User data el (%p) != cur (%p)", __func__, da, aq->cur_flip);
    }

    drmu_atomic_unref(&aq->last_flip);
    aq->last_flip = aq->cur_flip;
    aq->cur_flip = NULL;

    if (aq->next_flip != NULL)
        atomic_q_attempt_commit_next(aq);

    pthread_cond_broadcast(&aq->cond);
    pthread_mutex_unlock(&aq->lock);
}

static int
atomic_q_flush(drmu_atomic_q_t * const aq)
{
    struct timespec ts;
    int rv = 0;

    pthread_mutex_lock(&aq->lock);
    clock_gettime(CLOCK_MONOTONIC, &ts);
    ts.tv_sec += 1;  // We should never timeout if all is well - 1 sec is plenty

    // Can flush next safely
    drmu_atomic_unref(&aq->next_flip);

    // Wait for cur to finish - seems to confuse the world otherwise
    while (aq->cur_flip != NULL) {
        if ((rv = pthread_cond_timedwait(&aq->cond, &aq->lock, &ts)) != 0)
            break;
    }

    pthread_mutex_unlock(&aq->lock);
    return rv;
}

// 'consumes' da
static int
atomic_q_queue(drmu_atomic_q_t * const aq, drmu_atomic_t * da)
{
    int rv = 0;

    pthread_mutex_lock(&aq->lock);

    if (aq->next_flip != NULL) {
        // We already have something pending or retrying - merge the new with it
        rv = drmu_atomic_merge(aq->next_flip, &da);
    }
    else {
        aq->next_flip = da;

        // No pending commit?
        if (aq->cur_flip == NULL)
            rv = atomic_q_attempt_commit_next(aq);
    }

    pthread_mutex_unlock(&aq->lock);
    return rv;
}

// Consumes the passed atomic structure as it isn't copied
// * arguably should copy & unref if ref count != 0
int
drmu_atomic_queue(drmu_atomic_t ** ppda)
{
    drmu_atomic_t * da = *ppda;

    if (da == NULL)
        return 0;
    *ppda = NULL;

    return atomic_q_queue(env_atomic_q(drmu_atomic_env(da)), da);
}

int
drmu_env_queue_wait(drmu_env_t * const du)
{

    drmu_atomic_q_t *const aq = env_atomic_q(du);
    struct timespec ts;
    int rv = 0;

    pthread_mutex_lock(&aq->lock);
    clock_gettime(CLOCK_MONOTONIC, &ts);
    ts.tv_sec += 1;  // We should never timeout if all is well - 1 sec is plenty

    // Next should clear quickly
    while (aq->next_flip != NULL) {
        if ((rv = pthread_cond_timedwait(&aq->cond, &aq->lock, &ts)) != 0)
            break;
    }

    pthread_mutex_unlock(&aq->lock);
    return rv;
}

static void
atomic_q_uninit(drmu_atomic_q_t * const aq)
{
    polltask_delete(&aq->retry_task);
    drmu_atomic_unref(&aq->next_flip);
    drmu_atomic_unref(&aq->cur_flip);
    drmu_atomic_unref(&aq->last_flip);
    pthread_cond_destroy(&aq->cond);
    pthread_mutex_destroy(&aq->lock);
}

static void
atomic_q_init(drmu_atomic_q_t * const aq)
{
    pthread_condattr_t condattr;

    aq->next_flip = NULL;
    pthread_mutex_init(&aq->lock, NULL);

    pthread_condattr_init(&condattr);
    pthread_condattr_setclock(&condattr, CLOCK_MONOTONIC);
    pthread_cond_init(&aq->cond, &condattr);
    pthread_condattr_destroy(&condattr);
}

//----------------------------------------------------------------------------
//
// Pool fns

typedef struct drmu_fb_list_s {
    drmu_fb_t * head;
    drmu_fb_t * tail;
} drmu_fb_list_t;

typedef struct drmu_pool_s {
    atomic_int ref_count;  // 0 == 1 ref for ease of init

    struct drmu_env_s * du;

    pthread_mutex_t lock;
    int dead;

    unsigned int seq;  // debug

    unsigned int fb_count;
    unsigned int fb_max;

    drmu_fb_list_t free_fbs;
} drmu_pool_t;

static void
fb_list_add_tail(drmu_fb_list_t * const fbl, drmu_fb_t * const dfb)
{
    assert(dfb->prev == NULL && dfb->next == NULL);

    if (fbl->tail == NULL)
        fbl->head = dfb;
    else
        fbl->tail->next = dfb;
    dfb->prev = fbl->tail;
    fbl->tail = dfb;
}

static drmu_fb_t *
fb_list_extract(drmu_fb_list_t * const fbl, drmu_fb_t * const dfb)
{
    if (dfb == NULL)
        return NULL;

    if (dfb->prev == NULL)
        fbl->head = dfb->next;
    else
        dfb->prev->next = dfb->next;

    if (dfb->next == NULL)
        fbl->tail = dfb->prev;
    else
        dfb->next->prev = dfb->prev;

    dfb->next = NULL;
    dfb->prev = NULL;
    return dfb;
}

static drmu_fb_t *
fb_list_extract_head(drmu_fb_list_t * const fbl)
{
    return fb_list_extract(fbl, fbl->head);
}

static drmu_fb_t *
fb_list_peek_head(drmu_fb_list_t * const fbl)
{
    return fbl->head;
}

static bool
fb_list_is_empty(drmu_fb_list_t * const fbl)
{
    return fbl->head == NULL;
}

static void
pool_free_pool(drmu_pool_t * const pool)
{
    drmu_fb_t * dfb;
    while ((dfb = fb_list_extract_head(&pool->free_fbs)) != NULL)
        drmu_fb_unref(&dfb);
}

static void
pool_free(drmu_pool_t * const pool)
{
    pool_free_pool(pool);
    pthread_mutex_destroy(&pool->lock);
    free(pool);
}

void
drmu_pool_unref(drmu_pool_t ** const pppool)
{
    drmu_pool_t * const pool = *pppool;

    if (pool == NULL)
        return;
    *pppool = NULL;

    if (atomic_fetch_sub(&pool->ref_count, 1) != 0)
        return;

    pool_free(pool);
}

drmu_pool_t *
drmu_pool_ref(drmu_pool_t * const pool)
{
    atomic_fetch_add(&pool->ref_count, 1);
    return pool;
}

drmu_pool_t *
drmu_pool_new(drmu_env_t * const du, unsigned int total_fbs_max)
{
    drmu_pool_t * const pool = calloc(1, sizeof(*pool));

    if (pool == NULL) {
        drmu_err(du, "Failed pool env alloc");
        return NULL;
    }

    pool->du = du;
    pool->fb_max = total_fbs_max;
    pthread_mutex_init(&pool->lock, NULL);

    return pool;
}

static int
pool_fb_pre_delete_cb(drmu_fb_t * dfb, void * v)
{
    drmu_pool_t * pool = v;

    // Ensure we cannot end up in a delete loop
    drmu_fb_pre_delete_unset(dfb);

    // If dead set then might as well delete now
    // It should all work without this shortcut but this reclaims
    // storage quicker
    if (pool->dead) {
        drmu_pool_unref(&pool);
        return 0;
    }

    drmu_fb_ref(dfb);  // Restore ref

    pthread_mutex_lock(&pool->lock);
    fb_list_add_tail(&pool->free_fbs, dfb);
    pthread_mutex_unlock(&pool->lock);

    // May cause suicide & recursion on fb delete, but that should be OK as
    // the 1 we return here should cause simple exit of fb delete
    drmu_pool_unref(&pool);
    return 1;  // Stop delete
}

drmu_fb_t *
drmu_pool_fb_new_dumb(drmu_pool_t * const pool, uint32_t w, uint32_t h, const uint32_t format)
{
    drmu_env_t * const du = pool->du;
    drmu_fb_t * dfb;

    pthread_mutex_lock(&pool->lock);

    dfb = fb_list_peek_head(&pool->free_fbs);
    while (dfb != NULL) {
        if (fb_try_reuse(dfb, w, h, format)) {
            fb_list_extract(&pool->free_fbs, dfb);
            break;
        }
        dfb = dfb->next;
    }

    if (dfb == NULL) {
        if (pool->fb_count >= pool->fb_max && !fb_list_is_empty(&pool->free_fbs)) {
            --pool->fb_count;
            dfb = fb_list_extract_head(&pool->free_fbs);
        }
        ++pool->fb_count;
        pthread_mutex_unlock(&pool->lock);

        drmu_fb_unref(&dfb);  // Will free the dfb as pre-delete CB will be unset
        if ((dfb = drmu_fb_realloc_dumb(du, NULL, w, h, format)) == NULL) {
            --pool->fb_count;  // ??? lock
            return NULL;
        }
    }
    else {
        pthread_mutex_unlock(&pool->lock);
    }

    drmu_fb_pre_delete_set(dfb, pool_fb_pre_delete_cb, pool);
    drmu_pool_ref(pool);
    return dfb;
}

// Mark pool as dead (i.e. no new allocs) and unref it
// Simple unref will also work but this reclaims storage faster
// Actual pool structure will persist until all referencing fbs are deleted too
void
drmu_pool_delete(drmu_pool_t ** const pppool)
{
    drmu_pool_t * pool = *pppool;

    if (pool == NULL)
        return;
    *pppool = NULL;

    pool->dead = 1;
    pool_free_pool(pool);

    drmu_pool_unref(&pool);
}

//----------------------------------------------------------------------------
//
// Plane fns

typedef struct drmu_plane_s {
    struct drmu_env_s * du;

    // Unlike most ref counts in drmu this is 0 for unrefed, 2 for single ref
    // and 1 for whilst unref cleanup is in progress. Guards dc
    atomic_int ref_count;
    struct drmu_crtc_s * dc;    // NULL if not in use
    bool saved;

    int plane_type;
    struct drm_mode_get_plane plane;

    void * formats_in;
    size_t formats_in_len;
    const struct drm_format_modifier_blob * fmts_hdr;

    struct {
        uint32_t crtc_id;
        uint32_t fb_id;
        uint32_t crtc_h;
        uint32_t crtc_w;
        uint32_t crtc_x;
        uint32_t crtc_y;
        uint32_t src_h;
        uint32_t src_w;
        uint32_t src_x;
        uint32_t src_y;
        drmu_prop_range_t * alpha;
        drmu_prop_enum_t * color_encoding;
        drmu_prop_enum_t * color_range;
        drmu_prop_enum_t * pixel_blend_mode;
        drmu_prop_bitmask_t * rotation;
        drmu_prop_range_t * chroma_siting_h;
        drmu_prop_range_t * chroma_siting_v;
        drmu_prop_range_t * zpos;
    } pid;
    uint64_t rot_vals[8];

} drmu_plane_t;

static int
plane_set_atomic(drmu_atomic_t * const da,
                 drmu_plane_t * const dp,
                 drmu_fb_t * const dfb,
                int32_t crtc_x, int32_t crtc_y,
                uint32_t crtc_w, uint32_t crtc_h,
                uint32_t src_x, uint32_t src_y,
                uint32_t src_w, uint32_t src_h)
{
    const uint32_t plid = dp->plane.plane_id;
    drmu_atomic_add_prop_value(da, plid, dp->pid.crtc_id, dfb == NULL ? 0 : drmu_crtc_id(dp->dc));
    drmu_atomic_add_prop_fb(da, plid, dp->pid.fb_id, dfb);
    drmu_atomic_add_prop_value(da, plid, dp->pid.crtc_x, crtc_x);
    drmu_atomic_add_prop_value(da, plid, dp->pid.crtc_y, crtc_y);
    drmu_atomic_add_prop_value(da, plid, dp->pid.crtc_w, crtc_w);
    drmu_atomic_add_prop_value(da, plid, dp->pid.crtc_h, crtc_h);
    drmu_atomic_add_prop_value(da, plid, dp->pid.src_x,  src_x);
    drmu_atomic_add_prop_value(da, plid, dp->pid.src_y,  src_y);
    drmu_atomic_add_prop_value(da, plid, dp->pid.src_w,  src_w);
    drmu_atomic_add_prop_value(da, plid, dp->pid.src_h,  src_h);
    return 0;
}

int
drmu_atomic_plane_add_alpha(struct drmu_atomic_s * const da, const drmu_plane_t * const dp, const int alpha)
{
    if (alpha == DRMU_PLANE_ALPHA_UNSET)
        return 0;
    return drmu_atomic_add_prop_range(da, dp->plane.plane_id, dp->pid.alpha, alpha);
}

int
drmu_atomic_plane_add_zpos(struct drmu_atomic_s * const da, const drmu_plane_t * const dp, const int zpos)
{
    return drmu_atomic_add_prop_range(da, dp->plane.plane_id, dp->pid.zpos, zpos);
}

int
drmu_atomic_plane_add_rotation(struct drmu_atomic_s * const da, const drmu_plane_t * const dp, const int rot)
{
    if (!dp->pid.rotation)
        return rot == DRMU_PLANE_ROTATION_0 ? 0 : -EINVAL;
    if (rot < 0 || rot >= 8 || !dp->rot_vals[rot])
        return -EINVAL;
    return drmu_atomic_add_prop_bitmask(da, dp->plane.plane_id, dp->pid.rotation, dp->rot_vals[rot]);
}

int
drmu_atomic_plane_add_chroma_siting(struct drmu_atomic_s * const da, const drmu_plane_t * const dp, const drmu_chroma_siting_t siting)
{
    int rv = 0;

    if (!dp->pid.chroma_siting_h || !dp->pid.chroma_siting_v)
        return -ENOENT;

    if (!drmu_chroma_siting_eq(siting, DRMU_CHROMA_SITING_UNSPECIFIED)) {
        const uint32_t plid = dp->plane.plane_id;
        rv = drmu_atomic_add_prop_range(da, plid, dp->pid.chroma_siting_h, siting.x);
        rv = rvup(rv, drmu_atomic_add_prop_range(da, plid, dp->pid.chroma_siting_v, siting.y));
    }
    return rv;
}

int
drmu_atomic_plane_add_fb(drmu_atomic_t * const da, drmu_plane_t * const dp,
    drmu_fb_t * const dfb, const drmu_rect_t pos)
{
    int rv;
    const uint32_t plid = dp->plane.plane_id;

    if (dfb == NULL) {
        rv = plane_set_atomic(da, dp, NULL,
                              0, 0, 0, 0,
                              0, 0, 0, 0);
    }
    else {
        rv = plane_set_atomic(da, dp, dfb,
                              pos.x, pos.y,
                              pos.w, pos.h,
                              dfb->crop.x + (dfb->active.x << 16), dfb->crop.y + (dfb->active.y << 16),
                              dfb->crop.w, dfb->crop.h);
    }
    if (rv != 0 || dfb == NULL)
        return rv;

    drmu_atomic_add_prop_enum(da, plid, dp->pid.pixel_blend_mode, dfb->pixel_blend_mode);
    drmu_atomic_add_prop_enum(da, plid, dp->pid.color_encoding,   dfb->color_encoding);
    drmu_atomic_add_prop_enum(da, plid, dp->pid.color_range,      dfb->color_range);
    drmu_atomic_plane_add_chroma_siting(da, dp, dfb->chroma_siting);
    return rv != 0 ? -errno : 0;
}

uint32_t
drmu_plane_id(const drmu_plane_t * const dp)
{
    return dp->plane.plane_id;
}

unsigned int
drmu_plane_type(const drmu_plane_t * const dp)
{
    return dp->plane_type;
}

const uint32_t *
drmu_plane_formats(const drmu_plane_t * const dp, unsigned int * const pCount)
{
    *pCount = dp->fmts_hdr->count_formats;
    return (const uint32_t *)((const uint8_t *)dp->formats_in + dp->fmts_hdr->formats_offset);
}

bool
drmu_plane_format_check(const drmu_plane_t * const dp, const uint32_t format, const uint64_t modifier)
{
    const struct drm_format_modifier * const mods = (const struct drm_format_modifier *)((const uint8_t *)dp->formats_in + dp->fmts_hdr->modifiers_offset);
    const uint32_t * const fmts = (const uint32_t *)((const uint8_t *)dp->formats_in + dp->fmts_hdr->formats_offset);
    uint64_t modbase = modifier;
    unsigned int i;

    if (!format)
        return false;

    // If broadcom then remove parameters before checking
    if ((modbase >> 56) == DRM_FORMAT_MOD_VENDOR_BROADCOM)
        modbase = fourcc_mod_broadcom_mod(modbase);

    // * Simplistic lookup; Could be made much faster

    for (i = 0; i != dp->fmts_hdr->count_modifiers; ++i) {
        const struct drm_format_modifier * const mod = mods + i;
        uint64_t fbits;
        unsigned int j;

        if (mod->modifier != modbase)
            continue;

        for (fbits = mod->formats, j = mod->offset; fbits; fbits >>= 1, ++j) {
            if ((fbits & 1) != 0 && fmts[j] == format)
                return true;
        }
    }
    return false;
}

void
drmu_plane_unref(drmu_plane_t ** const ppdp)
{
    drmu_plane_t * const dp = *ppdp;

    if (dp == NULL)
        return;
    *ppdp = NULL;

    if (atomic_fetch_sub(&dp->ref_count, 1) != 2)
        return;
    dp->dc = NULL;
    atomic_store(&dp->ref_count, 0);
}

drmu_plane_t *
drmu_plane_ref(drmu_plane_t * const dp)
{
    if (dp)
        atomic_fetch_add(&dp->ref_count, 1);
    return dp;
}

// Associate a plane with a crtc and ref it
// Returns -EBUSY if plane already associated
int
drmu_plane_ref_crtc(drmu_plane_t * const dp, drmu_crtc_t * const dc)
{
    drmu_env_t * const du = dp->du;

    static const int ref0 = 0;
    if (!atomic_compare_exchange_strong(&dp->ref_count, &ref0, 2))
        return -EBUSY;
    dp->dc = dc;

    // 1st time through save state
    if (!dp->saved && env_object_state_save(du, drmu_plane_id(dp), DRM_MODE_OBJECT_PLANE) == 0)
        dp->saved = true;

    return 0;
}

drmu_plane_t *
drmu_plane_new_find(drmu_crtc_t * const dc, const drmu_plane_new_find_ok_fn cb, void * const v)
{
    uint32_t i;
    drmu_env_t * const du = drmu_crtc_env(dc);
    drmu_plane_t * dp = NULL;
    drmu_plane_t * dp_t;
    const uint32_t crtc_mask = (uint32_t)1 << drmu_crtc_idx(dc);

    for (i = 0; (dp_t = drmu_env_plane_find_n(du, i)) != NULL; ++i) {
        // Is unused?
        // Availible for this crtc?
        if (dp_t->dc != NULL ||
            (dp_t->plane.possible_crtcs & crtc_mask) == 0)
            continue;

        if (cb(dp_t, v)) {
            dp = dp_t;
            break;
        }
    }
    return dp;
}

static bool plane_find_type_cb(const drmu_plane_t * dp, void * v)
{
    const unsigned int * const pReq = v;
    return (*pReq & drmu_plane_type(dp)) != 0;
}

drmu_plane_t *
drmu_plane_new_find_type(drmu_crtc_t * const dc, const unsigned int req_type)
{
    drmu_env_t * const du = drmu_crtc_env(dc);
    drmu_plane_t * const dp = drmu_plane_new_find(dc, plane_find_type_cb, (void*)&req_type);
    if (dp == NULL) {
        drmu_err(du, "%s: No plane found for types %#x", __func__, req_type);
        return NULL;
    }
    return dp;
}

static void
plane_uninit(drmu_plane_t * const dp)
{
    drmu_prop_range_delete(&dp->pid.alpha);
    drmu_prop_range_delete(&dp->pid.chroma_siting_h);
    drmu_prop_range_delete(&dp->pid.chroma_siting_v);
    drmu_prop_enum_delete(&dp->pid.color_encoding);
    drmu_prop_enum_delete(&dp->pid.color_range);
    drmu_prop_enum_delete(&dp->pid.pixel_blend_mode);
    drmu_prop_enum_delete(&dp->pid.rotation);
    free(dp->formats_in);
    dp->formats_in = NULL;
}


static int
plane_init(drmu_env_t * const du, drmu_plane_t * const dp, const uint32_t plane_id)
{
    drmu_props_t *props;
    int rv;

    memset(dp, 0, sizeof(*dp));
    dp->du = du;

    dp->plane.plane_id = plane_id;
    if ((rv = drmu_ioctl(du, DRM_IOCTL_MODE_GETPLANE, &dp->plane)) != 0) {
        drmu_err(du, "%s: drmModeGetPlane failed: %s", __func__, strerror(-rv));
        return rv;
    }

    if ((props = props_new(du, dp->plane.plane_id, DRM_MODE_OBJECT_PLANE)) == NULL)
        return -EINVAL;

#if TRACE_PROP_NEW
    drmu_info(du, "Plane id %d:", plane_id);
    props_dump(props);
#endif

    if ((dp->pid.crtc_id = props_name_to_id(props, "CRTC_ID")) == 0 ||
        (dp->pid.fb_id  = props_name_to_id(props, "FB_ID")) == 0 ||
        (dp->pid.crtc_h = props_name_to_id(props, "CRTC_H")) == 0 ||
        (dp->pid.crtc_w = props_name_to_id(props, "CRTC_W")) == 0 ||
        (dp->pid.crtc_x = props_name_to_id(props, "CRTC_X")) == 0 ||
        (dp->pid.crtc_y = props_name_to_id(props, "CRTC_Y")) == 0 ||
        (dp->pid.src_h  = props_name_to_id(props, "SRC_H")) == 0 ||
        (dp->pid.src_w  = props_name_to_id(props, "SRC_W")) == 0 ||
        (dp->pid.src_x  = props_name_to_id(props, "SRC_X")) == 0 ||
        (dp->pid.src_y  = props_name_to_id(props, "SRC_Y")) == 0 ||
        props_name_get_blob(props, "IN_FORMATS", &dp->formats_in, &dp->formats_in_len) != 0)
    {
        drmu_err(du, "%s: failed to find required id", __func__);
        props_free(props);
        return -EINVAL;
    }
    dp->fmts_hdr = dp->formats_in;

    dp->pid.alpha            = drmu_prop_range_new(du, props_name_to_id(props, "alpha"));
    dp->pid.color_encoding   = drmu_prop_enum_new(du, props_name_to_id(props, "COLOR_ENCODING"));
    dp->pid.color_range      = drmu_prop_enum_new(du, props_name_to_id(props, "COLOR_RANGE"));
    dp->pid.pixel_blend_mode = drmu_prop_enum_new(du, props_name_to_id(props, "pixel blend mode"));
    dp->pid.rotation         = drmu_prop_enum_new(du, props_name_to_id(props, "rotation"));
    dp->pid.chroma_siting_h  = drmu_prop_range_new(du, props_name_to_id(props, "CHROMA_SITING_H"));
    dp->pid.chroma_siting_v  = drmu_prop_range_new(du, props_name_to_id(props, "CHROMA_SITING_V"));
    dp->pid.zpos             = drmu_prop_range_new(du, props_name_to_id(props, "zpos"));

    dp->rot_vals[DRMU_PLANE_ROTATION_0] = drmu_prop_bitmask_value(dp->pid.rotation, "rotate-0");
    if (dp->rot_vals[DRMU_PLANE_ROTATION_0]) {
        // Flips MUST be combined with a rotate
        if ((dp->rot_vals[DRMU_PLANE_ROTATION_X_FLIP] = drmu_prop_bitmask_value(dp->pid.rotation, "reflect-x")) != 0)
            dp->rot_vals[DRMU_PLANE_ROTATION_X_FLIP] |= dp->rot_vals[DRMU_PLANE_ROTATION_0];
        if ((dp->rot_vals[DRMU_PLANE_ROTATION_Y_FLIP] = drmu_prop_bitmask_value(dp->pid.rotation, "reflect-y")) != 0)
            dp->rot_vals[DRMU_PLANE_ROTATION_Y_FLIP] |= dp->rot_vals[DRMU_PLANE_ROTATION_0];
    }
    dp->rot_vals[DRMU_PLANE_ROTATION_180] = drmu_prop_bitmask_value(dp->pid.rotation, "rotate-180");
    if (!dp->rot_vals[DRMU_PLANE_ROTATION_180] && dp->rot_vals[DRMU_PLANE_ROTATION_X_FLIP] && dp->rot_vals[DRMU_PLANE_ROTATION_Y_FLIP])
        dp->rot_vals[DRMU_PLANE_ROTATION_180] = dp->rot_vals[DRMU_PLANE_ROTATION_X_FLIP] | dp->rot_vals[DRMU_PLANE_ROTATION_Y_FLIP];

    {
        const drmu_propinfo_t * const pinfo = props_name_to_propinfo(props, "type");
        drmu_prop_enum_t * etype = drmu_prop_enum_new(du, props_name_to_id(props, "type"));
        const uint64_t * p;

        if ((p = drmu_prop_enum_value(etype, "Primary")) && *p == pinfo->val)
            dp->plane_type = DRMU_PLANE_TYPE_PRIMARY;
        else if ((p = drmu_prop_enum_value(etype, "Cursor")) && *p == pinfo->val)
            dp->plane_type = DRMU_PLANE_TYPE_CURSOR;
        else if ((p = drmu_prop_enum_value(etype, "Overlay")) && *p == pinfo->val)
            dp->plane_type = DRMU_PLANE_TYPE_OVERLAY;
        else {
            drmu_debug(du, "Unexpected plane type: %"PRId64, pinfo->val);
            dp->plane_type = DRMU_PLANE_TYPE_UNKNOWN;
        }
        drmu_prop_enum_delete(&etype);
    }

    props_free(props);
    return 0;
}

//----------------------------------------------------------------------------
//
// Env fns

typedef struct drmu_env_s {
    atomic_int ref_count;  // 0 == 1 ref for ease of init
    int fd;
    uint32_t plane_count;
    uint32_t conn_count;
    uint32_t crtc_count;
    drmu_plane_t * planes;
    drmu_conn_t * conns;
    drmu_crtc_t * crtcs;

    drmu_log_env_t log;

    // global env for atomic flip
    drmu_atomic_q_t aq;
    // global env for bo tracking
    drmu_bo_env_t boe;
    // global atomic for restore op
    drmu_atomic_t * da_restore;

    struct pollqueue * pq;
    struct polltask * pt;
} drmu_env_t;

// Retrieve the the n-th conn
// Use for iteration
// Returns NULL when none left
drmu_crtc_t *
drmu_env_crtc_find_n(drmu_env_t * const du, const unsigned int n)
{
    return n >= du->crtc_count ? NULL : du->crtcs + n;
}

// Retrieve the the n-th conn
// Use for iteration
// Returns NULL when none left
drmu_conn_t *
drmu_env_conn_find_n(drmu_env_t * const du, const unsigned int n)
{
    return n >= du->conn_count ? NULL : du->conns + n;
}

drmu_plane_t *
drmu_env_plane_find_n(drmu_env_t * const du, const unsigned int n)
{
    return n >= du->plane_count ? NULL : du->planes + n;
}

int
drmu_ioctl(const drmu_env_t * const du, unsigned long req, void * arg)
{
    while (ioctl(du->fd, req, arg)) {
        const int err = errno;
        // DRM docn suggests we should try again on EAGAIN as well as EINTR
        // and drm userspace does this.
        if (err != EINTR && err != EAGAIN)
            return -err;
    }
    return 0;
}

static void
env_free_planes(drmu_env_t * const du)
{
    uint32_t i;
    for (i = 0; i != du->plane_count; ++i)
        plane_uninit(du->planes + i);
    free(du->planes);
    du->plane_count = 0;
    du->planes = NULL;
}

static void
env_free_conns(drmu_env_t * const du)
{
    uint32_t i;
    for (i = 0; i != du->conn_count; ++i)
        conn_uninit(du->conns + i);
    free(du->conns);
    du->conn_count = 0;
    du->conns = NULL;
}

static void
env_free_crtcs(drmu_env_t * const du)
{
    uint32_t i;
    for (i = 0; i != du->crtc_count; ++i)
        crtc_uninit(du->crtcs + i);
    free(du->crtcs);
    du->crtc_count = 0;
    du->crtcs = NULL;
}


static int
env_planes_populate(drmu_env_t * const du, unsigned int n, const uint32_t * const ids)
{
    uint32_t i;
    int rv;

    if ((du->planes = calloc(n, sizeof(*du->planes))) == NULL) {
        drmu_err(du, "Plane array alloc failed");
        return -ENOMEM;
    }

    for (i = 0; i != n; ++i) {
        if ((rv = plane_init(du, du->planes + i, ids[i])) != 0)
            goto fail2;
        du->plane_count = i + 1;
    }
    return 0;

fail2:
    env_free_planes(du);
    return rv;
}

// Doesn't clean up on error - assumes that env construction will abort and
// that will tidy up for us
static int
env_conn_populate(drmu_env_t * const du, unsigned int n, const uint32_t * const ids)
{
    unsigned int i;
    int rv;

    if (n == 0) {
        drmu_err(du, "No connectors");
        return -EINVAL;
    }

    if ((du->conns = calloc(n, sizeof(*du->conns))) == NULL) {
        drmu_err(du, "Failed to malloc conns");
        return -ENOMEM;
    }

    for (i = 0; i != n; ++i) {
        if ((rv = conn_init(du, du->conns + i, i, ids[i])) != 0)
            return rv;
        du->conn_count = i + 1;
    }

    return 0;
}

static int
env_crtc_populate(drmu_env_t * const du, unsigned int n, const uint32_t * const ids)
{
    unsigned int i;
    int rv;

    if (n == 0) {
        drmu_err(du, "No crtcs");
        return -EINVAL;
    }

    if ((du->crtcs = malloc(n * sizeof(*du->crtcs))) == NULL) {
        drmu_err(du, "Failed to malloc conns");
        return -ENOMEM;
    }

    for (i = 0; i != n; ++i) {
        if ((rv = crtc_init(du, du->crtcs + i, i, ids[i])) != 0)
            return rv;
        du->crtc_count = i + 1;
    }

    return 0;
}



int
drmu_fd(const drmu_env_t * const du)
{
    return du->fd;
}

const struct drmu_log_env_s *
drmu_env_log(const drmu_env_t * const du)
{
    return &du->log;
}

static struct drmu_bo_env_s *
env_boe(drmu_env_t * const du)
{
    return &du->boe;
}

static struct pollqueue *
env_pollqueue(const drmu_env_t * const du)
{
    return du->pq;
}

static struct drmu_atomic_q_s *
env_atomic_q(drmu_env_t * const du)
{
    return &du->aq;
}


static void
env_free(drmu_env_t * const du)
{
    if (!du)
        return;

    atomic_q_flush(&du->aq);

    pollqueue_unref(&du->pq);
    polltask_delete(&du->pt);

    atomic_q_uninit(&du->aq);

    if (du->da_restore) {
        int rv;
        if ((rv = drmu_atomic_commit(du->da_restore, DRM_MODE_ATOMIC_ALLOW_MODESET)) != 0) {
            drmu_atomic_t * bad = drmu_atomic_new(du);
            drmu_atomic_commit_test(du->da_restore, DRM_MODE_ATOMIC_TEST_ONLY | DRM_MODE_ATOMIC_ALLOW_MODESET, bad);
            drmu_atomic_sub(du->da_restore, bad);
            if ((rv = drmu_atomic_commit(du->da_restore, DRM_MODE_ATOMIC_ALLOW_MODESET)) != 0)
                drmu_err(du, "Failed to restore old mode on exit: %s", strerror(-rv));
            else
                drmu_err(du, "Failed to completely restore old mode on exit");

            if (drmu_env_log(du)->max_level >= DRMU_LOG_LEVEL_DEBUG)
                drmu_atomic_dump(bad);
            drmu_atomic_unref(&bad);
        }
        drmu_atomic_unref(&du->da_restore);
    }

    env_free_planes(du);
    env_free_conns(du);
    env_free_crtcs(du);
    drmu_bo_env_uninit(&du->boe);

    close(du->fd);
    free(du);
}

void
drmu_env_unref(drmu_env_t ** const ppdu)
{
    drmu_env_t * const du = *ppdu;
    if (!du)
        return;
    *ppdu = NULL;

    if (atomic_fetch_sub(&du->ref_count, 1) != 0)
        return;

    env_free(du);
}

drmu_env_t *
drmu_env_ref(drmu_env_t * const du)
{
    if (du)
        atomic_fetch_add(&du->ref_count, 1);
    return du;
}

static int
env_object_state_save(drmu_env_t * const du, const uint32_t obj_id, const uint32_t obj_type)
{
    drmu_props_t * props;
    drmu_atomic_t * da;
    int rv;

    if (!du->da_restore)
        return -EINVAL;

    if ((props = props_new(du, obj_id, obj_type)) == NULL)
        return -ENOENT;

    if ((da = drmu_atomic_new(du)) == NULL) {
        rv = -ENOMEM;
        goto fail;
    }

    if ((rv = drmu_atomic_props_add_save(da, obj_id, props)) != 0)
        goto fail;

    props_free(props);
    return drmu_atomic_env_restore_add_snapshot(&da);

fail:
    if (props)
        props_free(props);
    return rv;
}

int
drmu_env_restore_enable(drmu_env_t * const du)
{
    if (du->da_restore)
        return 0;
    if ((du->da_restore = drmu_atomic_new(du)) == NULL)
        return -ENOMEM;
    return 0;
}

bool
drmu_env_restore_is_enabled(const drmu_env_t * const du)
{
    return du->da_restore != NULL;
}

int
drmu_atomic_env_restore_add_snapshot(drmu_atomic_t ** const ppda)
{
    drmu_atomic_t * da = *ppda;
    drmu_atomic_t * fails = NULL;
    drmu_env_t * const du = drmu_atomic_env(da);

    *ppda = NULL;

    if (!du || !du->da_restore) {
        drmu_atomic_unref(&da);
        return 0;
    }

    // Lose anything we can't restore
    fails = drmu_atomic_new(du);
    if (drmu_atomic_commit_test(da, DRM_MODE_ATOMIC_ALLOW_MODESET | DRM_MODE_ATOMIC_TEST_ONLY, fails) != 0)
        drmu_atomic_sub(da, fails);
    drmu_atomic_unref(&fails);

    return drmu_atomic_merge(du->da_restore, &da);
}

static void
drmu_env_polltask_cb(void * v, short revents)
{
    drmu_env_t * const du = v;
    drmEventContext ctx = {
        .version = DRM_EVENT_CONTEXT_VERSION,
        .page_flip_handler2 = drmu_atomic_page_flip_cb,
    };

    if (revents == 0) {
        drmu_debug(du, "%s: Timeout", __func__);
    }
    else {
        drmHandleEvent(du->fd, &ctx);
    }

    pollqueue_add_task(du->pt, 1000);
}

static int
env_set_client_cap(drmu_env_t * const du, uint64_t cap_id, uint64_t cap_val)
{
    struct drm_set_client_cap cap = {
        .capability = cap_id,
        .value = cap_val
    };
    return drmu_ioctl(du, DRM_IOCTL_SET_CLIENT_CAP, &cap);
}

// Closes fd on failure
drmu_env_t *
drmu_env_new_fd(const int fd, const struct drmu_log_env_s * const log)
{
    drmu_env_t * const du = calloc(1, sizeof(*du));
    int rv;
    uint32_t * conn_ids = NULL;
    uint32_t * crtc_ids = NULL;
    uint32_t * plane_ids = NULL;

    if (!du) {
        drmu_err_log(log, "Failed to create du: No memory");
        close(fd);
        return NULL;
    }

    du->log = (log == NULL) ? drmu_log_env_none : *log;
    du->fd = fd;

    drmu_bo_env_init(&du->boe);
    atomic_q_init(&du->aq);

    if ((du->pq = pollqueue_new()) == NULL) {
        drmu_err(du, "Failed to create pollqueue");
        goto fail1;
    }
    if ((du->pt = polltask_new(du->pq, du->fd, POLLIN | POLLPRI, drmu_env_polltask_cb, du)) == NULL) {
        drmu_err(du, "Failed to create polltask");
        goto fail1;
    }

    // We want the primary plane for video
    if ((rv = env_set_client_cap(du, DRM_CLIENT_CAP_UNIVERSAL_PLANES, 1)) != 0)
        drmu_debug(du, "Failed to set universal planes cap");
    // We need atomic for almost everything we do
    if ((rv = env_set_client_cap(du, DRM_CLIENT_CAP_ATOMIC, 1)) != 0) {
        drmu_err(du, "Failed to set atomic cap");
        goto fail1;
    }
    // We can understand AR info
    if ((rv = env_set_client_cap(du, DRM_CLIENT_CAP_ASPECT_RATIO, 1)) != 0)
        drmu_debug(du, "Failed to set AR cap");
    // We would like to see writeback connectors
    if ((rv = env_set_client_cap(du, DRM_CLIENT_CAP_WRITEBACK_CONNECTORS, 1)) != 0)
        drmu_debug(du, "Failed to set writeback cap");

    {
        struct drm_mode_get_plane_res res;
        uint32_t req_planes = 0;

        do {
            memset(&res, 0, sizeof(res));
            res.plane_id_ptr     = (uintptr_t)plane_ids;
            res.count_planes     = req_planes;

            if ((rv = drmu_ioctl(du, DRM_IOCTL_MODE_GETPLANERESOURCES, &res)) != 0) {
                drmu_err(du, "Failed to get resources: %s", strerror(-rv));
                goto fail1;
            }
        } while ((rv = retry_alloc_u32(&plane_ids, &req_planes, res.count_planes)) == 1);
        if (rv < 0)
            goto fail1;

        if (env_planes_populate(du, res.count_planes, plane_ids) != 0)
            goto fail1;

        free(plane_ids);
        plane_ids = NULL;
    }

    {
        struct drm_mode_card_res res;
        uint32_t req_conns = 0;
        uint32_t req_crtcs = 0;

        for (;;) {
            memset(&res, 0, sizeof(res));
            res.crtc_id_ptr      = (uintptr_t)crtc_ids;
            res.connector_id_ptr = (uintptr_t)conn_ids;
            res.count_crtcs      = req_crtcs;
            res.count_connectors = req_conns;

            if ((rv = drmu_ioctl(du, DRM_IOCTL_MODE_GETRESOURCES, &res)) != 0) {
                drmu_err(du, "Failed to get resources: %s", strerror(-rv));
                goto fail1;
            }

            if (res.count_crtcs <= req_crtcs && res.count_connectors <= req_conns)
                break;

            if (retry_alloc_u32(&conn_ids, &req_conns, res.count_connectors) < 0 ||
                retry_alloc_u32(&crtc_ids, &req_crtcs, res.count_crtcs) < 0)
                goto fail1;
        }

        if (env_conn_populate(du, res.count_connectors, conn_ids) != 0)
            goto fail1;
        if (env_crtc_populate(du, res.count_crtcs,      crtc_ids) != 0)
            goto fail1;

        free(conn_ids);
        free(crtc_ids);
        conn_ids = NULL;
        crtc_ids = NULL;
    }

    pollqueue_add_task(du->pt, 1000);

    free(plane_ids);
    return du;

fail1:
    env_free(du);
    free(conn_ids);
    free(crtc_ids);
    free(plane_ids);
    return NULL;
}

drmu_env_t *
drmu_env_new_open(const char * name, const struct drmu_log_env_s * const log2)
{
    const struct drmu_log_env_s * const log = (log2 == NULL) ? &drmu_log_env_none : log2;
    int fd = drmOpen(name, NULL);
    if (fd == -1) {
        drmu_err_log(log, "Failed to open %s", name);
        return NULL;
    }
    return drmu_env_new_fd(fd, log);
}

//----------------------------------------------------------------------------
//
// Logging

static void
log_none_cb(void * v, enum drmu_log_level_e level, const char * fmt, va_list vl)
{
    (void)v;
    (void)level;
    (void)fmt;
    (void)vl;
}

const struct drmu_log_env_s drmu_log_env_none = {
    .fn = log_none_cb,
    .v = NULL,
    .max_level = DRMU_LOG_LEVEL_NONE
};

void
drmu_log_generic(const struct drmu_log_env_s * const log, const enum drmu_log_level_e level,
                 const char * const fmt, ...)
{
    va_list vl;
    va_start(vl, fmt);
    log->fn(log->v, level, fmt, vl);
    va_end(vl);
}


