/*
 * 86Box    A hypervisor and IBM PC system emulator that specializes in
 *          running old operating systems and software designed for IBM
 *          PC systems and compatibles from 1981 through fairly recent
 *          system designs based on the PCI bus.
 *
 *          This file is part of the 86Box distribution.
 *
 *          Voodoo Graphics, 2, Banshee, 3 emulation.
 *
 * Authors: Sarah Walker, <https://pcem-emulator.co.uk/>
 *          leilei
 *
 *          Copyright 2008-2020 Sarah Walker.
 */
#include <stdarg.h>
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
#include <string.h>
#include <stdlib.h>
#include <stddef.h>
#include <wchar.h>
#include <math.h>
#define HAVE_STDARG_H
#include <86box/86box.h>
#include "cpu.h"
#include <86box/machine.h>
#include <86box/device.h>
#include <86box/mem.h>
#include <86box/pci.h>
#include <86box/rom.h>
#include <86box/timer.h>
#include <86box/device.h>
#include <86box/plat.h>
#include <86box/thread.h>
#include <86box/video.h>
#include <86box/vid_svga.h>
#include <86box/vid_voodoo_common.h>
#include <86box/vid_voodoo_blitter.h>
#include <86box/vid_voodoo_display.h>
#include <86box/vid_voodoo_dither.h>
#include <86box/vid_voodoo_fb.h>
#include <86box/vid_voodoo_fifo.h>
#include <86box/vid_voodoo_reg.h>
#include <86box/vid_voodoo_regs.h>
#include <86box/vid_voodoo_render.h>
#include <86box/vid_voodoo_texture.h>

rgba8_t rgb332[0x100];
rgba8_t ai44[0x100];
rgba8_t rgb565[0x10000];
rgba8_t argb1555[0x10000];
rgba8_t argb4444[0x10000];
rgba8_t ai88[0x10000];

int tris = 0;

#ifdef ENABLE_VOODOO_LOG
int voodoo_do_log = ENABLE_VOODOO_LOG;

static void
voodoo_log(const char *fmt, ...)
{
    va_list ap;

    if (voodoo_do_log) {
        va_start(ap, fmt);
        pclog_ex(fmt, ap);
        va_end(ap);
    }
}
#else
#    define voodoo_log(fmt, ...)
#endif

static int
voodoo_env_is_disabled(const char *value)
{
    /* Accept common "off" values for env overrides. */
    return !strcmp(value, "0") || !strcmp(value, "off") || !strcmp(value, "false") || !strcmp(value, "disabled");
}

static void
voodoo_init_relax_settings(voodoo_t *voodoo)
{
    const char *relax_env = getenv("VOODOO_LFB_RELAX");
    const char *wait_env  = getenv("VOODOO_WAIT_STATS");
    int         relax_enabled = 1;

    /* Default to front-sync relax mode; wait stats are opt-in. */
    if (!relax_env || !*relax_env) {
        relax_env = "4";
    } else if (voodoo_env_is_disabled(relax_env)) {
        relax_enabled = 0;
    }

    voodoo->wait_stats_explicit = (wait_env && *wait_env);
    voodoo->wait_stats_enabled = voodoo->wait_stats_explicit && !voodoo_env_is_disabled(wait_env);

    voodoo->lfb_relax_enabled = relax_enabled;
    voodoo->lfb_relax_full = relax_enabled && (strcmp(relax_env, "full") == 0);
    voodoo->lfb_relax_ignore_cmdfifo = relax_enabled && (!strcmp(relax_env, "nocmdfifo") || !strcmp(relax_env, "2") || !strcmp(relax_env, "3") || !strcmp(relax_env, "4") || !strcmp(relax_env, "frontsync"));
    voodoo->lfb_relax_ignore_draw = relax_enabled && (!strcmp(relax_env, "nodraw") || !strcmp(relax_env, "2") || !strcmp(relax_env, "3") || !strcmp(relax_env, "4") || !strcmp(relax_env, "frontsync"));
    voodoo->lfb_relax_ignore_fb_writes = relax_enabled && (!strcmp(relax_env, "nowrites") || !strcmp(relax_env, "3") || !strcmp(relax_env, "4") || !strcmp(relax_env, "frontsync"));
    voodoo->lfb_relax_front_sync = relax_enabled && (!strcmp(relax_env, "4") || !strcmp(relax_env, "frontsync"));
}

static void
voodoo_update_queued_buffers(voodoo_t *voodoo)
{
    switch (voodoo->queued_lfbMode & LFB_WRITE_MASK) {
        case LFB_WRITE_FRONT:
            voodoo->queued_fb_write_buffer = voodoo->queued_disp_buffer;
            break;
        case LFB_WRITE_BACK:
            voodoo->queued_fb_write_buffer = voodoo->queued_draw_buffer;
            break;
        default:
            voodoo->queued_fb_write_buffer = voodoo->queued_disp_buffer;
            break;
    }

    switch (voodoo->queued_fbzMode & FBZ_DRAW_MASK) {
        case FBZ_DRAW_FRONT:
            voodoo->queued_fb_draw_buffer = voodoo->queued_disp_buffer;
            break;
        case FBZ_DRAW_BACK:
            voodoo->queued_fb_draw_buffer = voodoo->queued_draw_buffer;
            break;
        default:
            voodoo->queued_fb_draw_buffer = voodoo->queued_draw_buffer;
            break;
    }
}

void
voodoo_recalc(voodoo_t *voodoo)
{
    uint32_t buffer_offset = ((voodoo->fbiInit2 >> 11) & 511) * 4096;

    if (voodoo->type >= VOODOO_BANSHEE)
        return;

    voodoo->params.front_offset = voodoo->disp_buffer * buffer_offset;
    voodoo->back_offset         = voodoo->draw_buffer * buffer_offset;

    voodoo->buffer_cutoff = TRIPLE_BUFFER ? (buffer_offset * 4) : (buffer_offset * 3);
    if (TRIPLE_BUFFER)
        voodoo->params.aux_offset = buffer_offset * 3;
    else
        voodoo->params.aux_offset = buffer_offset * 2;

    switch (voodoo->lfbMode & LFB_WRITE_MASK) {
        case LFB_WRITE_FRONT:
            voodoo->fb_write_offset = voodoo->params.front_offset;
            voodoo->fb_write_buffer = voodoo->disp_buffer;
            break;
        case LFB_WRITE_BACK:
            voodoo->fb_write_offset = voodoo->back_offset;
            voodoo->fb_write_buffer = voodoo->draw_buffer;
            break;

        default:
            /*BreakNeck sets invalid LFB write buffer select*/
            voodoo->fb_write_offset = voodoo->params.front_offset;
            break;
    }

    switch (voodoo->lfbMode & LFB_READ_MASK) {
        case LFB_READ_FRONT:
            voodoo->fb_read_offset = voodoo->params.front_offset;
            break;
        case LFB_READ_BACK:
            voodoo->fb_read_offset = voodoo->back_offset;
            break;
        case LFB_READ_AUX:
            voodoo->fb_read_offset = voodoo->params.aux_offset;
            break;

        default:
            fatal("voodoo_recalc : unknown lfb source\n");
    }

    switch (voodoo->params.fbzMode & FBZ_DRAW_MASK) {
        case FBZ_DRAW_FRONT:
            voodoo->params.draw_offset = voodoo->params.front_offset;
            voodoo->fb_draw_buffer     = voodoo->disp_buffer;
            break;
        case FBZ_DRAW_BACK:
            voodoo->params.draw_offset = voodoo->back_offset;
            voodoo->fb_draw_buffer     = voodoo->draw_buffer;
            break;

        default:
            fatal("voodoo_recalc : unknown draw buffer\n");
    }

    voodoo->block_width = ((voodoo->fbiInit1 >> 4) & 15) * 2;
    if (voodoo->fbiInit6 & (1 << 30))
        voodoo->block_width += 1;
    if (voodoo->fbiInit1 & (1 << 24))
        voodoo->block_width += 32;
    voodoo->row_width            = voodoo->block_width * 32 * 2;
    voodoo->params.row_width     = voodoo->row_width;
    voodoo->aux_row_width        = voodoo->row_width;
    voodoo->params.aux_row_width = voodoo->aux_row_width;
}

static uint16_t
voodoo_readw(uint32_t addr, void *priv)
{
    voodoo_t *voodoo = (voodoo_t *) priv;

    addr &= 0xffffff;

    cycles -= voodoo->read_time;

    if ((addr & 0xc00000) == 0x400000) /*Framebuffer*/
    {
        if (SLI_ENABLED) {
            const voodoo_set_t *set = voodoo->set;
            int                 y   = (addr >> 11) & 0x3ff;

            if (y & 1)
                voodoo = set->voodoos[1];
            else
                voodoo = set->voodoos[0];
        }

        /* Reads from aux/draw/write regions must see completed rendering. */
        int need_sync = (voodoo->fb_read_offset == voodoo->params.aux_offset) ||
                        (voodoo->fb_read_offset == voodoo->params.draw_offset) ||
                        (voodoo->fb_read_offset == voodoo->fb_write_offset);
        int do_sync = 0;
        int read_buf = -1;

        if (voodoo->fb_read_offset == voodoo->params.front_offset)
            read_buf = VOODOO_BUF_FRONT;
        else if (voodoo->fb_read_offset == voodoo->back_offset)
            read_buf = VOODOO_BUF_BACK;
        else if (voodoo->fb_read_offset == voodoo->params.aux_offset)
            read_buf = VOODOO_BUF_AUX;

        if (!need_sync && voodoo->lfb_relax_front_sync && read_buf >= 0 && read_buf != VOODOO_BUF_BACK)
            need_sync = 1;

        if (need_sync) {
            if (!voodoo->lfb_relax_enabled)
                do_sync = 1;
            else if (voodoo->lfb_relax_full)
                do_sync = 0;
            else {
                /* In relax mode, only back-buffer reads can skip the full FIFO flush. */
                int pending_buf = 0;
                int pending_unknown = 0;

                if (read_buf >= 0 && read_buf < VOODOO_BUF_COUNT) {
                    if (!voodoo->lfb_relax_ignore_fb_writes)
                        pending_buf += voodoo->pending_fb_writes_buf[read_buf];
                    if (!voodoo->lfb_relax_ignore_draw)
                        pending_buf += voodoo->pending_draw_cmds_buf[read_buf];
                }

                if (!voodoo->lfb_relax_ignore_fb_writes)
                    pending_unknown += voodoo->pending_fb_writes_buf[VOODOO_BUF_UNKNOWN];
                if (!voodoo->lfb_relax_ignore_draw)
                    pending_unknown += voodoo->pending_draw_cmds_buf[VOODOO_BUF_UNKNOWN];
                if (!voodoo->lfb_relax_ignore_cmdfifo) {
                    if ((voodoo->cmdfifo_depth_rd != voodoo->cmdfifo_depth_wr) || voodoo->cmdfifo_in_sub)
                        pending_unknown++;
                    if ((voodoo->cmdfifo_depth_rd_2 != voodoo->cmdfifo_depth_wr_2) || voodoo->cmdfifo_in_sub_2)
                        pending_unknown++;
                }

                if (read_buf != VOODOO_BUF_BACK)
                    do_sync = 1;
                else
                    do_sync = (pending_buf || pending_unknown);
            }

            if (do_sync) {
                voodoo->flush = 1;
                while (!FIFO_EMPTY) {
                    voodoo_wake_fifo_thread_now(voodoo);
                    thread_wait_event(voodoo->fifo_empty_event, -1);
                }
                voodoo_wait_for_render_thread_idle(voodoo);
                voodoo->flush = 0;
            } else if (voodoo->lfb_relax_enabled && !voodoo->lfb_relax_full) {
                voodoo_wait_for_render_thread_idle(voodoo);
            }
        }

        return voodoo_fb_readw(addr, voodoo);
    }

    return 0xffff;
}

static uint32_t
voodoo_readl(uint32_t addr, void *priv)
{
    voodoo_t *voodoo = (voodoo_t *) priv;
    uint32_t  temp   = 0xffffffff;
    int       fifo_size;
    voodoo->rd_count++;
    addr &= 0xffffff;

    cycles -= voodoo->read_time;

    if (addr & 0x800000) { /*Texture*/
        if (voodoo->wait_stats_enabled)
            voodoo->readl_tex_count++;
    } else if (addr & 0x400000) /*Framebuffer*/
    {
        uint64_t fifo_wait_start = 0;
        uint64_t fifo_wait_spins = 0;
        int      fifo_wait_active = 0;
        int      need_sync = 0;
        int      do_sync = 0;
        int      read_buf = -1;

        if (SLI_ENABLED) {
            const voodoo_set_t *set = voodoo->set;
            int                 y   = (addr >> 11) & 0x3ff;

            if (y & 1)
                voodoo = set->voodoos[1];
            else
                voodoo = set->voodoos[0];
        }

        if (voodoo->wait_stats_enabled)
            voodoo->readl_fb_count++;

        if (voodoo->fb_read_offset == voodoo->params.front_offset)
            read_buf = VOODOO_BUF_FRONT;
        else if (voodoo->fb_read_offset == voodoo->back_offset)
            read_buf = VOODOO_BUF_BACK;
        else if (voodoo->fb_read_offset == voodoo->params.aux_offset)
            read_buf = VOODOO_BUF_AUX;

        /* Reads from aux/draw/write regions must see completed rendering. */
        need_sync = (voodoo->fb_read_offset == voodoo->params.aux_offset) ||
                    (voodoo->fb_read_offset == voodoo->params.draw_offset) ||
                    (voodoo->fb_read_offset == voodoo->fb_write_offset);
        if (!need_sync && voodoo->lfb_relax_front_sync && read_buf >= 0 && read_buf != VOODOO_BUF_BACK)
            need_sync = 1;
        if (need_sync) {
            if (!voodoo->lfb_relax_enabled)
                do_sync = 1;
            else if (voodoo->lfb_relax_full)
                do_sync = 0;
            else {
                /* In relax mode, only back-buffer reads can skip the full FIFO flush. */
                int pending_buf = 0;
                int pending_unknown = 0;

                if (read_buf >= 0 && read_buf < VOODOO_BUF_COUNT) {
                    if (!voodoo->lfb_relax_ignore_fb_writes)
                        pending_buf += voodoo->pending_fb_writes_buf[read_buf];
                    if (!voodoo->lfb_relax_ignore_draw)
                        pending_buf += voodoo->pending_draw_cmds_buf[read_buf];
                }

                if (!voodoo->lfb_relax_ignore_fb_writes)
                    pending_unknown += voodoo->pending_fb_writes_buf[VOODOO_BUF_UNKNOWN];
                if (!voodoo->lfb_relax_ignore_draw)
                    pending_unknown += voodoo->pending_draw_cmds_buf[VOODOO_BUF_UNKNOWN];
                if (!voodoo->lfb_relax_ignore_cmdfifo) {
                    if ((voodoo->cmdfifo_depth_rd != voodoo->cmdfifo_depth_wr) || voodoo->cmdfifo_in_sub)
                        pending_unknown++;
                    if ((voodoo->cmdfifo_depth_rd_2 != voodoo->cmdfifo_depth_wr_2) || voodoo->cmdfifo_in_sub_2)
                        pending_unknown++;
                }

                if (read_buf != VOODOO_BUF_BACK)
                    do_sync = 1;
                else
                    do_sync = (pending_buf || pending_unknown);
            }
        }

        if (voodoo->wait_stats_enabled) {
            if (do_sync)
                voodoo->readl_fb_sync_count++;
            else
                voodoo->readl_fb_nosync_count++;
            if (read_buf >= 0) {
                if (do_sync)
                    voodoo->readl_fb_sync_buf[read_buf]++;
                else
                    voodoo->readl_fb_nosync_buf[read_buf]++;
            }
            if (need_sync && voodoo->lfb_relax_enabled && !do_sync) {
                voodoo->readl_fb_relaxed_count++;
                if (read_buf >= 0)
                    voodoo->readl_fb_relaxed_buf[read_buf]++;
            }
        }

        if (do_sync) {
            voodoo->flush = 1;
            while (!FIFO_EMPTY) {
                if (voodoo->wait_stats_enabled) {
                    if (!fifo_wait_active) {
                        fifo_wait_active = 1;
                        fifo_wait_start  = plat_timer_read();
                        voodoo->fifo_empty_waits++;
                    }
                    fifo_wait_spins++;
                }
                voodoo_wake_fifo_thread_now(voodoo);
                thread_wait_event(voodoo->fifo_empty_event, -1);
            }
            if (fifo_wait_active) {
                voodoo->fifo_empty_wait_ticks += plat_timer_read() - fifo_wait_start;
                voodoo->fifo_empty_spin_checks += fifo_wait_spins;
            }
            voodoo_wait_for_render_thread_idle(voodoo);
            voodoo->flush = 0;
        } else if (need_sync && voodoo->lfb_relax_enabled && !voodoo->lfb_relax_full) {
            voodoo_wait_for_render_thread_idle(voodoo);
        }

        temp = voodoo_fb_readl(addr, voodoo);
    } else {
        if (voodoo->wait_stats_enabled)
            voodoo->readl_reg_count++;

        switch (addr & 0x3fc) {
            case SST_status:
                {
                    int fifo_entries = FIFO_ENTRIES;
                    int swap_count   = voodoo->swap_count;
                    int written      = voodoo->cmd_written + voodoo->cmd_written_fifo + voodoo->cmd_written_fifo_2;
                    int busy         = (written - voodoo->cmd_read) ||
                               (voodoo->cmdfifo_depth_rd != voodoo->cmdfifo_depth_wr) ||
                               voodoo->voodoo_busy ||
                               RENDER_VOODOO_BUSY(voodoo, 0) ||
                               (voodoo->render_threads >= 2 && RENDER_VOODOO_BUSY(voodoo, 1)) ||
                               (voodoo->render_threads == 4 && (RENDER_VOODOO_BUSY(voodoo, 2) || RENDER_VOODOO_BUSY(voodoo, 3)));

                    if (SLI_ENABLED && voodoo->type != VOODOO_2) {
                        voodoo_t *voodoo_other  = (voodoo == voodoo->set->voodoos[0]) ? voodoo->set->voodoos[1] : voodoo->set->voodoos[0];
                        int       other_written = voodoo_other->cmd_written + voodoo_other->cmd_written_fifo + voodoo->cmd_written_fifo_2;

                        if (voodoo_other->swap_count > swap_count)
                            swap_count = voodoo_other->swap_count;
                        if ((voodoo_other->fifo_write_idx - voodoo_other->fifo_read_idx) > fifo_entries)
                            fifo_entries = voodoo_other->fifo_write_idx - voodoo_other->fifo_read_idx;
                        if ((other_written - voodoo_other->cmd_read) ||
                            (voodoo_other->cmdfifo_depth_rd != voodoo_other->cmdfifo_depth_wr) ||
                            voodoo_other->voodoo_busy ||
                            RENDER_VOODOO_BUSY(voodoo_other, 0) ||
                            (voodoo_other->render_threads >= 2 && RENDER_VOODOO_BUSY(voodoo_other, 1)) ||
                            (voodoo_other->render_threads == 4 && (RENDER_VOODOO_BUSY(voodoo_other, 2) || RENDER_VOODOO_BUSY(voodoo_other, 3))))
                            busy = 1;
                        if (!voodoo_other->voodoo_busy)
                            voodoo_wake_fifo_thread(voodoo_other);
                    }

                    fifo_size = 0xffff - fifo_entries;
                    temp      = fifo_size << 12;
                    if (fifo_size < 0x40)
                        temp |= fifo_size;
                    else
                        temp |= 0x3f;
                    if (swap_count < 7)
                        temp |= (swap_count << 28);
                    else
                        temp |= (7 << 28);
                    if (!voodoo->v_retrace)
                        temp |= 0x40;

                    if (busy)
                        temp |= 0x380; /*Busy*/

                    if (!voodoo->voodoo_busy)
                        voodoo_wake_fifo_thread(voodoo);
                }
                break;

            case SST_fbzColorPath:
                voodoo_flush(voodoo);
                temp = voodoo->params.fbzColorPath;
                break;
            case SST_fogMode:
                voodoo_flush(voodoo);
                temp = voodoo->params.fogMode;
                break;
            case SST_alphaMode:
                voodoo_flush(voodoo);
                temp = voodoo->params.alphaMode;
                break;
            case SST_fbzMode:
                voodoo_flush(voodoo);
                temp = voodoo->params.fbzMode;
                break;
            case SST_lfbMode:
                voodoo_flush(voodoo);
                temp = voodoo->lfbMode;
                break;
            case SST_clipLeftRight:
                voodoo_flush(voodoo);
                temp = voodoo->params.clipRight | (voodoo->params.clipLeft << 16);
                break;
            case SST_clipLowYHighY:
                voodoo_flush(voodoo);
                temp = voodoo->params.clipHighY | (voodoo->params.clipLowY << 16);
                break;

            case SST_stipple:
                voodoo_flush(voodoo);
                temp = voodoo->params.stipple;
                break;
            case SST_color0:
                voodoo_flush(voodoo);
                temp = voodoo->params.color0;
                break;
            case SST_color1:
                voodoo_flush(voodoo);
                temp = voodoo->params.color1;
                break;

            case SST_fbiPixelsIn:
                temp = voodoo->fbiPixelsIn & 0xffffff;
                break;
            case SST_fbiChromaFail:
                temp = voodoo->fbiChromaFail & 0xffffff;
                break;
            case SST_fbiZFuncFail:
                temp = voodoo->fbiZFuncFail & 0xffffff;
                break;
            case SST_fbiAFuncFail:
                temp = voodoo->fbiAFuncFail & 0xffffff;
                break;
            case SST_fbiPixelsOut:
                temp = voodoo->fbiPixelsOut & 0xffffff;
                break;

            case SST_fbiInit4:
                temp = voodoo->fbiInit4;
                break;
            case SST_fbiInit0:
                temp = voodoo->fbiInit0;
                break;
            case SST_fbiInit1:
                temp = voodoo->fbiInit1;
                break;
            case SST_fbiInit2:
                if (voodoo->initEnable & 0x04)
                    temp = voodoo->dac_readdata;
                else
                    temp = voodoo->fbiInit2;
                break;
            case SST_fbiInit3:
                temp = voodoo->fbiInit3 | (1 << 10) | (2 << 8);
                break;

            case SST_vRetrace:
                temp = voodoo->line & 0x1fff;
                break;
            case SST_hvRetrace:
                {
                    uint64_t line_time = (uint64_t) (voodoo->line_time >> 32);
                    uint64_t diff      = (timer_get_ts_int(&voodoo->timer) > tsc) ? (timer_get_ts_int(&voodoo->timer) - tsc) : 0;
                    uint64_t pre_div   = diff * voodoo->h_total;
                    uint64_t post_div  = pre_div / line_time;
                    uint64_t h_pos     = (voodoo->h_total - 1) - post_div;

                    if (h_pos >= voodoo->h_total)
                        h_pos = 0;

                    temp = voodoo->line & 0x1fff;
                    temp |= (h_pos << 16);
                }
                break;

            case SST_fbiInit5:
                temp = voodoo->fbiInit5 & ~0x1ff;
                break;
            case SST_fbiInit6:
                temp = voodoo->fbiInit6;
                break;
            case SST_fbiInit7:
                temp = voodoo->fbiInit7 & ~0xff;
                break;

            case SST_cmdFifoBaseAddr:
                temp = voodoo->cmdfifo_base >> 12;
                temp |= (voodoo->cmdfifo_end >> 12) << 16;
                break;

            case SST_cmdFifoRdPtr:
                temp = voodoo->cmdfifo_rp;
                break;
            case SST_cmdFifoAMin:
                temp = voodoo->cmdfifo_amin;
                break;
            case SST_cmdFifoAMax:
                temp = voodoo->cmdfifo_amax;
                break;
            case SST_cmdFifoDepth:
                temp = voodoo->cmdfifo_depth_wr - voodoo->cmdfifo_depth_rd;
                break;

            default:
                voodoo_log("voodoo_readl  : bad addr %08X\n", addr);
                temp = 0xffffffff;
        }
    }

    return temp;
}

static void
voodoo_writew(uint32_t addr, uint16_t val, void *priv)
{
    voodoo_t *voodoo = (voodoo_t *) priv;
    voodoo->wr_count++;
    addr &= 0xffffff;

    cycles -= voodoo->write_time;

    if ((addr & 0xc00000) == 0x400000) /*Framebuffer*/
        voodoo_queue_command(voodoo, addr | FIFO_WRITEW_FB, val);
}

static void
voodoo_writel(uint32_t addr, uint32_t val, void *priv)
{
    voodoo_t *voodoo = (voodoo_t *) priv;

    voodoo->wr_count++;

    addr &= 0xffffff;

    if (addr == voodoo->last_write_addr + 4)
        cycles -= voodoo->burst_time;
    else
        cycles -= voodoo->write_time;
    voodoo->last_write_addr = addr;

    if (addr & 0x800000) /*Texture*/
    {
        voodoo->tex_count++;
        voodoo_queue_command(voodoo, addr | FIFO_WRITEL_TEX, val);
    } else if (addr & 0x400000) /*Framebuffer*/
    {
        voodoo_queue_command(voodoo, addr | FIFO_WRITEL_FB, val);
    } else if ((addr & 0x200000) && (voodoo->fbiInit7 & FBIINIT7_CMDFIFO_ENABLE)) {
#if 0
        voodoo_log("Write CMDFIFO %08x(%08x) %08x  %08x\n", addr, (voodoo->cmdfifo_base + (addr & 0x3fffc)) & voodoo->fb_mask, val, (voodoo->cmdfifo_base + (addr & 0x3fffc)) & voodoo->fb_mask);
#endif
        *(uint32_t *) &voodoo->fb_mem[(voodoo->cmdfifo_base + (addr & 0x3fffc)) & voodoo->fb_mask] = val;
        voodoo->cmdfifo_depth_wr++;

        /* Voodoo1: use higher CMDFIFO threshold to reduce wake frequency */
        if (voodoo->type == VOODOO_1) {
            if ((voodoo->cmdfifo_depth_wr - voodoo->cmdfifo_depth_rd) < 20)
                voodoo_wake_fifo_thread(voodoo);
        }
        /* Other cards (Voodoo2, Banshee, Voodoo3, ...) keep the original behavior */
        else {
            if ((voodoo->cmdfifo_depth_wr - voodoo->cmdfifo_depth_rd) < 20)
                voodoo_wake_fifo_thread(voodoo);
        }
    } else
        switch (addr & 0x3fc) {
            case SST_intrCtrl:
                fatal("intrCtrl write %08x\n", val);
                break;
            
            case SST_userIntrCMD:
                fatal("userIntrCMD write %08x\n", val);
                break;

            case SST_swapbufferCMD:
                voodoo->cmd_written++;
                thread_wait_mutex(voodoo->swap_mutex);
                voodoo->swap_count++;
                thread_release_mutex(voodoo->swap_mutex);
                if (voodoo->fbiInit7 & FBIINIT7_CMDFIFO_ENABLE)
                    return;
                voodoo_queue_command(voodoo, addr | FIFO_WRITEL_REG, val);
                if (!voodoo->voodoo_busy)
                    voodoo_wake_fifo_threads(voodoo->set, voodoo);
                break;
            case SST_triangleCMD:
                if (voodoo->fbiInit7 & FBIINIT7_CMDFIFO_ENABLE)
                    return;
                voodoo->cmd_written++;
                voodoo_queue_command(voodoo, addr | FIFO_WRITEL_REG, val);
                if (!voodoo->voodoo_busy)
                    voodoo_wake_fifo_threads(voodoo->set, voodoo);
                break;
            case SST_ftriangleCMD:
                if (voodoo->fbiInit7 & FBIINIT7_CMDFIFO_ENABLE)
                    return;
                voodoo->cmd_written++;
                voodoo_queue_command(voodoo, addr | FIFO_WRITEL_REG, val);
                if (!voodoo->voodoo_busy)
                    voodoo_wake_fifo_threads(voodoo->set, voodoo);
                break;
            case SST_fastfillCMD:
                if (voodoo->fbiInit7 & FBIINIT7_CMDFIFO_ENABLE)
                    return;
                voodoo->cmd_written++;
                voodoo_queue_command(voodoo, addr | FIFO_WRITEL_REG, val);
                if (!voodoo->voodoo_busy)
                    voodoo_wake_fifo_threads(voodoo->set, voodoo);
                break;
            case SST_nopCMD:
                if (voodoo->fbiInit7 & FBIINIT7_CMDFIFO_ENABLE)
                    return;
                voodoo->cmd_written++;
                voodoo_queue_command(voodoo, addr | FIFO_WRITEL_REG, val);
                if (!voodoo->voodoo_busy)
                    voodoo_wake_fifo_threads(voodoo->set, voodoo);
                break;

            case SST_fbiInit4:
                if (voodoo->initEnable & 0x01) {
                    voodoo->fbiInit4  = val;
                    voodoo->read_time = pci_nonburst_time + pci_burst_time * ((voodoo->fbiInit4 & 1) ? 2 : 1);
#if 0
                    voodoo_log("fbiInit4 write %08x - read_time=%i\n", val, voodoo->read_time);
#endif
                }
                break;
            case SST_backPorch:
                voodoo->backPorch = val;
                break;
            case SST_videoDimensions:
                voodoo->videoDimensions = val;
                voodoo->h_disp          = (val & 0xfff) + 1;
                voodoo->v_disp          = (val >> 16) & 0xfff;
                if ((voodoo->v_disp == 386) || (voodoo->v_disp == 402) ||
                    (voodoo->v_disp == 482) || (voodoo->v_disp == 602))
                    voodoo->v_disp     -= 2;
                break;
            case SST_fbiInit0:
                if (voodoo->initEnable & 0x01) {
                    int old_vga_pass = voodoo->fbiInit0 & FBIINIT0_VGA_PASS;
                    voodoo->fbiInit0 = val;
                    thread_wait_mutex(voodoo->force_blit_mutex);
                    voodoo->can_blit = (voodoo->fbiInit0 & FBIINIT0_VGA_PASS) ? 1 : 0;
                    if (!voodoo->can_blit)
                        voodoo->force_blit_count = 0;
                    thread_release_mutex(voodoo->force_blit_mutex);

                    /* When VGA pass-through becomes active, mark all lines dirty to force full refresh */
                    if (!old_vga_pass && (val & FBIINIT0_VGA_PASS))
                        memset(voodoo->dirty_line, 1, sizeof(voodoo->dirty_line));

                    if (voodoo->set->nr_cards == 2)
                        svga_set_override(voodoo->svga, (voodoo->set->voodoos[0]->fbiInit0 | voodoo->set->voodoos[1]->fbiInit0) & 1);
                    else
                        svga_set_override(voodoo->svga, val & 1);
                    if (val & FBIINIT0_GRAPHICS_RESET) {
                        /*Reset display/draw buffer selection. This may not actually
                          happen here on a real Voodoo*/
                        voodoo->disp_buffer = 0;
                        voodoo->draw_buffer = 1;
                        voodoo->queued_disp_buffer = voodoo->disp_buffer;
                        voodoo->queued_draw_buffer = voodoo->draw_buffer;
                        voodoo->queued_lfbMode     = voodoo->lfbMode;
                        voodoo->queued_fbzMode     = voodoo->params.fbzMode;
                        voodoo_update_queued_buffers(voodoo);
                        voodoo_recalc(voodoo);
                        voodoo->front_offset = voodoo->params.front_offset;
                    }
                    svga_recalctimings(voodoo->svga);
                }
                break;
            case SST_fbiInit1:
                if (voodoo->initEnable & 0x01) {
                    if ((voodoo->fbiInit1 & FBIINIT1_VIDEO_RESET) && !(val & FBIINIT1_VIDEO_RESET)) {
                        voodoo->line = 0;
                        thread_wait_mutex(voodoo->swap_mutex);
                        voodoo->swap_count = 0;
                        thread_release_mutex(voodoo->swap_mutex);
                        voodoo->retrace_count = 0;
                    }
                    voodoo->fbiInit1   = (val & ~5) | (voodoo->fbiInit1 & 5);
                    voodoo->write_time = pci_nonburst_time + pci_burst_time * ((voodoo->fbiInit1 & 2) ? 1 : 0);
                    voodoo->burst_time = pci_burst_time * ((voodoo->fbiInit1 & 2) ? 2 : 1);
#if 0
                    voodoo_log("fbiInit1 write %08x - write_time=%i burst_time=%i\n", val, voodoo->write_time, voodoo->burst_time);
#endif
                }
                break;
            case SST_fbiInit2:
                if (voodoo->initEnable & 0x01) {
                    voodoo->fbiInit2 = val;
                    voodoo_recalc(voodoo);
                }
                break;
            case SST_fbiInit3:
                if (voodoo->initEnable & 0x01)
                    voodoo->fbiInit3 = val;
                break;

            case SST_hSync:
                voodoo->hSync   = val;
                voodoo->h_total = (val & 0xffff) + (val >> 16);
                voodoo_pixelclock_update(voodoo);
                break;
            case SST_vSync:
                voodoo->vSync   = val;
                voodoo->v_total = (val & 0xffff) + (val >> 16);
                break;

            case SST_clutData:
                voodoo->clutData[(val >> 24) & 0x3f].b = val & 0xff;
                voodoo->clutData[(val >> 24) & 0x3f].g = (val >> 8) & 0xff;
                voodoo->clutData[(val >> 24) & 0x3f].r = (val >> 16) & 0xff;
                if (val & 0x20000000) {
                    voodoo->clutData[(val >> 24) & 0x3f].b = 255;
                    voodoo->clutData[(val >> 24) & 0x3f].g = 255;
                    voodoo->clutData[(val >> 24) & 0x3f].r = 255;
                }
                voodoo->clutData_dirty = 1;
                break;

            case SST_dacData:
                voodoo->dac_reg      = (val >> 8) & 7;
                voodoo->dac_readdata = 0xff;
                if (val & 0x800) {
                    //                        voodoo_log("  dacData read %i %02X\n", voodoo->dac_reg, voodoo->dac_data[7]);
                    if (voodoo->dac_reg == 5) {
                        switch (voodoo->dac_data[7]) {
                            case 0x01:
                                voodoo->dac_readdata = 0x55;
                                break;
                            case 0x07:
                                voodoo->dac_readdata = 0x71;
                                break;
                            case 0x0b:
                                voodoo->dac_readdata = 0x79;
                                break;

                            default:
                                break;
                        }
                    } else
                        voodoo->dac_readdata = voodoo->dac_data[voodoo->dac_readdata & 7];
                } else {
                    if (voodoo->dac_reg == 5) {
                        if (!voodoo->dac_reg_ff)
                            voodoo->dac_pll_regs[voodoo->dac_data[4] & 0xf] = (voodoo->dac_pll_regs[voodoo->dac_data[4] & 0xf] & 0xff00) | val;
                        else
                            voodoo->dac_pll_regs[voodoo->dac_data[4] & 0xf] = (voodoo->dac_pll_regs[voodoo->dac_data[4] & 0xf] & 0xff) | (val << 8);
#if 0
                        voodoo_log("Write PLL reg %x %04x\n", voodoo->dac_data[4] & 0xf, voodoo->dac_pll_regs[voodoo->dac_data[4] & 0xf]);
#endif
                        voodoo->dac_reg_ff = !voodoo->dac_reg_ff;
                        if (!voodoo->dac_reg_ff)
                            voodoo->dac_data[4]++;

                    } else {
                        voodoo->dac_data[voodoo->dac_reg] = val & 0xff;
                        voodoo->dac_reg_ff                = 0;
                    }
                    voodoo_pixelclock_update(voodoo);
                }
                break;

            case SST_scrFilter:
                if (voodoo->initEnable & 0x01) {
                    voodoo->scrfilterEnabled   = 1;
                    voodoo->scrfilterThreshold = val; /* update the threshold values and generate a new lookup table if necessary */

                    if (val < 1)
                        voodoo->scrfilterEnabled = 0;
                    voodoo_threshold_check(voodoo);
                    voodoo_log("Voodoo Filter: %06x\n", val);
                }
                break;

            case SST_fbiInit5:
                if (voodoo->initEnable & 0x01)
                    voodoo->fbiInit5 = (val & ~0x41e6) | (voodoo->fbiInit5 & 0x41e6);
                break;
            case SST_fbiInit6:
                if (voodoo->initEnable & 0x01)
                    voodoo->fbiInit6 = val;
                break;
            case SST_fbiInit7:
                if (voodoo->initEnable & 0x01) {
                    voodoo->fbiInit7        = val;
                    voodoo->cmdfifo_enabled = val & 0x100;
                }
                break;

            case SST_cmdFifoBaseAddr:
                voodoo->cmdfifo_base = (val & 0x3ff) << 12;
                voodoo->cmdfifo_end  = ((val >> 16) & 0x3ff) << 12;
#if 0
                voodoo_log("CMDFIFO base=%08x end=%08x\n", voodoo->cmdfifo_base, voodoo->cmdfifo_end);
#endif
                break;

            case SST_cmdFifoRdPtr:
                voodoo->cmdfifo_rp = val;
                break;
            case SST_cmdFifoAMin:
                voodoo->cmdfifo_amin = val;
                break;
            case SST_cmdFifoAMax:
                voodoo->cmdfifo_amax = val;
                break;
            case SST_cmdFifoDepth:
                voodoo->cmdfifo_depth_rd = 0;
                voodoo->cmdfifo_depth_wr = val & 0xffff;
                break;

            default:
                if (voodoo->fbiInit7 & FBIINIT7_CMDFIFO_ENABLE) {
                    voodoo_log("Unknown register write in CMDFIFO mode %08x %08x\n", addr, val);
                } else {
                    voodoo_queue_command(voodoo, addr | FIFO_WRITEL_REG, val);
                }
                break;
        }
}

static uint16_t
voodoo_snoop_readw(uint32_t addr, void *priv)
{
    const voodoo_set_t *set = (voodoo_set_t *) priv;

    return voodoo_readw(addr, set->voodoos[0]);
}
static uint32_t
voodoo_snoop_readl(uint32_t addr, void *priv)
{
    const voodoo_set_t *set = (voodoo_set_t *) priv;

    return voodoo_readl(addr, set->voodoos[0]);
}

static void
voodoo_snoop_writew(uint32_t addr, uint16_t val, void *priv)
{
    const voodoo_set_t *set = (voodoo_set_t *) priv;

    voodoo_writew(addr, val, set->voodoos[0]);
    voodoo_writew(addr, val, set->voodoos[1]);
}
static void
voodoo_snoop_writel(uint32_t addr, uint32_t val, void *priv)
{
    const voodoo_set_t *set = (voodoo_set_t *) priv;

    voodoo_writel(addr, val, set->voodoos[0]);
    voodoo_writel(addr, val, set->voodoos[1]);
}

static void
voodoo_recalcmapping(voodoo_set_t *set)
{
    if (set->nr_cards == 2) {
        if (set->voodoos[0]->pci_enable && set->voodoos[0]->memBaseAddr) {
            if (set->voodoos[0]->type == VOODOO_2 && set->voodoos[1]->initEnable & (1 << 23)) {
                voodoo_log("voodoo_recalcmapping (pri) with snoop : memBaseAddr %08X\n", set->voodoos[0]->memBaseAddr);
                mem_mapping_disable(&set->voodoos[0]->mapping);
                mem_mapping_set_addr(&set->snoop_mapping, set->voodoos[0]->memBaseAddr, 0x01000000);
            } else if (set->voodoos[1]->pci_enable && (set->voodoos[0]->memBaseAddr == set->voodoos[1]->memBaseAddr)) {
                voodoo_log("voodoo_recalcmapping (pri) (sec) same addr : memBaseAddr %08X\n", set->voodoos[0]->memBaseAddr);
                mem_mapping_disable(&set->voodoos[0]->mapping);
                mem_mapping_disable(&set->voodoos[1]->mapping);
                mem_mapping_set_addr(&set->snoop_mapping, set->voodoos[0]->memBaseAddr, 0x01000000);
                return;
            } else {
                voodoo_log("voodoo_recalcmapping (pri) : memBaseAddr %08X\n", set->voodoos[0]->memBaseAddr);
                mem_mapping_disable(&set->snoop_mapping);
                mem_mapping_set_addr(&set->voodoos[0]->mapping, set->voodoos[0]->memBaseAddr, 0x01000000);
            }
        } else {
            voodoo_log("voodoo_recalcmapping (pri) : disabled\n");
            mem_mapping_disable(&set->voodoos[0]->mapping);
        }

        if (set->voodoos[1]->pci_enable && set->voodoos[1]->memBaseAddr) {
            voodoo_log("voodoo_recalcmapping (sec) : memBaseAddr %08X\n", set->voodoos[1]->memBaseAddr);
            mem_mapping_set_addr(&set->voodoos[1]->mapping, set->voodoos[1]->memBaseAddr, 0x01000000);
        } else {
            voodoo_log("voodoo_recalcmapping (sec) : disabled\n");
            mem_mapping_disable(&set->voodoos[1]->mapping);
        }
    } else {
        voodoo_t *voodoo = set->voodoos[0];

        if (voodoo->pci_enable && voodoo->memBaseAddr) {
            voodoo_log("voodoo_recalcmapping : memBaseAddr %08X\n", voodoo->memBaseAddr);
            mem_mapping_set_addr(&voodoo->mapping, voodoo->memBaseAddr, 0x01000000);
        } else {
            voodoo_log("voodoo_recalcmapping : disabled\n");
            mem_mapping_disable(&voodoo->mapping);
        }
    }
}

uint8_t
voodoo_pci_read(int func, int addr, UNUSED(int len), void *priv)
{
    const voodoo_t *voodoo = (voodoo_t *) priv;

    if (func)
        return 0;

#if 0
    voodoo_log("Voodoo PCI read %08X PC=%08x\n", addr, cpu_state.pc);
#endif

    switch (addr) {
        case 0x00:
            return 0x1a; /*3Dfx*/
        case 0x01:
            return 0x12;

        case 0x02:
            if (voodoo->type == VOODOO_2)
                return 0x02; /*Voodoo 2*/
            else
                return 0x01; /*SST-1 (Voodoo Graphics)*/
        case 0x03:
            return 0x00;

        case 0x04:
            return voodoo->pci_enable ? 0x02 : 0x00; /*Respond to memory accesses*/

        case 0x08:
            return 2; /*Revision ID*/
        case 0x09:
            return 0; /*Programming interface*/
        case 0x0a:
            return 0;
        case 0x0b:
            return 0x04;

        case 0x10:
            return 0x00; /*memBaseAddr*/
        case 0x11:
            return 0x00;
        case 0x12:
            return 0x00;
        case 0x13:
            return voodoo->memBaseAddr >> 24;

        case 0x40:
            return voodoo->initEnable & 0xff;
        case 0x41:
            if (voodoo->type == VOODOO_2)
                return 0x50 | ((voodoo->initEnable >> 8) & 0x0f);
            return (voodoo->initEnable >> 8) & 0x0f;
        case 0x42:
            return (voodoo->initEnable >> 16) & 0xff;
        case 0x43:
            return (voodoo->initEnable >> 24) & 0xff;

        default:
            break;
    }
    return 0;
}

void
voodoo_pci_write(int func, int addr, UNUSED(int len), uint8_t val, void *priv)
{
    voodoo_t *voodoo = (voodoo_t *) priv;

    if (func)
        return;

#if 0
    voodoo_log("Voodoo PCI write %04X %02X PC=%08x\n", addr, val, cpu_state.pc);
#endif

    switch (addr) {
        case 0x04:
            voodoo->pci_enable = val & 2;
            voodoo_recalcmapping(voodoo->set);
            break;

        case 0x13:
            voodoo->memBaseAddr = val << 24;
            voodoo_recalcmapping(voodoo->set);
            break;

        case 0x40:
            voodoo->initEnable = (voodoo->initEnable & ~0x000000ff) | val;
            break;
        case 0x41:
            voodoo->initEnable = (voodoo->initEnable & ~0x0000ff00) | (val << 8);
            break;
        case 0x42:
            voodoo->initEnable = (voodoo->initEnable & ~0x00ff0000) | (val << 16);
            voodoo_recalcmapping(voodoo->set);
            break;
        case 0x43:
            voodoo->initEnable = (voodoo->initEnable & ~0xff000000) | (val << 24);
            voodoo_recalcmapping(voodoo->set);
            break;

        default:
            break;
    }
}

static void
voodoo_speed_changed(void *priv)
{
    const voodoo_set_t *voodoo_set = (voodoo_set_t *) priv;

    voodoo_pixelclock_update(voodoo_set->voodoos[0]);
    voodoo_set->voodoos[0]->read_time  = pci_nonburst_time + pci_burst_time * ((voodoo_set->voodoos[0]->fbiInit4 & 1) ? 2 : 1);
    voodoo_set->voodoos[0]->write_time = pci_nonburst_time + pci_burst_time * ((voodoo_set->voodoos[0]->fbiInit1 & 2) ? 1 : 0);
    voodoo_set->voodoos[0]->burst_time = pci_burst_time * ((voodoo_set->voodoos[0]->fbiInit1 & 2) ? 2 : 1);
    if (voodoo_set->nr_cards == 2) {
        voodoo_pixelclock_update(voodoo_set->voodoos[1]);
        voodoo_set->voodoos[1]->read_time  = pci_nonburst_time + pci_burst_time * ((voodoo_set->voodoos[1]->fbiInit4 & 1) ? 2 : 1);
        voodoo_set->voodoos[1]->write_time = pci_nonburst_time + pci_burst_time * ((voodoo_set->voodoos[1]->fbiInit1 & 2) ? 1 : 0);
        voodoo_set->voodoos[1]->burst_time = pci_burst_time * ((voodoo_set->voodoos[1]->fbiInit1 & 2) ? 2 : 1);
    }
#if 0
    voodoo_log("Voodoo read_time=%i write_time=%i burst_time=%i %08x %08x\n", voodoo->read_time, voodoo->write_time, voodoo->burst_time, voodoo->fbiInit1, voodoo->fbiInit4);
#endif
}

static void
voodoo_force_blit(void *priv)
{
    const voodoo_set_t *voodoo_set = (voodoo_set_t *) priv;

    thread_wait_mutex(voodoo_set->voodoos[0]->force_blit_mutex);
    if (voodoo_set->voodoos[0]->can_blit) {
        voodoo_set->voodoos[0]->force_blit_count++;
    }
    thread_release_mutex(voodoo_set->voodoos[0]->force_blit_mutex);
    if (voodoo_set->nr_cards == 2) {
        thread_wait_mutex(voodoo_set->voodoos[1]->force_blit_mutex);
        if (voodoo_set->voodoos[1]->can_blit) {
            voodoo_set->voodoos[1]->force_blit_count++;
        }
        thread_release_mutex(voodoo_set->voodoos[1]->force_blit_mutex);
    }
}

void *
voodoo_card_init(void)
{
    int       c;
    voodoo_t *voodoo = calloc(1, sizeof(voodoo_t));

    voodoo_init_relax_settings(voodoo);
    voodoo->bilinear_enabled  = device_get_config_int("bilinear");
    voodoo->dithersub_enabled = device_get_config_int("dithersub");
    voodoo->scrfilter         = device_get_config_int("dacfilter");
    voodoo->texture_size      = device_get_config_int("texture_memory");
    voodoo->texture_mask      = (voodoo->texture_size << 20) - 1;
    voodoo->fb_size           = device_get_config_int("framebuffer_memory");
    voodoo->fb_mask           = (voodoo->fb_size << 20) - 1;
    voodoo->render_threads    = device_get_config_int("render_threads");
    voodoo->odd_even_mask     = voodoo->render_threads - 1;
#ifndef NO_CODEGEN
    voodoo->use_recompiler = device_get_config_int("recompiler");
#endif
    voodoo->type = device_get_config_int("type");
    switch (voodoo->type) {
        case VOODOO_1:
            voodoo->dual_tmus = 0;
            break;
        case VOODOO_SB50:
            voodoo->dual_tmus = 1;
            break;
        case VOODOO_2:
            voodoo->dual_tmus = 1;
            break;

        default:
            break;
    }

    if (voodoo->type == VOODOO_2) /*generate filter lookup tables*/
        voodoo_generate_filter_v2(voodoo);
    else
        voodoo_generate_filter_v1(voodoo);

    pci_add_card(PCI_ADD_NORMAL, voodoo_pci_read, voodoo_pci_write, voodoo, &voodoo->pci_slot);

    mem_mapping_add(&voodoo->mapping, 0, 0, NULL, voodoo_readw, voodoo_readl, NULL, voodoo_writew, voodoo_writel, NULL, MEM_MAPPING_EXTERNAL, voodoo);

    voodoo->fb_mem     = calloc(1, 4 * 1024 * 1024);
    voodoo->tex_mem[0] = calloc(1, voodoo->texture_size * 1024 * 1024);
    if (voodoo->dual_tmus)
        voodoo->tex_mem[1] = calloc(1, voodoo->texture_size * 1024 * 1024);
    voodoo->tex_mem_w[0] = (uint16_t *) voodoo->tex_mem[0];
    voodoo->tex_mem_w[1] = (uint16_t *) voodoo->tex_mem[1];

    for (c = 0; c < TEX_CACHE_MAX; c++) {
        voodoo->texture_cache[0][c].data     = calloc(1, (256 * 256 + 256 * 256 + 128 * 128 + 64 * 64 + 32 * 32 + 16 * 16 + 8 * 8 + 4 * 4 + 2 * 2) * 4);
        voodoo->texture_cache[0][c].base     = -1; /*invalid*/
        voodoo->texture_cache[0][c].refcount = 0;
        if (voodoo->dual_tmus) {
            voodoo->texture_cache[1][c].data     = calloc(1, (256 * 256 + 256 * 256 + 128 * 128 + 64 * 64 + 32 * 32 + 16 * 16 + 8 * 8 + 4 * 4 + 2 * 2) * 4);
            voodoo->texture_cache[1][c].base     = -1; /*invalid*/
            voodoo->texture_cache[1][c].refcount = 0;
        }
    }

    timer_add(&voodoo->timer, voodoo_callback, voodoo, 1);

    voodoo->svga     = svga_get_pri();
    voodoo->fbiInit0 = 0;

    voodoo->wake_fifo_thread         = thread_create_event();
    voodoo->wake_render_thread[0]    = thread_create_event();
    voodoo->wake_render_thread[1]    = thread_create_event();
    voodoo->wake_render_thread[2]    = thread_create_event();
    voodoo->wake_render_thread[3]    = thread_create_event();
    voodoo->wake_main_thread         = thread_create_event();
    voodoo->fifo_not_full_event      = thread_create_event();
    voodoo->fifo_empty_event         = thread_create_event();
    thread_set_event(voodoo->fifo_empty_event);
    ATOMIC_STORE(voodoo->fifo_empty_signaled, 1);
    voodoo->render_not_full_event[0] = thread_create_event();
    voodoo->render_not_full_event[1] = thread_create_event();
    voodoo->render_not_full_event[2] = thread_create_event();
    voodoo->render_not_full_event[3] = thread_create_event();
    voodoo->fifo_thread_run          = 1;
    voodoo->fifo_thread              = thread_create(voodoo_fifo_thread, voodoo);
    voodoo->render_thread_run[0]     = 1;
    voodoo->render_thread[0]         = thread_create(voodoo_render_thread_1, voodoo);
    if (voodoo->render_threads >= 2) {
        voodoo->render_thread_run[1] = 1;
        voodoo->render_thread[1]     = thread_create(voodoo_render_thread_2, voodoo);
    }
    if (voodoo->render_threads == 4) {
        voodoo->render_thread_run[2] = 1;
        voodoo->render_thread[2]     = thread_create(voodoo_render_thread_3, voodoo);
        voodoo->render_thread_run[3] = 1;
        voodoo->render_thread[3]     = thread_create(voodoo_render_thread_4, voodoo);
    }
    voodoo->swap_mutex = thread_create_mutex();
    timer_add(&voodoo->wake_timer, voodoo_wake_timer, (void *) voodoo, 0);

    for (c = 0; c < 0x100; c++) {
        rgb332[c].r = c & 0xe0;
        rgb332[c].g = (c << 3) & 0xe0;
        rgb332[c].b = (c << 6) & 0xc0;
        rgb332[c].r = rgb332[c].r | (rgb332[c].r >> 3) | (rgb332[c].r >> 6);
        rgb332[c].g = rgb332[c].g | (rgb332[c].g >> 3) | (rgb332[c].g >> 6);
        rgb332[c].b = rgb332[c].b | (rgb332[c].b >> 2);
        rgb332[c].b = rgb332[c].b | (rgb332[c].b >> 4);
        rgb332[c].a = 0xff;

        ai44[c].a = (c & 0xf0) | ((c & 0xf0) >> 4);
        ai44[c].r = (c & 0x0f) | ((c & 0x0f) << 4);
        ai44[c].g = ai44[c].b = ai44[c].r;
    }

    for (c = 0; c < 0x10000; c++) {
        rgb565[c].r = (c >> 8) & 0xf8;
        rgb565[c].g = (c >> 3) & 0xfc;
        rgb565[c].b = (c << 3) & 0xf8;
        rgb565[c].r |= (rgb565[c].r >> 5);
        rgb565[c].g |= (rgb565[c].g >> 6);
        rgb565[c].b |= (rgb565[c].b >> 5);
        rgb565[c].a = 0xff;

        argb1555[c].r = (c >> 7) & 0xf8;
        argb1555[c].g = (c >> 2) & 0xf8;
        argb1555[c].b = (c << 3) & 0xf8;
        argb1555[c].r |= (argb1555[c].r >> 5);
        argb1555[c].g |= (argb1555[c].g >> 5);
        argb1555[c].b |= (argb1555[c].b >> 5);
        argb1555[c].a = (c & 0x8000) ? 0xff : 0;

        argb4444[c].a = (c >> 8) & 0xf0;
        argb4444[c].r = (c >> 4) & 0xf0;
        argb4444[c].g = c & 0xf0;
        argb4444[c].b = (c << 4) & 0xf0;
        argb4444[c].a |= (argb4444[c].a >> 4);
        argb4444[c].r |= (argb4444[c].r >> 4);
        argb4444[c].g |= (argb4444[c].g >> 4);
        argb4444[c].b |= (argb4444[c].b >> 4);

        ai88[c].a = (c >> 8);
        ai88[c].r = c & 0xff;
        ai88[c].g = c & 0xff;
        ai88[c].b = c & 0xff;
    }
#ifndef NO_CODEGEN
    voodoo_codegen_init(voodoo);
#endif

    voodoo->disp_buffer = 0;
    voodoo->draw_buffer = 1;
    voodoo->queued_disp_buffer = voodoo->disp_buffer;
    voodoo->queued_draw_buffer = voodoo->draw_buffer;
    voodoo->queued_lfbMode     = voodoo->lfbMode;
    voodoo->queued_fbzMode     = voodoo->params.fbzMode;
    voodoo_update_queued_buffers(voodoo);

    voodoo->force_blit_count = 0;
    voodoo->can_blit         = 0;
    voodoo->force_blit_mutex = thread_create_mutex();

    return voodoo;
}

void *
voodoo_2d3d_card_init(int type)
{
    int       c;
    voodoo_t *voodoo = calloc(1, sizeof(voodoo_t));

    voodoo_init_relax_settings(voodoo);
    voodoo->bilinear_enabled  = device_get_config_int("bilinear");
    voodoo->dithersub_enabled = device_get_config_int("dithersub");
    voodoo->scrfilter         = device_get_config_int("dacfilter");
    voodoo->render_threads    = device_get_config_int("render_threads");
    voodoo->odd_even_mask     = voodoo->render_threads - 1;
#ifndef NO_CODEGEN
    voodoo->use_recompiler = device_get_config_int("recompiler");
#endif
    voodoo->type      = type;
    voodoo->dual_tmus = (type == VOODOO_3) ? 1 : 0;

    /*generate filter lookup tables*/
    voodoo_generate_filter_v2(voodoo);

    for (c = 0; c < TEX_CACHE_MAX; c++) {
        voodoo->texture_cache[0][c].data     = calloc(1, (256 * 256 + 256 * 256 + 128 * 128 + 64 * 64 + 32 * 32 + 16 * 16 + 8 * 8 + 4 * 4 + 2 * 2) * 4);
        voodoo->texture_cache[0][c].base     = -1; /*invalid*/
        voodoo->texture_cache[0][c].refcount = 0;
        if (voodoo->dual_tmus) {
            voodoo->texture_cache[1][c].data     = calloc(1, (256 * 256 + 256 * 256 + 128 * 128 + 64 * 64 + 32 * 32 + 16 * 16 + 8 * 8 + 4 * 4 + 2 * 2) * 4);
            voodoo->texture_cache[1][c].base     = -1; /*invalid*/
            voodoo->texture_cache[1][c].refcount = 0;
        }
    }

    timer_add(&voodoo->timer, voodoo_callback, voodoo, 1);

    voodoo->fbiInit0 = 0;

    voodoo->wake_fifo_thread         = thread_create_event();
    voodoo->wake_render_thread[0]    = thread_create_event();
    voodoo->wake_render_thread[1]    = thread_create_event();
    voodoo->wake_render_thread[2]    = thread_create_event();
    voodoo->wake_render_thread[3]    = thread_create_event();
    voodoo->wake_main_thread         = thread_create_event();
    voodoo->fifo_not_full_event      = thread_create_event();
    voodoo->fifo_empty_event         = thread_create_event();
    thread_set_event(voodoo->fifo_empty_event);
    ATOMIC_STORE(voodoo->fifo_empty_signaled, 1);
    voodoo->render_not_full_event[0] = thread_create_event();
    voodoo->render_not_full_event[1] = thread_create_event();
    voodoo->render_not_full_event[2] = thread_create_event();
    voodoo->render_not_full_event[3] = thread_create_event();
    voodoo->fifo_thread_run          = 1;
    voodoo->fifo_thread              = thread_create(voodoo_fifo_thread, voodoo);
    voodoo->render_thread_run[0]     = 1;
    voodoo->render_thread[0]         = thread_create(voodoo_render_thread_1, voodoo);
    if (voodoo->render_threads >= 2) {
        voodoo->render_thread_run[1] = 1;
        voodoo->render_thread[1]     = thread_create(voodoo_render_thread_2, voodoo);
    }
    if (voodoo->render_threads == 4) {
        voodoo->render_thread_run[2] = 1;
        voodoo->render_thread[2]     = thread_create(voodoo_render_thread_3, voodoo);
        voodoo->render_thread_run[3] = 1;
        voodoo->render_thread[3]     = thread_create(voodoo_render_thread_4, voodoo);
    }
    voodoo->swap_mutex = thread_create_mutex();
    timer_add(&voodoo->wake_timer, voodoo_wake_timer, (void *) voodoo, 0);

    for (c = 0; c < 0x100; c++) {
        rgb332[c].r = c & 0xe0;
        rgb332[c].g = (c << 3) & 0xe0;
        rgb332[c].b = (c << 6) & 0xc0;
        rgb332[c].r = rgb332[c].r | (rgb332[c].r >> 3) | (rgb332[c].r >> 6);
        rgb332[c].g = rgb332[c].g | (rgb332[c].g >> 3) | (rgb332[c].g >> 6);
        rgb332[c].b = rgb332[c].b | (rgb332[c].b >> 2);
        rgb332[c].b = rgb332[c].b | (rgb332[c].b >> 4);
        rgb332[c].a = 0xff;

        ai44[c].a = (c & 0xf0) | ((c & 0xf0) >> 4);
        ai44[c].r = (c & 0x0f) | ((c & 0x0f) << 4);
        ai44[c].g = ai44[c].b = ai44[c].r;
    }

    for (c = 0; c < 0x10000; c++) {
        rgb565[c].r = (c >> 8) & 0xf8;
        rgb565[c].g = (c >> 3) & 0xfc;
        rgb565[c].b = (c << 3) & 0xf8;
        rgb565[c].r |= (rgb565[c].r >> 5);
        rgb565[c].g |= (rgb565[c].g >> 6);
        rgb565[c].b |= (rgb565[c].b >> 5);
        rgb565[c].a = 0xff;

        argb1555[c].r = (c >> 7) & 0xf8;
        argb1555[c].g = (c >> 2) & 0xf8;
        argb1555[c].b = (c << 3) & 0xf8;
        argb1555[c].r |= (argb1555[c].r >> 5);
        argb1555[c].g |= (argb1555[c].g >> 5);
        argb1555[c].b |= (argb1555[c].b >> 5);
        argb1555[c].a = (c & 0x8000) ? 0xff : 0;

        argb4444[c].a = (c >> 8) & 0xf0;
        argb4444[c].r = (c >> 4) & 0xf0;
        argb4444[c].g = c & 0xf0;
        argb4444[c].b = (c << 4) & 0xf0;
        argb4444[c].a |= (argb4444[c].a >> 4);
        argb4444[c].r |= (argb4444[c].r >> 4);
        argb4444[c].g |= (argb4444[c].g >> 4);
        argb4444[c].b |= (argb4444[c].b >> 4);

        ai88[c].a = (c >> 8);
        ai88[c].r = c & 0xff;
        ai88[c].g = c & 0xff;
        ai88[c].b = c & 0xff;
    }
#ifndef NO_CODEGEN
    voodoo_codegen_init(voodoo);
#endif

    voodoo->disp_buffer = 0;
    voodoo->draw_buffer = 1;
    voodoo->queued_disp_buffer = voodoo->disp_buffer;
    voodoo->queued_draw_buffer = voodoo->draw_buffer;
    voodoo->queued_lfbMode     = voodoo->lfbMode;
    voodoo->queued_fbzMode     = voodoo->params.fbzMode;
    voodoo_update_queued_buffers(voodoo);

    voodoo->force_blit_count = 0;
    voodoo->can_blit         = 0;
    voodoo->force_blit_mutex = thread_create_mutex();

    return voodoo;
}

void *
voodoo_init(UNUSED(const device_t *info))
{
    voodoo_set_t *voodoo_set = calloc(1, sizeof(voodoo_set_t));
    uint32_t      tmuConfig  = 1;
    int           type;

    type = device_get_config_int("type");

    voodoo_set->nr_cards        = device_get_config_int("sli") ? 2 : 1;
    voodoo_set->voodoos[0]      = voodoo_card_init();
    voodoo_set->voodoos[0]->set = voodoo_set;
    if (voodoo_set->nr_cards == 2) {
        voodoo_set->voodoos[1] = voodoo_card_init();

        voodoo_set->voodoos[1]->set = voodoo_set;

        if (type == VOODOO_2) {
            voodoo_set->voodoos[0]->fbiInit5 |= FBIINIT5_MULTI_CVG;
            voodoo_set->voodoos[1]->fbiInit5 |= FBIINIT5_MULTI_CVG;
        } else {
            voodoo_set->voodoos[0]->fbiInit1 |= FBIINIT1_MULTI_SST;
            voodoo_set->voodoos[1]->fbiInit1 |= FBIINIT1_MULTI_SST;
        }
    }

    switch (type) {
        case VOODOO_1:
            if (voodoo_set->nr_cards == 2)
                tmuConfig = 1 | (3 << 3);
            else
                tmuConfig = 1;
            break;
        case VOODOO_SB50:
            if (voodoo_set->nr_cards == 2)
                tmuConfig = 1 | (3 << 3) | (3 << 6) | (2 << 9);
            else
                tmuConfig = 1 | (3 << 6);
            break;
        case VOODOO_2:
            tmuConfig = 1 | (3 << 6);
            break;

        default:
            break;
    }

    voodoo_set->voodoos[0]->tmuConfig = tmuConfig;
    if (voodoo_set->nr_cards == 2)
        voodoo_set->voodoos[1]->tmuConfig = tmuConfig;

    mem_mapping_add(&voodoo_set->snoop_mapping, 0, 0, NULL, voodoo_snoop_readw, voodoo_snoop_readl, NULL, voodoo_snoop_writew, voodoo_snoop_writel, NULL, MEM_MAPPING_EXTERNAL, voodoo_set);

    return voodoo_set;
}

void
voodoo_card_close(voodoo_t *voodoo)
{
    voodoo->fifo_thread_run = 0;
    thread_set_event(voodoo->wake_fifo_thread);
    thread_wait(voodoo->fifo_thread);
    voodoo->render_thread_run[0] = 0;
    thread_set_event(voodoo->wake_render_thread[0]);
    thread_wait(voodoo->render_thread[0]);
    if (voodoo->render_threads >= 2) {
        voodoo->render_thread_run[1] = 0;
        thread_set_event(voodoo->wake_render_thread[1]);
        thread_wait(voodoo->render_thread[1]);
    }
    if (voodoo->render_threads == 4) {
        voodoo->render_thread_run[2] = 0;
        thread_set_event(voodoo->wake_render_thread[2]);
        thread_wait(voodoo->render_thread[2]);
        voodoo->render_thread_run[3] = 0;
        thread_set_event(voodoo->wake_render_thread[3]);
        thread_wait(voodoo->render_thread[3]);
    }
    thread_destroy_event(voodoo->fifo_not_full_event);
    thread_destroy_event(voodoo->fifo_empty_event);
    thread_destroy_event(voodoo->wake_main_thread);
    thread_destroy_event(voodoo->wake_fifo_thread);
    thread_destroy_event(voodoo->wake_render_thread[0]);
    thread_destroy_event(voodoo->wake_render_thread[1]);
    thread_destroy_event(voodoo->render_not_full_event[0]);
    thread_destroy_event(voodoo->render_not_full_event[1]);

    if (voodoo->wait_stats_enabled && voodoo->wait_stats_explicit) {
        pclog("Voodoo wait stats (type=%d): fifo_full waits=%" PRIu64 " ticks=%" PRIu64 " spins=%" PRIu64
              ", fifo_empty waits=%" PRIu64 " ticks=%" PRIu64 " spins=%" PRIu64
              ", render_wait waits=%" PRIu64 " ticks=%" PRIu64 " spins=%" PRIu64
              ", readl fb=%" PRIu64 " sync=%" PRIu64 " nosync=%" PRIu64 " relaxed=%" PRIu64 " relax=%d full=%d nocmdfifo=%d nodraw=%d nowrites=%d frontsync=%d"
              " sync_buf f=%" PRIu64 " b=%" PRIu64 " a=%" PRIu64
              " nosync_buf f=%" PRIu64 " b=%" PRIu64 " a=%" PRIu64
              " relaxed_buf f=%" PRIu64 " b=%" PRIu64 " a=%" PRIu64
              " reg=%" PRIu64 " tex=%" PRIu64 "\n",
              voodoo->type,
              voodoo->fifo_full_waits,
              voodoo->fifo_full_wait_ticks,
              voodoo->fifo_full_spin_checks,
              voodoo->fifo_empty_waits,
              voodoo->fifo_empty_wait_ticks,
              voodoo->fifo_empty_spin_checks,
              voodoo->render_waits,
              voodoo->render_wait_ticks,
              voodoo->render_wait_spin_checks,
              voodoo->readl_fb_count,
              voodoo->readl_fb_sync_count,
              voodoo->readl_fb_nosync_count,
              voodoo->readl_fb_relaxed_count,
              voodoo->lfb_relax_enabled,
              voodoo->lfb_relax_full,
              voodoo->lfb_relax_ignore_cmdfifo,
              voodoo->lfb_relax_ignore_draw,
              voodoo->lfb_relax_ignore_fb_writes,
              voodoo->lfb_relax_front_sync,
              voodoo->readl_fb_sync_buf[0],
              voodoo->readl_fb_sync_buf[1],
              voodoo->readl_fb_sync_buf[2],
              voodoo->readl_fb_nosync_buf[0],
              voodoo->readl_fb_nosync_buf[1],
              voodoo->readl_fb_nosync_buf[2],
              voodoo->readl_fb_relaxed_buf[0],
              voodoo->readl_fb_relaxed_buf[1],
              voodoo->readl_fb_relaxed_buf[2],
              voodoo->readl_reg_count,
              voodoo->readl_tex_count);
    }

    for (uint8_t c = 0; c < TEX_CACHE_MAX; c++) {
        if (voodoo->dual_tmus)
            free(voodoo->texture_cache[1][c].data);
        free(voodoo->texture_cache[0][c].data);
    }
#ifndef NO_CODEGEN
    voodoo_codegen_close(voodoo);
#endif
    if (voodoo->type < VOODOO_BANSHEE && voodoo->fb_mem) {
        free(voodoo->fb_mem);
        if (voodoo->dual_tmus)
            free(voodoo->tex_mem[1]);
        free(voodoo->tex_mem[0]);
    }

    thread_close_mutex(voodoo->force_blit_mutex);

    free(voodoo);
}

void
voodoo_close(void *priv)
{
    voodoo_set_t *voodoo_set = (voodoo_set_t *) priv;

    if (voodoo_set->nr_cards == 2)
        voodoo_card_close(voodoo_set->voodoos[1]);
    voodoo_card_close(voodoo_set->voodoos[0]);

    free(voodoo_set);
}

static const device_config_t voodoo_config[] = {
  // clang-format off
    {
        .name           = "type",
        .description    = "Voodoo type",
        .type           = CONFIG_SELECTION,
        .default_int    = 0,
        .file_filter    = NULL,
        .spinner        = { 0 },
        .selection = {
            { .description = "3Dfx Voodoo Graphics",              .value = VOODOO_1    },
            { .description = "Obsidian SB50 + Amethyst (2 TMUs)", .value = VOODOO_SB50 },
            { .description = "3Dfx Voodoo 2",                     .value = VOODOO_2    },
            { .description = ""                                                        }
        },
        .bios           = { { 0 } }
    },
    {
        .name           = "framebuffer_memory",
        .description    = "Framebuffer memory size",
        .type           = CONFIG_SELECTION,
        .default_string = NULL,
        .default_int    = 2,
        .file_filter    = NULL,
        .spinner        = { 0 },
        .selection      = {
            { .description = "2 MB", .value = 2 },
            { .description = "4 MB", .value = 4 },
            { .description = ""                 }
        },
        .bios           = { { 0 } }
    },
    {
        .name           = "texture_memory",
        .description    = "Texture memory size",
        .type           = CONFIG_SELECTION,
        .default_string = NULL,
        .default_int    = 2,
        .file_filter    = NULL,
        .spinner        = { 0 },
        .selection      = {
            { .description = "2 MB", .value = 2 },
            { .description = "4 MB", .value = 4 },
            { .description = ""                 }
        },
        .bios           = { { 0 } }
    },
    {
        .name           = "bilinear",
        .description    = "Bilinear filtering",
        .type           = CONFIG_BINARY,
        .default_string = NULL,
        .default_int    = 1,
        .file_filter    = NULL,
        .spinner        = { 0 },
        .selection      = { { 0 } },
        .bios           = { { 0 } }
    },
    {
        .name           = "dithersub",
        .description    = "Dither subtraction",
        .type           = CONFIG_BINARY,
        .default_string = NULL,
        .default_int    = 1,
        .file_filter    = NULL,
        .spinner        = { 0 },
        .selection      = { { 0 } },
        .bios           = { { 0 } }
    },
    {
        .name           = "dacfilter",
        .description    = "Screen Filter",
        .type           = CONFIG_BINARY,
        .default_string = NULL,
        .default_int    = 0,
        .file_filter    = NULL,
        .spinner        = { 0 },
        .selection      = { { 0 } },
        .bios           = { { 0 } }
    },
    {
        .name           = "render_threads",
        .description    = "Render threads",
        .type           = CONFIG_SELECTION,
        .default_string = NULL,
        .default_int    = 2,
        .file_filter    = NULL,
        .spinner        = { 0 },
        .selection      = {
            { .description = "1", .value = 1 },
            { .description = "2", .value = 2 },
            { .description = "4", .value = 4 },
            { .description = ""              }
        },
        .bios           = { { 0 } }
    },
    {
        .name           = "sli",
        .description    = "SLI",
        .type           = CONFIG_BINARY,
        .default_string = NULL,
        .default_int    = 0,
        .file_filter    = NULL,
        .spinner        = { 0 },
        .selection      = { { 0 } },
        .bios           = { { 0 } }
    },
#ifndef NO_CODEGEN
    {
        .name           = "recompiler",
        .description    = "Dynamic Recompiler",
        .type           = CONFIG_BINARY,
        .default_string = NULL,
        .default_int    = 1,
        .file_filter    = NULL,
        .spinner        = { 0 },
        .selection      = { { 0 } },
        .bios           = { { 0 } }
    },
#endif
    { .name = "", .description = "", .type = CONFIG_END }
  // clang-format on
};

const device_t voodoo_device = {
    .name          = "3Dfx Voodoo Graphics",
    .internal_name = "voodoo",
    .flags         = DEVICE_PCI,
    .local         = 0,
    .init          = voodoo_init,
    .close         = voodoo_close,
    .reset         = NULL,
    .available     = NULL,
    .speed_changed = voodoo_speed_changed,
    .force_redraw  = voodoo_force_blit,
    .config        = voodoo_config
};
