mirror of
https://github.com/JetSetIlly/Gopher2600.git
synced 2024-06-02 20:18:20 -04:00
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:
parent
54e3abd04f
commit
8ccae4e70f
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
46
hardware/television/specification/rotation.go
Normal file
46
hardware/television/specification/rotation.go
Normal 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"
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue