/* Copyright 1996 Acorn Computers Ltd
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/* ScrModes.c */

/*
 * ScreenModes module main code.
 */

#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <stdlib.h>
#include <stdbool.h>

#include "kernel.h"
#include "swis.h"
#include "Global/RISCOS.h"
#include "Global/GraphicsV.h"
#include "Global/Services.h"
#include "Global/VduExt.h"
#include "Global/Variables.h"
#include "Global/FileTypes.h"
#include "Global/OsBytes.h"
#include "Global/CMOS.h"
#include "Global/ModHand.h"
#include "Global/VIDCList.h"
#undef   Module_Title /* CMHG defines this too */
#include "Interface/HighFSI.h"
#include "Interface/ScrModes.h"

#include "ScrModesv.h"
#include "errors.h"
#include "monitors.h"
#include "modex.h"
#include "mdfsupport.h"
#include "edidsupport.h"

/* Switch to define whether live EDID read from the monitor is in use.
 * If the graphics driver supports it and MonitorType is EDID this
 * will cause the EDID to be reloaded if the graphics driver changes.
 * Should an MDF be loaded, this behaviour is cancelled (latching).
 */
static bool using_edid = false;

/* Pointer to (root block of) current monitor definition structure.
 * This becomes valid (non NULL) on successful completion of a
 * *loadmodefile command.  The memory space it consumes is thereafter
 * released on either (a) shutdown of the module or (b) successful
 * completion of another *loadmodefile command.
 */
MonitorDescriptionRef current_monitor = NULL; /* not defined to start with */

ModeSelectorRef preferred_mode = NULL; /* The preferred mode */

/* The sync type for the preferred mode
 * NB only service_monitorleadtranslation uses this value
 */
int preferred_sync_type = 0;

/* Keep a copy of the current monitor type at the time of the first
 * successful *LoadModeFile, to restore on exit.
 */
int old_monitortype = -1; /* -1 means we haven't loaded a file yet */

/* Bit mask of which control list items are supported by the current driver */
static uint32_t supported_controllist_items = 0;

static void check_supported_controllist_items(int driver)
{
    _kernel_swi_regs r;
    uint32_t items[ControlList_InvalidReason+1];
    r.r[0] = GVReadInfo_ControlListItems;
    r.r[1] = (int) items;
    r.r[2] = sizeof(items);
    r.r[4] = GraphicsV_ReadInfo | (driver<<24);
    r.r[9] = GraphicsV;
    _kernel_swi(OS_CallAVector, &r, &r);

    supported_controllist_items = 0;

    if (r.r[4] != 0)
    {
        return;
    }
    int len = (r.r[2] >= 0 ? sizeof(items) - r.r[2] : sizeof(items));
    len >>= 2;
    int idx = 0;
    while (idx < len)
    {
        if (items[idx] < 32)
        {
            supported_controllist_items |= 1u << items[idx];
        }
        idx++;
    }
}

/* Construct an error block from the given ScreenModes module-specific
 * error code and the list of up to 3 arguments.  To ensure easy
 * internationalisability, this is done using text from our Messages
 * file, via MessageTrans_Lookup.  Note that all errors from this
 * module cause the whole process to stop (i.e. there should only be
 * one error generated for any *loadmodefile command) so don't worry
 * about efficiency; e.g. we could keep the file open and the handle
 * lying around, but there really isn't any point.
 */
_kernel_oserror *messages_lookup(char *buffer, size_t space, const char *token,
                                 const char *arg0, const char *arg1, const char *arg2)
{
    /* Handle for MessageTrans. */
    int file_data[4];
    _kernel_oserror *res;

    /* Open the Messages file */
    res = _swix(MessageTrans_OpenFile, _INR(0,2),
                file_data, Module_MessagesFile, 0);
    if (res != NULL)
    {
        return res;
    }

    res = _swix(MessageTrans_Lookup, _INR(0,7),
                file_data, token, buffer, space, arg0, arg1, arg2, NULL);
    /* Always close the messages file, ignoring possible but most unlikely errors */
    (void)_swix(MessageTrans_CloseFile, _IN(0), file_data);
    if (res != NULL)
    {
        buffer[0] = '?'; buffer[1] = '\0'; /* lookup failed */
    }
    return res;
}

_kernel_oserror *error(int error, const char *arg0, const char *arg1, const char *arg2)
{
    /* Where the final returned message is constructed */
    static _kernel_oserror theerror;
    _kernel_oserror *res;
    char token[8];

    sprintf(token, "E%02d", error);
    res = messages_lookup(theerror.errmess, sizeof(theerror.errmess), token, arg0, arg1, arg2);
    if (res != NULL)
    {
        return res;
    }
    /* Construct the rest of the error block, i.e. the error number */
    theerror.errnum = ERROR_BASE + error;
    return &theerror;
}

_kernel_oserror *new_monitordescription(MonitorDescriptionRef *description)
{
    MonitorDescriptionRef md;

    md = (MonitorDescriptionRef) calloc(1, sizeof(MonitorDescription));
    if (md == NULL)
    {
        return error(ERR_NOSPACE, 0, 0, 0);
    }

    messages_lookup(md->name, sizeof(md->name), "NoName", NULL, NULL, NULL);

    md->output_format = -1; /* Output format */
    md->external_clock = -1; /* External clock not present */
    md->modelist = NULL; /* Empty initially */
    md->audio_formats = NULL; /* Empty initially */

    *description = md;

    return NULL;
}

void free_monitordescription(MonitorDescriptionRef description)
{
    ModeDescriptionRef md;
    md = description->modelist;
    while (md)
    {
        ModeDescriptionRef td = md;
        md = md->next;
        free(td);
    }
    if (description->audio_formats)
    {
        free(description->audio_formats);
    }
    free(description);
}

void release_currentmonitor(void)
{
    if (current_monitor)
    {
        IFDEBUG printf("releasing previous monitor description\n");
        free_monitordescription(current_monitor);
    }
}

void compute_modedescription(ModeDescriptionRef md)
{
    uint32_t vtot, htot;
    int pn;
    ModeDef *mp = &md->definition.timings; /* for terseness! */
    for (htot = 0, vtot = 0, pn = 0; pn < FR__COUNT; ++pn)
    {
        htot += mp->hpar[pn];
        vtot += mp->vpar[pn];
    }
    md->line_hz = mp->pixel_khz * 1000 / htot;  /* compute line frequency in Hz */
    md->frame_mhz = md->line_hz * 1000 / vtot; /* frame frequency in milliHz, high-prec */
    if (mp->interlaced)
    {
        md->frame_mhz /= 2; /* if interlaced, one frame is 2 fields */
    }
    md->frame_hz = (md->frame_mhz + 500) / 1000; /* & low-prec for user integer Hz matching */
}

/* For efficiency in handling the Service_ModeExtension service call,
 * we keep the list of supported modes in order.  The keys used for
 * comparison of modes are (in order of use):
 *   (1) increasing x resolution
 *   (2) increasing y resolution
 *   (3) decreasing frame rate
 *   (4) increasing peak datarate at a given depth, i.e. pixelrate
 */
static int modes_inorder(ModeDescriptionRef m1, ModeDescriptionRef m2)
{
    if (m1->definition.timings.xres < m2->definition.timings.xres)
    {
        return 1;
    }
    if (m1->definition.timings.xres > m2->definition.timings.xres)
    {
        return 0;
    }
    if (m1->definition.timings.yres < m2->definition.timings.yres)
    {
        return 1;
    }
    if (m1->definition.timings.yres > m2->definition.timings.yres)
    {
        return 0;
    }
    if (m1->frame_mhz > m2->frame_mhz)
    {
        return 1;
    }
    if (m1->frame_mhz < m2->frame_mhz)
    {
        return 0;
    }
    if (m1->definition.timings.pixel_khz < m2->definition.timings.pixel_khz)
    {
        return 1;
    }
    if (m1->definition.timings.pixel_khz > m2->definition.timings.pixel_khz)
    {
        return 0;
    }
    return 1;                           /* arbitrary here - modes seem the same! */
}

void sort_modelist(ModeDescriptionRef *list)
{
    ModeDescriptionRef prev, this, next;
    int swapped;
    do
    {
        prev = NULL;                    /* marks being at start of list */
        this = *list;                   /* not NULL */
        next = this->next;              /* might be NULL, for 1-entry list */
        swapped = 0;
        while (next)
        {
            if (!modes_inorder(this, next))
            {
                ModeDescriptionRef t;
                /* Move whatever is pointing at this to point at next */
                if (prev == NULL)
                {
                    *list = next;       /* swap at start of list */
                }
                else
                {
                    prev->next = next;
                }
                /* The new successor to this record is what was after the next one */
                this->next = next->next;
                /* The new successor to what was the next record is now this record */
                next->next = this;
                /* Swap our local this and next pointers */
                t = this;  this = next;  next = t;
                swapped = 1;
            }
            /* Step all the pointers on by one in the (perhaps re-ordered) list */
            prev = this;
            this = next;
            next = next->next;
        }
    } while (swapped);
}

_kernel_oserror *set_monitortype(int monitortype)
{
    _kernel_swi_regs regs;
    regs.r[0] = 3;
    regs.r[1] = monitortype;
    return _kernel_swi(OS_ScreenMode, &regs, &regs);
}

int read_monitortype(void)
{
    _kernel_swi_regs regs;
    _kernel_oserror *res;
    regs.r[0] = 1;
    res = _kernel_swi(OS_ReadSysInfo, &regs, &regs);
    if (res)
    {
        return -1;
    }
    else
    {
        return regs.r[1];
    }
}

_kernel_oserror *restore_monitortype(void)
{
    int temp = old_monitortype;
    if (temp != -1)
    {
        old_monitortype = -1;
        return set_monitortype(temp);
    }
    return NULL;
}

static _kernel_oserror *open_modefile(const char *filename, FILE **handle)
{
    _kernel_oserror *res;
    FILE *f = fopen(filename, "r");
    *handle = f;
    if (f != NULL)
    {
        res = NULL;                     /* all OK */
    }
    else
    {
        res = _kernel_last_oserror();
        if (res == NULL)
        {
            _kernel_swi_regs regs;
            /* Couldn't open file but only C library knows why: let's find out. */
            regs.r[0] = OSFind_OpenIn | open_nopath | open_nodir | open_mustopen;
            regs.r[1] = (int) filename;
            regs.r[2] = 0;
            res = _kernel_swi(OS_Find, &regs, &regs);
            if (res == NULL)
            {
                /* Hmmm, fopen failed but we *can* open it: give up!
                 * First re-close this file handle.
                 */
                int fh = regs.r[0];
                regs.r[0] = OSFind_Close;
                regs.r[1] = fh;
                (void) _kernel_swi(OS_Find, &regs, &regs);
                res = error(ERR_OPENFAIL, filename, 0, 0);
            }
        }
    }
    return res;
}

static _kernel_oserror *loadmodefile(const char *file)
{
  _kernel_oserror *res;
  FILE *handle;

  res = open_modefile(file, &handle);
  if (res != NULL)
  {
      IFDEBUG printf("failed to open modefile\n");
  }
  else
  {
      /* Now need to differentiate if text file or EDID file. */
      /* We do so by looking for the EDID header 0x00ffffffffffff00 */
      int is_edid = 1;
      int c;
      for (int i = 0; i<8;i++)
      {
          c = getc(handle);
          if (((i == 0) || (i == 7)) && (c != 0x00))
          {
              is_edid = 0;
          }
          if (((i > 0) && (i < 7)) && (c != 0xff))
          {
              is_edid = 0;
          }
      }

      if (is_edid == 1)
      {
          /* Close the file - we need to load the whole file to map it
           * onto the EDID struct.
           */
          fclose(handle);
          /* Now pass loadEDID the path */
          res = loadedid(file);
      }
      else
      {
          rewind(handle); /* Reset the pointer to the beginning */
          res = loadtextMDF(file, handle);
      }
      if (res == NULL)
      {
          /* Use of an MDF cancels (latching) the automatic EDID side effects */
          using_edid = false;
      }
  }
  return res;
}

static _kernel_oserror *savemodefile(const char *file)
{
    char  *fbuf;
    FILE  *f = NULL;
    size_t length;
    ModeDescriptionRef this;

    if (current_monitor == NULL)
    {
        return error(ERR_NOMODEFILE, 0, 0, 0);
    }

    /* open file for new mode file if required */
    /* file is a string terminated in 0x0D */
    length = strcspn(file, "\r");
    fbuf = malloc(length + 1);
    if (fbuf)
    {
        memcpy(fbuf, file, length);
        fbuf[length] = '\0';
        f = fopen(fbuf, "w");
        free(fbuf);
    }
    if (f == NULL)
    {
        return _kernel_last_oserror();
    }
    fprintf(f, "# Exported from ScrModes " Module_VersionString "\n"
               "file_format:1\n"
               "monitor_title:%s\n", current_monitor->name);
    if (current_monitor->dpms_state!=-1)
    {
        fprintf(f, "DPMS_state:%d\n\n", current_monitor->dpms_state);
    }
    this = current_monitor->modelist;
    while (this)
    {
        ModeDef *mp = &this->definition.timings;
        fprintf(f, "# Mode: %d x %d @ %dHz\n", mp->xres, mp->yres, this->frame_hz);
        fprintf(f, "startmode\n"
                   " mode_name:%s\n"
                   " x_res:%d\n"
                   " y_res:%d\n"
                   " pixel_rate:%d\n", this->definition.name, mp->xres, mp->yres, mp->pixel_khz);
        fprintf(f, " h_timings:%d,%d,%d,%d,%d,%d\n", mp->hpar[0], mp->hpar[1], mp->hpar[2],
                                                     mp->hpar[3], mp->hpar[4], mp->hpar[5]);
        fprintf(f, " v_timings:%d,%d,%d,%d,%d,%d\n", mp->vpar[0], mp->vpar[1], mp->vpar[2],
                                                     mp->vpar[3], mp->vpar[4], mp->vpar[5]);
        fprintf(f, " sync_pol:%d\n", mp->syncpol);
        if (mp->interlaced == 1)
        {
            fprintf(f, " interlaced\n");
        }
        fprintf(f,"endmode\n\n");
        this = this->next;
    }
    fclose(f);

    return NULL;
}

/* List of pixel formats supported by the kernel, in priority order for higher BPP search by service_modetranslation */
/* Also, for GraphicsV drivers that don't support GraphicsV_PixelFormats, the first few entries are expected to be the 'old formats' that they may support */
static const PixelFormat pixelformats[] = {
    {1,0,0},
    {3,0,1},
    {15,0,2},
    {255,ModeFlag_FullPalette,3},
    {65535,0,4},
    {-1,0,5},
    /* RISC OS 6 64K */
    {65535,  ModeFlag_64k,         4},
    /* Red/blue swapped */
    {65535,  ModeFlag_DataFormatSub_RGB,              4},
    {-1,     ModeFlag_DataFormatSub_RGB,              5},
    {65535,  ModeFlag_DataFormatSub_RGB+ModeFlag_64k, 4},
    /* 4K */
    {4095,   0,                                       4},
    {4095,   ModeFlag_DataFormatSub_RGB,              4},
    /* Alpha */
    {65535,  ModeFlag_DataFormatSub_Alpha,                                         4},
    {-1,     ModeFlag_DataFormatSub_Alpha,                                         5},
    {65535,  ModeFlag_DataFormatSub_Alpha+ModeFlag_64k,                            4},
    {65535,  ModeFlag_DataFormatSub_Alpha+ModeFlag_DataFormatSub_RGB,              4},
    {-1,     ModeFlag_DataFormatSub_Alpha+ModeFlag_DataFormatSub_RGB,              5},
    {65535,  ModeFlag_DataFormatSub_Alpha+ModeFlag_DataFormatSub_RGB+ModeFlag_64k, 4},
    {4095,   ModeFlag_DataFormatSub_Alpha,                                         4},
    {4095,   ModeFlag_DataFormatSub_Alpha+ModeFlag_DataFormatSub_RGB,              4},
};

#define OLDFORMATS pixelformats
#define NUM_OLDFORMATS 6

/* Convert an old pixel depth value to a PixelFormat value */
static void pixelformat_from_depth(PixelFormatRef pf,int depth)
{
    pf->log2bpp = depth;
    pf->ncolour = (1<<(1<<depth))-1;
    pf->modeflags = (depth==3?ModeFlag_FullPalette:0);
}

/* Can this PixelFormat be represented as a pixel depth? (if so, pf->log2bpp is the value) */
bool is_old_format(const PixelFormat *pf)
{
    if (pf->log2bpp > 5)
        return false;
    if (pf->ncolour != (1<<(1<<pf->log2bpp))-1)
        return false;
    if (pf->modeflags != (pf->log2bpp==3?ModeFlag_FullPalette:0))
        return false;
    return true;
}

/* Return current GraphicsV driver number */
static int current_graphicsv_driver(void)
{
    int variables[2];
    variables[0] = VduExt_CurrentGraphicsVDriver;
    variables[1] = -1;
    _swix(OS_ReadVduVariables, _INR(0,1), variables, variables);
    return variables[0];
}

/* Return mask of which pixel formats are supported by the driver (from pixelformats table) */
static int get_supported_pixel_formats(int driver)
{
    _kernel_swi_regs r;
    r.r[4] = GraphicsV_PixelFormats | (driver<<24);
    r.r[9] = GraphicsV;
    _kernel_swi(OS_CallAVector, &r, &r);
    if (r.r[4] != 0)
    {
        /* Old driver, just spam it with some basic formats */
        return (1<<NUM_OLDFORMATS)-1;
    }
    /* Scan the list to build up a flag word of supported formats */
    const PixelFormat *list = pixelformats;
    const PixelFormat *f = (const PixelFormat *) r.r[0];
    int bit = 1;
    int supported = 0;
    while (--r.r[1] >= 0)
    {
        if (f->modeflags & ModeFlag_DataFormatFamily_Mask)
        {
            continue;
        }
        /* Note that for each step of the outer loop, we retain the 'list' and
           'bit' values from the inner loop so that drivers which arrange
           their format lists in a similar order to ours will require fewer
           iterations */
        for (int i=0;i<sizeof(pixelformats)/sizeof(pixelformats[0]);i++)
        {
            bool match = false;
            if ((f->ncolour == list->ncolour) && (f->modeflags == list->modeflags) && (f->log2bpp == list->log2bpp))
            {
                supported |= bit;
                match = true;
            }
            if (bit == (1 << ((sizeof(pixelformats)/sizeof(pixelformats[0]))-1)))
            {
                bit = 1;
                list = pixelformats;
            }
            else
            {
                bit = bit<<1;
                list++;
            }
            if (match)
            {
                break;
            }
        }
        f++;
    }
    return supported;
}

/* Build a VIDC list
 * Returns the number of control list items
 */
static int build_a_vidclist(VIDCListRef vp, ModeDescriptionRef mp, const PixelFormat *pf)
{
    int fn;
    int ctrllistpos = 0;

    /* Fill in the fields */
    vp->format = 3;
    vp->depth = pf->log2bpp;
    for (fn = 0; fn < FR__COUNT; ++fn)
    {
        vp->hpar[fn] = mp->definition.timings.hpar[fn];
        vp->vpar[fn] = mp->definition.timings.vpar[fn];
    }
    vp->pixelrate = mp->definition.timings.pixel_khz;
    vp->syncpol = mp->definition.timings.syncpol;
    if (mp->definition.timings.interlaced)
    {
        vp->syncpol |= SyncPol_Interlace | SyncPol_InterlaceFields;
    }

    /* Only specify ModeFlags & NColour if necessary? */
    if (!is_old_format(pf))
    {
        vp->vcparam[ctrllistpos].index = ControlList_NColour;
        vp->vcparam[ctrllistpos].value = pf->ncolour;
        ctrllistpos++;
        vp->vcparam[ctrllistpos].index = ControlList_ModeFlags;
        vp->vcparam[ctrllistpos].value = pf->modeflags;
        ctrllistpos++;
    }

    if (current_monitor->dpms_state != -1)
    {
        vp->vcparam[ctrllistpos].index = ControlList_DPMSState;
        vp->vcparam[ctrllistpos].value = current_monitor->dpms_state;
        ctrllistpos++;
    }

    if (current_monitor->lcd_support != 0)
    {
        IFDEBUG printf("I'm an LCD panel & I'm going to tell the Kernel!\n");
        vp->vcparam[ctrllistpos].index = ControlList_LCDMode;
        vp->vcparam[ctrllistpos].value = LCDMODE_VALUE;
        ctrllistpos++;
        if (current_monitor->lcd_support != 3)
        {
            vp->vcparam[ctrllistpos].index = ControlList_LCDDualPanelMode;
            vp->vcparam[ctrllistpos].value = LCDDUAL_VALUE;
            ctrllistpos++;
        }
        vp->vcparam[ctrllistpos].index = ControlList_LCDOffset0;
        vp->vcparam[ctrllistpos].value = LCDOFF0_VALUE;
            ctrllistpos++;
        vp->vcparam[ctrllistpos].index = ControlList_LCDOffset1;
        vp->vcparam[ctrllistpos].value = LCDOFF1_VALUE;
            ctrllistpos++;
        vp->vcparam[ctrllistpos].index = ControlList_DACControl;
        vp->vcparam[ctrllistpos].value = DACCTRL_VALUE;
            ctrllistpos++;
    }

    if (mp->definition.timings.external_clock != -1)
    {
        vp->vcparam[ctrllistpos].index = ControlList_HClockSelect;
        vp->vcparam[ctrllistpos].value = mp->definition.timings.external_clock;
        ctrllistpos++;
    }

    if (current_monitor->output_format != -1)
    {
        vp->vcparam[ctrllistpos].index = ControlList_OutputFormat;
        vp->vcparam[ctrllistpos].value = current_monitor->output_format;
        ctrllistpos++;
    }

    vp->vcparam[ctrllistpos].index = -1;      /* List terminator */

    return ctrllistpos;
}

static uint32_t true_bpp(const PixelFormat *pf)
{
    /* Get true bpp value */
    uint32_t bpp = 1<<pf->log2bpp;
    if (pf->log2bpp == 6) /* 24bit packed formats */
        bpp = 24;
    else if (pf->log2bpp == 7) /* YUV formats - TODO! */
        return 0;
    return bpp;
}

static int default_extrabytes(ModeDescriptionRef mp, const PixelFormat *pf)
{
    uint32_t mx = mp->definition.timings.xres;
    uint32_t bpp = true_bpp(pf);
    uint32_t linelength;
    if (bpp < 8)
    {
        uint8_t shift = 3 - pf->log2bpp;
        uint32_t round = (1 << shift) - 1;
        linelength = (mx + round) >> shift;
    }
    else
    {
        uint8_t mult = bpp>>3;
        linelength = mx * mult;
    }

    /* The OS requires that framebuffer rows start on word boundaries;
     * if padding is needed for this, signal it to the driver via ExtraBytes,
     * so that drivers don't need to know of the OS's limitation (although
     * we do assume that the driver doesn't request that ExtraBytes be
     * modified to something inappropriate!)
     */
    return ((linelength+3) & ~3) - linelength;
}

/* Test for whether a given mode definition is usable within the
 * specified data rate and video memory size bounds, at a given pixel
 * depth.  Limitations of the current video controller chip (if known)
 * are also factored in to the test.
 * If the mode is valid, it optionally copies the VIDC list into the
 * provided buffer.
 */
static int mode_valid(ModeDescriptionRef mp, const PixelFormat *pf,
                       uint32_t maxdatarate /* kB/s */,
                       uint32_t maxdatasize /* bytes */,
                       int extrabytes,
                       VIDCListRef outlist)
{
    _kernel_swi_regs r;
    VIDCList         vetlist;
    uint32_t pixrate = mp->definition.timings.pixel_khz;
    uint32_t mx = mp->definition.timings.xres;
    uint32_t my = mp->definition.timings.yres;
    uint32_t pixels = mx * my;
    uint32_t datarate, datasize;

    uint32_t bpp = true_bpp(pf);
    if (!bpp)
    {
        return 0;
    }

    /* Convert from pixels to bytes: method varies according to depth */
    if ((bpp > 4) && (current_monitor->lcd_support == 1))
    {
        return 0;         /** Eek! 4bpp is max for b/w panels! **/
    }

    uint32_t linelength;
    if (bpp < 8)
    {
        uint8_t shift = 3 - pf->log2bpp;
        uint32_t round = (1 << shift) - 1;
        datarate = (pixrate + round) >> shift;
        linelength = (mx + round) >> shift;
    }
    else
    {
        uint8_t mult = bpp>>3;
        datarate = pixrate * mult;
        linelength = mx * mult;
    }
    datasize = (linelength + extrabytes) * my;

    /* Check global data rate/size limits */
    if (datarate > maxdatarate || datasize > maxdatasize)
    {
        return 0;
    }

    /* Apply video-controller-specific checks */
    int driver = current_graphicsv_driver();

    r.r[4] = GraphicsV_PixelFormats | (driver<<24);
    r.r[9] = GraphicsV;
    _kernel_swi(OS_CallAVector, &r, &r);
    if (r.r[4] == 0)
    {
        int i=r.r[1];
        PixelFormatRef formats = (PixelFormatRef) r.r[0];
        while (i)
        {
            if ((formats->ncolour == pf->ncolour)
              && (formats->modeflags == pf->modeflags)
              && (formats->log2bpp == pf->log2bpp))
                break;
            formats++;
            i--;
        }
        if (i == 0)
            return 0;    /* Had a definitive answer from GraphicsV that this bpp is forbidden */
    }
    else
    {
        /* Try the old DisplayFeatures call */
        if (!is_old_format(pf))
            return 0;    /* Not a format supported by DisplayFeatures */
        r.r[4] = GraphicsV_DisplayFeatures | (driver<<24);
        r.r[9] = GraphicsV;
        _kernel_swi(OS_CallAVector, &r, &r);
        if ((r.r[4] == 0) && ((r.r[1] & (1<<pf->log2bpp)) == 0))
        {
            return 0;    /* Had a definitive answer from GraphicsV that this bpp is forbidden */
        }
    }

    /* Propose the resulting mode to the graphics driver */
    int ctrllistpos = build_a_vidclist(&vetlist, mp, pf);

    if (extrabytes < 0)
    {
        extrabytes = 0;
    }
    if (extrabytes > 0)
    {
        if (!(supported_controllist_items & (1 << ControlList_ExtraBytes)))
        {
            return 0; /* ExtraBytes wanted by user but not supported by driver */
        }
        /* Check this won't push us over maxdatasize */
        if (extrabytes * my + datasize > maxdatasize)
        {
            return 0;
        }
        vetlist.vcparam[ctrllistpos].index = ControlList_ExtraBytes;
        vetlist.vcparam[ctrllistpos].value = extrabytes;
        ctrllistpos++;
        vetlist.vcparam[ctrllistpos].index = -1;      /* List terminator */
    }

    r.r[0] = (int)&vetlist;
    r.r[1] = 0; /* Flags */
    r.r[4] = GraphicsV_VetMode2 | (driver<<24);
    r.r[9] = GraphicsV;
    _kernel_swi(OS_CallAVector, &r, &r);
    if ((r.r[4] == 0) && !(r.r[0] & ~7)) /* Call claimed, and no unknown flags returned? */
    {
        if ((r.r[0] & GVVetMode2_ResultMask) == 0)
        {
            return 0; /* Had a definitive answer from GraphicsV that it no-likey */
        }
        else if (r.r[0] & GVVetMode2_ExtraBytes_Invalid)
        {
            /* Check to see if this ExtraBytes value will cause us to exceed
             * maxdatasize.
             */
            datasize += r.r[2] * my;
            if (datasize > maxdatasize)
            {
                return 0;
            }
            /* ExtraBytes value is acceptable, use it */
            if ((ctrllistpos > 0) && (vetlist.vcparam[ctrllistpos-1].index == ControlList_ExtraBytes))
            {
                ctrllistpos--;
            }
            vetlist.vcparam[ctrllistpos].index = ControlList_ExtraBytes;
            vetlist.vcparam[ctrllistpos].value = r.r[2];
            ctrllistpos++;
            vetlist.vcparam[ctrllistpos].index = -1;      /* List terminator */
        }
    }
    else
    {
        /* Try the old vet call */
        r.r[0] = (int)&vetlist;
        r.r[1] = NULL;
        r.r[4] = GraphicsV_VetMode | (driver<<24);
        _kernel_swi(OS_CallAVector, &r, &r);
        if ((r.r[4] == 0) && (r.r[0] != 0))
        {
            return 0; /* Had a definitive answer from GraphicsV that it no-likey */
        }
    }

    /* No obvious conflicts, accept the mode */
    if (outlist != NULL)
    {
        *outlist = vetlist;
    }
    return 1;
}

bool find_deepest_by_xyhz(PixelFormatRef fp, ModeDescriptionRef mp)
{
    _kernel_swi_regs r;
    const PixelFormat *pflist;
    size_t pflistlen;
    int    driver, maxlog2bpp = 0;
    bool   found = false;

    /* See if the driver is helpful in listing the pixel formats */
    driver = current_graphicsv_driver();
    r.r[4] = GraphicsV_PixelFormats | (driver<<24);
    r.r[9] = GraphicsV;
    _kernel_swi(OS_CallAVector, &r, &r);
    if (r.r[4] == 0)
    {
        pflist = (PixelFormatRef) r.r[0];
        pflistlen = r.r[1];
    }
    else
    {
        /* Use the old list */
        pflist = OLDFORMATS;
        pflistlen = NUM_OLDFORMATS;
    }
    IFDEBUG printf("Trying %d pixel formats\n", pflistlen);

    /* The format list isn't necessarily sorted by depth, so just try them
     * all to find the highest log2bpp.
     */
    while (pflistlen)
    {
        if (mode_valid(mp, pflist, INT32_MAX, INT32_MAX, default_extrabytes(mp, pflist), NULL))
        {
            /* That depth is achievable, keep it if bigger */
            if (pflist->log2bpp > maxlog2bpp)
            {
                maxlog2bpp = pflist->log2bpp;
                IFDEBUG printf("Deeper mode_valid at log2bpp = %d\n", maxlog2bpp);
                *fp = *pflist;
                found = true;
            }
        }
        pflist++;
        pflistlen--;
    }

    return found;
}

static ModeDescriptionRef find_by_xy(ModeDescriptionRef mp,
                                      uint32_t xres, uint32_t yres, int *count)
{
    while (mp && mp->definition.timings.xres < xres)
        mp = mp->next;
    while (mp && mp->definition.timings.xres == xres && mp->definition.timings.yres < yres)
        mp = mp->next;
    if (mp && mp->definition.timings.xres == xres && mp->definition.timings.yres == yres)
    {
        int entries;
        ModeDescriptionRef head = mp;
        entries = 0;
        do
        {
            ++entries;
            mp = mp->next;
        } while (mp && mp->definition.timings.xres == xres &&
                 mp->definition.timings.yres == yres);
        *count = entries;
        return head;
    }
    return NULL;
}

static int restrict_bandwidth(int os_limit)
{
    /* On a HAL based kernel the kernel doesn't know what the bandwidth limitations
     * for a given mode are (an LCD graphics controller might for example be able
     * to use local SRAM buffering for smallish modes, but swap to shared slow DRAM
     * for bigger modes, both of which have differing bandwidths). The kernel
     * therefore just guesses a value, which we choose to ignore here and leave any
     * vetting up to GraphicsV.
     */
    UNUSED(os_limit);

    return INT32_MAX;
}

static void service_modeextension(_kernel_swi_regs *regs)
{
    /* Static allocation of a single VIDCList, for return from
     * Service_ModeExtension in the case that we have a mode
     * satisfying the requirements.  The data in this block is
     * overwritten by the next Service_ModeExtension call which we
     * also satisfy.  NB. Since we currently have no mechanism whereby
     * additional fields can be specified (e.g. in the ModeInfo file)
     * to be returned in a video control parameters list at the end of
     * the main block, there is no need for this block to be variable
     * length.  If that were ever added, the space would need to be
     * claimed either by assuming some maximum possible VCP list size
     * and keeping it static, or allocating the right size
     * dynamically; the latter case would imply keeping a file-scope
     * static pointer so that the space can be released as required
     * when module_shutdown is called.
     *
     * TMD 02-Nov-93 - A video control parameters list is now appended
     * if the file includes the DPMS_state keyword. However since this
     * is only 1 pair of words, I have allocated it statically.
     * If no list is necessary, the 1st word holds -1.
     * If a list is necessary, the 1st word holds DPMS_INDEX, the 2nd
     * holds the dpms value, and the 3rd word holds -1.
     *
     * WT 19-Jan-95 - The parameters list is extended to cater for LCD
     * panels, both single and dual panel, as defined in the LCD_support
     * field of the mode definition file. So, now 6 words are statically
     * allocated.
     */
    static VIDCList thevidclist;

    ModeSelectorRef    sel;
    ModeDescriptionRef mp;
    uint32_t dataratelimit, datasizelimit;
    int nmodes;
    PixelFormat pf;

    /* Check for being passed a mode selector as opposed to a mode
     * number - we only handle the former.
     */
    if (!IS_MODE_SEL_PTR(regs->r[2]))
    {
        return;
    }

    /* Check for suitable monitor type being requested - we handle
     * don't-care case as well as explicit file-specified monitor
     * type, but don't touch other specific monitor class codes.
     */
    if (regs->r[3] != -1 && regs->r[3] != MONITOR_FILE)
    {
        return;                         /* pass service on */
    }

    /* Address the mode selector (R2 on entry has been found to be a
     * pointer), and check for known format (bit 0 set, bits 7..1
     * clear).
     */
    sel = MODESEL(regs->r[2]);
    if (sel->bit0 != 1 || sel->format != 0)
    {
        return;                         /* pass service on */
    }

    /* Examine the mode selector to deduce the pixel format */
    pixelformat_from_depth(&pf,sel->depth);
    ModeParam *param = sel->param;
    int orig_linelength = ((sel->xresol << sel->depth) + 7) >> 3;
    int linelength = orig_linelength;
    while (param->index != -1)
    {
        if (param->index == VduExt_ModeFlags)
            pf.modeflags = param->value;
        else if (param->index == VduExt_NColour)
            pf.ncolour = param->value;
        else if (param->index == VduExt_LineLength)
            linelength = param->value;
        param++;
    }

    /* BBC gap modes have Log2BPP 1, NColour 1 */
    if ((pf.modeflags & ModeFlag_BBCGapMode) && (pf.log2bpp == 1) && (pf.ncolour == 1))
    {
        pf.ncolour = 3;
    }

    /* Mask out unwanted mode flags */
    pf.modeflags &= ~(ModeFlag_NonGraphic | ModeFlag_Teletext | ModeFlag_GapMode | ModeFlag_BBCGapMode | ModeFlag_HiResMono | ModeFlag_DoubleVertical | ModeFlag_HardScrollDisabled | ModeFlag_InterlacedMode);

    /* Massage flags for RGB modes a bit */
    if ((pf.modeflags & ModeFlag_DataFormatFamily_Mask) == ModeFlag_DataFormatFamily_RGB)
    {
        /* Detect 64 colour modes and convert to 256 colour */
        if ((pf.ncolour == 63) && (pf.log2bpp == 3))
        {
            pf.ncolour = 255;
            pf.modeflags |= ModeFlag_FullPalette;
        }
        /* Clear the greyscale flag. TODO - keep it so greyscale-only devices can make use of it? */
        pf.modeflags &= ~ModeFlag_GreyscalePalette;
    }

    /* Bail if we see something unexpected */
    if (pf.modeflags & ~(ModeFlag_FullPalette | ModeFlag_64k | ModeFlag_ChromaSubsampleMode | ModeFlag_DataFormat_Mask))
        return;

    /* The OS requires that framebuffer rows start on word boundaries;
     * if padding is needed for this, signal it to the driver via ExtraBytes,
     * so that drivers don't need to know of the OS's limitation (although
     * we do assume that the driver doesn't request that ExtraBytes be
     * modified to something inappropriate!)
     */
    linelength = (linelength+3) & ~3;
    int extrabytes = linelength - orig_linelength;

    /* Pick up data size limit from args to service call */
    datasizelimit = regs->r[5]; /* data size is measured in bytes */

    /* Current Screen Mode selection API FuncSpec (0197,290/FS, Issue
     * D) says data rate figure in R4 is in bytes/sec.  Since Medusa
     * h/ware can do up to 170,000,000 bytes/sec, and that number is a
     * factor of only 12 times smaller than the max number in a 32-bit
     * signed integer, it would seem more future-proof and consistent
     * (cf. pixel rate specs which use kHz not Hz) to use 1000's of
     * bytes/sec, but for now convert to preferred units as used internally.
     */
    dataratelimit = restrict_bandwidth(regs->r[4]) / 1000;

    /* Scan all available modes, looking for a match.  First find the
     * subset of all known modes which have the right resolution.
     * They are contiguous on the master list.  We track where to stop
     * by knowing how many of them there are (find_by_xy counts up)
     * rather than by rechecking the pointer and x/y fields every
     * time.
     */
    mp = find_by_xy(current_monitor->modelist, sel->xresol, sel->yresol, &nmodes);
    if (mp == NULL)
    {
        return;                         /* no match */
    }
    do
    {
        /* Test whether this mode can satisfy the requirements */
        if ((sel->framerate == -1 || sel->framerate == mp->frame_hz) &&
            mode_valid(mp, &pf, dataratelimit, datasizelimit, extrabytes, &thevidclist))
        {
            /* And claim the service */
            regs->r[1] = 0;                 /* Service_Serviced */
            regs->r[3] = (int)&thevidclist; /* return pointer to VIDC list to use (mode_valid will have filled this in for us) */
            regs->r[4] = NULL;              /* marks no workspace list(mode selector given) */
            return;
        }
        mp = mp->next;
    } while (--nmodes);
}

static void service_enumeratescreenmodes(_kernel_swi_regs *regs)
{
    ModeDescriptionRef mp;
    uint32_t dataratelimit, datasizelimit;
    const PixelFormat *pf,*pflist;
    int numformats,pflistlen;
    _kernel_swi_regs r;

    /* We do monitor type 7 only */
    if (regs->r[3] != MONITOR_FILE || !current_monitor)
    {
        return;
    }

    /* See comments re. data-rate spec in service_modeextension */
    dataratelimit = restrict_bandwidth(regs->r[4]) / 1000;
    datasizelimit = regs->r[5];

    /* Get list of pixel formats supported by driver */
    int driver = current_graphicsv_driver();

    r.r[4] = GraphicsV_PixelFormats | (driver<<24);
    r.r[9] = GraphicsV;
    _kernel_swi(OS_CallAVector, &r, &r);
    if (r.r[4] == 0)
    {
        pflist = (PixelFormatRef) r.r[0];
        pflistlen = r.r[1];
    }
    else
    {
        /* Use the old list */
        pflist = OLDFORMATS;
        pflistlen = NUM_OLDFORMATS;
    }

    /* Scan all available modes at all available pixel formats */
    mp = current_monitor->modelist;
    pf = pflist;
    numformats = pflistlen;

    for (;;)
    {
        if (!mode_valid(mp, pf, dataratelimit, datasizelimit, default_extrabytes(mp, pf), NULL))
        {
            /* TMD 03-Nov-93: Fix bug MED-00833
             * Code used to set depth to 5 here, assuming that if the mode
             * was invalid at a low depth, it would be invalid at all higher
             * depths. This is a wrong assumption when a mode can fail because
             * the length of the line is not a suitable multiple.
             */
        }
        else
        {
            /* OK, it fits; what to do with it?  Follow algorithm from F.S. */
            if (regs->r[2] > 0)
            {
                /* skipping (in a partial enumeration) - nowt to do */
            }
            else
            {
                int nlen = strlen(mp->definition.name);
                int entrysize = 24 + ((nlen + 1 + 3) & ~3);
                bool old_format = is_old_format(pf);
                if (!old_format)
                {
                    entrysize += 8;
                }
                if (regs->r[6] != 0)
                {
                    /* Enumeration case - filling in block */
                    if (regs->r[7] >= entrysize)
                    {
                        if (old_format)
                        {
                            ModeInfoBlockRef ip = (ModeInfoBlockRef)regs->r[6];
                            int i;

                            /* Copy the mode information into the supplied data buffer */
                            ip->blocksize = entrysize;
                            ip->format = 0;  ip->flags = 0;  ip->bit0 = 1;
                            ip->xresol = mp->definition.timings.xres;
                            ip->yresol = mp->definition.timings.yres;
                            ip->depth = pf->log2bpp; /* log2(bits/pixel) */
                            ip->framerate = mp->frame_hz; /* integer Hz value used here */

                            /* Copy name + 1 terminating null into block */
                            strcpy(ip->name, mp->definition.name);
                            /* Pad name field out with 0's to N*4 */
                            for (i = nlen+1; (i & 3) != 0; ++i)
                                ip->name[i] = 0;
                        }
                        else
                        {
                            ModeInfoBlock1Ref ip = (ModeInfoBlock1Ref)regs->r[6];
                            int i;

                            /* Copy the mode information into the supplied data buffer */
                            ip->blocksize = entrysize;
                            ip->format = 1;  ip->flags = 0;  ip->bit0 = 1;
                            ip->xresol = mp->definition.timings.xres;
                            ip->yresol = mp->definition.timings.yres;
                            ip->pixelformat = *pf;
                            ip->framerate = mp->frame_hz; /* integer Hz value used here */

                            /* Copy name + 1 terminating null into block */
                            strcpy(ip->name, mp->definition.name);
                            /* Pad name field out with 0's to N*4 */
                            for (i = nlen+1; (i & 3) != 0; ++i)
                                ip->name[i] = 0;
                        }
                        /* Step buffer pointer past this new entry */
                        regs->r[6] += entrysize;
                    }
                    else
                    {
                        regs->r[1] = 0; /* Service_Serviced */
                        return;
                    }
                }
                /* Update remaining size of user data area */
                regs->r[7] -= entrysize;
            }
            /* count down matching modes in calling r2 */
            --regs->r[2];
        }

        /* Move on to next mode, if there are any left */
        if (numformats--)
        {
            pf++;                       /* next deeper mode of current def'n */
        }
        else
        {
            mp = mp->next;              /* next mode in list */
            if (mp == NULL)
            {
                return;                 /* no more modes from this module */
            }
            pf = pflist;                /* start with min. depth */
            numformats = pflistlen;
        }
    }
}

static int modevar(int mode, int var, int dfault)
{
    int value, flags;
    if (_swix(OS_ReadModeVariable, _INR(0,1)|_OUT(2)|_OUT(_FLAGS),
              mode, var, &value, &flags) || (flags & _C)) {
        value = dfault;
    }
    return value;
}

static bool resolution_supported(int x,int y,int doublepixel)
{
    int nmodes;
    if (find_by_xy(current_monitor->modelist, x, y, &nmodes))
    {
        return true;
    }
    if (!doublepixel)
    {
        return false;
    }
    /* Try non-doublepixel version */
    return find_by_xy(current_monitor->modelist, x >> 1, y, &nmodes) != NULL;
}

/* Call Service_ModeExtension, but also tries downgrading doublepixel modes to non-doublepixel */
static bool offer_doublepixel_modeextension(ModeSelector *m, int *doublepixel)
{
    /* Assume that we're the only thing providing support for this monitor type */
    _kernel_swi_regs regs;
    regs.r[1] = Service_ModeExtension;
    regs.r[2] = (int) m;
    regs.r[3] = MONITOR_FILE;
    regs.r[4] = INT32_MAX;
    regs.r[5] = INT32_MAX;
    if (!*doublepixel)
    {
        service_modeextension(&regs);
        return (regs.r[1] == Service_Serviced);
    }
    /* Double-pixel only valid for <= 16bpp */
    if (m->depth <= 4)
    {
        service_modeextension(&regs);
        if (regs.r[1] == Service_Serviced)
        {
            return true;
        }
    }
    /* Halve the width and try again */
    int orig = m->xresol;
    m->xresol = orig>>1;
    service_modeextension(&regs);
    if (regs.r[1] == Service_Serviced)
    {
        *doublepixel = false;
        return true;
    }
    m->xresol = orig;
    return false;
}

static const char compared_mode_variables[] = {
    VduExt_XWindLimit, /* Compare resolution first since that has the most variance */
    VduExt_YWindLimit,
    VduExt_ModeFlags,
    VduExt_Log2BPP,
    VduExt_XEigFactor,
    VduExt_YEigFactor,
    VduExt_Log2BPC,
    VduExt_ScrRCol,
    VduExt_ScrBRow,
    VduExt_LineLength,
    VduExt_NColour,
    VduExt_ScreenSize,
};
static const int num_compared_mode_variables = sizeof(compared_mode_variables);

static int mode_selector_to_mode_number(const ModeSelector *m)
{
    /* Attempt to map back to a numbered mode, looking for an exact match in all mode variables (except YShftFactor) */
    int vars[num_compared_mode_variables];
    for (int i=0;i<num_compared_mode_variables;i++)
    {
        vars[i] = modevar((int) m, compared_mode_variables[i], 0);
    }
    for (int mode=0;mode<64;mode++) /* Only check kernel modes? */
    {
        for(int i=0;i<num_compared_mode_variables;i++)
        {
            int val = modevar(mode, compared_mode_variables[i], 0);
            if (val != vars[i])
            {
                goto nextmode;
            }
        }
        return mode;
nextmode:;
    }
    return -1;
}

/* Some industry-standard resolutions to use as fallbacks.
   We could simply try all the resolutions listed in the MDF/EDID, but that
   might not work so well if we end up picking a mode with an odd aspect ratio.
 */
static const int fallback_resolutions[] = {
    640, 350,
    640, 400,
    720, 400,
    640, 480,
    720, 480,
    720, 576,
    800, 600,
    1024, 768,
    1280, 1024,
    1280, 720,
    1920, 1080,
};
static const int num_fallbacks = sizeof(fallback_resolutions)/8;

/*

  Find an alternative mode which is supported by the hardware:

  * Try higher BPPs (including true colour)
  * Try lower BPPs
  * Try alternate resolutions

  Check for validity using Service_ModeExtension (although since we're only
  dealing with known modes for known monitor types, we could optimise it to
  go straight to HandleServiceModeExtension instead)

  Be careful with special mode types (double-vertical, BBC gap mode, etc.)
  because Service_ModeExtension won't be aware of kernel restrictions (and
  using OS_CheckModeValid instead will introduce the risk of recursion
  - although maybe we can mitigate that by only passing mode selector blocks
  to OS_CheckModeValid?)

  Non-teletext special mode type handling:

  * Allow special mode types to be downgraded to non-special types:
    * Double-pixel to normal
    * BBC gap mode to regular gap mode
    * Double-vertical to normal
    * Hi-res mono to regular mono (always downgraded)
  * There's no need to downgrade gap modes to non-gap modes, since gap modes
    can be supported in any BPP
  * Double-pixel is the only one that will affect Service_ModeExtension, since
    that affects the physical resolution of the mode. So we'll potentially have
    to vet double-pixel modes twice (once with it enabled, once with it
    disabled)
  * Theoretically we could upgrade regular modes to double-pixel in order to
    work around hardware restrictions - but since double-pixel modes aren't
    widely supported it's probably best to avoid that.

 */
static void service_modetranslation(_kernel_swi_regs *regs)
{
    /* We do monitor type 7 only */
    if (regs->r[3] != MONITOR_FILE || !current_monitor)
    {
        return;
    }

    /* Mode selector block with space for 8 parameters:
       ModeFlags, NColour, XEig, YEig, Log2BPC, ScrRCol, ScrBRow, XWindLimit */
    union {
        ModeSelector m;
        char c[sizeof(ModeSelector) + sizeof(ModeParam)*7 + 4];
    } u;

    /* Convert input mode to selector block (as a standard bitmap mode) */
    u.m.bit0 = 1;
    u.m.format = 0;
    u.m.flags = 0;
    int log2bpc = modevar(regs->r[2], VduExt_Log2BPC, 3);
    int log2bpp = modevar(regs->r[2], VduExt_Log2BPP, 3);
    int doublepixel = log2bpc - log2bpp;
    if (log2bpp > 5) /* TODO: Handle unusual colour depths better */
    {
        log2bpp = 5;
    }
    int orig_xresol = (modevar(regs->r[2], VduExt_XWindLimit, 639) + 1) << doublepixel;
    u.m.xresol = orig_xresol;
    int orig_yresol = modevar(regs->r[2], VduExt_YWindLimit, 479) + 1;
    u.m.yresol = orig_yresol;
    u.m.framerate = -1; /* Don't care (yet) */
    u.m.param[0].index = VduExt_ModeFlags;
    u.m.param[1].index = VduExt_NColour;
    u.m.param[2].index = -1;

    int driver = current_graphicsv_driver();
    int formatmask = get_supported_pixel_formats(driver);

    int orig_modeflags = modevar(regs->r[2], VduExt_ModeFlags, ModeFlag_FullPalette);

    /* Restrict pixel formats for teletext modes */
    if (orig_modeflags & ModeFlag_Teletext)
    {
        formatmask &= ~7;
    }

    int fallback_phase = 0;
    int fallback_res = -1;

    while(1)
    {
        /* Assume that we're the only thing providing support for this monitor type, and peek at
           the mode list to see if this resolution is supported at all */
        if (resolution_supported(u.m.xresol, u.m.yresol, doublepixel))
        {
            /* Try same-or-higher colour depths */
            int depthidx = log2bpp-1;
            while ((1 << ++depthidx) <= formatmask)
            {
                if (!(formatmask & (1 << depthidx)))
                {
                    continue;
                }
                u.m.depth = pixelformats[depthidx].log2bpp;
                u.m.param[0].value = pixelformats[depthidx].modeflags;
                u.m.param[1].value = pixelformats[depthidx].ncolour;
                if (offer_doublepixel_modeextension(&u.m, &doublepixel))
                {
                    goto found;
                }
            }

            /* Try lower colour depths */
            depthidx = log2bpp;
            while (--depthidx >= 0)
            {
                if (!(formatmask & (1 << depthidx)))
                {
                    continue;
                }
                u.m.depth = pixelformats[depthidx].log2bpp;
                u.m.param[0].value = pixelformats[depthidx].modeflags;
                u.m.param[1].value = pixelformats[depthidx].ncolour;
                if (offer_doublepixel_modeextension(&u.m, &doublepixel))
                {
                    goto found;
                }
            }
        }
        /* Try assorted fallback resolutions */
        switch (fallback_phase)
        {
        case 0:
            /* Fallback resolutions, but only if higher than current */
            while (++fallback_res < num_fallbacks)
            {
                u.m.xresol = fallback_resolutions[fallback_res*2];
                u.m.yresol = fallback_resolutions[fallback_res*2+1];
                if (((u.m.xresol >= orig_xresol) && (u.m.yresol >= orig_yresol))
                 && ((u.m.xresol != orig_xresol) || (u.m.yresol != orig_yresol)))
                {
                    break;
                }
            }
            if (fallback_res < num_fallbacks)
            {
                break;
            }
            /* Fall through ... */
            fallback_phase = 1;
        case 1:
            /* Preferred mode (assumed to be the highest res mode the monitor supports) */
            if ((preferred_mode != NULL) && (preferred_mode->bit0 == 1) && (u.m.xresol != preferred_mode->xresol) && (u.m.yresol != preferred_mode->yresol))
            {
                u.m.xresol = preferred_mode->xresol;
                u.m.yresol = preferred_mode->yresol;
                break;
            }
            /* Fall through... */
            fallback_phase = 2;
            fallback_res = num_fallbacks;
        default:
            /* Fallback resolutions, but lower resolutions */
            while (--fallback_res >= 0)
            {
                u.m.xresol = fallback_resolutions[fallback_res*2];
                u.m.yresol = fallback_resolutions[fallback_res*2+1];
                if (!((u.m.xresol >= orig_xresol) && (u.m.yresol >= orig_yresol))
                 && ((u.m.xresol != orig_xresol) || (u.m.yresol != orig_yresol)))
                {
                    break;
                }
            }
            if (fallback_res >= 0)
            {
                break;
            }
            /* Give up */
            return;
        }
    }
found:;
    /* Successfully found a mode
       Fill in any extra attributes that are necessary for the mode */

    ModeParam *p = &u.m.param[2];
    /* XEig, YEig are always stored */
    p->index = VduExt_XEigFactor;
    p->value = modevar(regs->r[2], VduExt_XEigFactor, 1);
    p++;
    p->index = VduExt_YEigFactor;
    p->value = modevar(regs->r[2], VduExt_YEigFactor, 1);
    p++;

    /* Log2BPC, XWindLimit, ScrRCol only needed for double-pixel */
    if (doublepixel)
    {
        p->index = VduExt_Log2BPC;
        p->value = u.m.depth+1;
        p++;
        p->index = VduExt_XWindLimit;
        p->value = (u.m.xresol>>1)-1;
        p++;
        p->index = VduExt_ScrRCol;
        p->value = (u.m.xresol>>4)-1;
        p++;
    }

    int modeflags = u.m.param[0].value | (orig_modeflags & (ModeFlag_GapMode+ModeFlag_BBCGapMode+ModeFlag_DoubleVertical+ModeFlag_NonGraphic+ModeFlag_Teletext));

    /* Teletext doesn't need any more attributes specified, and doesn't want the following rules applying */
    if (!(orig_modeflags & ModeFlag_Teletext))
    {
        /* (non-Teletext) Double-vertical only supported by kernel for 1bpp */
        if (u.m.depth)
        {
            modeflags &= ~ModeFlag_DoubleVertical;
        }

        if (modeflags & ModeFlag_BBCGapMode)
        {
            /* BBC gap modes only supported by kernel for 2bpp */
            if (u.m.depth != 1)
            {
                modeflags &= ~(ModeFlag_BBCGapMode | ModeFlag_NonGraphic); /* Assume BBC gap modes and teletext are the only non-graphic modes (and teletext won't get here). So if it's no longer a BBC gap mode, it's no longer non-graphic. */
            }
            else
            {
                /* Set the correct NColour value if it's remaining a BBC gap mode */
                p->index = VduExt_NColour;
                p->value = 1;
                p++;
            }
        }

        /* Downgrade to partial 8bpp palette, for consistency with numbered modes */
        if (u.m.depth == 3)
        {
            modeflags &= ~ModeFlag_FullPalette;
            u.m.param[1].value = 63;
        }

        /* ScrBRow needed for gap modes, double-vertical */
        if (modeflags & (ModeFlag_GapMode | ModeFlag_DoubleVertical))
        {
            int charheight = 8;
            if (modeflags & ModeFlag_GapMode)
            {
                charheight = 10;
            }
            if (modeflags & ModeFlag_DoubleVertical)
            {
                charheight <<= 1;
            }
            p->index = VduExt_ScrBRow;
            p->value = (u.m.yresol / charheight) - 1;
            p++;
        }
    }
    else
    {
        /* If the new mode is higher-res than the original, restrict ScrRCol & ScrBRow so that software which expects standard teletext (i.e. mode 7) resolution will continue to work */
        int charwidth = 3; /* Shift */
        int charheight = 10; /* Rows */
        if (modeflags & ModeFlag_DoubleVertical)
        {
            /* HiResTTX */
            charwidth = 4;
            charheight = 20;
        }
        int orig_cols = modevar(regs->r[2], VduExt_ScrRCol, 39) + 1;
        int orig_rows = modevar(regs->r[2], VduExt_ScrBRow, 24) + 1;
        if ((u.m.xresol >> charwidth) > orig_cols)
        {
            p->index = VduExt_ScrRCol;
            p->value = orig_cols - 1;
            p++;
        }
        if ((u.m.yresol / charheight) > orig_rows)
        {
            p->index = VduExt_ScrBRow;
            p->value = orig_rows - 1;
            p++;
        }
    }

    u.m.param[0].value = modeflags;
    p->index = -1;

    int mode_number = mode_selector_to_mode_number(&u.m);
    if (mode_number != -1)
    {
        regs->r[1] = Service_Serviced;
        regs->r[2] = mode_number;
        return;
    }

    /* Copy the mode selector block to somewhere safe so we can return it */
    static char static_mode[sizeof(ModeSelector) + sizeof(ModeParam)*7 + 4];
    memcpy(static_mode, u.c, sizeof(u.c));
    regs->r[1] = Service_Serviced;
    regs->r[2] = (int) static_mode;
}

static void service_displaychanged(_kernel_swi_regs *regs)
{
    switch (regs->r[0])
    {
        case DisplayChanged_Changing:
            /* The current driver is changing. We must recache any
             * driver-specific things which we store.
             */
            check_supported_controllist_items(regs->r[2]);

            /*
             * Check the monitor's still there.
             * Note that doing it here means:
             * a) ScreenModes must precede the driver if you want a mode defined
             *    during module init AND the MonitorType is EDID. If the MonitorType
             *    is some other value, including Auto, it doesn't matter
             *    where ScreenModes is as the Kernel will use a built in numbered mode.
             * b) Loading ScreenModes off disc won't automatically load the EDID data
             *    as this service call doesn't happen. Calling readedid() from module
             *    init isn't possible because the module isn't linked to the service
             *    call chain, so Display Manager et al can't enumerate the modes
             *    with OS_ScreenMode 2. This is only a problem for developers, who
             *    can manually re-read it (standalone builds are only partly supported today).
            */
            readedid(regs->r[2], using_edid);
            break;
    }
}

static void service_monitorleadtranslation(_kernel_swi_regs *regs)
{
    if ((preferred_mode != NULL) && (preferred_mode->bit0 == 1))
    {
        regs->r[1] = Service_Serviced;
        regs->r[3] = (int) preferred_mode;
        regs->r[4] = MONITOR_FILE;
        regs->r[5] = preferred_sync_type;
    }
}

static int we_are_preferred(void *pw)
{
    _kernel_swi_regs sregs;
    sregs.r[0] = ModHandReason_LookupName;
    sregs.r[1] = (int)"ScreenModes";
    (void)_kernel_swi(OS_Module, &sregs, &sregs);
    return (sregs.r[4] == *(int *)pw);
}

static const int samplerates[] =
{
     32000*1024,
     44100*1024,
     48000*1024,
     88200*1024,
     96000*1024,
    176400*1024,
    192000*1024,
};

static _kernel_oserror *swi_enumerateaudioformats(_kernel_swi_regs *regs)
{
    if (!current_monitor)
    {
        return error(ERR_NOMODEFILE, 0, 0, 0);
    }

    /* In:  r0 = flags:
     *           bit 0: 0 -> read raw data
     *                  1 -> read friendly data
     *      r1 = format code to start from (-1 = first)
     *      r2 = index within format code (-1 = first)
     * Out: r1 = format code of this entry (-1 if no more formats)
     *      r2 = index within format code (-1 if no more formats)
     *      r3 = max channels
     *      raw data:
     *        r4 = audio short descriptor byte 2
     *        r5 = audio short descriptor byte 3
     *      friendly data:
     *        r4 = sample rate (Hz*1024)
     *        r5 = LPCM: bit depth
     *             codes 2-8: max bit rate in Hz
     *             other codes: not supported by this API
     *
     * n.b. index values (r2) differ between raw & friendly modes
     */
    if (regs->r[0] & ~1)
    {
        return error(ERR_BADENUMAUDIO, 0, 0, 0);
    }
    /* Find the right index to start from */
    AudioFormat *current = current_monitor->audio_formats;
    int count = current_monitor->audio_format_count;
    int format = regs->r[1];
    int start_index = regs->r[2];
    while (count && (current->format_code < format))
    {
        current++;
        count--;
    }
    if (regs->r[0] & 1)
    {
        /* Friendly mode */
        /* Skip formats we don't understand */
        int my_index = 0;
find_next_entry:;
        int sample_rate = 0;
        int bits = 0;
        while (count && ((current->format_code < 1) || (current->format_code > 8)))
        {
            current++;
            count--;
        }
        if (!count)
        {
            regs->r[1] = -1;
            regs->r[2] = -1;
            return NULL;
        }
        bool is_LPCM = (current->format_code == 1);
        if (is_LPCM)
        {
            if ((current->format_specific & 0xf8) || !(current->format_specific & 0x7))
            {
                /* Weird bit depth mask for LPCM, skip entry */
                current++;
                count--;
                goto find_next_entry;
            }
        }
find_next_sub_entry:
        /* Find first sample_rate & bits value */
        while (!(current->sample_rates & (1<<sample_rate)))
        {
            sample_rate++;
        }
        if (is_LPCM)
        {
            while (!(current->format_specific & (1<<bits)))
            {
                bits++;
            }
        }
        /* Skip entries as necessary */
        if ((current->format_code == format) && (my_index <= start_index))
        {
            my_index++;
            /* Step to next sample rate */
            sample_rate++;
            if ((1<<sample_rate) > current->sample_rates)
            {
                /* Step to next bit depth value */
                sample_rate = 0;
                bits++;
                if (!is_LPCM || ((1<<bits) > current->format_specific))
                {
                    /* Step to next format list entry */
                    current++;
                    count--;
                    goto find_next_entry;
                }
            }
            goto find_next_sub_entry;
        }
        if (current->format_code > format)
        {
            my_index = 0;
        }
        regs->r[1] = current->format_code;
        regs->r[2] = my_index;
        regs->r[3] = current->max_channels;
        regs->r[4] = samplerates[sample_rate];
        regs->r[5] = is_LPCM ? (16 + 4*bits) : (current->format_specific*8000);

        return NULL;
    }
    else
    {
        /* Raw mode */
        int my_index = 0;
        while (count && (current->format_code == format) && (my_index <= start_index))
        {
            current++;
            count--;
            my_index++;
        }
        if (!count)
        {
            regs->r[1] = -1;
            regs->r[2] = -1;
            return NULL;
        }
        if (current->format_code > format)
        {
            my_index = 0;
        }
        regs->r[1] = current->format_code;
        regs->r[2] = my_index;
        regs->r[3] = current->max_channels;
        regs->r[4] = current->sample_rates;
        regs->r[5] = current->format_specific;

        return NULL;
    }
}

/* EXPORTED */
_kernel_oserror *ScreenModes_initialise(const char *cmd_tail, int podule_base, void *pw)
{
    char typevar[16];
    int  config;

    /* Keep a preferred mode with space for 2 extra mode variables */
    preferred_mode = calloc(1, sizeof(ModeSelector) + (2 * sizeof(ModeParam)));

    /* Define the EDID type name */
    sprintf(typevar, "File$Type_%03X", FileType_EDID);
    _swix(OS_SetVarVal, _INR(0,4), typevar, FileType_EDID_Name,
                                   strlen(FileType_EDID_Name), 0, VarType_String);

    /* Look at the MonitorType to see whether to proactively parse EDID */
    if (!_swix(OS_Byte, _INR(0,1) | _OUT(2), OsByte_ReadCMOS, VduCMOS, &config))
    {
        using_edid = ((config & MonitorTypeBits) >> MonitorTypeShift) == MONITOR_EDID;
    }

    check_supported_controllist_items(current_graphicsv_driver());

    UNUSED(cmd_tail);
    UNUSED(podule_base);
    UNUSED(pw);

    return NULL;
}

/* EXPORTED */
_kernel_oserror *ScreenModes_final(int fatal, int podule, void *pw)
{
    /* Free the space claimed for the current monitor
     * description (if any) and revert to the configured monitor type,
     * when the module is shut down.
     */
    (void) restore_monitortype(); /* restore old value */
    release_currentmonitor();
    release_edidblocks();
    if (preferred_mode)
    {
        free(preferred_mode);
    }

    UNUSED(pw);
    UNUSED(podule);
    UNUSED(fatal);

    return NULL;
}

/* EXPORTED */
void ScreenModes_servicecall(int servicecallno, _kernel_swi_regs *regs, void *pw)
{
    if ((current_monitor == NULL) && (servicecallno != Service_DisplayChanged))
    {
        return;                         /* nothing loaded */
    }

    /* Ignore service calls if we are not the current preferred instantiation */
    if (!we_are_preferred(pw))
    {
        return;
    }

    if (servicecallno == Service_ModeExtension)
    {
        service_modeextension(regs);
    }
    else if (servicecallno == Service_EnumerateScreenModes)
    {
        service_enumeratescreenmodes(regs);
    }
    else if (servicecallno == Service_ModeTranslation)
    {
        service_modetranslation(regs);
    }
    else if (servicecallno == Service_DisplayChanged)
    {
        service_displaychanged(regs);
    }
    else if (servicecallno == Service_MonitorLeadTranslation)
    {
        service_monitorleadtranslation(regs);
    }
}

/* EXPORTED */
_kernel_oserror *ScreenModes_cmdhandler(const char *arg_string, int argc, int cmd_no, void *pw)
{
    _kernel_oserror *result;

    switch (cmd_no)
    {
        case CMD_LoadModeFile:
            result = loadmodefile(arg_string);
            break;
        case CMD_SaveModeFile:
            result = savemodefile(arg_string);
            break;
        default:
            return NULL;
    }

    UNUSED(pw);
    UNUSED(argc);

    return result;
}

/* EXPORTED */
_kernel_oserror *ScreenModes_swihandler(int swi_no, _kernel_swi_regs *r, void *pw)
{
    _kernel_oserror *result;

    switch (swi_no)
    {
        case ScreenModes_ReadInfo - ScreenModes_00:
            switch (r->r[0])    /* r0 = subreason code */
            {
                case ScreenModes_ReadInfo_MonitorName: /* Return pointer to monitor name */
                    if (current_monitor)
                    {
                        r->r[0] = (int) &current_monitor->name;
                        result = NULL;
                    }
                    else
                    {
                        /* No monitor description file loaded */
                        result = error(ERR_NOMODEFILE, 0, 0, 0);
                    }
                    break;

                case ScreenModes_ReadInfo_DPMS: /* Read supported DPMS state of monitor */
                    if (current_monitor)
                    {
                        r->r[0] = current_monitor->dpms_state;
                        result = NULL;
                    }
                    else
                    {
                        /* No monitor description file loaded */
                        result = error(ERR_NOMODEFILE, 0, 0, 0);
                    }
                    break;

                case ScreenModes_ReadInfo_SpeakerMask: /* Read monitor speaker mask */
                    if (current_monitor)
                    {
                        r->r[0] = current_monitor->speaker_mask; /* Mask */
                        r->r[1] = current_monitor->speaker_mask_provided ? 0x7f : 0; /* Validity of each bit */
                        result = NULL;
                    }
                    else
                    {
                        /* No monitor description file loaded */
                        result = error(ERR_NOMODEFILE, 0, 0, 0);
                    }
                    break;

                default:        /* Unknown ScreenModes_ReadInfo call */
                    result = error(ERR_BADREADINFO, 0, 0, 0);
            }
            break;

        case ScreenModes_EnumerateAudioFormats - ScreenModes_00:
            return swi_enumerateaudioformats(r);

        case ScreenModes_Features - ScreenModes_00:
            r->r[0] = ScreenModes_Features_EDID; /* These extra features are supported */
            result = NULL;
            break;

        default:                /* Unknown ScreenModes SWI */
            return error_BAD_SWI;
    }
    UNUSED(pw);

    return result;
}

/* EOF ScrModes.c */