diff --git a/README.md b/README.md index ef4e080f..58492a16 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,10 @@ This project aims to bring the most modern and convenient gaming experience to u Note: **Due to the high cost of hosting, I will Hibernate the servers for a while. I'm working on a big change and will turn on hosting again. Sorry for that :(** You can try hosting your own service following the instruction in the next session. -![screenshot](document/img/landing-page.gif) -![screenshot](document/img/landing-page-gb.png) +Screenshot | Screenshot +:-------------------------:|:-------------------------: +![screenshot](document/img/landing-page-ps-hm.png)|![screenshot](document/img/landing-page-ps-x4.png) +![screenshot](document/img/landing-page-gb.png)|![screenshot](document/img/landing-page.gif) ## Feature 1. Cloud gaming: Game logic is hosted on a remote server. User doesn't have to install or setup anything. Images and audio are streamed to user in the most optimal way. diff --git a/config/config.go b/config/config.go index 3b0a5ca3..31e7be26 100644 --- a/config/config.go +++ b/config/config.go @@ -23,7 +23,8 @@ var WSWait = 20 * time.Second var MatchWorkerRandom = false var ProdEnv = "prod" -var Codec = CODEC_H264 +//var Codec = CODEC_H264 +var Codec = CODEC_VP8 var FileTypeToEmulator = map[string]string{ "gba": "gba", "cue": "pcsx", diff --git a/cws/cws.go b/cws/cws.go index 47e3dc78..9b1dd50b 100644 --- a/cws/cws.go +++ b/cws/cws.go @@ -3,6 +3,7 @@ package cws import ( "encoding/json" "log" + "runtime/debug" "sync" "time" @@ -96,6 +97,7 @@ func (c *Client) Receive(id string, f func(response WSPacket) (request WSPacket) defer func() { if err := recover(); err != nil { log.Println("Recovered from err ", err) + log.Println(debug.Stack()) } }() diff --git a/document/img/landing-page-gb.png b/document/img/landing-page-gb.png index 3c2d860c..24ba8d87 100644 Binary files a/document/img/landing-page-gb.png and b/document/img/landing-page-gb.png differ diff --git a/document/img/landing-page-ps-hm.png b/document/img/landing-page-ps-hm.png new file mode 100644 index 00000000..c35da9cf Binary files /dev/null and b/document/img/landing-page-ps-hm.png differ diff --git a/document/img/landing-page-ps-x4.png b/document/img/landing-page-ps-x4.png new file mode 100644 index 00000000..7f0e8f13 Binary files /dev/null and b/document/img/landing-page-ps-x4.png differ diff --git a/emulator/gameview.go b/emulator/gameview.go index 3525483a..b5688ea5 100644 --- a/emulator/gameview.go +++ b/emulator/gameview.go @@ -28,7 +28,7 @@ const ( left2 right2 ) -const NumKeys = 8 +const NumKeys = 10 // Audio consts const ( diff --git a/go.mod b/go.mod index 1677cd96..81b15a5f 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.12 require ( cloud.google.com/go v0.43.0 github.com/gen2brain/x264-go v0.0.0-20180306035800-58f586137654 + github.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7 github.com/gofrs/uuid v3.2.0+incompatible github.com/gorilla/websocket v1.4.0 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect diff --git a/go.sum b/go.sum index 7812dfff..17e1c56a 100644 --- a/go.sum +++ b/go.sum @@ -18,6 +18,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/gen2brain/x264-go v0.0.0-20180306035800-58f586137654 h1:RNpogAT5Qz69YLIu6+92q5sSw61PjjhDj4upH2el5pk= github.com/gen2brain/x264-go v0.0.0-20180306035800-58f586137654/go.mod h1:17kvfYQKi9/QHiKPeqmJW0YuDPZEgy72tSBVmweSyiE= +github.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7 h1:SCYMcCJ89LjRGwEa0tRluNRiMjZHalQZrVrvTbPh+qw= +github.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7/go.mod h1:482civXOzJJCPzJ4ZOX/pwvXBWSnzD4OKMdH4ClKGbk= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= diff --git a/libretro/cores/mednafen_psx_hw_libretro.so b/libretro/cores/mednafen_psx_hw_libretro.so new file mode 100755 index 00000000..3d55ac19 Binary files /dev/null and b/libretro/cores/mednafen_psx_hw_libretro.so differ diff --git a/libretro/cores/mednafen_psx_libretro.so b/libretro/cores/mednafen_psx_libretro.so new file mode 100755 index 00000000..cc37c5d3 Binary files /dev/null and b/libretro/cores/mednafen_psx_libretro.so differ diff --git a/libretro/cores/mednafen_snes_libretro.so b/libretro/cores/mednafen_snes_libretro.so new file mode 100755 index 00000000..b6f59d4a Binary files /dev/null and b/libretro/cores/mednafen_snes_libretro.so differ diff --git a/libretro/nanoarch/naemulator.go b/libretro/nanoarch/naemulator.go index aac900d4..77f37e35 100644 --- a/libretro/nanoarch/naemulator.go +++ b/libretro/nanoarch/naemulator.go @@ -66,10 +66,9 @@ var NAEmulator *naEmulator // TODO: Load from config var emulatorCorePath = map[string]string{ - "gba": "libretro/cores/mgba_libretro.so", - //"pcsx": "libretro/cores/mednafen_psx_libretro.so", + "gba": "libretro/cores/mgba_libretro.so", + "pcsx": "libretro/cores/mednafen_psx_libretro.so", //"pcsx": "libretro/cores/mednafen_psx_hw_libretro.so", - "pcsx": "libretro/cores/pcsx_rearmed_libretro.so", "arcade": "libretro/cores/fbalpha2012_neogeo_libretro.so", "mame": "libretro/cores/mame2016_libretro.so", } @@ -97,8 +96,12 @@ func (na *naEmulator) listenInput() { // input from javascript follows bitmap. Ex: 00110101 // we decode the bitmap and send to channel for inpBitmap := range NAEmulator.inputChannel { - for k := 0; k < len(na.keys); k++ { - key := bindRetroKeys[k] + for k := 0; k < len(bindRetroKeys); k++ { + key, ok := bindRetroKeys[k] + if ok == false { + continue + } + if (inpBitmap & 1) == 1 { na.keys[key] = true } else { diff --git a/libretro/nanoarch/nanoarch.go b/libretro/nanoarch/nanoarch.go index ea075e2f..1e68c097 100644 --- a/libretro/nanoarch/nanoarch.go +++ b/libretro/nanoarch/nanoarch.go @@ -15,6 +15,7 @@ import ( "unsafe" "github.com/giongto35/cloud-game/emulator" + "github.com/go-gl/gl/v2.1/gl" ) /* @@ -71,20 +72,22 @@ var scale = 3.0 const bufSize = 1024 * 4 -const joypadNumKeys = C.RETRO_DEVICE_ID_JOYPAD_R3 +const joypadNumKeys = C.RETRO_DEVICE_ID_JOYPAD_R3 + 1 -var joy [joypadNumKeys + 1]bool +var joy [joypadNumKeys]bool var ewidth, eheight int var bindRetroKeys = map[int]int{ 0: C.RETRO_DEVICE_ID_JOYPAD_A, 1: C.RETRO_DEVICE_ID_JOYPAD_B, - 2: C.RETRO_DEVICE_ID_JOYPAD_SELECT, - 3: C.RETRO_DEVICE_ID_JOYPAD_START, - 4: C.RETRO_DEVICE_ID_JOYPAD_UP, - 5: C.RETRO_DEVICE_ID_JOYPAD_DOWN, - 6: C.RETRO_DEVICE_ID_JOYPAD_LEFT, - 7: C.RETRO_DEVICE_ID_JOYPAD_RIGHT, + 2: C.RETRO_DEVICE_ID_JOYPAD_X, + 3: C.RETRO_DEVICE_ID_JOYPAD_Y, + 4: C.RETRO_DEVICE_ID_JOYPAD_SELECT, + 5: C.RETRO_DEVICE_ID_JOYPAD_START, + 6: C.RETRO_DEVICE_ID_JOYPAD_UP, + 7: C.RETRO_DEVICE_ID_JOYPAD_DOWN, + 8: C.RETRO_DEVICE_ID_JOYPAD_LEFT, + 9: C.RETRO_DEVICE_ID_JOYPAD_RIGHT, } type CloudEmulator interface { @@ -121,47 +124,68 @@ func videoConfigure(geom *C.struct_retro_game_geometry) (int, int) { fmt.Println("Failed to create the video texture") } - video.pitch = uint32(geom.base_width) * video.bpp return int(math.Round(nwidth)), int(math.Round(nheight)) } //export coreVideoRefresh func coreVideoRefresh(data unsafe.Pointer, width C.unsigned, height C.unsigned, pitch C.size_t) { - if uint32(pitch) != video.pitch { - video.pitch = uint32(pitch) - } + bytesPerRow := int(uint32(pitch) / video.bpp) if data != nil { - NAEmulator.imageChannel <- toImageRGBA(data) + NAEmulator.imageChannel <- toImageRGBA(data, bytesPerRow) } } // toImageRGBA convert nanoarch 2d array to image.RGBA -func toImageRGBA(data unsafe.Pointer) *image.RGBA { +func toImageRGBA(data unsafe.Pointer, bytesPerRow int) *image.RGBA { // Convert unsafe Pointer to bytes array var bytes []byte - // TODO: Investigate this - // seems like there is a padding of slice. - // If the resolution is 240 * 160. I have to convert to 256 * 160 slice. - // If the resolution is 320 * 240. I can keep it to 320 * 240. - // I'm making assumption that the slice is packed and it has padding to fill 64 - var w = 0 - for w < ewidth { - w += 64 - } sh := (*reflect.SliceHeader)(unsafe.Pointer(&bytes)) sh.Data = uintptr(data) - sh.Len = w * eheight * 2 - sh.Cap = w * eheight * 2 + sh.Len = bytesPerRow * eheight * 4 + sh.Cap = bytesPerRow * eheight * 4 + if video.pixFmt == gl.UNSIGNED_SHORT_5_6_5 { + return to565Image(data, bytes, bytesPerRow) + } else if video.pixFmt == gl.UNSIGNED_INT_8_8_8_8_REV { + return to8888Image(data, bytes, bytesPerRow) + } + return nil +} + +func to8888Image(data unsafe.Pointer, bytes []byte, bytesPerRow int) *image.RGBA { seek := 0 // Convert bytes array to image // TODO: Reduce overhead of copying to bytes array by accessing unsafe.Pointer directly image := image.NewRGBA(image.Rect(0, 0, ewidth, eheight)) for y := 0; y < eheight; y++ { - for x := 0; x < w; x++ { + for x := 0; x < bytesPerRow; x++ { + if x < ewidth { + b8 := bytes[seek] + g8 := bytes[seek+1] + r8 := bytes[seek+2] + a8 := bytes[seek+3] + + image.Set(x, y, color.RGBA{byte(r8), byte(g8), byte(b8), byte(a8)}) + } + seek += 4 + } + } + + // TODO: Resize Image + return image +} + +func to565Image(data unsafe.Pointer, bytes []byte, bytesPerRow int) *image.RGBA { + seek := 0 + + // Convert bytes array to image + // TODO: Reduce overhead of copying to bytes array by accessing unsafe.Pointer directly + image := image.NewRGBA(image.Rect(0, 0, ewidth, eheight)) + for y := 0; y < eheight; y++ { + for x := 0; x < bytesPerRow; x++ { if x < ewidth { var bi int bi = (int)(bytes[seek]) + ((int)(bytes[seek+1]) << 8) @@ -179,14 +203,15 @@ func toImageRGBA(data unsafe.Pointer) *image.RGBA { } } + // TODO: Resize Image return image } //export coreInputPoll func coreInputPoll() { - for i := range NAEmulator.keys { - joy[i] = NAEmulator.keys[i] - } + //for i := range NAEmulator.keys { + //joy[i] = NAEmulator.keys[i] + //} } //export coreInputState @@ -195,7 +220,7 @@ func coreInputState(port C.unsigned, device C.unsigned, index C.unsigned, id C.u return 0 } - if id < 255 && joy[id] { + if id < 255 && NAEmulator.keys[id] { return 1 } return 0 @@ -261,8 +286,11 @@ func coreEnvironment(cmd C.unsigned, data unsafe.Pointer) C.bool { if *format > C.RETRO_PIXEL_FORMAT_RGB565 { return false } - return true + return videoSetPixelFormat(*format) case C.RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY: + path := (**C.char)(data) + *path = C.CString("./libretro/system") + return true case C.RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY: path := (**C.char)(data) *path = C.CString(".") @@ -455,3 +483,32 @@ func nanoarchShutdown() { func nanoarchRun() { C.bridge_retro_run(retroRun) } + +func videoSetPixelFormat(format uint32) C.bool { + if video.texID != 0 { + log.Fatal("Tried to change pixel format after initialization.") + } + + switch format { + case C.RETRO_PIXEL_FORMAT_0RGB1555: + video.pixFmt = gl.UNSIGNED_SHORT_5_5_5_1 + video.pixType = gl.BGRA + video.bpp = 2 + break + case C.RETRO_PIXEL_FORMAT_XRGB8888: + video.pixFmt = gl.UNSIGNED_INT_8_8_8_8_REV + video.pixType = gl.BGRA + video.bpp = 4 + break + case C.RETRO_PIXEL_FORMAT_RGB565: + video.pixFmt = gl.UNSIGNED_SHORT_5_6_5 + video.pixType = gl.RGB + video.bpp = 2 + break + default: + log.Fatalf("Unknown pixel type %v", format) + } + + fmt.Printf("Video pixel: %v %v %v %v %v", video, format, C.RETRO_PIXEL_FORMAT_0RGB1555, C.RETRO_PIXEL_FORMAT_XRGB8888, C.RETRO_PIXEL_FORMAT_RGB565) + return true +} diff --git a/libretro/system/scph5500.bin b/libretro/system/scph5500.bin new file mode 100644 index 00000000..47428391 Binary files /dev/null and b/libretro/system/scph5500.bin differ diff --git a/libretro/system/scph5501.bin b/libretro/system/scph5501.bin new file mode 100644 index 00000000..9870cc36 Binary files /dev/null and b/libretro/system/scph5501.bin differ diff --git a/libretro/system/scph5502.bin b/libretro/system/scph5502.bin new file mode 100644 index 00000000..ee5725d5 Binary files /dev/null and b/libretro/system/scph5502.bin differ diff --git a/static/js/controller.js b/static/js/controller.js index d8dfbc1b..f3876447 100644 --- a/static/js/controller.js +++ b/static/js/controller.js @@ -129,9 +129,9 @@ function sendKeyState() { console.log(`Key state string: ${bits} ==> ${data}`); - // send packed keystate - var arrBuf = new Uint8Array(1); - arrBuf[0] = data; + var arrBuf = new Uint8Array(2); + arrBuf[0] = data & ((1 << 8) - 1); + arrBuf[1] = data >> 8; inputChannel.send(arrBuf); unchangePacket--; @@ -193,6 +193,8 @@ function doButtonUp(name) { case "join": case "a": case "b": + case "x": + case "y": case "start": case "select": startGame(); diff --git a/static/js/gesture_keyboard.js b/static/js/gesture_keyboard.js index 9c949c6f..a53c1fde 100644 --- a/static/js/gesture_keyboard.js +++ b/static/js/gesture_keyboard.js @@ -10,8 +10,10 @@ const KEYBOARD_MAP = { 90: "a", // z 88: "b", // x - 67: "start", // c - 86: "select", // v + 67: "x", // c + 86: "y", // v + 13: "start", // start + 16: "select", // select // non-game 81: "quit", // q diff --git a/static/js/global.js b/static/js/global.js index df544354..fe8c7b93 100644 --- a/static/js/global.js +++ b/static/js/global.js @@ -3,7 +3,7 @@ */ const DEBUG = false; -const KEY_BIT = ["a", "b", "select", "start", "up", "down", "left", "right"]; +const KEY_BIT = ["a", "b", "x", "y", "select", "start", "up", "down", "left", "right"]; const INPUT_FPS = 100; const INPUT_STATE_PACKET = 1; const PINGPONGPS = 5; @@ -29,6 +29,8 @@ let keyState = { // control a: false, b: false, + x: false, + y: false, start: false, select: false, diff --git a/util/gamelist/games.go b/util/gamelist/games.go index 495b6f38..6801f23c 100644 --- a/util/gamelist/games.go +++ b/util/gamelist/games.go @@ -60,7 +60,7 @@ func isValidGameType(gamePath string) bool { // getGameInfo returns game info from a path func getGameInfo(path string) GameInfo { // Remove prefix to obtain file names - fileName := path[len(gamePath)+1:] + fileName := filepath.Base(path) ext := filepath.Ext(fileName) return GameInfo{ Name: strings.TrimSuffix(fileName, ext), diff --git a/webrtc/webrtc.go b/webrtc/webrtc.go index 1a25bda0..d8d3e071 100644 --- a/webrtc/webrtc.go +++ b/webrtc/webrtc.go @@ -155,8 +155,8 @@ func (w *WebRTC) StartClient(remoteSession string, iceCandidates []string) (stri // Register text message handling inputTrack.OnMessage(func(msg webrtc.DataChannelMessage) { - // TODO: Can add recover here - w.InputChannel <- int(msg.Data[0]) + // TODO: Can add recover here + generalize + w.InputChannel <- int(msg.Data[1])<<8 + int(msg.Data[0]) }) inputTrack.OnClose(func() { diff --git a/worker/handlers.go b/worker/handlers.go index 867fa144..e8d9e03f 100644 --- a/worker/handlers.go +++ b/worker/handlers.go @@ -88,15 +88,14 @@ func (h *Handler) GetOverlordClient() *OverlordClient { // detachPeerConn detach/remove a peerconnection from current room func (h *Handler) detachPeerConn(pc *webrtc.WebRTC) { log.Println("Detach peerconnection") - roomID := pc.RoomID - room := h.getRoom(roomID) + room := h.getRoom(pc.RoomID) if room == nil { return } - // If room has no sessions, close room if !room.EmptySessions() { room.RemoveSession(pc) + // If no more session in that room, we close that room if room.EmptySessions() { log.Println("No session in room") room.Close() diff --git a/worker/room/room.go b/worker/room/room.go index 47481d6c..1ee38e23 100644 --- a/worker/room/room.go +++ b/worker/room/room.go @@ -57,6 +57,7 @@ func NewRoom(roomID string, gameName string, onlineStorage *storage.Client) *Roo roomID = generateRoomID(gameName) } else { gameName = getGameNameFromRoomID(roomID) + log.Println("Get Gamename from RoomID", gameName) } gameInfo := gamelist.GetGameInfoFromName(gameName) @@ -168,9 +169,9 @@ func (r *Room) startWebRTCSession(peerconnection *webrtc.WebRTC, playerIndex int } if peerconnection.IsConnected() { - // the first 8 bits belong to player 1 - // the next 8 belongs to player 2 ... - // We standardize and put it to inputChannel (16 bits) + // the first 10 bits belong to player 1 + // the next 10 belongs to player 2 ... + // We standardize and put it to inputChannel (20 bits) input = input << ((uint(playerIndex) - 1) * emulator.NumKeys) select { case r.inputChannel <- input: