TV rotation

cartridges can control TV rotation if necessary. only movie cart does
this for now

alt + <cursor keys> manually flips the screen rotation
This commit is contained in:
JetSetIlly 2023-10-04 19:18:59 +01:00
parent 54e3abd04f
commit 8ccae4e70f
14 changed files with 183 additions and 18 deletions

View file

@ -18,6 +18,7 @@ package environment
import (
"github.com/jetsetilly/gopher2600/hardware/preferences"
"github.com/jetsetilly/gopher2600/hardware/television"
"github.com/jetsetilly/gopher2600/hardware/television/specification"
"github.com/jetsetilly/gopher2600/random"
)
@ -28,6 +29,7 @@ type Label string
// implementation
type Television interface {
GetSpecID() string
SetRotation(specification.Rotation)
}
// Environment is used to provide context for an emulation. Particularly useful

View file

@ -18,6 +18,7 @@ package sdlimgui
import (
"github.com/jetsetilly/gopher2600/gui/crt"
"github.com/jetsetilly/gopher2600/gui/sdlimgui/framebuffer"
"github.com/jetsetilly/gopher2600/hardware/television/specification"
)
type crtSeqPrefs struct {
@ -163,7 +164,8 @@ func (sh *crtSequencer) flushPhosphor() {
//
// integerScaling instructs the scaling shader not to perform any smoothing
func (sh *crtSequencer) process(env shaderEnvironment, moreProcessing bool,
numScanlines int, numClocks int, image textureSpec, prefs crtSeqPrefs) uint32 {
numScanlines int, numClocks int, rotation specification.Rotation,
image textureSpec, prefs crtSeqPrefs) uint32 {
// we'll be chaining many shaders together so use internal projection
env.useInternalProj = true
@ -239,12 +241,12 @@ func (sh *crtSequencer) process(env shaderEnvironment, moreProcessing bool,
// leaves pixels from a previous shader in the texture.
sh.seq.Clear(crtSeqMore)
env.srcTextureID = sh.seq.Process(crtSeqMore, func() {
sh.effectsShaderFlipped.(*crtSeqEffectsShader).setAttributesArgs(env, numScanlines, numClocks, prefs)
sh.effectsShaderFlipped.(*crtSeqEffectsShader).setAttributesArgs(env, numScanlines, numClocks, rotation, prefs)
env.draw()
})
} else {
env.useInternalProj = false
sh.effectsShader.(*crtSeqEffectsShader).setAttributesArgs(env, numScanlines, numClocks, prefs)
sh.effectsShader.(*crtSeqEffectsShader).setAttributesArgs(env, numScanlines, numClocks, rotation, prefs)
}
} else {
if moreProcessing {

View file

@ -20,6 +20,7 @@ import (
"github.com/go-gl/gl/v3.2-core/gl"
"github.com/jetsetilly/gopher2600/gui/sdlimgui/shaders"
"github.com/jetsetilly/gopher2600/hardware/television/specification"
)
type crtSeqEffectsShader struct {
@ -47,6 +48,7 @@ type crtSeqEffectsShader struct {
noiseLevel int32
fringingAmount int32
time int32
rotation int32
}
func newCrtSeqEffectsShader(yflip bool) shaderProgram {
@ -79,13 +81,16 @@ func newCrtSeqEffectsShader(yflip bool) shaderProgram {
sh.noiseLevel = gl.GetUniformLocation(sh.handle, gl.Str("NoiseLevel"+"\x00"))
sh.fringingAmount = gl.GetUniformLocation(sh.handle, gl.Str("FringingAmount"+"\x00"))
sh.time = gl.GetUniformLocation(sh.handle, gl.Str("Time"+"\x00"))
sh.rotation = gl.GetUniformLocation(sh.handle, gl.Str("Rotation"+"\x00"))
return sh
}
// most shader attributes can be discerened automatically but number of
// scanlines, clocks and whether to add noise to the image is context sensitive.
func (sh *crtSeqEffectsShader) setAttributesArgs(env shaderEnvironment, numScanlines int, numClocks int, prefs crtSeqPrefs) {
func (sh *crtSeqEffectsShader) setAttributesArgs(env shaderEnvironment,
numScanlines int, numClocks int, rotation specification.Rotation, prefs crtSeqPrefs) {
sh.shader.setAttributes(env)
gl.Uniform2f(sh.screenDim, float32(env.width), float32(env.height))
@ -110,4 +115,5 @@ func (sh *crtSeqEffectsShader) setAttributesArgs(env shaderEnvironment, numScanl
gl.Uniform1f(sh.noiseLevel, float32(prefs.NoiseLevel))
gl.Uniform1f(sh.fringingAmount, float32(prefs.FringingAmount))
gl.Uniform1f(sh.time, float32(time.Now().Nanosecond())/100000000.0)
gl.Uniform1i(sh.rotation, int32(rotation))
}

View file

@ -205,7 +205,7 @@ func (sh *dbgScrShader) setAttributes(env shaderEnvironment) {
prefs.Bevel = false
env.srcTextureID = sh.crt.process(env, true,
sh.img.wm.dbgScr.numScanlines, specification.ClksVisible,
sh.img.wm.dbgScr.numScanlines, specification.ClksVisible, specification.NormalRotation,
sh.img.wm.dbgScr, prefs)
} else {
// if crtPreview is disabled we still go through the crt process. we do
@ -223,7 +223,7 @@ func (sh *dbgScrShader) setAttributes(env shaderEnvironment) {
prefs.Enabled = false
env.srcTextureID = sh.crt.process(env, true,
sh.img.wm.dbgScr.numScanlines, specification.ClksVisible,
sh.img.wm.dbgScr.numScanlines, specification.ClksVisible, specification.NormalRotation,
sh.img.wm.dbgScr, prefs)
}

View file

@ -64,6 +64,6 @@ func (sh *playscrShader) setAttributes(env shaderEnvironment) {
sh.screenshot.process(env, sh.img.playScr)
sh.crt.process(env, false,
sh.img.playScr.visibleScanlines, specification.ClksVisible,
sh.img.playScr.visibleScanlines, specification.ClksVisible, sh.img.screen.rotation.Load().(specification.Rotation),
sh.img.playScr, newCrtSeqPrefs(sh.img.crtPrefs))
}

View file

@ -180,7 +180,7 @@ func (sh *screenshotSequencer) crtProcess(env shaderEnvironment, scalingImage te
}
textureID := sh.crt.process(env, true,
sh.img.playScr.visibleScanlines, specification.ClksVisible,
sh.img.playScr.visibleScanlines, specification.ClksVisible, sh.img.screen.rotation.Load().(specification.Rotation),
sh.img.playScr, prefs)
// reduce exposure count and return if there is still more to do
@ -352,7 +352,7 @@ func (sh *screenshotSequencer) compositeFinalise(env shaderEnvironment, composit
// pass composite image through CRT shaders
textureID := sh.crt.process(env, true,
sh.img.playScr.visibleScanlines, specification.ClksVisible,
sh.img.playScr.visibleScanlines, specification.ClksVisible, sh.img.screen.rotation.Load().(specification.Rotation),
sh, newCrtSeqPrefs(sh.img.crtPrefs))
gl.BindTexture(gl.TEXTURE_2D, textureID)

View file

@ -22,6 +22,7 @@ import (
"github.com/go-gl/gl/v3.2-core/gl"
"github.com/inkyblackness/imgui-go/v4"
"github.com/jetsetilly/gopher2600/gui/fonts"
"github.com/jetsetilly/gopher2600/hardware/television/specification"
)
// note that values from the lazy package will not be updated in the service
@ -248,12 +249,20 @@ func (win *playScr) render() {
// must be called from with a critical section.
func (win *playScr) setScaling() {
rot := win.scr.rotation.Load().(specification.Rotation)
sz := win.img.plt.displaySize()
screenRegion := imgui.Vec2{sz[0], sz[1]}
screenRegion := imgui.Vec2{X: sz[0], Y: sz[1]}
w := float32(win.scr.crit.cropPixels.Bounds().Size().X)
h := float32(win.scr.crit.cropPixels.Bounds().Size().Y)
adjW := w * pixelWidth * win.scr.crit.frameInfo.Spec.AspectBias
adj := win.scr.crit.frameInfo.Spec.AspectBias
if rot == specification.NormalRotation || rot == specification.FlippedRotation {
adj *= pixelWidth
}
adjW := w * adj
var scaling float32
@ -285,7 +294,7 @@ func (win *playScr) setScaling() {
win.imagePosMax = screenRegion.Minus(win.imagePosMin)
win.yscaling = scaling
win.xscaling = scaling * pixelWidth * win.scr.crit.frameInfo.Spec.AspectBias
win.xscaling = scaling * adj
win.scaledWidth = w * win.xscaling
win.scaledHeight = h * win.yscaling

View file

@ -19,6 +19,7 @@ import (
"image"
"image/color"
"sync"
"sync/atomic"
"github.com/jetsetilly/gopher2600/coprocessor"
"github.com/jetsetilly/gopher2600/debugger/govern"
@ -45,6 +46,10 @@ type screen struct {
crit screenCrit
// atmoic access of rotation value. it's more convenient to be able to
// access this atomically, rather than via the screenCrit type
rotation atomic.Value // specification.Rotation
// list of renderers to call from render. renderers are added with
// addTextureRenderer()
renderers []textureRenderer
@ -183,6 +188,7 @@ func newScreen(img *SdlImgui) *screen {
emuWaitAck: make(chan bool),
}
scr.rotation.Store(specification.NormalRotation)
scr.crit.section.Lock()
scr.crit.overlay = reflection.OverlayLabels[reflection.OverlayNone]
@ -214,7 +220,19 @@ func newScreen(img *SdlImgui) *screen {
return scr
}
// SetFPSCap implements the television.FPSCap interface
// SetFPSCap implements the television.PixelRendererRotation interface
func (scr *screen) SetRotation(rotation specification.Rotation) {
scr.rotation.Store(rotation)
scr.crit.section.Lock()
defer scr.crit.section.Unlock()
// the only other component that needs to be aware of the rotation is the
// play screen
scr.img.playScr.resize()
}
// SetFPSCap implements the television.PixelRendererFPSCap interface
func (scr *screen) SetFPSCap(limit bool) {
scr.crit.section.Lock()
defer scr.crit.section.Unlock()

View file

@ -23,6 +23,7 @@ import (
"github.com/inkyblackness/imgui-go/v4"
"github.com/jetsetilly/gopher2600/debugger/govern"
"github.com/jetsetilly/gopher2600/hardware/television/specification"
"github.com/jetsetilly/gopher2600/logger"
"github.com/jetsetilly/gopher2600/notifications"
"github.com/jetsetilly/gopher2600/userinput"
@ -35,6 +36,7 @@ func (img *SdlImgui) serviceKeyboard(ev *sdl.KeyboardEvent) {
}
ctrl := ev.Keysym.Mod&sdl.KMOD_LCTRL == sdl.KMOD_LCTRL || ev.Keysym.Mod&sdl.KMOD_RCTRL == sdl.KMOD_RCTRL
alt := ev.Keysym.Mod&sdl.KMOD_LALT == sdl.KMOD_LALT || ev.Keysym.Mod&sdl.KMOD_RALT == sdl.KMOD_RALT
shift := ev.Keysym.Mod&sdl.KMOD_LSHIFT == sdl.KMOD_LSHIFT || ev.Keysym.Mod&sdl.KMOD_RSHIFT == sdl.KMOD_RSHIFT
// enable window searching based on keyboard modifiers
@ -79,6 +81,23 @@ func (img *SdlImgui) serviceKeyboard(ev *sdl.KeyboardEvent) {
img.quit()
}
case sdl.SCANCODE_LEFT:
if alt {
img.screen.SetRotation(specification.LeftRotation)
}
case sdl.SCANCODE_RIGHT:
if alt {
img.screen.SetRotation(specification.RightRotation)
}
case sdl.SCANCODE_UP:
if alt {
img.screen.SetRotation(specification.NormalRotation)
}
case sdl.SCANCODE_DOWN:
if alt {
img.screen.SetRotation(specification.FlippedRotation)
}
default:
handled = false
}

View file

@ -36,6 +36,8 @@ uniform float NoiseLevel;
uniform float FringingAmount;
uniform float Time;
// rotation values are the values in hardware/television/specification/rotation.go
uniform int Rotation;
// Gold Noise taken from: https://www.shadertoy.com/view/ltB3zD
// Coprighted to dcerisano@standard3d.com not sure of the licence
@ -108,12 +110,31 @@ void main() {
uv = mix(curve(uv), uv, m);
}
// bevel (if in use) should bend the same as the screen
// shineUV are the coordinates we use when applying the shine effect. we
// don't want this to be rotated
vec2 shineUV = uv;
// apply rotation
float textureRatio = ScreenDim.x / ScreenDim.y;
float rads = 1.5708 * Rotation;
uv -= 0.5;
/* if (mod(Rotation, 2) == 1) { */
/* uv.x /= 0.5; */
/* } */
uv = vec2(
cos(rads) * uv.x + sin(rads) * uv.y,
cos(rads) * uv.y - sin(rads) * uv.x
);
uv += 0.5;
// bevel (if in use) should be curved and rotated the same as the screen texture
vec2 uv_bevel = uv;
// reduce size of main display if bevel is active
// reduce size of main texture and shine if bevel is active. note that we don't reduce
// the size of the bevel but we do rotate it later
if (Bevel == 1) {
uv = (uv - 0.5) * 1.1 + 0.5;
shineUV = (shineUV - 0.5) * 1.1 + 0.5;
}
// after this point every UV reference is to the curved UV
@ -211,8 +232,8 @@ void main() {
// shine affect
if (Shine == 1) {
vec2 uv_shine = (uv - 0.5) * 1.2 + 0.5;
float shine = (1.0-uv_shine.s)*(1.0-uv_shine.t);
vec2 shineUV = (shineUV - 0.5) * 1.2 + 0.5;
float shine = (1.0-shineUV.s)*(1.0-shineUV.t);
Crt_Color = mix(Crt_Color, vec4(1.0), shine*0.05);
}

View file

@ -23,6 +23,7 @@ import (
"github.com/jetsetilly/gopher2600/environment"
"github.com/jetsetilly/gopher2600/hardware/memory/cartridge/mapper"
"github.com/jetsetilly/gopher2600/hardware/memory/memorymap"
"github.com/jetsetilly/gopher2600/hardware/television/specification"
"github.com/jetsetilly/gopher2600/logger"
)
@ -198,6 +199,9 @@ type state struct {
// the stream has failed because bad data has been encountered
streamFail bool
// the most recent rotation instruction. can only ever changes for version 2 streams
rotation byte
}
// what part of the OSD is currently being display.
@ -285,6 +289,10 @@ func NewMoviecart(env *environment.Environment, loader cartridgeloader.Loader) (
// field starts off in the end position
cart.state.streamIndex = 1
// read next field straight away. this has the advantage of triggering the
// first screen rotation in time for the attract screen
cart.nextField()
return cart, nil
}
@ -887,6 +895,21 @@ func (cart *Moviecart) nextField() {
// version number
switch cart.state.streamBuffer[cart.state.streamIndex][4] & 0x80 {
case 0x80:
rotation := cart.state.streamBuffer[cart.state.streamIndex][4] & 0b11
if rotation != cart.state.rotation {
switch rotation {
case 0b00:
cart.env.TV.SetRotation(specification.NormalRotation)
case 0b01:
cart.env.TV.SetRotation(specification.RightRotation)
case 0b10:
cart.env.TV.SetRotation(specification.FlippedRotation)
case 0b11:
cart.env.TV.SetRotation(specification.LeftRotation)
}
cart.state.rotation = rotation
}
cart.state.format[cart.state.streamIndex].vsync = byte(cart.state.streamBuffer[cart.state.streamIndex][9])
cart.state.format[cart.state.streamIndex].vblank = byte(cart.state.streamBuffer[cart.state.streamIndex][10])
cart.state.format[cart.state.streamIndex].overscan = byte(cart.state.streamBuffer[cart.state.streamIndex][11])

View file

@ -17,6 +17,7 @@ package television
import (
"github.com/jetsetilly/gopher2600/hardware/television/signal"
"github.com/jetsetilly/gopher2600/hardware/television/specification"
)
// PixelRenderer implementations displays, or otherwise works with, visual
@ -96,9 +97,17 @@ type PixelRenderer interface {
EndRendering() error
}
// PixelRendererRotation is an extension to the PixelRenderer interface. Pixel
// renderes that implement this interface can show the television image in a
// rotated aspect. Not all pixel renderers need to worry about rotation.
type PixelRendererRotation interface {
SetRotation(specification.Rotation)
}
// PixelRendererFPSCap is an extension to the PixelRenderer interface. Pixel
// renderers that implement this interface will be notified when the
// television's frame capping policy is changed
// television's frame capping policy is changed. Not all pixel renderers need to
// worry about frame rate.
type PixelRendererFPSCap interface {
SetFPSCap(limit bool)
}

View file

@ -0,0 +1,46 @@
// This file is part of Gopher2600.
//
// Gopher2600 is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Gopher2600 is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Gopher2600. If not, see <https://www.gnu.org/licenses/>.
// Package specification contains the definitions, including colour, of the PAL
// and NTSC television protocols supported by the emulation.
package specification
// Rotation indicates the orienation of the television
type Rotation int
// List of valid Rotation values. The values are arranged so that they can be
// thought of as the number of 90 degrees turns to get to that position.
// Alternatively, multiplying the Rotation value by 1.5708 will give the number
// of radians required for the rotation
const (
NormalRotation Rotation = iota
LeftRotation
FlippedRotation
RightRotation
)
func (r Rotation) String() string {
switch r {
case NormalRotation:
return "normal"
case LeftRotation:
return "left"
case FlippedRotation:
return "flipped"
case RightRotation:
return "right"
}
return "unknown rotation"
}

View file

@ -927,3 +927,13 @@ func (tv *Television) GetLastSignal() signal.SignalAttributes {
func (tv *Television) GetCoords() coords.TelevisionCoords {
return tv.state.GetCoords()
}
// SetRotation instructs the television to a different orientation. In truth,
// the television just forwards the request to the pixel renderers.
func (tv *Television) SetRotation(rotation specification.Rotation) {
for _, r := range tv.renderers {
if s, ok := r.(PixelRendererRotation); ok {
s.SetRotation(rotation)
}
}
}