Repository: tauri
Issue: [feat] Please consider offering resolution control when in fullscreen
Labels: type: feature request,priority: 3 low

### Describe the problem

One important topic for game developers is resolution control.  

Quite frequently, so as not to need multiple versions of art assets (cost and quality control,) or to present a pixel-identical version of a game to multiple players with different hardware over a network, or less frequently to manage render time on very complex screens, or even in some cases to simply achieve a certain aesthetic, it is preferable to control the resolution of a screen.  Pixel-accurate stretches of images genuinely do not look the same as the screen in a different mode; there are too many opportunities for aliasing artifacts that one simply shouldn't have to spend time fighting; the impact on battery life for portable devices is significant.

I have a game that I would like to port using Tauri.  Currently, I use DirectX and a native hosting of MSHTML to render, but that's tied to Windows, and since I've learned that Tauri targets Macs, Linux, and will soon target phones, I thought Tauri might be a nice platform to tackle portability

It's worth noting that this was the #1 request in DirectX 1 and 2 (DirectX hilariously didn't offer this until 3, because the Microsoft people were certain it was regressive; within a year of implementing it, more than 90% of AAA games were using it.)  

This was also an extremely common help request in the Flash days, how to accomplish this, because the platform supported it but the relevant documentation wasn't very well written.  I suggest this to support that it's well wanted by game developers.

On Windows, the relevant API for what I would like access to is [ChangeDisplaySettingsExA](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-changedisplaysettingsexa), and the call should probably be made with the flag `CDS_TEST`.

### Describe the solution you'd like

I'm kinda flexible here, and I don't use Tauri yet so this might be a braindead approach.  Also, I'm prone to use Windows phrasing for things because I don't know the Mac phrasing and Linux has different names for this stuff for each window manager

But at first blush, I would think adding a member to `/tooling/api/src/window.ts:`[WindowOptions](https://tauri.app/v1/api/js/interfaces/window.windowoptions/) to describe a screen's configuration, as well as a number to say which screen, might be the right thing to do?

[Something like](https://github.com/tauri-apps/tauri/blob/679abc6/tooling/api/src/window.ts#L1891):

```typescript
type DisplayConfig = {
    bitsPerPixel?: number | { 'at least': number } | false | undefined,  
    width?: number | false | { 'at least': number } | undefined,
    height?: number | false | { 'at least': number } | undefined,
    frequency?: number | { 'at least': number } | { 'multiple': number } | { 'exactly': number } | false | undefined,
    interlace?: boolean | undefined,
    textMode?: boolean | undefined,
};

type DisplayConfigPreferences = {
    allowContain?: boolean | 'horizontal' | 'vertical' | undefined,
    pixelRatio?: number | number[] | true | 'antialiased' | 'neighbor' | undefined 
};

type DisplayConfigRequest = DisplayConfig & DisplayConfigPreferences;

interface WindowOptions {
    // ... 

    display_target?: number | undefined,
    display_request?: DisplayConfigRequest | undefined
};
```

Admittedly, this approach would not allow single windows that spanned multiple screens, which will be a bummer to kiosk, hardwall, and dashboard vendors.  However, that's actually relatively difficult due to things like difference in font scale between devices, and probably isn't necessary; besides, they could just create multiple windows.

Meanings:

* `bitsPerPixel`: 
    * a `number` specifies an exact value.  
    * `{ 'at least': number }` is useful for when you need 15 bits but a display reports as 16, or 24 as 30.
    * `undefined` means to use the current device setting
    * `false` means to not care at all, meaning even regression is fine
* `width` (and equivalents for `height`):
    * `number`: use this specific width
    * `false`: do not consider `width` as a constraint when choosing a minimal screen sizing
        * if `false` is set for both `width` and `height`, one ought to end up with the smallest available screen; choose by pixel count
    * `{ 'at least': number }`: after filtering below the cut, do not consider `width` as a constraint when choosing a minimal screen sizing.  
    * `undefined`: use width specified in the parent object, `window.WindowOptions.width`; if that is also undefined, use the current screen width
* `frequency`
    * `number`: the closest frequency within 1% will be used (to cope with NTSC-rate 24/23.97, float error, etc); otherwise fail.
    * `{ 'exactly': number }`: only exactly the specified frequency, plus or minus `Number.EPSILON`, will be used; otherwise fail.
    * `{ 'multiple': number }`: the closest frequency within 1%, will be used.  If none, integer multiple extensions (12hz would be satisfied by 24 on some monitors, or 60 on others, eg) would be considered.  If none exist, fail.
    * `false`: do not consider frequency a constraint; choose the lowest frequency at-or-above the current from what's eventually available.
    * `undefined`: as `false`
* `interlace`
    * `true`: require interlacing to be on.  these APIs generally do not offer control of leading edge.
    * `false`: require interlacing to be off
    * `undefined`: as `false` (ie, do not generally consider interlaced modes, even if they're better fits otherwise)
* `textMode`
    * `true`: bring up a good old fashioned console CLI text mode prompt.  No, not a simulation.
    * `false`: regular screen
    * `undefined`: as `false`
* `allowContain`
    * `true`: if the resolution can't be matched, choose the next smallest fit, preferring smallest delta between requested and actual pixel count; expect the developer to handle their own letterboxing/pillarboxing
    * `false`: require an exact fit, within the governance of `pixelRatio`
    * `horizontal`: require an exact fit, within the governance of `pixelRatio`; prefer horizontal best-fits, as one would want for a side-scroller
    * `vertical`: require an exact fit, within the governance of `pixelRatio`; prefer vertical best-fits, as one would want for a top-scroller
    * `undefined`: as `true`
* `pixelRatio`
    * `true`: after finding the smallest screen resolution which can accommodate a request, scale the request by the largest integer factor that will still fit.  Useful for putting a small resolution on any size of screen. 
    * `number`: specifies the single pixel scale ratio other than native which will be considered for match.
    * `number[]`: specifies the list of pixel scale ratios other than native which will be considered for match.
    * `antialiased`
    * `antialiased_fractional`
    * `neighbor`
    * `neighbor_fractional`
    * `undefined`: as `true`

The relevant Windows API offers a fullscreen field, but we omit that because it exists on the parent object, and unlike with `height` / `width`, there is no sensible situation in which the two differ

If the mode isn't available (CDS_TEST failure on windows,) then we need to know how to cope with a missing mode.  An example of this might be if a developer asked for `1600x900` on a screen that does `1920x1080`.  Most screens of that size won't scale `1600x900`.  To that end we offer two features: `contain` and `pixelRatio`.

----

This suggests code in the following shape, probably with less annoying names than I chose:

```typescript
type DisplayConfig = {
    bitsPerPixel?: number | { 'at least': number } | false | undefined,  
    width?: number | false | { 'at least': number } | undefined,
    height?: number | false | { 'at least': number } | undefined,
    frequency?: number | { 'at least': number } | { 'multiple': number } | { 'exactly': number } | false | undefined,
    interlace?: boolean | undefined,
    textMode?: boolean | undefined,
};

type DisplayConfigPreferences = {
    contain?: boolean | 'horizontal' | 'vertical' | undefined,
    pixelRatio?: number | number[] | true | 'antialiased' | 'neighbor' | undefined 
};

type DisplayConfigRequest = DisplayConfig & DisplayConfigPreferences;

function enumerateAvailableSPScreenConfigs(): DisplayConfig[] { ... }
function assignScreenConfig(targetScreen: number, config: DisplayConfig, allowAA: boolean) { ... }
function bestFitScreenConfig(desiredConfig: DisplayConfigRequest): DisplayConfig { ... }
function releaseScreenConfig(targetScreen: number) { ... }
```

Note: many APIs will return available hardware-scaled modes with non-square pixels.  This approach requires that only resolutions with square pixels be considered.  As such, a 4:3 mode on a 16:9 screen would be acceptable if letterboxed, but not acceptable if distorted.  All major screen resolution APIs accommodate this.

This is relatively easily viewed as a decision tree: ([editor](https://stonecypher.github.io/jssm-viz-demo/graph_explorer.html?s=FAMwNg9g7gXABAJwJYHMAWAXA3MXwBEAEgIYDOiApgI4CuFpGFAJgDoB2pAxghRW3JwhsQqdm2IYkANwpgAngH58cAORz6KuAFoAfHGBw4+AMoUMcLjz7sMESrXqNWbQcNT4cBEuR4OGzdkteflcRFDEJaVlFZRU2CE1dfQMjYjBIKAB9LjSKJVUMBDpEvRTDfDSM7M5c+EK6dkroTNcMYiQ2fJUQNNIKEv1DIdT05pywCjqiikbRrNb2tngesD7lJLLhowBBNjk4DsYUCgQ4AFsaMEkABwn2CBA4DDQKezp-JgskAC8ZtjPiPsAEavGh9JgAOkMAHUkM8IDQMI1+BQAB7ETjmAEYThoAA0cB6SDAHlwwwqc2qtSe01mVQWHS69X62lKWxGVXGkxpDXElIZSx5FHWbPZRgAYh1Ps9XqQAelHBYanc2FA0EhcXBrmQ+uRiEqrGxGlJ2mBiEDiXD9owGFC4ABVPrsOHkEEMA5sI4nQlwu3i03Ox7xZ4dFA+jCkCGk4YpCmc5V5WJsGhnEEIAabONjBPwZOpk505oCrorPoZ9lZrJc3MptOF+ZCNodZa9YWswZi-AAeTY8jgaIxGD7MrgYCQHzEKZOGqVaVDhMHEAQ7ABwNB4LtAEkg5EZMjperyFy4EwhCpzCIMHj2ESwHaADLQOCIy0YORRzzkppVnNwPN1vl6UbRYmWmEpNnKb8qQmGt82XQCi2A5shRFDsKx7PsB0xYcXlHccnEnM5p04WcxzYMMekxJcV0BOAQWfDdDAAYQgM5tR4CJJBkOBiDYT5xnnSjbHgkAlzoxVLzo-ZriQVFZBPWQ2n3BjXigDo2BOD9NljKCuS6f8TgAbQAXXLIZK2g7kDIQEz6xaJDOliUsWV0CCOWzalrNshCG09RYW1WNsNjFCUpSeXC5UqRUuXYNUNTQLUdXoHiDWCY1TXNV9rUcO1HT+F1xPdQ4KGOU5Ly0mMv0pas-1rIzjLs4tYmZcCK1038vIanz7L85DmVQtzyklPjwtleUJndGLVXVTVtVIXUUqCawXAczKxzfJ4csMPLnQjQrzGK0rwwqoYdOqhMS1bMzIPO6lnMahzLsC67zPau7Wwe3rBWcgaQpMMx+3RbDrRedhfHeJxEoQYgiMYBBSCwG9TTgCAZQQNS1k-V7bpghdAs+ptHIKMD2zciyavu7qBSmOhfs7YaDzGqKGECbhggEIQwg5r7QxsXDwccAI2HYmGzBOBGLAB54JAhdgABVDwOcgRyYCgekucwakxsl3J-XIul4yQ5zIF7yd-Q2kGN0gCZAumqvjakLatm3kIACjOCBUYASjtrZ8AZ0aLHGiS4UkcjWcNO1tnm1B+GljBZbYAAFBAICkJBVZ4lFUWuChMSF48hLEoEMQAayeOwZXYVWZEgXOED9ZGkCDCAI-Z0gfleS9I0MHtXgee5e2tKA7FCVByAeEBp3Dthtm2LSzodiYS2hzEkCENJTbe3GQFXyQN7AF2ieCv3t+5XfB3Xvkj-gd3PYwH3Sb+gOR0ihV3UvMPwg4Nm+CjmOUBxzQDLdgKc04Z1eLxQGud87OELouU4JdODl1sKNauFBa4QHrluFuqVlrdwJLiT2fRA5LSNBwTu4UJCUAwDQBAHA0G9mIAgY4k0aBAi0AARlIn8QQasRCcCQHwK8WdPjXFTunVWfNqGoJVhg2QWDNK9w0vcR4Qhhwj25mEa28QQBT2QOROAc8F43SXomVQGlmGOC3jjKyFArEsypo9X2piPK40sTwRxUFqZwDvt7Fx5lo5ZyNmOMgBIgSIjgGpZ4yJJBaCtvOJg45zQTEhKSRebjzFxHsZ4jAmQL5rwPjYsxuYcmOHyXvK+aQj75BPvbTJpSHF5IKfva+Tivq3w9v4p+nYgkeMcOEyJ0S0CxKQPE0JHdDFJNICk5gJ1DBAA))

![image](https://user-images.githubusercontent.com/77482/184684526-989a0eef-9c02-4a66-a333-6a69d2547737.png)

----

Aside: why are these `foo?: thing | undefined`?  Isn't that redundant?  

No: `foo?: thing` doesn't allow explicit `undefined` members, and `foo: thing | undefined` doesn't allow omitted members.  Both would be useful here.

### Alternatives considered

A smaller implementation might start without the best-fitting API, without containment, without automatic pixel ratio seeking, without support for AA, neighbor, or fractional, drop support for textMode and interlacing, without support for `at least` or `horizontal` and `vertical`, in a pinch even drop frequency (not desirable!,) and have the end user implement.  And that would be fine.

A successful minimal implementation IMO would look like this, and could be progressively expanded into the above:

```typescript
type DisplayConfig = {
    bitsPerPixel?: number | undefined,  
    width?: number | undefined,
    height?: number | undefined
};

function enumerateAvailableSPScreenConfigs(): DisplayConfig[] { ... }
function assignScreenConfig(targetScreen: number, config: DisplayConfig) { ... }
function releaseScreenConfig(targetScreen: number) { ... }
```


### Additional context

if you would like game developers, this is genuinely a pretty big deal