From fde4a24158411f5db0dedade7dba94ee3b744700 Mon Sep 17 00:00:00 2001 From: giongto35 Date: Sat, 19 Oct 2019 02:29:07 +0800 Subject: [PATCH] Merge aspect ratio to master (#116) * Add frame scaling support (#107) * Update README.md with additional info about Windows builds * Add frame scaling * Add and enable bilinear scaling (#109) * Add and enable bilinear scaling * Use Go x/image lib for interpolation * Reformat the code goimport/gofmt * Move worker config into the pkg/worker directory * Change separator in the save file path allowing it to work on Windows (#113) --- cmd/overworker/main.go | 4 +- go.mod | 1 + go.sum | 3 + pkg/config/config.go | 3 + pkg/{ => config}/worker/config.go | 19 ++- pkg/emulator/libretro/image/color.go | 36 +++++ pkg/emulator/libretro/image/draw.go | 18 +++ pkg/emulator/libretro/image/scale.go | 135 ++++++++++++++++ pkg/emulator/libretro/nanoarch/naemulator.go | 12 +- pkg/emulator/libretro/nanoarch/nanoarch.go | 156 ++++--------------- pkg/emulator/type.go | 3 + pkg/worker/handlers.go | 10 +- pkg/worker/overworker.go | 8 +- pkg/worker/room/room.go | 44 +++++- 14 files changed, 312 insertions(+), 140 deletions(-) rename pkg/{ => config}/worker/config.go (65%) create mode 100644 pkg/emulator/libretro/image/color.go create mode 100644 pkg/emulator/libretro/image/draw.go create mode 100644 pkg/emulator/libretro/image/scale.go diff --git a/cmd/overworker/main.go b/cmd/overworker/main.go index eb61178f..ba6a1368 100644 --- a/cmd/overworker/main.go +++ b/cmd/overworker/main.go @@ -7,6 +7,8 @@ import ( "os/signal" "time" + config "github.com/giongto35/cloud-game/pkg/config/worker" + "github.com/giongto35/cloud-game/pkg/util/logging" "github.com/giongto35/cloud-game/pkg/worker" "github.com/golang/glog" @@ -16,7 +18,7 @@ import ( func main() { rand.Seed(time.Now().UTC().UnixNano()) - cfg := worker.NewDefaultConfig() + cfg := config.NewDefaultConfig() cfg.AddFlags(pflag.CommandLine) logging.Init() diff --git a/go.mod b/go.mod index f602fbb4..531dadef 100644 --- a/go.mod +++ b/go.mod @@ -11,5 +11,6 @@ require ( github.com/pion/webrtc/v2 v2.1.2 github.com/prometheus/client_golang v1.1.0 github.com/spf13/pflag v1.0.3 + golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 gopkg.in/hraban/opus.v2 v2.0.0-20180426093920-0f2e0b4fc6cd ) diff --git a/go.sum b/go.sum index 5946c707..562020ae 100644 --- a/go.sum +++ b/go.sum @@ -152,7 +152,10 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5 h1:58fnuSXlxZmFdJyvtTFVmV golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067 h1:KYGJGHOQy8oSi1fDlSpcZF0+juKwk/hEMv5SiwHogR0= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U= +golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= diff --git a/pkg/config/config.go b/pkg/config/config.go index 0bc77745..71e4cc7c 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -47,6 +47,9 @@ type EmulatorMeta struct { Height int AudioSampleRate int Fps int + BaseWidth int + BaseHeight int + Ratio float64 } var EmulatorConfig = map[string]EmulatorMeta{ diff --git a/pkg/worker/config.go b/pkg/config/worker/config.go similarity index 65% rename from pkg/worker/config.go rename to pkg/config/worker/config.go index e7c99ed9..8101c4f0 100644 --- a/pkg/worker/config.go +++ b/pkg/config/worker/config.go @@ -9,13 +9,23 @@ type Config struct { Port int OverlordAddress string + // video + Scale int + DisableCustomSize bool + Width int + Height int + MonitoringConfig monitoring.ServerMonitoringConfig } func NewDefaultConfig() Config { return Config{ - Port: 8800, - OverlordAddress: "ws://localhost:8000/wso", + Port: 8800, + OverlordAddress: "ws://localhost:8000/wso", + Scale: 1, + DisableCustomSize: false, + Width: 320, + Height: 240, MonitoringConfig: monitoring.ServerMonitoringConfig{ Port: 6601, URLPrefix: "/worker", @@ -28,6 +38,11 @@ func (c *Config) AddFlags(fs *pflag.FlagSet) *Config { fs.IntVarP(&c.Port, "port", "", 8800, "OverWorker server port") fs.StringVarP(&c.OverlordAddress, "overlordhost", "", c.OverlordAddress, "OverWorker URL to connect") + fs.IntVarP(&c.Scale, "scale", "s", c.Scale, "Set output viewport scale factor") + fs.BoolVarP(&c.DisableCustomSize, "disable-custom-size", "", c.DisableCustomSize, "Disable custom size") + fs.IntVarP(&c.Width, "width", "w", c.Width, "Set custom viewport width") + fs.IntVarP(&c.Height, "height", "h", c.Height, "Set custom viewport height") + fs.BoolVarP(&c.MonitoringConfig.MetricEnabled, "monitoring.metric", "m", c.MonitoringConfig.MetricEnabled, "Enable prometheus metric for server") fs.BoolVarP(&c.MonitoringConfig.ProfilingEnabled, "monitoring.pprof", "p", c.MonitoringConfig.ProfilingEnabled, "Enable golang pprof for server") fs.IntVarP(&c.MonitoringConfig.Port, "monitoring.port", "", c.MonitoringConfig.Port, "Monitoring server port") diff --git a/pkg/emulator/libretro/image/color.go b/pkg/emulator/libretro/image/color.go new file mode 100644 index 00000000..833782bf --- /dev/null +++ b/pkg/emulator/libretro/image/color.go @@ -0,0 +1,36 @@ +package image + +import ( + "image/color" +) + +const ( + // BIT_FORMAT_SHORT_5_5_5_1 has 5 bits R, 5 bits G, 5 bits B, 1 bit alpha + BIT_FORMAT_SHORT_5_5_5_1 = iota + // BIT_FORMAT_INT_8_8_8_8_REV has 8 bits R, 8 bits G, 8 bits B, 8 bit alpha + BIT_FORMAT_INT_8_8_8_8_REV + // BIT_FORMAT_SHORT_5_6_5 has 5 bits R, 6 bits G, 5 bits + BIT_FORMAT_SHORT_5_6_5 +) + +type Format func(data []byte, index int) color.RGBA + +func rgb565(data []byte, index int) color.RGBA { + pixel := (int)(data[index]) + ((int)(data[index+1]) << 8) + + return color.RGBA{ + R: byte(((pixel>>11)*255 + 15) / 31), + G: byte((((pixel>>5)&0x3F)*255 + 31) / 63), + B: byte(((pixel&0x1F)*255 + 15) / 31), + A: 255, + } +} + +func rgba8888(data []byte, index int) color.RGBA { + return color.RGBA{ + R: data[index+2], + G: data[index+1], + B: data[index], + A: data[index+3], + } +} diff --git a/pkg/emulator/libretro/image/draw.go b/pkg/emulator/libretro/image/draw.go new file mode 100644 index 00000000..5fd28a3c --- /dev/null +++ b/pkg/emulator/libretro/image/draw.go @@ -0,0 +1,18 @@ +package image + +import ( + "image" +) + +func DrawRgbaImage(pixFormat int, scaleType int, w int, h int, packedW int, vw int, vh int, bpp int, data []byte, image *image.RGBA) { + switch pixFormat { + case BIT_FORMAT_SHORT_5_6_5: + Resize(scaleType, rgb565, w, h, packedW, vw, vh, bpp, data, image) + case BIT_FORMAT_INT_8_8_8_8_REV: + Resize(scaleType, rgba8888, w, h, packedW, vw, vh, bpp, data, image) + case BIT_FORMAT_SHORT_5_5_5_1: + fallthrough + default: + image = nil + } +} diff --git a/pkg/emulator/libretro/image/scale.go b/pkg/emulator/libretro/image/scale.go new file mode 100644 index 00000000..300aada0 --- /dev/null +++ b/pkg/emulator/libretro/image/scale.go @@ -0,0 +1,135 @@ +package image + +import ( + "image" + "image/color" + + "golang.org/x/image/draw" +) + +const ( + // skips image interpolation + ScaleSkip = -1 + // initial image interpolation algorithm + ScaleOld = 0 + // nearest neighbour interpolation + ScaleNearestNeighbour = 1 + // bilinear interpolation + ScaleBilinear = 2 +) + +func Resize(scaleType int, fn Format, w int, h int, packedW int, vw int, vh int, bpp int, data []byte, out *image.RGBA) { + + // !to implement own image interfaces img.Pix = bytes[] + src := image.NewRGBA(image.Rect(0, 0, w, h)) + toRgba(fn, w, h, packedW, bpp, data, src) + + // !to do set it once instead switching on each iteration + // !to do skip resize if w=vw h=vh + switch scaleType { + case ScaleSkip: + skip(fn, w, h, packedW, vw, vh, bpp, data, src, out) + case ScaleNearestNeighbour: + draw.NearestNeighbor.Scale(out, out.Bounds(), src, src.Bounds(), draw.Src, nil) + //nearest(fn, w, h, packedW, vw, vh, bpp, data, src, out) + case ScaleBilinear: + draw.ApproxBiLinear.Scale(out, out.Bounds(), src, src.Bounds(), draw.Src, nil) + //bilinear(fn, w, h, packedW, vw, vh, bpp, data, src, out) + case ScaleOld: + fallthrough + default: + old(fn, w, h, packedW, vw, vh, bpp, data, src, out) + } +} + +func old(fn Format, w int, h int, packedW int, vw int, vh int, bpp int, data []byte, _ *image.RGBA, out *image.RGBA) { + seek := 0 + + scaleWidth := float64(vw) / float64(w) + scaleHeight := float64(vh) / float64(h) + + for y := 0; y < h; y++ { + y2 := int(float64(y) * scaleHeight) + for x := 0; x < packedW; x++ { + x2 := int(float64(x) * scaleWidth) + if x2 < vw { + out.Set(x2, y2, fn(data, seek)) + } + + seek += bpp + } + } +} + +func skip(fn Format, w int, h int, packedW int, _ int, _ int, bpp int, data []byte, _ *image.RGBA, out *image.RGBA) { + for y := 0; y < h; y++ { + for x := 0; x < w; x++ { + index := (y * packedW) + x + index *= bpp + out.Set(x, y, fn(data, index)) + } + } +} + +func nearest(fn Format, w int, h int, packedW int, vw int, vh int, bpp int, data []byte, _ *image.RGBA, out *image.RGBA) { + xRatio := ((w << 16) / vw) + 1 + yRatio := ((h << 16) / vh) + 1 + + for y := 0; y < vh; y++ { + y2 := (y * yRatio) >> 16 + for x := 0; x < vw; x++ { + x2 := (x * xRatio) >> 16 + + index := (y2 * packedW) + x2 + index *= bpp + + out.Set(x, y, fn(data, index)) + } + } +} + +// This implementation has some color bleeding issues +func bilinear(fn Format, w int, h int, packedW int, vw int, vh int, bpp int, data []byte, _ *image.RGBA, out *image.RGBA) { + xRatio := float64(w-1) / float64(vw) + yRatio := float64(h-1) / float64(vh) + + for y := 0; y < vh; y++ { + y2 := int(yRatio * float64(y)) + for x := 0; x < vw; x++ { + x2 := int(xRatio * float64(x)) + + w := (xRatio * float64(x)) - float64(x2) + h := (yRatio * float64(y)) - float64(y2) + + index := (y2 * packedW) + x2 + + a := fn(data, index*bpp) + b := fn(data, (index+1)*bpp) + c := fn(data, (index+packedW)*bpp) + d := fn(data, (index+packedW+1)*bpp) + + out.Set(x, y, color.RGBA{ + // don't sink the boat + R: byte(float64(a.R)*(1-w)*(1-h) + float64(b.R)*w*(1-h) + float64(c.R)*h*(1-w) + float64(d.R)*w*h), + G: byte(float64(a.G)*(1-w)*(1-h) + float64(b.G)*w*(1-h) + float64(c.G)*h*(1-w) + float64(d.G)*w*h), + B: byte(float64(a.B)*(1-w)*(1-h) + float64(b.B)*w*(1-h) + float64(c.B)*h*(1-w) + float64(d.B)*w*h), + //A: byte(float64(a.A)*(1-w)*(1-h) + float64(b.A)*w*(1-h) + float64(c.A)*h*(1-w) + float64(d.A)*w*h), + }) + } + } +} + +func toRgba(fn Format, w int, h int, packedW int, bpp int, data []byte, image *image.RGBA) { + for y := 0; y < h; y++ { + for x := 0; x < w; x++ { + index := (y*packedW + x) * bpp + c := fn(data, index) + i := (y-image.Rect.Min.Y)*image.Stride + (x-image.Rect.Min.X)*4 + s := image.Pix[i : i+4 : i+4] + s[0] = c.R + s[1] = c.G + s[2] = c.B + s[3] = c.A + } + } +} diff --git a/pkg/emulator/libretro/nanoarch/naemulator.go b/pkg/emulator/libretro/nanoarch/naemulator.go index 21ae7baa..ad982523 100644 --- a/pkg/emulator/libretro/nanoarch/naemulator.go +++ b/pkg/emulator/libretro/nanoarch/naemulator.go @@ -67,10 +67,6 @@ var outputImg *image.RGBA // NAEmulator implements CloudEmulator interface based on NanoArch(golang RetroArch) func NewNAEmulator(etype string, roomID string, imageChannel chan<- *image.RGBA, audioChannel chan<- []int16, inputChannel <-chan int) *naEmulator { meta := config.EmulatorConfig[etype] - ewidth = meta.Width - eheight = meta.Height - // outputImg is tmp img used for decoding and reuse in encoding flow - outputImg = image.NewRGBA(image.Rect(0, 0, ewidth, eheight)) return &naEmulator{ meta: meta, @@ -117,6 +113,14 @@ func (na *naEmulator) LoadMeta(path string) config.EmulatorMeta { return na.meta } +func (na *naEmulator) SetViewport(width int, height int) { + // outputImg is tmp img used for decoding and reuse in encoding flow + outputImg = image.NewRGBA(image.Rect(0, 0, width, height)) + + ewidth = width + eheight = height +} + func (na *naEmulator) Start() { na.playGame(na.gamePath) ticker := time.NewTicker(time.Second / 60) diff --git a/pkg/emulator/libretro/nanoarch/nanoarch.go b/pkg/emulator/libretro/nanoarch/nanoarch.go index 768f78b5..1e43e311 100644 --- a/pkg/emulator/libretro/nanoarch/nanoarch.go +++ b/pkg/emulator/libretro/nanoarch/nanoarch.go @@ -4,14 +4,13 @@ import ( "bufio" "errors" "fmt" - "image" - "image/color" "log" "os" "os/user" - "reflect" "sync" "unsafe" + + "github.com/giongto35/cloud-game/pkg/emulator/libretro/image" ) /* @@ -63,8 +62,6 @@ var video struct { bpp uint32 } -var scale = 3.0 - const bufSize = 1024 * 4 const joypadNumKeys = int(C.RETRO_DEVICE_ID_JOYPAD_R3 + 1) @@ -85,15 +82,6 @@ var bindRetroKeys = map[int]int{ 9: C.RETRO_DEVICE_ID_JOYPAD_RIGHT, } -const ( - // BIT_FORMAT_SHORT_5_5_5_1 has 5 bits R, 5 bits G, 5 bits B, 1 bit alpha - BIT_FORMAT_SHORT_5_5_5_1 = iota - // BIT_FORMAT_INT_8_8_8_8_REV has 8 bits R, 8 bits G, 8 bits B, 8 bit alpha - BIT_FORMAT_INT_8_8_8_8_REV - // BIT_FORMAT_SHORT_5_6_5 has 5 bits R, 6 bits G, 5 bits - BIT_FORMAT_SHORT_5_6_5 -) - type CloudEmulator interface { Start(path string) SaveGame(saveExtraFunc func() error) error @@ -102,109 +90,25 @@ type CloudEmulator interface { Close() } -func resizeToAspect(ratio float64, sw float64, sh float64) (dw float64, dh float64) { - if ratio <= 0 { - ratio = sw / sh - } - - if sw/sh < 1.0 { - dw = dh * ratio - dh = sh - } else { - dw = sw - dh = dw / ratio - } - return -} - //export coreVideoRefresh func coreVideoRefresh(data unsafe.Pointer, width C.unsigned, height C.unsigned, pitch C.size_t) { - bytesPerRow := int(uint32(pitch) / video.bpp) - - if data != nil { - NAEmulator.imageChannel <- toImageRGBA(data, bytesPerRow, int(width), int(height)) - } -} - -// toImageRGBA convert nanoarch 2d array to image.RGBA -func toImageRGBA(data unsafe.Pointer, bytesPerRow int, inputWidth, inputHeight int) *image.RGBA { - // Convert unsafe Pointer to bytes array - var bytes []byte - - // Convert bytes array to image - // TODO: Reduce overhead of copying to bytes array by accessing unsafe.Pointer directly - sh := (*reflect.SliceHeader)(unsafe.Pointer(&bytes)) - sh.Data = uintptr(data) - sh.Len = bytesPerRow * inputHeight * 4 - sh.Cap = bytesPerRow * inputHeight * 4 - - if video.pixFmt == BIT_FORMAT_SHORT_5_6_5 { - return to565Image(data, bytes, bytesPerRow, inputWidth, inputHeight) - } else if video.pixFmt == BIT_FORMAT_INT_8_8_8_8_REV { - return to8888Image(data, bytes, bytesPerRow, inputWidth, inputHeight) - } - return nil -} - -func to8888Image(data unsafe.Pointer, bytes []byte, bytesPerRow int, inputWidth, inputHeight int) *image.RGBA { - seek := 0 - - // scaleWidth and scaleHeight is the scale - scaleWidth := float64(ewidth) / float64(inputWidth) - scaleHeight := float64(eheight) / float64(inputHeight) - - for y := 0; y < inputHeight; y++ { - for x := 0; x < bytesPerRow; x++ { - xx := int(float64(x) * scaleWidth) - yy := int(float64(y) * scaleHeight) - if xx < ewidth { - b8 := bytes[seek] - g8 := bytes[seek+1] - r8 := bytes[seek+2] - a8 := bytes[seek+3] - - outputImg.Set(xx, yy, color.RGBA{byte(r8), byte(g8), byte(b8), byte(a8)}) - } - - seek += 4 - } + // some cores can return nothing + if data == nil { + return } - // TODO: Resize Image - return outputImg -} + // calculate real frame width in pixels from packed data (realWidth >= width) + packedWidth := int(uint32(pitch) / video.bpp) -func to565Image(data unsafe.Pointer, bytes []byte, bytesPerRow int, inputWidth, inputHeight int) *image.RGBA { - seek := 0 + // convert data from C + bytes := int(height) * packedWidth * int(video.bpp) + data_ := (*[1 << 30]byte)(data)[:bytes:bytes] - // scaleWidth and scaleHeight is the scale - scaleWidth := float64(ewidth) / float64(inputWidth) - scaleHeight := float64(eheight) / float64(inputHeight) + // !to move it on the other side of the channel + image.DrawRgbaImage(int(video.pixFmt), image.ScaleBilinear, int(width), int(height), + packedWidth, ewidth, eheight, int(video.bpp), data_, outputImg) - for y := 0; y < inputHeight; y++ { - for x := 0; x < bytesPerRow; x++ { - xx := int(float64(x) * scaleWidth) - yy := int(float64(y) * scaleHeight) - if xx < ewidth { - var bi int - bi = (int)(bytes[seek]) + ((int)(bytes[seek+1]) << 8) - b5 := bi & 0x1F - g6 := (bi >> 5) & 0x3F - r5 := (bi >> 11) - - b8 := (b5*255 + 15) / 31 - g8 := (g6*255 + 31) / 63 - r8 := (r5*255 + 15) / 31 - - outputImg.Set(xx, yy, color.RGBA{byte(r8), byte(g8), byte(b8), 255}) - } - - seek += 2 - } - } - - // TODO: Resize Image - return outputImg + NAEmulator.imageChannel <- outputImg } //export coreInputPoll @@ -434,15 +338,25 @@ func coreLoadGame(filename string) { C.bridge_retro_get_system_av_info(retroGetSystemAVInfo, &avi) + // Append the library name to the window title. + NAEmulator.meta.AudioSampleRate = int(avi.timing.sample_rate) + NAEmulator.meta.Fps = int(avi.timing.fps) + NAEmulator.meta.BaseWidth = int(avi.geometry.base_width) + NAEmulator.meta.BaseHeight = int(avi.geometry.base_height) + // set aspect ratio + /* Nominal aspect ratio of game. If aspect_ratio is <= 0.0, + an aspect ratio of base_width / base_height is assumed. + * A frontend could override this setting, if desired. */ + ratio := float64(avi.geometry.aspect_ratio) + if ratio <= 0.0 { + ratio = float64(avi.geometry.base_width) / float64(avi.geometry.base_height) + } + NAEmulator.meta.Ratio = ratio + fmt.Println("-----------------------------------") fmt.Println("--- System audio and video info ---") fmt.Println("-----------------------------------") - fmt.Println(" Aspect ratio: ", avi.geometry.aspect_ratio) - /* Nominal aspect ratio of game. If - * aspect_ratio is <= 0.0, an aspect ratio - * of base_width / base_height is assumed. - * A frontend could override this setting, - * if desired. */ + fmt.Println(" Aspect ratio: ", ratio) fmt.Println(" Base width: ", avi.geometry.base_width) /* Nominal video width of game. */ fmt.Println(" Base height: ", avi.geometry.base_height) /* Nominal video height of game. */ fmt.Println(" Max width: ", avi.geometry.max_width) /* Maximum possible width of game. */ @@ -450,10 +364,6 @@ func coreLoadGame(filename string) { fmt.Println(" Sample rate: ", avi.timing.sample_rate) /* Sampling rate of audio. */ fmt.Println(" FPS: ", avi.timing.fps) /* FPS of video content. */ fmt.Println("-----------------------------------") - - // Append the library name to the window title. - NAEmulator.meta.AudioSampleRate = int(avi.timing.sample_rate) - NAEmulator.meta.Fps = int(avi.timing.fps) } // serializeSize returns the amount of data the implementation requires to serialize @@ -500,15 +410,15 @@ func nanoarchRun() { func videoSetPixelFormat(format uint32) C.bool { switch format { case C.RETRO_PIXEL_FORMAT_0RGB1555: - video.pixFmt = BIT_FORMAT_SHORT_5_5_5_1 + video.pixFmt = image.BIT_FORMAT_SHORT_5_5_5_1 video.bpp = 2 break case C.RETRO_PIXEL_FORMAT_XRGB8888: - video.pixFmt = BIT_FORMAT_INT_8_8_8_8_REV + video.pixFmt = image.BIT_FORMAT_INT_8_8_8_8_REV video.bpp = 4 break case C.RETRO_PIXEL_FORMAT_RGB565: - video.pixFmt = BIT_FORMAT_SHORT_5_6_5 + video.pixFmt = image.BIT_FORMAT_SHORT_5_6_5 video.bpp = 2 break default: diff --git a/pkg/emulator/type.go b/pkg/emulator/type.go index bdcd0d12..b2ae62e3 100644 --- a/pkg/emulator/type.go +++ b/pkg/emulator/type.go @@ -7,6 +7,9 @@ type CloudEmulator interface { // LoadMeta returns meta data of emulator. Refer below LoadMeta(path string) config.EmulatorMeta // Start is called after LoadGame + + SetViewport(width int, height int) + Start() // SaveGame save game state, saveExtraFunc is callback to do extra step. Ex: save to google cloud SaveGame(saveExtraFunc func() error) error diff --git a/pkg/worker/handlers.go b/pkg/worker/handlers.go index f73d7e17..d4014380 100644 --- a/pkg/worker/handlers.go +++ b/pkg/worker/handlers.go @@ -6,6 +6,8 @@ import ( "path" "time" + "github.com/giongto35/cloud-game/pkg/config/worker" + "github.com/giongto35/cloud-game/pkg/util" "github.com/giongto35/cloud-game/pkg/webrtc" storage "github.com/giongto35/cloud-game/pkg/worker/cloud-storage" @@ -26,6 +28,7 @@ type Handler struct { oClient *OverlordClient // Raw address of overlord overlordHost string + cfg worker.Config // Rooms map : RoomID -> Room rooms map[string]*room.Room // ID of the current server globalwise @@ -37,7 +40,7 @@ type Handler struct { } // NewHandler returns a new server -func NewHandler(overlordHost string) *Handler { +func NewHandler(cfg worker.Config) *Handler { // Create offline storage folder createOfflineStorage() @@ -46,7 +49,8 @@ func NewHandler(overlordHost string) *Handler { return &Handler{ rooms: map[string]*room.Room{}, sessions: map[string]*Session{}, - overlordHost: overlordHost, + overlordHost: cfg.OverlordAddress, + cfg: cfg, onlineStorage: onlineStorage, } } @@ -135,7 +139,7 @@ func (h *Handler) createNewRoom(gameName string, roomID string, playerIndex int, // or the roomID doesn't have any running sessions (room was closed) // we spawn a new room if roomID == "" || !h.isRoomRunning(roomID) { - room := room.NewRoom(roomID, gameName, videoEncoderType, h.onlineStorage) + room := room.NewRoom(roomID, gameName, videoEncoderType, h.onlineStorage, h.cfg) // TODO: Might have race condition h.rooms[room.ID] = room return room diff --git a/pkg/worker/overworker.go b/pkg/worker/overworker.go index 89a85396..62468b35 100644 --- a/pkg/worker/overworker.go +++ b/pkg/worker/overworker.go @@ -8,18 +8,20 @@ import ( "net/http" "strconv" + "github.com/giongto35/cloud-game/pkg/config/worker" + "github.com/giongto35/cloud-game/pkg/monitoring" "github.com/golang/glog" ) type OverWorker struct { ctx context.Context - cfg Config + cfg worker.Config monitoringServer *monitoring.ServerMonitoring } -func New(ctx context.Context, cfg Config) *OverWorker { +func New(ctx context.Context, cfg worker.Config) *OverWorker { return &OverWorker{ ctx: ctx, cfg: cfg, @@ -50,7 +52,7 @@ func (o *OverWorker) Shutdown() { // initializeWorker setup a worker func (o *OverWorker) initializeWorker() { - worker := NewHandler(o.cfg.OverlordAddress) + worker := NewHandler(o.cfg) defer func() { log.Println("Close worker") diff --git a/pkg/worker/room/room.go b/pkg/worker/room/room.go index a8c6dff0..6e03ae31 100644 --- a/pkg/worker/room/room.go +++ b/pkg/worker/room/room.go @@ -4,6 +4,7 @@ import ( "image" "io/ioutil" "log" + "math" "math/rand" "os" "runtime" @@ -11,6 +12,8 @@ import ( "strings" "sync" + "github.com/giongto35/cloud-game/pkg/config/worker" + "github.com/giongto35/cloud-game/pkg/config" "github.com/giongto35/cloud-game/pkg/emulator" "github.com/giongto35/cloud-game/pkg/emulator/libretro/nanoarch" @@ -49,8 +52,10 @@ type Room struct { //meta emulator.Meta } +const separator = "___" + // NewRoom creates a new room -func NewRoom(roomID string, gameName string, videoEncoderType string, onlineStorage *storage.Client) *Room { +func NewRoom(roomID string, gameName string, videoEncoderType string, onlineStorage *storage.Client, cfg worker.Config) *Room { // If no roomID is given, generate it from gameName // If the is roomID, get gameName from roomID if roomID == "" { @@ -101,7 +106,27 @@ func NewRoom(roomID string, gameName string, videoEncoderType string, onlineStor room.director = getEmulator(emuName, roomID, imageChannel, audioChannel, inputChannel) gameMeta := room.director.LoadMeta(game.Path) - go room.startVideo(gameMeta.Width, gameMeta.Height, videoEncoderType) + var nwidth, nheight int + if !cfg.DisableCustomSize { + baseAspectRatio := float64(gameMeta.BaseWidth) / float64(gameMeta.Height) + nwidth, nheight = resizeToAspect(baseAspectRatio, cfg.Width, cfg.Height) + log.Printf("Viewport size will be changed from %dx%d (%f) -> %dx%d", cfg.Width, cfg.Height, + baseAspectRatio, nwidth, nheight) + } else { + log.Println("Viewport custom size is disabled, base size will be used instead") + nwidth, nheight = gameMeta.BaseWidth, gameMeta.BaseHeight + } + + if cfg.Scale > 1 { + nwidth, nheight = nwidth*cfg.Scale, nheight*cfg.Scale + log.Printf("Viewport size has scaled to %dx%d", nwidth, nheight) + } + + room.director.SetViewport(nwidth, nheight) + + log.Println("meta: ", gameMeta) + + go room.startVideo(nwidth, nheight, videoEncoderType) go room.startAudio(gameMeta.AudioSampleRate) room.director.Start() @@ -114,6 +139,17 @@ func NewRoom(roomID string, gameName string, videoEncoderType string, onlineStor return room } +func resizeToAspect(ratio float64, sw int, sh int) (dw int, dh int) { + // ratio is always > 0 + dw = int(math.Round(float64(sh)*ratio/2) * 2) + dh = sh + if dw > sw { + dw = sw + dh = int(math.Round(float64(sw)/ratio/2) * 2) + } + return +} + // create director func getEmulator(emuName string, roomID string, imageChannel chan<- *image.RGBA, audioChannel chan<- []int16, inputChannel <-chan int) emulator.CloudEmulator { nanoarch.Init(emuName, roomID, imageChannel, audioChannel, inputChannel) @@ -123,7 +159,7 @@ func getEmulator(emuName string, roomID string, imageChannel chan<- *image.RGBA, // getGameNameFromRoomID parse roomID to get roomID and gameName func getGameNameFromRoomID(roomID string) string { - parts := strings.Split(roomID, "|") + parts := strings.Split(roomID, separator) if len(parts) <= 1 { return "" } @@ -134,7 +170,7 @@ func getGameNameFromRoomID(roomID string) string { func generateRoomID(gameName string) string { // RoomID contains random number + gameName // Next time when we only get roomID, we can launch game based on gameName - roomID := strconv.FormatInt(rand.Int63(), 16) + "|" + gameName + roomID := strconv.FormatInt(rand.Int63(), 16) + separator + gameName return roomID }