/*	$NetBSD: viogpu.c,v 1.2 2025/10/04 03:58:38 thorpej Exp $ */
/*	$OpenBSD: viogpu.c,v 1.3 2023/05/29 08:13:35 sf Exp $ */

/*
 * Copyright (c) 2024-2025 The NetBSD Foundation, Inc.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
 * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

/*
 * Copyright (c) 2021-2023 joshua stein <jcs@openbsd.org>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include <sys/cdefs.h>

#include <sys/param.h>
#include <sys/systm.h>
#include <sys/bus.h>
#include <sys/condvar.h>
#include <sys/device.h>
#include <sys/intr.h>
#include <sys/kernel.h>
#include <sys/mutex.h>

#include <dev/pci/virtioreg.h>
#include <dev/pci/virtiovar.h>
#include <dev/pci/viogpu.h>

#include <dev/rasops/rasops.h>

#include <dev/wscons/wsconsio.h>
#include <dev/wscons/wsdisplayvar.h>
#include <dev/wscons/wsdisplay_vconsvar.h>

#include <prop/proplib.h>

struct viogpu_softc;

static int	viogpu_match(device_t, cfdata_t, void *);
static void	viogpu_attach(device_t, device_t, void *);
static void	viogpu_attach_postintr(device_t);
static int	viogpu_cmd_sync(struct viogpu_softc *, void *, size_t, void *,
				size_t);
static int	viogpu_cmd_req(struct viogpu_softc *, void *, size_t, size_t);
static void	viogpu_screen_update(void *);
static int	viogpu_vq_done(struct virtqueue *vq);

static int	viogpu_get_display_info(struct viogpu_softc *);
static int	viogpu_create_2d(struct viogpu_softc *, uint32_t, uint32_t,
				 uint32_t);
static int	viogpu_set_scanout(struct viogpu_softc *, uint32_t, uint32_t,
				   uint32_t, uint32_t);
static int	viogpu_attach_backing(struct viogpu_softc *, uint32_t,
				      bus_dmamap_t);
static int	viogpu_transfer_to_host_2d(struct viogpu_softc *sc, uint32_t,
					   uint32_t, uint32_t, uint32_t,
					   uint32_t);
static int	viogpu_flush_resource(struct viogpu_softc *, uint32_t,
				      uint32_t, uint32_t, uint32_t, uint32_t);

static int	viogpu_wsioctl(void *, void *, u_long, void *, int,
			       struct lwp *);

static void 	viogpu_init_screen(void *, struct vcons_screen *, int, long *);

static void	viogpu_cursor(void *, int, int, int);
static void	viogpu_putchar(void *, int, int, u_int, long);
static void	viogpu_copycols(void *, int, int, int, int);
static void	viogpu_erasecols(void *, int, int, int, long);
static void	viogpu_copyrows(void *, int, int, int);
static void	viogpu_eraserows(void *, int, int, long);
static void	viogpu_replaceattr(void *, long, long);

struct virtio_gpu_resource_attach_backing_entries {
	struct virtio_gpu_ctrl_hdr hdr;
	__le32 resource_id;
	__le32 nr_entries;
	struct virtio_gpu_mem_entry entries[1];
} __packed;

#define VIOGPU_CMD_DMA_SIZE \
    MAX(sizeof(struct virtio_gpu_resp_display_info), \
    MAX(sizeof(struct virtio_gpu_resource_create_2d), \
    MAX(sizeof(struct virtio_gpu_set_scanout), \
    MAX(sizeof(struct virtio_gpu_resource_attach_backing_entries), \
    MAX(sizeof(struct virtio_gpu_transfer_to_host_2d), \
    sizeof(struct virtio_gpu_resource_flush)))))) + \
    sizeof(struct virtio_gpu_ctrl_hdr)

struct viogpu_softc {
	device_t		sc_dev;
	struct virtio_softc	*sc_virtio;
#define	VQCTRL	0
#define	VQCURS	1
	struct virtqueue	sc_vqs[2];

	bus_dma_segment_t	sc_dma_seg;
	bus_dmamap_t		sc_dma_map;
	void			*sc_cmd;
	int			sc_fence_id;

	int			sc_fb_height;
	int			sc_fb_width;
	bus_dma_segment_t	sc_fb_dma_seg;
	bus_dmamap_t		sc_fb_dma_map;
	size_t			sc_fb_dma_size;
	void			*sc_fb_dma_kva;

	struct wsscreen_descr		sc_wsd;
	const struct wsscreen_descr	*sc_scrlist[1];
	struct wsscreen_list		sc_wsl;
	struct vcons_data		sc_vd;
	struct vcons_screen		sc_vcs;
	bool				is_console;

	void	(*ri_cursor)(void *, int, int, int);
	void	(*ri_putchar)(void *, int, int, u_int, long);
	void	(*ri_copycols)(void *, int, int, int, int);
	void	(*ri_erasecols)(void *, int, int, int, long);
	void	(*ri_copyrows)(void *, int, int, int);
	void	(*ri_eraserows)(void *, int, int, long);
	void	(*ri_replaceattr)(void *, long, long);

	/*
	 * sc_mutex protects is_requesting, needs_update, and req_wait. It is
	 * also held while submitting and reading the return values of
	 * asynchronous commands and for the full duration of synchronous
	 * commands.
	 */
	kmutex_t		sc_mutex;
	bool			is_requesting;
	bool			needs_update;
	kcondvar_t		req_wait;
	void			*update_soft_ih;
	size_t			cur_cmd_size;
	size_t			cur_ret_size;
};

CFATTACH_DECL_NEW(viogpu, sizeof(struct viogpu_softc),
		  viogpu_match, viogpu_attach, NULL, NULL);

#if VIOGPU_DEBUG
#define VIOGPU_FEATURES		(VIRTIO_GPU_F_VIRGL | VIRTIO_GPU_F_EDID)
#else
#define VIOGPU_FEATURES		0
#endif

static struct wsdisplay_accessops viogpu_accessops = {
	.ioctl        = viogpu_wsioctl,
	.mmap         = NULL, /* This would require signalling on write to
	                       * update the screen. */
	.alloc_screen = NULL,
	.free_screen  = NULL,
	.show_screen  = NULL,
	.load_font    = NULL,
	.pollc        = NULL,
	.scroll       = NULL,
};

static int
viogpu_match(device_t parent, cfdata_t match, void *aux)
{
	struct virtio_attach_args *va = aux;

	if (va->sc_childdevid == VIRTIO_DEVICE_ID_GPU)
		return 1;

	return 0;
}

static void
viogpu_attach(device_t parent, device_t self, void *aux)
{
	struct viogpu_softc *sc = device_private(self);
	struct virtio_softc *vsc = device_private(parent);
	int error;

	if (virtio_child(vsc) != NULL) {
		aprint_error("child already attached for %s\n",
		    device_xname(parent));
		return;
	}

	sc->sc_dev = self;
	sc->sc_virtio = vsc;

	mutex_init(&sc->sc_mutex, MUTEX_DEFAULT, IPL_NONE);
	cv_init(&sc->req_wait, "vgpu_req");
	sc->update_soft_ih = softint_establish(SOFTINT_NET,
	    viogpu_screen_update, sc);
	sc->needs_update = false;
	sc->is_requesting = false;
	sc->sc_fence_id = 0;

	virtio_child_attach_start(vsc, self, IPL_VM,
	    VIOGPU_FEATURES, VIRTIO_COMMON_FLAG_BITS);

	if (!virtio_version_1(vsc)) {
		aprint_error_dev(sc->sc_dev, "requires virtio version 1\n");
		goto err;
	}

	/* Allocate command and cursor virtqueues. */
	virtio_init_vq_vqdone(vsc, &sc->sc_vqs[VQCTRL], 0, viogpu_vq_done);
	error = virtio_alloc_vq(vsc, &sc->sc_vqs[VQCTRL], NBPG, 1, "control");
	if (error != 0) {
		aprint_error_dev(sc->sc_dev, "alloc_vq failed: %d\n", error);
		goto err;
	}

	virtio_init_vq_vqdone(vsc, &sc->sc_vqs[VQCURS], 1, viogpu_vq_done);
	error = virtio_alloc_vq(vsc, &sc->sc_vqs[VQCURS], NBPG, 1, "cursor");
	if (error != 0) {
		aprint_error_dev(sc->sc_dev, "alloc_vq failed: %d\n", error);
		goto free_vq0;
	}

	if (virtio_child_attach_finish(vsc, sc->sc_vqs,
	    __arraycount(sc->sc_vqs), NULL,
	    VIRTIO_F_INTR_MPSAFE | VIRTIO_F_INTR_SOFTINT) != 0)
		goto free_vqs;

	/* Interrupts are required for synchronous commands in attachment. */
	config_interrupts(self, viogpu_attach_postintr);

	return;

free_vqs:
	virtio_free_vq(vsc, &sc->sc_vqs[VQCURS]);
free_vq0:
	virtio_free_vq(vsc, &sc->sc_vqs[VQCTRL]);
err:
	virtio_child_attach_failed(vsc);
	cv_destroy(&sc->req_wait);
	mutex_destroy(&sc->sc_mutex);
	return;
}

static void
viogpu_attach_postintr(device_t self)
{
	struct viogpu_softc *sc = device_private(self);
	struct virtio_softc *vsc = sc->sc_virtio;
	struct wsemuldisplaydev_attach_args waa;
	struct rasops_info *ri;
	long defattr;
	int nsegs;
	int error;

	/* Set up DMA space for sending commands. */
	error = bus_dmamap_create(virtio_dmat(vsc), VIOGPU_CMD_DMA_SIZE, 1,
	    VIOGPU_CMD_DMA_SIZE, 0, BUS_DMA_NOWAIT | BUS_DMA_ALLOCNOW,
	    &sc->sc_dma_map);
	if (error != 0) {
		aprint_error_dev(sc->sc_dev, "bus_dmamap_create failed: %d\n",
		    error);
		goto err;
	}
	error = bus_dmamem_alloc(virtio_dmat(vsc), VIOGPU_CMD_DMA_SIZE, 16, 0,
	    &sc->sc_dma_seg, 1, &nsegs, BUS_DMA_NOWAIT);
	if (error != 0) {
		aprint_error_dev(sc->sc_dev, "bus_dmamem_alloc failed: %d\n",
		    error);
		goto destroy;
	}
	error = bus_dmamem_map(virtio_dmat(vsc), &sc->sc_dma_seg, nsegs,
	    VIOGPU_CMD_DMA_SIZE, &sc->sc_cmd, BUS_DMA_NOWAIT);
	if (error != 0) {
		aprint_error_dev(sc->sc_dev, "bus_dmamem_map failed: %d\n",
		    error);
		goto free;
	}
	memset(sc->sc_cmd, 0, VIOGPU_CMD_DMA_SIZE);
	error = bus_dmamap_load(virtio_dmat(vsc), sc->sc_dma_map, sc->sc_cmd,
	    VIOGPU_CMD_DMA_SIZE, NULL, BUS_DMA_NOWAIT);
	if (error != 0) {
		aprint_error_dev(sc->sc_dev, "bus_dmamap_load failed: %d\n",
		    error);
		goto unmap;
	}

	if (viogpu_get_display_info(sc) != 0)
		goto unmap;

	/* Set up DMA space for actual framebuffer. */
	sc->sc_fb_dma_size = sc->sc_fb_width * sc->sc_fb_height * 4;
	error = bus_dmamap_create(virtio_dmat(vsc), sc->sc_fb_dma_size, 1,
	    sc->sc_fb_dma_size, 0, BUS_DMA_NOWAIT | BUS_DMA_ALLOCNOW,
	    &sc->sc_fb_dma_map);
	if (error != 0) {
		aprint_error_dev(sc->sc_dev, "bus_dmamap_create failed: %d\n",
		    error);
		goto unmap;
	}
	error = bus_dmamem_alloc(virtio_dmat(vsc), sc->sc_fb_dma_size, 1024, 0,
	    &sc->sc_fb_dma_seg, 1, &nsegs, BUS_DMA_NOWAIT);
	if (error != 0) {
		aprint_error_dev(sc->sc_dev, "bus_dmamem_alloc failed: %d\n",
		    error);
		goto fb_destroy;
	}
	error = bus_dmamem_map(virtio_dmat(vsc), &sc->sc_fb_dma_seg, nsegs,
	    sc->sc_fb_dma_size, &sc->sc_fb_dma_kva, BUS_DMA_NOWAIT);
	if (error != 0) {
		aprint_error_dev(sc->sc_dev, "bus_dmamem_map failed: %d\n",
		    error);
		goto fb_free;
	}
	memset(sc->sc_fb_dma_kva, 0, sc->sc_fb_dma_size);
	error = bus_dmamap_load(virtio_dmat(vsc), sc->sc_fb_dma_map,
	    sc->sc_fb_dma_kva, sc->sc_fb_dma_size, NULL, BUS_DMA_NOWAIT);
	if (error != 0) {
		aprint_error_dev(sc->sc_dev, "bus_dmamap_load failed: %d\n",
		    error);
		goto fb_unmap;
	}

	if (viogpu_create_2d(sc, 1, sc->sc_fb_width, sc->sc_fb_height) != 0)
		goto fb_unmap;

	if (viogpu_attach_backing(sc, 1, sc->sc_fb_dma_map) != 0)
		goto fb_unmap;

	if (viogpu_set_scanout(sc, 0, 1, sc->sc_fb_width,
	    sc->sc_fb_height) != 0)
		goto fb_unmap;

#ifdef WSDISPLAY_MULTICONS
	sc->is_console = true;
#else
	sc->is_console = device_getprop_bool(self, "is_console");
#endif

	sc->sc_wsd = (struct wsscreen_descr){
		"std",
		0, 0,
		NULL,
		8, 16,
		WSSCREEN_WSCOLORS | WSSCREEN_HILIT,
		NULL
	};

	sc->sc_scrlist[0] = &sc->sc_wsd;
	sc->sc_wsl.nscreens = __arraycount(sc->sc_scrlist);
	sc->sc_wsl.screens = sc->sc_scrlist;

	vcons_init(&sc->sc_vd, sc, &sc->sc_wsd, &viogpu_accessops);
	sc->sc_vd.init_screen = viogpu_init_screen;

	vcons_init_screen(&sc->sc_vd, &sc->sc_vcs, 1, &defattr);
	sc->sc_vcs.scr_flags |= VCONS_SCREEN_IS_STATIC;
	ri = &sc->sc_vcs.scr_ri;

	sc->sc_wsd.textops = &ri->ri_ops;
	sc->sc_wsd.capabilities = ri->ri_caps;
	sc->sc_wsd.nrows = ri->ri_rows;
	sc->sc_wsd.ncols = ri->ri_cols;

	if (sc->is_console) {
		wsdisplay_cnattach(&sc->sc_wsd, ri, 0, 0, defattr);
		vcons_replay_msgbuf(&sc->sc_vcs);
	}

	device_printf(sc->sc_dev, "%dx%d, %dbpp\n", ri->ri_width,
	    ri->ri_height, ri->ri_depth);

	waa.scrdata = &sc->sc_wsl;
	waa.accessops = &viogpu_accessops;
	waa.accesscookie = &sc->sc_vd;
	waa.console = sc->is_console;

	config_found(self, &waa, wsemuldisplaydevprint, CFARGS_NONE);

	return;

fb_unmap:
	bus_dmamem_unmap(virtio_dmat(vsc), &sc->sc_fb_dma_kva,
	    sc->sc_fb_dma_size);
fb_free:
	bus_dmamem_free(virtio_dmat(vsc), &sc->sc_fb_dma_seg, 1);
fb_destroy:
	bus_dmamap_destroy(virtio_dmat(vsc), sc->sc_fb_dma_map);
unmap:
	bus_dmamem_unmap(virtio_dmat(vsc), &sc->sc_cmd, VIOGPU_CMD_DMA_SIZE);
free:
	bus_dmamem_free(virtio_dmat(vsc), &sc->sc_dma_seg, 1);
destroy:
	bus_dmamap_destroy(virtio_dmat(vsc), sc->sc_dma_map);
err:
	aprint_error_dev(sc->sc_dev, "DMA setup failed\n");
	virtio_free_vq(vsc, &sc->sc_vqs[VQCURS]);
	virtio_free_vq(vsc, &sc->sc_vqs[VQCTRL]);
	virtio_child_attach_failed(vsc);
	cv_destroy(&sc->req_wait);
	mutex_destroy(&sc->sc_mutex);
	return;
}

/*
 * This carries out a command synchronously, unlike the commands used to
 * update the screen.
 */
static int
viogpu_cmd_sync(struct viogpu_softc *sc, void *cmd, size_t cmd_size,
		void *ret, size_t ret_size)
{
	int error;

	mutex_enter(&sc->sc_mutex);

	while (sc->is_requesting == true)
		cv_wait(&sc->req_wait, &sc->sc_mutex);

	error = viogpu_cmd_req(sc, cmd, cmd_size, ret_size);
	if (error != 0)
		goto out;

	while (sc->is_requesting == true)
		cv_wait(&sc->req_wait, &sc->sc_mutex);

	if (ret != NULL)
		memcpy(ret, (char *)sc->sc_cmd + cmd_size, ret_size);

out:
	mutex_exit(&sc->sc_mutex);

	return error;
}

static void
viogpu_screen_update(void *arg)
{
	struct viogpu_softc *sc = arg;

	mutex_enter(&sc->sc_mutex);

	if (sc->is_requesting == false)
		viogpu_transfer_to_host_2d(sc, 1, 0, 0, sc->sc_fb_width,
		    sc->sc_fb_height);
	else
		sc->needs_update = true;

	mutex_exit(&sc->sc_mutex);
}

static int
viogpu_cmd_req(struct viogpu_softc *sc, void *cmd, size_t cmd_size,
	       size_t ret_size)
{
	struct virtio_softc *vsc = sc->sc_virtio;
	struct virtqueue *vq = &sc->sc_vqs[VQCTRL];
	struct virtio_gpu_ctrl_hdr *hdr =
	    (struct virtio_gpu_ctrl_hdr *)sc->sc_cmd;
	int slot, error;

	memcpy(sc->sc_cmd, cmd, cmd_size);
	memset((char *)sc->sc_cmd + cmd_size, 0, ret_size);

#if VIOGPU_DEBUG
	printf("%s: [%zu -> %zu]: ", __func__, cmd_size, ret_size);
	for (int i = 0; i < cmd_size; i++) {
		printf(" %02x", ((unsigned char *)sc->sc_cmd)[i]);
	}
	printf("\n");
#endif

	hdr->flags |= virtio_rw32(vsc, VIRTIO_GPU_FLAG_FENCE);
	hdr->fence_id = virtio_rw64(vsc, ++sc->sc_fence_id);

	error = virtio_enqueue_prep(vsc, vq, &slot);
	if (error != 0)
		panic("%s: control vq busy", device_xname(sc->sc_dev));

	error = virtio_enqueue_reserve(vsc, vq, slot,
	    sc->sc_dma_map->dm_nsegs + 1);
	if (error != 0)
		panic("%s: control vq busy", device_xname(sc->sc_dev));

	bus_dmamap_sync(virtio_dmat(vsc), sc->sc_dma_map, 0, cmd_size,
	    BUS_DMASYNC_PREWRITE);
	virtio_enqueue_p(vsc, vq, slot, sc->sc_dma_map, 0, cmd_size, true);

	bus_dmamap_sync(virtio_dmat(vsc), sc->sc_dma_map, cmd_size, ret_size,
	    BUS_DMASYNC_PREREAD);
	virtio_enqueue_p(vsc, vq, slot, sc->sc_dma_map, cmd_size, ret_size,
	    false);

	virtio_enqueue_commit(vsc, vq, slot, true);

	sc->cur_cmd_size = cmd_size;
	sc->cur_ret_size = ret_size;
	sc->is_requesting = true;

	return 0;
}

static int
viogpu_vq_done(struct virtqueue *vq)
{
	struct virtio_softc *vsc = vq->vq_owner;
	struct viogpu_softc *sc = device_private(virtio_child(vsc));
	struct virtio_gpu_ctrl_hdr *resp;
	int slot, len;
	uint32_t cmd_type, resp_type;
	uint64_t resp_fence, expect_fence;
	bool next_req_sent = false;

	mutex_enter(&sc->sc_mutex);

	while (virtio_dequeue(vsc, vq, &slot, &len) != 0)
		;

	virtio_dequeue_commit(vsc, vq, slot);

	bus_dmamap_sync(virtio_dmat(vsc), sc->sc_dma_map, 0, sc->cur_cmd_size,
	    BUS_DMASYNC_POSTWRITE);
	bus_dmamap_sync(virtio_dmat(vsc), sc->sc_dma_map, sc->cur_cmd_size,
	    sc->cur_ret_size, BUS_DMASYNC_POSTREAD);

	resp = (struct virtio_gpu_ctrl_hdr *)((char *)sc->sc_cmd +
	    sc->cur_cmd_size);

	cmd_type = virtio_rw32(vsc,
	    ((struct virtio_gpu_ctrl_hdr *)sc->sc_cmd)->type);
	resp_type = virtio_rw32(vsc, resp->type);
	resp_fence = virtio_rw64(vsc, resp->fence_id);
	expect_fence = sc->sc_fence_id;

	switch (cmd_type) {
	case VIRTIO_GPU_CMD_TRANSFER_TO_HOST_2D:
		/* The second command for screen updating must be issued. */
		if (resp_type == VIRTIO_GPU_RESP_OK_NODATA) {
			viogpu_flush_resource(sc, 1, 0, 0, sc->sc_fb_width,
			    sc->sc_fb_height);
			next_req_sent = true;
		}
		break;
	case VIRTIO_GPU_CMD_RESOURCE_FLUSH:
		if (sc->needs_update == true) {
			viogpu_transfer_to_host_2d(sc, 1, 0, 0,
			    sc->sc_fb_width, sc->sc_fb_height);
			sc->needs_update = false;
			next_req_sent = true;
		}
		break;
	default:
		/* Other command types are called synchronously. */
		break;
	}

	if (next_req_sent == false) {
		sc->is_requesting = false;
		cv_broadcast(&sc->req_wait);
	}

	mutex_exit(&sc->sc_mutex);

	if (resp_type != VIRTIO_GPU_RESP_OK_NODATA) {
		switch (cmd_type) {
		case VIRTIO_GPU_CMD_TRANSFER_TO_HOST_2D:
			device_printf(sc->sc_dev,
			    "failed TRANSFER_TO_HOST: %d\n", resp_type);
			break;
		case VIRTIO_GPU_CMD_RESOURCE_FLUSH:
			device_printf(sc->sc_dev,
			    "failed RESOURCE_FLUSH: %d\n", resp_type);
			break;
		default:
			break;
		}
	}

	if (resp_fence != expect_fence)
		printf("%s: return fence id not right (0x%" PRIx64 " != 0x%"
		    PRIx64 ")\n", __func__, resp_fence, expect_fence);

	return 0;
}

static int
viogpu_get_display_info(struct viogpu_softc *sc)
{
	struct virtio_softc *vsc = sc->sc_virtio;
	struct virtio_gpu_ctrl_hdr hdr = { 0 };
	struct virtio_gpu_resp_display_info info = { 0 };

	hdr.type = virtio_rw32(vsc, VIRTIO_GPU_CMD_GET_DISPLAY_INFO);

	viogpu_cmd_sync(sc, &hdr, sizeof(hdr), &info, sizeof(info));

	if (virtio_rw32(vsc, info.hdr.type) !=
	    VIRTIO_GPU_RESP_OK_DISPLAY_INFO) {
		device_printf(sc->sc_dev, "failed getting display info\n");
		return 1;
	}

	if (!info.pmodes[0].enabled) {
		device_printf(sc->sc_dev, "pmodes[0] is not enabled\n");
		return 1;
	}

	sc->sc_fb_width = virtio_rw32(vsc, info.pmodes[0].r.width);
	sc->sc_fb_height = virtio_rw32(vsc, info.pmodes[0].r.height);

	return 0;
}

static int
viogpu_create_2d(struct viogpu_softc *sc, uint32_t resource_id, uint32_t width,
		 uint32_t height)
{
	struct virtio_softc *vsc = sc->sc_virtio;
	struct virtio_gpu_resource_create_2d res = { 0 };
	struct virtio_gpu_ctrl_hdr resp = { 0 };

	res.hdr.type = virtio_rw32(vsc, VIRTIO_GPU_CMD_RESOURCE_CREATE_2D);
	res.resource_id = virtio_rw32(vsc, resource_id);
	res.format = virtio_rw32(vsc, VIRTIO_GPU_FORMAT_B8G8R8X8_UNORM);
	res.width = virtio_rw32(vsc, width);
	res.height = virtio_rw32(vsc, height);

	viogpu_cmd_sync(sc, &res, sizeof(res), &resp, sizeof(resp));

	if (virtio_rw32(vsc, resp.type) != VIRTIO_GPU_RESP_OK_NODATA) {
		device_printf(sc->sc_dev, "failed CREATE_2D: %d\n",
		    virtio_rw32(vsc, resp.type));
		return 1;
	}

	return 0;
}

static int
viogpu_set_scanout(struct viogpu_softc *sc, uint32_t scanout_id,
		   uint32_t resource_id, uint32_t width, uint32_t height)
{
	struct virtio_softc *vsc = sc->sc_virtio;
	struct virtio_gpu_set_scanout ss = { 0 };
	struct virtio_gpu_ctrl_hdr resp = { 0 };

	ss.hdr.type = virtio_rw32(vsc, VIRTIO_GPU_CMD_SET_SCANOUT);
	ss.scanout_id = virtio_rw32(vsc, scanout_id);
	ss.resource_id = virtio_rw32(vsc, resource_id);
	ss.r.width = virtio_rw32(vsc, width);
	ss.r.height = virtio_rw32(vsc, height);

	viogpu_cmd_sync(sc, &ss, sizeof(ss), &resp, sizeof(resp));

	if (virtio_rw32(vsc, resp.type) != VIRTIO_GPU_RESP_OK_NODATA) {
		device_printf(sc->sc_dev, "failed SET_SCANOUT: %d\n",
		    virtio_rw32(vsc, resp.type));
		return 1;
	}

	return 0;
}

static int
viogpu_attach_backing(struct viogpu_softc *sc, uint32_t resource_id,
		      bus_dmamap_t dmamap)
{
	struct virtio_softc *vsc = sc->sc_virtio;
	struct virtio_gpu_resource_attach_backing_entries backing = { 0 };
	struct virtio_gpu_ctrl_hdr resp = { 0 };

	backing.hdr.type = virtio_rw32(vsc,
	    VIRTIO_GPU_CMD_RESOURCE_ATTACH_BACKING);
	backing.resource_id = virtio_rw32(vsc, resource_id);
	backing.nr_entries = virtio_rw32(vsc, __arraycount(backing.entries));
	backing.entries[0].addr = virtio_rw64(vsc, dmamap->dm_segs[0].ds_addr);
	backing.entries[0].length = virtio_rw32(vsc,
	    dmamap->dm_segs[0].ds_len);

	if (dmamap->dm_nsegs > 1)
		printf("%s: TODO: send all %d segs\n", __func__,
		    dmamap->dm_nsegs);

#if VIOGPU_DEBUG
	printf("%s: backing addr 0x%" PRIx64 " length %d\n", __func__,
	    backing.entries[0].addr, backing.entries[0].length);
#endif

	viogpu_cmd_sync(sc, &backing, sizeof(backing), &resp, sizeof(resp));

	if (virtio_rw32(vsc, resp.type) != VIRTIO_GPU_RESP_OK_NODATA) {
		device_printf(sc->sc_dev, "failed ATTACH_BACKING: %d\n",
		    virtio_rw32(vsc, resp.type));
		return 1;
	}

	return 0;
}

static int
viogpu_transfer_to_host_2d(struct viogpu_softc *sc, uint32_t resource_id,
			   uint32_t x, uint32_t y, uint32_t width,
			   uint32_t height)
{
	struct virtio_softc *vsc = sc->sc_virtio;
	struct virtio_gpu_transfer_to_host_2d tth = { 0 };

	tth.hdr.type = virtio_rw32(vsc, VIRTIO_GPU_CMD_TRANSFER_TO_HOST_2D);
	tth.resource_id = virtio_rw32(vsc, resource_id);
	tth.r.x = virtio_rw32(vsc, x);
	tth.r.y = virtio_rw32(vsc, y);
	tth.r.width = virtio_rw32(vsc, width);
	tth.r.height = virtio_rw32(vsc, height);
	tth.offset = virtio_rw64(vsc, (y * sc->sc_fb_width + x) *
	    4 /* bpp / 8 */);

	viogpu_cmd_req(sc, &tth, sizeof(tth),
	    sizeof(struct virtio_gpu_ctrl_hdr));

	return 0;
}

static int
viogpu_flush_resource(struct viogpu_softc *sc, uint32_t resource_id,
		      uint32_t x, uint32_t y, uint32_t width, uint32_t height)
{
	struct virtio_softc *vsc = sc->sc_virtio;
	struct virtio_gpu_resource_flush flush = { 0 };

	flush.hdr.type = virtio_rw32(vsc, VIRTIO_GPU_CMD_RESOURCE_FLUSH);
	flush.resource_id = virtio_rw32(vsc, resource_id);
	flush.r.x = virtio_rw32(vsc, x);
	flush.r.y = virtio_rw32(vsc, y);
	flush.r.width = virtio_rw32(vsc, width);
	flush.r.height = virtio_rw32(vsc, height);

	viogpu_cmd_req(sc, &flush, sizeof(flush),
	    sizeof(struct virtio_gpu_ctrl_hdr));

	return 0;
}

static int
viogpu_wsioctl(void *v, void *vs, u_long cmd, void *data, int flag,
	       struct lwp *l)
{
	struct rasops_info *ri = v;
	struct wsdisplayio_fbinfo *fbi;
	struct wsdisplay_fbinfo *wdf;

	switch (cmd) {
	case WSDISPLAYIO_GTYPE:
		*(u_int *)data = WSDISPLAY_TYPE_VIOGPU;
		return 0;
	case WSDISPLAYIO_GET_FBINFO:
		fbi = (struct wsdisplayio_fbinfo *)data;
		return wsdisplayio_get_fbinfo(ri, fbi);
	case WSDISPLAYIO_GINFO:
		wdf = (struct wsdisplay_fbinfo *)data;
		wdf->height = ri->ri_height;
		wdf->width = ri->ri_width;
		wdf->depth = ri->ri_depth;
		wdf->cmsize = 0;
		return 0;
	case WSDISPLAYIO_LINEBYTES:
		*(u_int *)data = ri->ri_stride;
		return 0;
	case WSDISPLAYIO_SMODE:
		return 0;
	case WSDISPLAYIO_GVIDEO:
	case WSDISPLAYIO_SVIDEO:
		return 0;
	}

	return EPASSTHROUGH;
}

static void
viogpu_init_screen(void *cookie, struct vcons_screen *scr, int existing,
		   long *defattr)
{
	struct viogpu_softc *sc = cookie;
	struct rasops_info *ri = &scr->scr_ri;

	ri->ri_bits = sc->sc_fb_dma_kva;
	ri->ri_flg = RI_CENTER | RI_CLEAR;
#if BYTE_ORDER == BIG_ENDIAN
	ri->ri_flg |= RI_BSWAP;
#endif
	ri->ri_depth = 32;
	ri->ri_width = sc->sc_fb_width;
	ri->ri_height = sc->sc_fb_height;
	ri->ri_stride = ri->ri_width * ri->ri_depth / 8;
	ri->ri_bpos = 0;	/* B8G8R8X8 */
	ri->ri_bnum = 8;
	ri->ri_gpos = 8;
	ri->ri_gnum = 8;
	ri->ri_rpos = 16;
	ri->ri_rnum = 8;
	rasops_init(ri, 0, 0);
	ri->ri_caps = WSSCREEN_WSCOLORS | WSSCREEN_HILIT;
	rasops_reconfig(ri, sc->sc_fb_height / ri->ri_font->fontheight,
	    sc->sc_fb_width / ri->ri_font->fontwidth);

	/*
	 * Replace select text operations with wrappers that update the screen
	 * after the operation.
	 */
	sc->ri_cursor = ri->ri_ops.cursor;
	sc->ri_putchar = ri->ri_ops.putchar;
	sc->ri_copycols = ri->ri_ops.copycols;
	sc->ri_erasecols = ri->ri_ops.erasecols;
	sc->ri_copyrows = ri->ri_ops.copyrows;
	sc->ri_eraserows = ri->ri_ops.eraserows;
	sc->ri_replaceattr = ri->ri_ops.replaceattr;
	ri->ri_ops.cursor = ri->ri_ops.cursor == NULL ? NULL : viogpu_cursor;
	ri->ri_ops.putchar = ri->ri_ops.putchar == NULL ? NULL :
	    viogpu_putchar;
	ri->ri_ops.copycols = ri->ri_ops.copycols == NULL ? NULL :
	    viogpu_copycols;
	ri->ri_ops.erasecols = ri->ri_ops.erasecols == NULL ? NULL :
	    viogpu_erasecols;
	ri->ri_ops.copyrows = ri->ri_ops.copyrows == NULL ? NULL :
	    viogpu_copyrows;
	ri->ri_ops.eraserows = ri->ri_ops.eraserows == NULL ? NULL :
	    viogpu_eraserows;
	ri->ri_ops.replaceattr = ri->ri_ops.replaceattr == NULL ? NULL :
	    viogpu_replaceattr;
}

static void
viogpu_cursor(void *c, int on, int row, int col)
{
	struct rasops_info *ri = c;
	struct vcons_screen *vscr = ri->ri_hw;
	struct viogpu_softc *sc = vscr->scr_vd->cookie;

	sc->ri_cursor(c, on, row, col);

	softint_schedule(sc->update_soft_ih);
}

static void
viogpu_putchar(void *c, int row, int col, u_int uc, long attr)
{
	struct rasops_info *ri = c;
	struct vcons_screen *vscr = ri->ri_hw;
	struct viogpu_softc *sc = vscr->scr_vd->cookie;

	sc->ri_putchar(c, row, col, uc, attr);

	softint_schedule(sc->update_soft_ih);
}

static void
viogpu_copycols(void *c, int row, int srccol, int dstcol, int ncols)
{
	struct rasops_info *ri = c;
	struct vcons_screen *vscr = ri->ri_hw;
	struct viogpu_softc *sc = vscr->scr_vd->cookie;

	sc->ri_copycols(c, row, srccol, dstcol, ncols);

	softint_schedule(sc->update_soft_ih);
}

static void
viogpu_erasecols(void *c, int row, int startcol, int ncols, long attr)
{
	struct rasops_info *ri = c;
	struct vcons_screen *vscr = ri->ri_hw;
	struct viogpu_softc *sc = vscr->scr_vd->cookie;

	sc->ri_erasecols(c, row, startcol, ncols, attr);

	softint_schedule(sc->update_soft_ih);
}

static void
viogpu_copyrows(void *c, int srcrow, int dstrow, int nrows)
{
	struct rasops_info *ri = c;
	struct vcons_screen *vscr = ri->ri_hw;
	struct viogpu_softc *sc = vscr->scr_vd->cookie;

	sc->ri_copyrows(c, srcrow, dstrow, nrows);

	softint_schedule(sc->update_soft_ih);
}

static void
viogpu_eraserows(void *c, int row, int nrows, long attr)
{
	struct rasops_info *ri = c;
	struct vcons_screen *vscr = ri->ri_hw;
	struct viogpu_softc *sc = vscr->scr_vd->cookie;

	sc->ri_eraserows(c, row, nrows, attr);

	softint_schedule(sc->update_soft_ih);
}

static void
viogpu_replaceattr(void *c, long oldattr, long newattr)
{
	struct rasops_info *ri = c;
	struct vcons_screen *vscr = ri->ri_hw;
	struct viogpu_softc *sc = vscr->scr_vd->cookie;

	sc->ri_replaceattr(c, oldattr, newattr);

	softint_schedule(sc->update_soft_ih);
}
