Skip to content

Commit

Permalink
TV rotation
Browse files Browse the repository at this point in the history
cartridges can control TV rotation if necessary. only movie cart does
this for now

alt + <cursor keys> manually flips the screen rotation
  • Loading branch information
JetSetIlly committed Oct 7, 2023
1 parent 54e3abd commit 8ccae4e
Show file tree
Hide file tree
Showing 14 changed files with 183 additions and 18 deletions.
2 changes: 2 additions & 0 deletions environment/environment.go
Expand Up @@ -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"
)

Expand All @@ -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
Expand Down
8 changes: 5 additions & 3 deletions gui/sdlimgui/glsl_crtseq.go
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
8 changes: 7 additions & 1 deletion gui/sdlimgui/glsl_crtseq_effects.go
Expand Up @@ -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 {
Expand Down Expand Up @@ -47,6 +48,7 @@ type crtSeqEffectsShader struct {
noiseLevel int32
fringingAmount int32
time int32
rotation int32
}

func newCrtSeqEffectsShader(yflip bool) shaderProgram {
Expand Down Expand Up @@ -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))
Expand All @@ -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))
}
4 changes: 2 additions & 2 deletions gui/sdlimgui/glsl_dbgscr.go
Expand Up @@ -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
Expand All @@ -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)
}

Expand Down
2 changes: 1 addition & 1 deletion gui/sdlimgui/glsl_playscr.go
Expand Up @@ -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))
}
4 changes: 2 additions & 2 deletions gui/sdlimgui/glsl_screenshot.go
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down
15 changes: 12 additions & 3 deletions gui/sdlimgui/playscr.go
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
20 changes: 19 additions & 1 deletion gui/sdlimgui/screen.go
Expand Up @@ -19,6 +19,7 @@ import (
"image"
"image/color"
"sync"
"sync/atomic"

"github.com/jetsetilly/gopher2600/coprocessor"
"github.com/jetsetilly/gopher2600/debugger/govern"
Expand All @@ -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
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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()
Expand Down
19 changes: 19 additions & 0 deletions gui/sdlimgui/service_keyboard.go
Expand Up @@ -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"
Expand All @@ -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
Expand Down Expand Up @@ -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
}
Expand Down
29 changes: 25 additions & 4 deletions gui/sdlimgui/shaders/crt_effects.frag
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
}

Expand Down
23 changes: 23 additions & 0 deletions hardware/memory/cartridge/moviecart/moviecart.go
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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])
Expand Down
11 changes: 10 additions & 1 deletion hardware/television/protocol.go
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down

0 comments on commit 8ccae4e

Please sign in to comment.