Integrate with RetroArch to have Universal emulator. (#64)

* WIP

* Add libretro

* Update input retro

* Update cloud retro input:

* Integrate libretro remove GL

* Launch emulator by game type

* Save and load

* Update deeplink to game directly

* Done channel for naemulator

* each server handle only one session

* Only provide available clients

* Emulator based on game

* Remove all OpenGL related

* Retroarch audio

* Audio encoding mono

* Experiment with libretro audio

* Audio without datachannel

* Audio opus internal

* Remove unnecessary code

* Resampled

* Limit time frame

* Start Room and Audio by game

* Dynamic video image allocation

* Dynamic Video buffer allocation based on video resolution

* Move encoder to room

* width and height from emulator

* Padding images to fix multipler of 64

* Remove OpenGL

* Remove Open AL

* Remove OpenGL from Go mod

* Move away nanoarch logic in naemulator

* Remove unecessary savefiles.go

* Optimize Docker for caching

* Update ReadME

* Update README to introduce Retro

* Update README

* Update README.md
This commit is contained in:
giongto35 2019-08-02 22:06:26 +08:00 committed by GitHub
parent 642691ef23
commit c7caa87624
43 changed files with 3373 additions and 299 deletions

8
Dockerfile vendored
View file

@ -1,14 +1,14 @@
From golang:1.12
RUN apt-get update
RUN apt-get install pkg-config libvpx-dev libopus-dev libopusfile-dev -y
RUN mkdir -p /cloud-game
COPY . /cloud-game/
WORKDIR /cloud-game
# Install server dependencies
RUN apt-get update
RUN apt-get install pkg-config libvpx-dev libopus-dev libopusfile-dev -y
RUN go install ./cmd
EXPOSE 8000

21
README.md vendored
View file

@ -8,20 +8,22 @@ For the best gaming experience, please select the closest region to you.
---
Open source Cloud Gaming Service building on [WebRTC](https://github.com/pion) using browser as the main platform.
CloudRetro, Open source Web-based Cloud Gaming Service building on [WebRTC](https://github.com/pion) and [LibRetro](https://retroarch.com/).
This project aims to bring the most convenient gaming experience to gamer. You can play any games on your browser directly, which is fully compatible on multi-platform like Desktop, Android, IOS. This flexibility enables modern online gaming experience to retro games starting with NES in this current release.
This project aims to bring the most modern and convenient gaming experience to user. You can play any retro games on your browser directly, which is fully compatible on multi-platform like Desktop, Android, IOS. This flexibility also enables online gaming experience to retro games.
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 :(**
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)
## 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.
2. Cross-platform compatibility: The game is run on webbrowser, the most universal built-in app. No console, plugin, external app or devices are needed. The device must support webRTC to perform streaming. Joystick is also supported.
4. Emulator agnostic: The game can be play directly without emulator selection and initialization as long as the its cores are supported by RetroArch.
3. Vertically scaled + Load balancing: We can add more machines to handle more traffic. The closest server with highest free resource will be assigned to user.
4. Cloud storage: Game state is storing on online storage, so you can come back to continue playing in a game.
5. Cloud storage: Game state is storing on online storage, so you can come back to continue playing in a game.
## Run on local by Docker
@ -59,9 +61,9 @@ And run
- The project is inspired by Google Stadia. The most important question comes to everyone mind is how good is the latency? Will gaming experience is affected by the network? I did some researches on that topic and WebRTC seems to be the most suitable protocol for that purpose. I limited the project scope and made a POC of Cloud-gaming. The result indeed looks very promising.
### How good is the result
### Why retroArch?
- My estimation is that it requires 10Mbps per NES game session with resolution 256 * 240.
- I first started the experiment with NES emulator. After seeing some positive result, I take a step further to integrate with RetroArch to challenge the system with high-end games like Playstation. In my opinion, combining RetroArch and Cloud will bring the best of both world together. The versality of RetroArch and the utility of streaming can deliver the most portable gaming experience to users.
### Why is the game lag for some people?
@ -69,9 +71,11 @@ And run
- Cloud-gaming is based on WebRTC peer to peer, so there are some cases direct communication is not possible because of the firewall. In that case, relay communication happens and the game is not smooth. You can find a public network and retry.
- The current state of project is hosted on a limited resource, so during high traffic, the game might got lag due to CPU is overused, not because of the network. Besides, my memory management is not working properly sometimes and game sessions are not fully separated, so the game session can lag over time. In that case, please reload or continue your game by clicking share and reopen the old game.
### Why NES but not some more modern games?
### If RetroArch is already multi-platform, why do we need to make it cloud?
- For the purpose of latency demonstration and fast iteration, I picked NES but integrating with other emulators like GBA, NDS and even Playstation is also possible. For High-end games, there will be problems with hardware and infrastructure. Google has a lot of resource and its distributed GPU will enhance this cloud-gaming use case. My resource is not as abundant, so I consider NES emulator for my first step.
- RetroArch is universal emulator but it still faces the issue of performance when running on low end mobile device or web frontend. As the logic is completely handled remotely, there will be no performance issue on the game. Even Playstation can be played smoothly on the service.
- RetroArch requires loading cores and games but these steps are no longer necessary in Cloud-Gaming service. Games information and cores are stored in cloud storage.
- However, High-end games requires a lot of computing power and it will involves an upgrade on hardware and infrastructure. Google has a lot of resource and its distributed GPU will enhance this cloud-gaming use case. My resource is not as abundant, so I prefer picking light retro games.
### Why Web browser as the main platform?
@ -80,6 +84,7 @@ And run
## Credits
* *Pion* Webrtc team for the incredible Golang Webrtc library and their supports https://github.com/pion/webrtc/.
* *Nanoarch* Golang RetroArch https://github.com/libretro/go-nanoarch and https://retroarch.com.
* *fogleman* for the awesome NES emulator https://github.com/fogleman/nes.
* *poi5305* for the video encoding https://github.com/poi5305/go-yuv2webRTC.
* And last but not least, my longtime friend Tri as the co-author.

View file

@ -11,7 +11,6 @@ import (
"time"
"github.com/giongto35/cloud-game/cws"
"github.com/giongto35/cloud-game/emulator"
"github.com/giongto35/cloud-game/overlord"
gamertc "github.com/giongto35/cloud-game/webrtc"
"github.com/giongto35/cloud-game/worker"
@ -393,7 +392,7 @@ func TestReconnectRoomNoLocal(t *testing.T) {
worker.GetOverlordClient().Close()
worker.Close()
// Remove room on local
path := emulator.GetSavePath(saveRoomID)
path := util.GetSavePath(saveRoomID)
os.Remove(path)
// Expect Google cloud call

View file

@ -14,9 +14,8 @@ var Port = flag.String("port", "8000", "Port of the game")
var IsMonitor = flag.Bool("monitor", false, "Turn on monitor")
var FrontendSTUNTURN = flag.String("stunturn", DefaultSTUNTURN, "Frontend STUN TURN servers")
var Mode = flag.String("mode", "dev", "Environment")
var IsRetro = flag.Bool("isretro", true, "Is retro")
var Width = 256
var Height = 240
var WSWait = 20 * time.Second
var MatchWorkerRandom = false
var ProdEnv = "prod"

BIN
document/img/landing-page-gb.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 MiB

View file

@ -6,6 +6,7 @@ import (
"time"
"github.com/giongto35/cloud-game/emulator/nes"
"github.com/giongto35/cloud-game/util"
// "github.com/gordonklaus/portaudio"
)
@ -19,13 +20,14 @@ type Director struct {
inputChannel <-chan int
Done chan struct{}
roomID string
gamePath string
roomID string
}
const fps = 300
// NewDirector returns a new director
func NewDirector(roomID string, imageChannel chan<- *image.RGBA, audioChannel chan<- float32, inputChannel <-chan int) *Director {
func NewDirector(roomID string, imageChannel chan<- *image.RGBA, audioChannel chan<- float32, inputChannel <-chan int) CloudEmulator {
// TODO: return image channel from where it write
director := Director{}
director.Done = make(chan struct{}, 1)
@ -52,18 +54,7 @@ func (d *Director) SetView(view *GameView) {
//d.view.UpdateInput(input)
//}
// Step ...
func (d *Director) Step() {
timestamp := float64(time.Now().Nanosecond()) / float64(time.Second)
dt := timestamp - d.timestamp
d.timestamp = timestamp
if d.view != nil {
d.view.Update(timestamp, dt)
}
}
// Start ...
func (d *Director) Start(path string) {
func (d *Director) LoadMeta(path string) Meta {
// portaudio.Initialize()
// defer portaudio.Terminate()
@ -72,12 +63,41 @@ func (d *Director) Start(path string) {
// d.audio = audio
log.Println("Start game: ", path)
d.PlayGame(path)
d.Run()
d.gamePath = path
return Meta{
AudioSampleRate: 48000,
Fps: 300,
Width: 256,
Height: 240,
}
}
// Run ...
func (d *Director) Run() {
// Start ...
func (d *Director) Start() {
// portaudio.Initialize()
// defer portaudio.Terminate()
// audio := NewAudio()
// audio.Start()
// d.audio = audio
log.Println("Start game: ", d.gamePath)
d.playGame(d.gamePath)
d.run()
}
// step ...
func (d *Director) step() {
timestamp := float64(time.Now().Nanosecond()) / float64(time.Second)
dt := timestamp - d.timestamp
d.timestamp = timestamp
if d.view != nil {
d.view.Update(timestamp, dt)
}
}
// run ...
func (d *Director) run() {
c := time.Tick(time.Second / fps)
L:
for range c {
@ -94,14 +114,14 @@ L:
default:
}
d.Step()
d.step()
}
d.SetView(nil)
log.Println("Closed Director")
}
// PalyGame starts a game given a rom path
func (d *Director) PlayGame(path string) {
func (d *Director) playGame(path string) {
console, err := nes.NewConsole(path)
if err != nil {
log.Println("Err: Cannot load path, Got:", err)
@ -115,9 +135,9 @@ func (d *Director) SaveGame(saveExtraFunc func() error) error {
if d.roomID != "" {
d.view.Save(saveExtraFunc)
return nil
} else {
return nil
}
return nil
}
// LoadGame creates load events and doing extra step for load
@ -125,12 +145,21 @@ func (d *Director) LoadGame() error {
if d.roomID != "" {
d.view.Load()
return nil
} else {
return nil
}
return nil
}
// GetHashPath return the full path to hash file
func (d *Director) GetHashPath() string {
return savePath(d.roomID)
return util.GetSavePath(d.roomID)
}
func (d *Director) GetSampleRate() uint {
return SampleRate
}
// Close
func (d *Director) Close() {
close(d.Done)
}

View file

@ -5,6 +5,7 @@ import (
"image"
"github.com/giongto35/cloud-game/emulator/nes"
"github.com/giongto35/cloud-game/util"
)
// List key pressed
@ -31,10 +32,12 @@ const NumKeys = 8
// Audio consts
const (
SampleRate = 16000
Channels = 1
TimeFrame = 60
AppAudio = 1
//SampleRate = 16000
SampleRate = 48000
//SampleRate = 32768
Channels = 2
TimeFrame = 40
AppAudio = 1
)
type GameView struct {
@ -100,7 +103,7 @@ func (view *GameView) Enter() {
view.console.SetAudioChannel(view.audioChannel)
// load state if the saveFile file existed in the server (Join the old room)
if err := view.console.LoadState(savePath(view.saveFile)); err == nil {
if err := view.console.LoadState(util.GetSavePath(view.saveFile)); err == nil {
return
} else {
view.console.Reset()
@ -109,7 +112,7 @@ func (view *GameView) Enter() {
// load sram
cartridge := view.console.Cartridge
if cartridge.Battery != 0 {
if sram, err := readSRAM(sramPath(view.saveFile)); err == nil {
if sram, err := readSRAM(util.GetSRAMPath(view.saveFile)); err == nil {
cartridge.SRAM = sram
}
}
@ -122,7 +125,7 @@ func (view *GameView) Exit() {
// save sram
cartridge := view.console.Cartridge
if cartridge.Battery != 0 {
writeSRAM(sramPath(view.saveFile), cartridge.SRAM)
writeSRAM(util.GetSRAMPath(view.saveFile), cartridge.SRAM)
}
// close producer
@ -147,7 +150,7 @@ func (view *GameView) Update(t, dt float64) {
func (view *GameView) Save(extraSaveFunc func() error) {
// put saving event to queue, process in updateEvent
view.savingJob = &job{
path: savePath(view.saveFile),
path: util.GetSavePath(view.saveFile),
extraFunc: extraSaveFunc,
}
}
@ -155,7 +158,7 @@ func (view *GameView) Save(extraSaveFunc func() error) {
func (view *GameView) Load() {
// put saving event to queue, process in updateEvent
view.loadingJob = &job{
path: savePath(view.saveFile),
path: util.GetSavePath(view.saveFile),
extraFunc: nil,
}
}

View file

@ -116,6 +116,11 @@ func (apu *APU) Step() {
func (apu *APU) sendSample() {
output := apu.filterChain.Step(apu.output())
//stereo
select {
case apu.channel <- output:
default:
}
select {
case apu.channel <- output:
default:

25
emulator/type.go Normal file
View file

@ -0,0 +1,25 @@
package emulator
// CloudEmulator is the interface of cloud emulator. Currently NES emulator and RetroArch implements this in codebase
type CloudEmulator interface {
// LoadMeta returns meta data of emulator. Refer below
LoadMeta(path string) Meta
// Start is called after LoadGame
Start()
// SaveGame save game state, saveExtraFunc is callback to do extra step. Ex: save to google cloud
SaveGame(saveExtraFunc func() error) error
// LoadGame load game state
LoadGame() error
// GetHashPath returns the path emulator will save state to
GetHashPath() string
// Close will be called when the game is done
Close()
}
// Meta is metadata of game
type Meta struct {
AudioSampleRate int
Fps int
Width int
Height int
}

View file

@ -9,37 +9,12 @@ import (
"image/draw"
"image/gif"
"image/png"
"log"
"os"
"os/user"
"path"
"github.com/giongto35/cloud-game/emulator/nes"
)
var homeDir string
func init() {
u, err := user.Current()
if err != nil {
log.Fatalln(err)
}
homeDir = u.HomeDir
}
// Public call to get savePath
func GetSavePath(roomID string) string {
return savePath(roomID)
}
func sramPath(hash string) string {
return homeDir + "/.nes/sram/" + hash + ".dat"
}
func savePath(hash string) string {
return homeDir + "/.nes/save/" + hash + ".dat"
}
func combineButtons(a, b [8]bool) [8]bool {
var result [8]bool
for i := 0; i < 8; i++ {

1
go.mod vendored
View file

@ -8,6 +8,5 @@ require (
github.com/gorilla/websocket v1.4.0
github.com/pion/webrtc/v2 v2.0.23
github.com/prometheus/client_golang v0.9.3
github.com/satori/go.uuid v1.2.0
gopkg.in/hraban/opus.v2 v2.0.0-20180426093920-0f2e0b4fc6cd
)

38
go.sum vendored
View file

@ -10,11 +10,14 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24
github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE=
github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
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=
@ -23,15 +26,19 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/googleapis/gax-go/v2 v2.0.4 h1:hU4mGcQI4DaAYW+IbTun+2qEZVFxK0ySjQLTbS0VQKc=
@ -43,34 +50,36 @@ github.com/gortc/turn v0.7.3 h1:CE72C79erbcsfa6L/QDhKztcl2kDq1UK20ImrJWDt/w=
github.com/gortc/turn v0.7.3/go.mod h1:gvguwaGAFyv5/9KrcW9MkCgHALYD+e99mSM7pSCYYho=
github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/lucas-clemente/quic-go v0.7.1-0.20190401152353-907071221cf9 h1:tbuodUh2vuhOVZAdW3NEUvosFHUMJwUNl7jk/VSEiwc=
github.com/lucas-clemente/quic-go v0.7.1-0.20190401152353-907071221cf9/go.mod h1:PpMmPfPKO9nKJ/psF49ESTAGQSdfXxlg1otPbEB2nOw=
github.com/marten-seemann/qtls v0.2.3 h1:0yWJ43C62LsZt08vuQJDK1uC1czUc3FJeCLPoNAI4vA=
github.com/marten-seemann/qtls v0.2.3/go.mod h1:xzjG7avBwGGbdZ8dTGxlBnLArsVKLvwmjgmPuiQEcYk=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/pion/datachannel v1.4.3 h1:tqS6YiqqAiFCxGGhvn1K7fHEzemK9Aov025dE/isGFo=
github.com/pion/datachannel v1.4.3/go.mod h1:SpMJbuu8v+qbA94m6lWQwSdCf8JKQvgmdSHDNtcbe+w=
github.com/pion/dtls v1.3.4 h1:MdOMsCfd44m2iTrxtkzA6UndvYVjLWWjua7hxU8EXEA=
github.com/pion/dtls v1.3.4/go.mod h1:CjlPLfQdsTg3G4AEXjJp8FY5bRweBlxHrgoFrN+fQsk=
github.com/pion/dtls v1.3.5 h1:mBioifvh6JSE9pD4FtJh5WoizygoqkOJNJyS5Ns+y1U=
github.com/pion/dtls v1.3.5/go.mod h1:CjlPLfQdsTg3G4AEXjJp8FY5bRweBlxHrgoFrN+fQsk=
github.com/pion/ice v0.2.8 h1:DCFMO8yJRB6XGaRjjdHupsHvjcM72LJ9YwL/2Io2EXk=
github.com/pion/ice v0.2.8/go.mod h1:HyIp0mppSrUdw7DFLQfPgJWnPRRV96pnTV8irdrBGrA=
github.com/pion/ice v0.4.0 h1:BdTXHTjzdsJHGi9yMFnj9ffgr+Kg2oHVv1qk4B0mQ8A=
github.com/pion/ice v0.4.0/go.mod h1:/gw3aFmD/pBG8UM3TcEHs6HuaOEMSd/v1As3TodE7Ss=
github.com/pion/logging v0.2.1 h1:LwASkBKZ+2ysGJ+jLv1E/9H1ge0k1nTfi1X+5zirkDk=
github.com/pion/logging v0.2.1/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
github.com/pion/mdns v0.0.2 h1:T22Gg4dSuYVYsZ21oRFh9z7twzAm27+5PEKiABbjCvM=
github.com/pion/mdns v0.0.2/go.mod h1:VrN3wefVgtfL8QgpEblPUC46ag1reLIfpqekCnKunLE=
github.com/pion/quic v0.1.1 h1:D951FV+TOqI9A0rTF7tHx0Loooqz+nyzjEyj8o3PuMA=
github.com/pion/quic v0.1.1/go.mod h1:zEU51v7ru8Mp4AUBJvj6psrSth5eEFNnVQK5K48oV3k=
github.com/pion/rtcp v1.2.0 h1:rT2FptW5YHIern+4XlbGYnnsT26XGxurnkNLnzhtDXg=
github.com/pion/rtcp v1.2.0/go.mod h1:a5dj2d6BKIKHl43EnAOIrCczcjESrtPuMgfmL6/K6QM=
@ -78,14 +87,10 @@ github.com/pion/rtp v1.1.2 h1:ERNugzYHW9F2ldpwoARbeFGKRoq1REe5Jxdjvm/rOx8=
github.com/pion/rtp v1.1.2/go.mod h1:/l4cvcKd0D3u9JLs2xSVI95YkfXW87a3br3nqmVtSlE=
github.com/pion/sctp v1.6.3 h1:SC4vKOjcddK8tXiTNj05a+0/GyPpCmuNfeBA/rzNFqs=
github.com/pion/sctp v1.6.3/go.mod h1:cCqpLdYvgEUdl715+qbWtgT439CuQrAgy8BZTp0aEfA=
github.com/pion/sdp/v2 v2.1.1 h1:i3fAyjiLuQseYNo0BtCOPfzp91Ppb7vasRGmUUTog28=
github.com/pion/sdp/v2 v2.1.1/go.mod h1:idSlWxhfWQDtTy9J05cgxpHBu/POwXN2VDRGYxT/EjU=
github.com/pion/sdp/v2 v2.2.0 h1:JiixCEU8g6LbSsh1Bg5SOk0TPnJrn2HBOA1yJ+mRYhI=
github.com/pion/sdp/v2 v2.2.0/go.mod h1:idSlWxhfWQDtTy9J05cgxpHBu/POwXN2VDRGYxT/EjU=
github.com/pion/srtp v1.2.4 h1:wwGKC5ewuBukkZ+i+pZ8aO33+t6z2y/XRiYtyP0Xpv0=
github.com/pion/srtp v1.2.4/go.mod h1:52qiP0g3FVMG/5NL6Ko8Vr2qirevKH+ukYbNS/4EX40=
github.com/pion/stun v0.2.2 h1:0IJCwJFOdEmHzz4oxl9SBGLlJbnNbF+0h6XSOmuE034=
github.com/pion/stun v0.2.2/go.mod h1:TChCNKgwnFiFG/c9K+zqEdd6pO6tlODb9yN1W/zVfsE=
github.com/pion/stun v0.3.0/go.mod h1:xrCld6XM+6GWDZdvjPlLMsTU21rNxnO6UO8XsAvHr/M=
github.com/pion/stun v0.3.1 h1:d09JJzOmOS8ZzIp8NppCMgrxGZpJ4Ix8qirfNYyI3BA=
github.com/pion/stun v0.3.1/go.mod h1:xrCld6XM+6GWDZdvjPlLMsTU21rNxnO6UO8XsAvHr/M=
@ -94,16 +99,16 @@ github.com/pion/transport v0.7.0 h1:EsXN8TglHMlKZMo4ZGqwK6QgXBu0WYg7wfGMWIXsS+w=
github.com/pion/transport v0.7.0/go.mod h1:iWZ07doqOosSLMhZ+FXUTq+TamDoXSllxpbGcfkCmbE=
github.com/pion/transport v0.8.0 h1:YHZnWBBrBuMqkuvMFUHeAETXS+LgfwW1IsVd2K2cyW8=
github.com/pion/transport v0.8.0/go.mod h1:nAmRRnn+ArVtsoNuwktvAD+jrjSD7pA+H3iRmZwdUno=
github.com/pion/turn v1.1.4 h1:yGxcasBvge4idNjxjowePn8oW43C4v70bXroBBKLyKY=
github.com/pion/turn v1.1.4/go.mod h1:2O2GFDGO6+hJ5gsyExDhoNHtVcacPB1NOyc81gkq0WA=
github.com/pion/turnc v0.0.6 h1:FHsmwYvdJ8mhT1/ZtWWer9L0unEb7AyRgrymfWy6mEY=
github.com/pion/turnc v0.0.6/go.mod h1:4MSFv5i0v3MRkDLdo5eF9cD/xJtj1pxSphHNnxKL2W8=
github.com/pion/webrtc/v2 v2.0.15 h1:0n8P+sYfGN515RnkdRyc4SD1r+0BZ5ts7SDSUxxlkmY=
github.com/pion/webrtc/v2 v2.0.15/go.mod h1:e1xwQPR2XVoDWTj5uGSKfHB9Xk4oWRt7mk4bVfXHi8E=
github.com/pion/webrtc/v2 v2.0.23 h1:v/tDKsP4zB6Sj+Wx861fLsaNmbwWbxacciHUhetH288=
github.com/pion/webrtc/v2 v2.0.23/go.mod h1:AgremGibyNcHWIEkDbXt4ujKzKBO3tMuoYXybVRa8zo=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3 h1:9iH4JKXLzFbOAdtqv/a+j8aewx2Y8lAjAydhbaScPF8=
@ -118,17 +123,12 @@ github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084 h1:sofwID9zm4tzrgykg80hfFph1mryUeLRsUfoocVVmRY=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/satori/go.uuid v1.0.0 h1:6QDKTa2a+CpXmqIFypEOKZUreVG3iCcrb8vbCkHTDsY=
github.com/satori/go.uuid v1.0.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/satori/go.uuid v1.1.0 h1:B9KXyj+GzIpJbV7gmr873NsY6zpbxNy24CBtGrk7jHo=
github.com/satori/go.uuid v1.1.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
go.opencensus.io v0.21.0 h1:mU6zScU4U1YAFPHEHYk+3JC4SY7JxgkqS10ZOSyksNg=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
@ -158,6 +158,7 @@ golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4Iltr
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6 h1:bjcUS9ztw9kFmmIxJInhon/0Is3p+EHBKNgquIzo1OI=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -178,6 +179,7 @@ golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3
google.golang.org/api v0.5.0 h1:lj9SyhMzyoa38fgFF0oO2T6pjs5IzkLPKfVtxpyCRMM=
google.golang.org/api v0.5.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
@ -186,11 +188,15 @@ google.golang.org/genproto v0.0.0-20190508193815-b515fa19cec8/go.mod h1:VzzqZJRn
google.golang.org/grpc v1.19.0 h1:cfg4PD8YEdSFnm7qLV4++93WcmhH2nIUhMjhdCvl3j8=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/hraban/opus.v2 v2.0.0-20180426093920-0f2e0b4fc6cd h1:oAcaGkUcXajq9yi+UKvzJMSKEb++XegVTSQjOlu3MVU=
gopkg.in/hraban/opus.v2 v2.0.0-20180426093920-0f2e0b4fc6cd/go.mod h1:/L5E7a21VWl8DeuCPKxQBdVG5cy+L0MRZ08B1wnqt7g=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

BIN
libretro/core/testdata/Polar Rescue (USA).vec vendored Executable file

Binary file not shown.

Binary file not shown.

BIN
libretro/core/testdata/vecx_libretro.dll vendored Executable file

Binary file not shown.

BIN
libretro/core/testdata/vecx_libretro.dylib vendored Executable file

Binary file not shown.

BIN
libretro/core/testdata/vecx_libretro.so vendored Executable file

Binary file not shown.

BIN
libretro/cores/citra_libretro.so vendored Normal file

Binary file not shown.

BIN
libretro/cores/desmume_libretro.so vendored Normal file

Binary file not shown.

BIN
libretro/cores/mesen_libretro.so vendored Normal file

Binary file not shown.

BIN
libretro/cores/mess2015_libretro.so vendored Normal file

Binary file not shown.

BIN
libretro/cores/meteor_libretro.so vendored Normal file

Binary file not shown.

BIN
libretro/cores/mgba_libretro.so vendored Normal file

Binary file not shown.

BIN
libretro/cores/nestopia_libretro.so vendored Normal file

Binary file not shown.

BIN
libretro/cores/pcsx_rearmed_libretro.so vendored Normal file

Binary file not shown.

BIN
libretro/cores/prosystem_libretro.so vendored Normal file

Binary file not shown.

BIN
libretro/cores/snes9x2010_libretro.so vendored Normal file

Binary file not shown.

BIN
libretro/cores/snes9x_libretro.so vendored Normal file

Binary file not shown.

BIN
libretro/cores/yabasanshiro_libretro.so vendored Normal file

Binary file not shown.

127
libretro/nanoarch/cfuncs.go Normal file
View file

@ -0,0 +1,127 @@
package nanoarch
/*
#include "libretro.h"
#include <stdbool.h>
#include <stdarg.h>
#include <stdio.h>
void bridge_retro_init(void *f) {
return ((void (*)(void))f)();
}
void bridge_retro_deinit(void *f) {
return ((void (*)(void))f)();
}
unsigned bridge_retro_api_version(void *f) {
return ((unsigned (*)(void))f)();
}
void bridge_retro_get_system_info(void *f, struct retro_system_info *si) {
return ((void (*)(struct retro_system_info *))f)(si);
}
void bridge_retro_get_system_av_info(void *f, struct retro_system_av_info *si) {
return ((void (*)(struct retro_system_av_info *))f)(si);
}
bool bridge_retro_set_environment(void *f, void *callback) {
return ((bool (*)(retro_environment_t))f)((retro_environment_t)callback);
}
void bridge_retro_set_video_refresh(void *f, void *callback) {
((bool (*)(retro_video_refresh_t))f)((retro_video_refresh_t)callback);
}
void bridge_retro_set_input_poll(void *f, void *callback) {
((bool (*)(retro_input_poll_t))f)((retro_input_poll_t)callback);
}
void bridge_retro_set_input_state(void *f, void *callback) {
((bool (*)(retro_input_state_t))f)((retro_input_state_t)callback);
}
void bridge_retro_set_audio_sample(void *f, void *callback) {
((bool (*)(retro_audio_sample_t))f)((retro_audio_sample_t)callback);
}
void bridge_retro_set_audio_sample_batch(void *f, void *callback) {
((bool (*)(retro_audio_sample_batch_t))f)((retro_audio_sample_batch_t)callback);
}
bool bridge_retro_load_game(void *f, struct retro_game_info *gi) {
return ((bool (*)(struct retro_game_info *))f)(gi);
}
void bridge_retro_unload_game(void *f) {
return ((void (*)(void))f)();
}
void bridge_retro_run(void *f) {
return ((void (*)(void))f)();
}
size_t bridge_retro_get_memory_size(void *f, unsigned id) {
return ((size_t (*)(unsigned))f)(id);
}
void* bridge_retro_get_memory_data(void *f, unsigned id) {
return ((void* (*)(unsigned))f)(id);
}
size_t bridge_retro_serialize_size(void *f) {
return ((size_t (*)(void))f)();
}
bool bridge_retro_serialize(void *f, void *data, size_t size) {
return ((bool (*)(void*, size_t))f)(data, size);
}
bool bridge_retro_unserialize(void *f, void *data, size_t size) {
return ((bool (*)(void*, size_t))f)(data, size);
}
bool coreEnvironment_cgo(unsigned cmd, void *data) {
bool coreEnvironment(unsigned, void*);
return coreEnvironment(cmd, data);
}
void coreVideoRefresh_cgo(void *data, unsigned width, unsigned height, size_t pitch) {
void coreVideoRefresh(void*, unsigned, unsigned, size_t);
return coreVideoRefresh(data, width, height, pitch);
}
void coreInputPoll_cgo() {
void coreInputPoll();
return coreInputPoll();
}
int16_t coreInputState_cgo(unsigned port, unsigned device, unsigned index, unsigned id) {
int16_t coreInputState(unsigned, unsigned, unsigned, unsigned);
return coreInputState(port, device, index, id);
}
void coreAudioSample_cgo(int16_t left, int16_t right) {
void coreAudioSample(int16_t, int16_t);
coreAudioSample(left, right);
}
size_t coreAudioSampleBatch_cgo(const int16_t *data, size_t frames) {
size_t coreAudioSampleBatch(const int16_t*, size_t);
return coreAudioSampleBatch(data, frames);
}
void coreLog_cgo(enum retro_log_level level, const char *fmt, ...) {
char msg[4096] = {0};
va_list va;
va_start(va, fmt);
vsnprintf(msg, sizeof(msg), fmt, va);
va_end(va);
void coreLog(enum retro_log_level level, const char *msg);
coreLog(level, msg);
}
*/
import "C"

2167
libretro/nanoarch/libretro.h vendored Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,172 @@
package nanoarch
import (
"image"
"log"
"time"
emulator "github.com/giongto35/cloud-game/emulator"
"github.com/giongto35/cloud-game/util"
)
/*
#include "libretro.h"
#cgo LDFLAGS: -ldl
#include <stdlib.h>
#include <stdio.h>
#include <dlfcn.h>
#include <string.h>
void bridge_retro_deinit(void *f);
unsigned bridge_retro_api_version(void *f);
void bridge_retro_get_system_info(void *f, struct retro_system_info *si);
void bridge_retro_get_system_av_info(void *f, struct retro_system_av_info *si);
bool bridge_retro_set_environment(void *f, void *callback);
void bridge_retro_set_video_refresh(void *f, void *callback);
void bridge_retro_set_input_poll(void *f, void *callback);
void bridge_retro_set_input_state(void *f, void *callback);
void bridge_retro_set_audio_sample(void *f, void *callback);
void bridge_retro_set_audio_sample_batch(void *f, void *callback);
bool bridge_retro_load_game(void *f, struct retro_game_info *gi);
void bridge_retro_run(void *f);
size_t bridge_retro_get_memory_size(void *f, unsigned id);
void* bridge_retro_get_memory_data(void *f, unsigned id);
bool bridge_retro_serialize(void *f, void *data, size_t size);
bool bridge_retro_unserialize(void *f, void *data, size_t size);
size_t bridge_retro_serialize_size(void *f);
bool coreEnvironment_cgo(unsigned cmd, void *data);
void coreVideoRefresh_cgo(void *data, unsigned width, unsigned height, size_t pitch);
void coreInputPoll_cgo();
void coreAudioSample_cgo(int16_t left, int16_t right);
size_t coreAudioSampleBatch_cgo(const int16_t *data, size_t frames);
int16_t coreInputState_cgo(unsigned port, unsigned device, unsigned index, unsigned id);
void coreLog_cgo(enum retro_log_level level, const char *msg);
*/
import "C"
// naEmulator implements CloudEmulator
type naEmulator struct {
imageChannel chan<- *image.RGBA
audioChannel chan<- float32
inputChannel <-chan int
corePath string
gamePath string
roomID string
gameName string
isSavingLoading bool
keys []bool
done chan struct{}
meta emulator.Meta
}
var NAEmulator *naEmulator
// TODO: Load from config
var emulatorCorePath = map[string]string{
"gba": "libretro/cores/mgba_libretro.so",
"pcsx": "libretro/cores/pcsx_rearmed_libretro.so",
}
// NAEmulator implements CloudEmulator interface based on NanoArch(golang RetroArch)
func NewNAEmulator(etype string, roomID string, imageChannel chan<- *image.RGBA, audioChannel chan<- float32, inputChannel <-chan int) *naEmulator {
return &naEmulator{
corePath: emulatorCorePath[etype],
imageChannel: imageChannel,
audioChannel: audioChannel,
inputChannel: inputChannel,
keys: make([]bool, joypadNumKeys),
roomID: roomID,
done: make(chan struct{}, 1),
}
}
// Init initialize new RetroArch cloud emulator
func Init(etype string, roomID string, imageChannel chan<- *image.RGBA, audioChannel chan<- float32, inputChannel <-chan int) {
NAEmulator = NewNAEmulator(etype, roomID, imageChannel, audioChannel, inputChannel)
go NAEmulator.listenInput()
}
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++ {
if (inpBitmap & 1) == 1 {
key := bindRetroKeys[k]
na.keys[key] = true
}
inpBitmap >>= 1
}
}
}
func (na *naEmulator) LoadMeta(path string) emulator.Meta {
coreLoad(na.corePath)
coreLoadGame(path)
na.gamePath = path
return na.meta
}
func (na *naEmulator) Start() {
na.playGame(na.gamePath)
ticker := time.NewTicker(time.Second / 60)
for range ticker.C {
select {
case <-na.done:
nanoarchShutdown()
log.Println("Closed Director")
return
default:
}
na.GetLock()
nanoarchRun()
na.ReleaseLock()
}
}
func (na *naEmulator) playGame(path string) {
// When start game, we also try loading if there was a saved state
na.LoadGame()
}
func (na *naEmulator) SaveGame(saveExtraFunc func() error) error {
if na.roomID != "" {
err := na.Save()
if err != nil {
return err
}
err = saveExtraFunc()
if err != nil {
return err
}
}
return nil
}
func (na *naEmulator) LoadGame() error {
if na.roomID != "" {
err := na.Load()
if err != nil {
log.Println("Error: Cannot load", err)
return err
}
}
return nil
}
func (na *naEmulator) GetHashPath() string {
return util.GetSavePath(na.roomID)
}
func (na *naEmulator) Close() {
// Unload and deinit in the core.
close(na.done)
}

View file

@ -0,0 +1,461 @@
package nanoarch
import (
"bufio"
"errors"
"fmt"
"image"
"image/color"
"log"
"math"
"os"
"os/user"
"reflect"
"sync"
"unsafe"
"github.com/giongto35/cloud-game/emulator"
)
/*
#include "libretro.h"
#cgo LDFLAGS: -ldl
#include <stdlib.h>
#include <stdio.h>
#include <dlfcn.h>
#include <string.h>
void bridge_retro_init(void *f);
void bridge_retro_deinit(void *f);
unsigned bridge_retro_api_version(void *f);
void bridge_retro_get_system_info(void *f, struct retro_system_info *si);
void bridge_retro_get_system_av_info(void *f, struct retro_system_av_info *si);
bool bridge_retro_set_environment(void *f, void *callback);
void bridge_retro_set_video_refresh(void *f, void *callback);
void bridge_retro_set_input_poll(void *f, void *callback);
void bridge_retro_set_input_state(void *f, void *callback);
void bridge_retro_set_audio_sample(void *f, void *callback);
void bridge_retro_set_audio_sample_batch(void *f, void *callback);
bool bridge_retro_load_game(void *f, struct retro_game_info *gi);
void bridge_retro_unload_game(void *f);
void bridge_retro_run(void *f);
size_t bridge_retro_get_memory_size(void *f, unsigned id);
void* bridge_retro_get_memory_data(void *f, unsigned id);
bool bridge_retro_serialize(void *f, void *data, size_t size);
bool bridge_retro_unserialize(void *f, void *data, size_t size);
size_t bridge_retro_serialize_size(void *f);
bool coreEnvironment_cgo(unsigned cmd, void *data);
void coreVideoRefresh_cgo(void *data, unsigned width, unsigned height, size_t pitch);
void coreInputPoll_cgo();
void coreAudioSample_cgo(int16_t left, int16_t right);
size_t coreAudioSampleBatch_cgo(const int16_t *data, size_t frames);
int16_t coreInputState_cgo(unsigned port, unsigned device, unsigned index, unsigned id);
void coreLog_cgo(enum retro_log_level level, const char *msg);
*/
import "C"
var mu sync.Mutex
var video struct {
program uint32
vao uint32
texID uint32
pitch uint32
pixFmt uint32
pixType uint32
bpp uint32
}
var scale = 3.0
const bufSize = 1024 * 4
const joypadNumKeys = C.RETRO_DEVICE_ID_JOYPAD_R3
var joy [joypadNumKeys + 1]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,
}
type CloudEmulator interface {
SetView(view *emulator.GameView)
Start(path string)
SaveGame(saveExtraFunc func() error) error
LoadGame() error
GetHashPath() string
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
}
func videoConfigure(geom *C.struct_retro_game_geometry) (int, int) {
nwidth, nheight := resizeToAspect(float64(geom.aspect_ratio), float64(geom.base_width), float64(geom.base_height))
fmt.Println("media config", nwidth, nheight, geom.base_width, geom.base_height, geom.aspect_ratio, video.bpp, scale)
if video.texID == 0 {
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)
}
if data != nil {
NAEmulator.imageChannel <- toImageRGBA(data)
}
}
// toImageRGBA convert nanoarch 2d array to image.RGBA
func toImageRGBA(data unsafe.Pointer) *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
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++ {
if x < 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
image.Set(x, y, color.RGBA{byte(r8), byte(g8), byte(b8), 255})
}
seek += 2
}
}
return image
}
//export coreInputPoll
func coreInputPoll() {
for i := range NAEmulator.keys {
joy[i] = NAEmulator.keys[i]
}
for i := range NAEmulator.keys {
NAEmulator.keys[i] = false
}
}
//export coreInputState
func coreInputState(port C.unsigned, device C.unsigned, index C.unsigned, id C.unsigned) C.int16_t {
if port > 0 || index > 0 || device != C.RETRO_DEVICE_JOYPAD {
return 0
}
if id < 255 && joy[id] {
return 1
}
return 0
}
func min(a, b C.size_t) C.size_t {
if a < b {
return a
}
return b
}
func audioWrite2(buf unsafe.Pointer, frames C.size_t) C.size_t {
numFrames := int(frames) * 2
pcm := (*[1 << 30]int16)(unsafe.Pointer(buf))[:numFrames:numFrames]
for i := 0; i < numFrames; i += 1 {
s := float32(pcm[i])
NAEmulator.audioChannel <- s
}
return 2 * frames
}
//export coreAudioSample
func coreAudioSample(left C.int16_t, right C.int16_t) {
buf := []C.int16_t{left, right}
audioWrite2(unsafe.Pointer(&buf), 1)
}
//export coreAudioSampleBatch
func coreAudioSampleBatch(data unsafe.Pointer, frames C.size_t) C.size_t {
return audioWrite2(data, frames)
}
//export coreLog
func coreLog(level C.enum_retro_log_level, msg *C.char) {
fmt.Print("[Log]: ", C.GoString(msg))
}
//export coreEnvironment
func coreEnvironment(cmd C.unsigned, data unsafe.Pointer) C.bool {
switch cmd {
case C.RETRO_ENVIRONMENT_GET_USERNAME:
username := (**C.char)(data)
currentUser, err := user.Current()
if err != nil {
*username = C.CString("")
} else {
*username = C.CString(currentUser.Username)
}
break
case C.RETRO_ENVIRONMENT_GET_LOG_INTERFACE:
cb := (*C.struct_retro_log_callback)(data)
cb.log = (C.retro_log_printf_t)(C.coreLog_cgo)
break
case C.RETRO_ENVIRONMENT_GET_CAN_DUPE:
bval := (*C.bool)(data)
*bval = C.bool(true)
break
case C.RETRO_ENVIRONMENT_SET_PIXEL_FORMAT:
format := (*C.enum_retro_pixel_format)(data)
if *format > C.RETRO_PIXEL_FORMAT_RGB565 {
return false
}
return true
case C.RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY:
case C.RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY:
path := (**C.char)(data)
*path = C.CString(".")
return true
case C.RETRO_ENVIRONMENT_SHUTDOWN:
//window.SetShouldClose(true)
return true
case C.RETRO_ENVIRONMENT_GET_VARIABLE:
variable := (*C.struct_retro_variable)(data)
fmt.Println("[Env]: get variable:", C.GoString(variable.key))
return false
default:
//fmt.Println("[Env]: command not implemented", cmd)
return false
}
return true
}
func init() {
}
var retroInit unsafe.Pointer
var retroDeinit unsafe.Pointer
var retroAPIVersion unsafe.Pointer
var retroGetSystemInfo unsafe.Pointer
var retroGetSystemAVInfo unsafe.Pointer
var retroSetEnvironment unsafe.Pointer
var retroSetVideoRefresh unsafe.Pointer
var retroSetInputPoll unsafe.Pointer
var retroSetInputState unsafe.Pointer
var retroSetAudioSample unsafe.Pointer
var retroSetAudioSampleBatch unsafe.Pointer
var retroRun unsafe.Pointer
var retroLoadGame unsafe.Pointer
var retroUnloadGame unsafe.Pointer
var retroGetMemorySize unsafe.Pointer
var retroGetMemoryData unsafe.Pointer
var retroSerializeSize unsafe.Pointer
var retroSerialize unsafe.Pointer
var retroUnserialize unsafe.Pointer
func coreLoad(sofile string) {
mu.Lock()
h := C.dlopen(C.CString(sofile), C.RTLD_NOW)
if h == nil {
log.Fatalf("error loading %s\n", sofile)
}
retroInit = C.dlsym(h, C.CString("retro_init"))
retroDeinit = C.dlsym(h, C.CString("retro_deinit"))
retroAPIVersion = C.dlsym(h, C.CString("retro_api_version"))
retroGetSystemInfo = C.dlsym(h, C.CString("retro_get_system_info"))
retroGetSystemAVInfo = C.dlsym(h, C.CString("retro_get_system_av_info"))
retroSetEnvironment = C.dlsym(h, C.CString("retro_set_environment"))
retroSetVideoRefresh = C.dlsym(h, C.CString("retro_set_video_refresh"))
retroSetInputPoll = C.dlsym(h, C.CString("retro_set_input_poll"))
retroSetInputState = C.dlsym(h, C.CString("retro_set_input_state"))
retroSetAudioSample = C.dlsym(h, C.CString("retro_set_audio_sample"))
retroSetAudioSampleBatch = C.dlsym(h, C.CString("retro_set_audio_sample_batch"))
retroRun = C.dlsym(h, C.CString("retro_run"))
retroLoadGame = C.dlsym(h, C.CString("retro_load_game"))
retroUnloadGame = C.dlsym(h, C.CString("retro_unload_game"))
retroSerializeSize = C.dlsym(h, C.CString("retro_serialize_size"))
retroSerialize = C.dlsym(h, C.CString("retro_serialize"))
retroUnserialize = C.dlsym(h, C.CString("retro_unserialize"))
mu.Unlock()
C.bridge_retro_set_environment(retroSetEnvironment, C.coreEnvironment_cgo)
C.bridge_retro_set_video_refresh(retroSetVideoRefresh, C.coreVideoRefresh_cgo)
C.bridge_retro_set_input_poll(retroSetInputPoll, C.coreInputPoll_cgo)
C.bridge_retro_set_input_state(retroSetInputState, C.coreInputState_cgo)
C.bridge_retro_set_audio_sample(retroSetAudioSample, C.coreAudioSample_cgo)
C.bridge_retro_set_audio_sample_batch(retroSetAudioSampleBatch, C.coreAudioSampleBatch_cgo)
C.bridge_retro_init(retroInit)
v := C.bridge_retro_api_version(retroAPIVersion)
fmt.Println("Libretro API version:", v)
}
func slurp(path string, size int64) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
bytes := make([]byte, size)
buffer := bufio.NewReader(f)
_, err = buffer.Read(bytes)
if err != nil {
return nil, err
}
return bytes, nil
}
func coreLoadGame(filename string) {
file, err := os.Open(filename)
if err != nil {
panic(err)
}
fi, err := file.Stat()
if err != nil {
panic(err)
}
size := fi.Size()
fmt.Println("ROM size:", size)
gi := C.struct_retro_game_info{
path: C.CString(filename),
size: C.size_t(size),
}
si := C.struct_retro_system_info{}
C.bridge_retro_get_system_info(retroGetSystemInfo, &si)
var libName = C.GoString(si.library_name)
fmt.Println(" library_name:", libName)
fmt.Println(" library_version:", C.GoString(si.library_version))
fmt.Println(" valid_extensions:", C.GoString(si.valid_extensions))
fmt.Println(" need_fullpath:", si.need_fullpath)
fmt.Println(" block_extract:", si.block_extract)
if !si.need_fullpath {
bytes, err := slurp(filename, size)
if err != nil {
panic(err)
}
cstr := C.CString(string(bytes))
gi.data = unsafe.Pointer(cstr)
}
ok := C.bridge_retro_load_game(retroLoadGame, &gi)
if !ok {
log.Fatal("The core failed to load the content.")
}
avi := C.struct_retro_system_av_info{}
C.bridge_retro_get_system_av_info(retroGetSystemAVInfo, &avi)
ewidth, eheight = videoConfigure(&avi.geometry)
// 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.Width = ewidth
NAEmulator.meta.Height = eheight
}
// serializeSize returns the amount of data the implementation requires to serialize
// internal state (save states).
// Between calls to retro_load_game() and retro_unload_game(), the
// returned size is never allowed to be larger than a previous returned
// value, to ensure that the frontend can allocate a save state buffer once.
func serializeSize() uint {
return uint(C.bridge_retro_serialize_size(retroSerializeSize))
}
// serialize serializes internal state and returns the state as a byte slice.
func serialize(size uint) ([]byte, error) {
data := C.malloc(C.size_t(size))
ok := bool(C.bridge_retro_serialize(retroSerialize, data, C.size_t(size)))
if !ok {
return nil, errors.New("retro_serialize failed")
}
bytes := C.GoBytes(data, C.int(size))
return bytes, nil
}
// unserialize unserializes internal state from a byte slice.
func unserialize(bytes []byte, size uint) error {
ok := bool(C.bridge_retro_unserialize(retroUnserialize, unsafe.Pointer(&bytes[0]), C.size_t(size)))
if !ok {
return errors.New("retro_unserialize failed")
}
return nil
}
func nanoarchShutdown() {
C.bridge_retro_unload_game(retroUnloadGame)
C.bridge_retro_deinit(retroDeinit)
}
func nanoarchRun() {
C.bridge_retro_run(retroRun)
}

View file

@ -0,0 +1,71 @@
// Package savestates takes care of serializing and unserializing the game RAM
// to the host filesystem.
package nanoarch
/*
#include "libretro.h"
#cgo LDFLAGS: -ldl
#include <stdlib.h>
#include <stdio.h>
#include <dlfcn.h>
#include <string.h>
bool bridge_retro_serialize(void *f, void *data, size_t size);
bool bridge_retro_unserialize(void *f, void *data, size_t size);
size_t bridge_retro_serialize_size(void *f);
*/
import "C"
import (
"io/ioutil"
"sync"
)
var saveLock int32
var m sync.Mutex
func (na *naEmulator) GetLock() {
//atomic.CompareAndSwapInt32(&saveLock, 0, 1)
m.Lock()
}
func (na *naEmulator) ReleaseLock() {
//atomic.CompareAndSwapInt32(&saveLock, 1, 0)
m.Unlock()
}
// Save the current state to the filesystem. name is the name of the
// savestate file to save to, without extension.
func (na *naEmulator) Save() error {
path := na.GetHashPath()
na.GetLock()
defer na.ReleaseLock()
s := serializeSize()
bytes, err := serialize(s)
if err != nil {
return err
}
if err != nil {
return err
}
return ioutil.WriteFile(path, bytes, 0644)
}
// Load the state from the filesystem
func (na *naEmulator) Load() error {
path := na.GetHashPath()
na.GetLock()
defer na.ReleaseLock()
s := serializeSize()
bytes, err := ioutil.ReadFile(path)
if err != nil {
return err
}
err = unserialize(bytes, s)
return err
}

View file

@ -133,13 +133,15 @@ func (o *Server) WS(w http.ResponseWriter, r *http.Request) {
go client.Listen()
// Set up server
workerClients := o.getAvailableWorkers()
// SessionID will be the unique per frontend connection
sessionID := uuid.Must(uuid.NewV4()).String()
var serverID string
if config.MatchWorkerRandom {
serverID, err = o.findBestServerRandom()
serverID, err = findBestServerRandom(workerClients)
} else {
serverID, err = o.findBestServerFromBrowser(client)
serverID, err = findBestServerFromBrowser(workerClients, client)
}
if err != nil {
@ -158,6 +160,7 @@ func (o *Server) WS(w http.ResponseWriter, r *http.Request) {
// TODO:?
//defer wssession.Close()
log.Println("New client will conect to server", wssession.ServerID)
wssession.WorkerClient.IsAvailable = false
wssession.RouteBrowser()
@ -176,17 +179,31 @@ func (o *Server) WS(w http.ResponseWriter, r *http.Request) {
},
nil,
)
// WorkerClient become available again
wssession.WorkerClient.IsAvailable = true
}
// getAvailableWorkers returns the list of available worker
func (o *Server) getAvailableWorkers() map[string]*WorkerClient {
workerClients := map[string]*WorkerClient{}
for k, w := range o.workerClients {
if w.IsAvailable {
workerClients[k] = w
}
}
return workerClients
}
// findBestServer returns the best server for a session
func (o *Server) findBestServerRandom() (string, error) {
func findBestServerRandom(workerClients map[string]*WorkerClient) (string, error) {
// TODO: Find best Server by latency, currently return by ping
if len(o.workerClients) == 0 {
if len(workerClients) == 0 {
return "", errors.New("No server found")
}
r := rand.Intn(len(o.workerClients))
for k, _ := range o.workerClients {
r := rand.Intn(len(workerClients))
for k, _ := range workerClients {
if r == 0 {
return k, nil
}
@ -198,13 +215,13 @@ func (o *Server) findBestServerRandom() (string, error) {
// findBestServerFromBrowser returns the best server for a session
// All workers addresses are sent to user and user will ping to get latency
func (o *Server) findBestServerFromBrowser(client *BrowserClient) (string, error) {
func findBestServerFromBrowser(workerClients map[string]*WorkerClient, client *BrowserClient) (string, error) {
// TODO: Find best Server by latency, currently return by ping
if len(o.workerClients) == 0 {
if len(workerClients) == 0 {
return "", errors.New("No server found")
}
latencies := o.getLatencyMapFromBrowser(client)
latencies := getLatencyMapFromBrowser(workerClients, client)
log.Println("Latency map", latencies)
if len(latencies) == 0 {
@ -226,14 +243,14 @@ func (o *Server) findBestServerFromBrowser(client *BrowserClient) (string, error
}
// getLatencyMapFromBrowser get all latencies from worker to user
func (o *Server) getLatencyMapFromBrowser(client *BrowserClient) map[*WorkerClient]int64 {
func getLatencyMapFromBrowser(workerClients map[string]*WorkerClient, client *BrowserClient) map[*WorkerClient]int64 {
workersList := []*WorkerClient{}
latencyMap := map[*WorkerClient]int64{}
// addressList is the list of worker addresses
addressList := []string{}
for _, workerClient := range o.workerClients {
for _, workerClient := range workerClients {
workersList = append(workersList, workerClient)
addressList = append(addressList, workerClient.Address)
}

View file

@ -9,8 +9,9 @@ import (
type WorkerClient struct {
*cws.Client
ServerID string
Address string
ServerID string
Address string
IsAvailable bool
}
// RouteWorker are all routes server received from worker
@ -43,8 +44,9 @@ func (o *Server) RouteWorker(workerClient *WorkerClient) {
// NewWorkerClient returns a client connecting to worker. This connection exchanges information between workers and server
func NewWorkerClient(c *websocket.Conn, serverID string, address string) *WorkerClient {
return &WorkerClient{
Client: cws.NewClient(c),
ServerID: serverID,
Address: address,
Client: cws.NewClient(c),
ServerID: serverID,
Address: address,
IsAvailable: true,
}
}

3
static/game.html vendored
View file

@ -33,7 +33,8 @@
There is still audio because current audio flow is not from media but it is manually encoded (technical webRTC challenge). Later, when we can integrate audio to media, we can face the issue with mute again .
https://developers.google.com/web/updates/2017/09/autoplay-policy-changes
-->
<video id="game-screen" autoplay=true muted poster="/static/img/screen_loading.gif"></video>
<!--<video id="game-screen" autoplay=true muted poster="/static/img/screen_loading.gif"></video>-->
<video id="game-screen" autoplay=true poster="/static/img/screen_loading.gif"></video>
<div id="menu-screen">
<div id="menu-container">

91
static/js/ws.js vendored
View file

@ -45,24 +45,14 @@ conn.onmessage = e => {
case "sdp":
log("Got remote sdp");
pc.setRemoteDescription(new RTCSessionDescription(JSON.parse(atob(d["data"]))));
//conn.send(JSON.stringify({"id": "sdpdon", "packet_id": d["packet_id"]}));
break;
case "requestOffer":
curPacketID = d["packet_id"];
log("Received request offer ", curPacketID)
startWebRTC();
//pc.createOffer({offerToReceiveVideo: true, offerToReceiveAudio: false}).then(d => {
//pc.setLocalDescription(d).catch(log);
//})
//case "sdpremote":
//log("Got remote sdp");
//pc.setRemoteDescription(new RTCSessionDescription(JSON.parse(atob(d["data"]))));
//conn.send(JSON.stringify({"id": "remotestart", "data": GAME_LIST[gameIdx].nes, "room_id": roomID.value, "player_index": parseInt(playerIndex.value, 10)}));inputTimer
//break;
case "heartbeat":
// console.log("Ping: ", Date.now() - d["data"])
// TODO: Calc time
break;
case "start":
@ -177,73 +167,6 @@ function startWebRTC() {
}
inputChannel.onclose = () => log('inputChannel has closed');
// audio channel, unordered + unreliable, id 1
var audioCtx = new (window.AudioContext || window.webkitAudioContext)();
var isInit = false;
var audioStack = [];
var nextTime = 0;
var packetIdx = 0;
function scheduleBuffers() {
while (audioStack.length) {
var buffer = audioStack.shift();
var source = audioCtx.createBufferSource();
source.buffer = buffer;
source.connect(audioCtx.destination);
// tracking linear time
if (nextTime == 0)
nextTime = audioCtx.currentTime + 0.1; /// add 100ms latency to work well across systems - tune this if you like
source.start(nextTime);
nextTime+=source.buffer.duration; // Make the next buffer wait the length of the last buffer before being played
};
}
sampleRate = 16000;
channels = 1;
bitDepth = 16;
decoder = new OpusDecoder(sampleRate, channels);
function decodeChunk(opusChunk) {
pcmChunk = decoder.decode_float(opusChunk);
myBuffer = audioCtx.createBuffer(channels, pcmChunk.length, sampleRate);
nowBuffering = myBuffer.getChannelData(0, bitDepth, sampleRate);
nowBuffering.set(pcmChunk);
return myBuffer;
}
audioChannel = pc.createDataChannel('b', {
ordered: false,
negotiated: true,
id: 1,
maxRetransmits: 0
})
audioChannel.onopen = () => {
log('audioChannel has opened');
audioReady = true;
// TODO: Event based
if (roomID != "") {
startGame()
}
}
audioChannel.onclose = () => log('audioChannel has closed');
audioChannel.onmessage = (e) => {
arr = new Uint8Array(e.data);
idx = arr[arr.length - 1];
// only accept missing 5 packets
if (idx < packetIdx && packetIdx - idx < 251) // 256 - 5
return;
packetIdx = idx;
audioStack.push(decodeChunk(e.data));
if (isInit || (audioStack.length > 10)) { // make sure we put at least 10 chunks in the buffer before starting
isInit = true;
scheduleBuffers();
}
}
pc.oniceconnectionstatechange = e => {
log(`iceConnectionState: ${pc.iceConnectionState}`);
@ -268,9 +191,12 @@ function startWebRTC() {
}
var stream = new MediaStream();
document.getElementById("game-screen").srcObject = stream;
// video channel
pc.ontrack = function (event) {
document.getElementById("game-screen").srcObject = event.streams[0];
stream.addTrack(event.track);
var promise = document.getElementById("game-screen").play();
if (promise !== undefined) {
promise.then(_ => {
@ -315,9 +241,11 @@ function startWebRTC() {
// receiver only tracks
pc.addTransceiver('video', {'direction': 'recvonly'});
pc.addTransceiver('audio', {'direction': 'recvonly'});
// create SDP
pc.createOffer({offerToReceiveVideo: true, offerToReceiveAudio: false}).then(d => {
pc.createOffer({offerToReceiveVideo: true, offerToReceiveAudio: true}).then(d => {
log(d.sdp)
pc.setLocalDescription(d).catch(log);
})
@ -329,7 +257,7 @@ function startGame() {
return false;
}
// TODO: Add while loop
if (!gameReady || !inputReady || !audioReady) {
if (!gameReady || !inputReady) {
popup("Game is not ready yet. Please wait");
return false;
}
@ -344,9 +272,6 @@ function startGame() {
// clear menu screen
stopGameInputTimer();
//$("#menu-screen").fadeOut(DEBUG ? 0 : 400, function() {
//$("#game-screen").show();
//});
$("#menu-screen").hide()
$("#game-screen").show();
$("#btn-save").show();

View file

@ -3,9 +3,9 @@ package util
import (
"image"
"log"
"os/user"
"unsafe"
"github.com/giongto35/cloud-game/config"
)
// https://stackoverflow.com/questions/9465815/rgb-to-yuv420-algorithm-efficiency
@ -55,7 +55,35 @@ func RgbaToYuv(rgba *image.RGBA) []byte {
}
// RgbaToYuvInplace convert to yuv from rgba inplace to yuv. Avoid reallocation
func RgbaToYuvInplace(rgba *image.RGBA, yuv []byte) {
stride := rgba.Stride - config.Width*4
C.rgba2yuv(unsafe.Pointer(&yuv[0]), unsafe.Pointer(&rgba.Pix[0]), C.int(config.Width), C.int(config.Height), C.int(stride))
func RgbaToYuvInplace(rgba *image.RGBA, yuv []byte, width, height int) {
stride := rgba.Stride - width*4
C.rgba2yuv(unsafe.Pointer(&yuv[0]), unsafe.Pointer(&rgba.Pix[0]), C.int(width), C.int(height), C.int(stride))
}
var homeDir string
func init() {
u, err := user.Current()
if err != nil {
log.Fatalln(err)
}
homeDir = u.HomeDir
}
// GetSavePath returns save location of game based on roomID
func GetSavePath(roomID string) string {
return savePath(roomID)
}
// GetSRAMPath returns SRAM location of game based on roomID
func GetSRAMPath(hash string) string {
return homeDir + "/.nes/sram/" + hash + ".dat"
}
func sramPath(hash string) string {
return homeDir + "/.nes/sram/" + hash + ".dat"
}
func savePath(hash string) string {
return homeDir + "/.nes/save/" + hash + ".dat"
}

View file

@ -92,13 +92,14 @@ type WebRTC struct {
}
// StartClient start webrtc
func (w *WebRTC) StartClient(remoteSession string, iceCandidates []string, width, height int) (string, error) {
func (w *WebRTC) StartClient(remoteSession string, iceCandidates []string) (string, error) {
defer func() {
if err := recover(); err != nil {
log.Println(err)
w.StopClient()
}
}()
var err error
// reset client
if w.isConnected {
@ -106,12 +107,6 @@ func (w *WebRTC) StartClient(remoteSession string, iceCandidates []string, width
time.Sleep(2 * time.Second)
}
encoder, err := vpxEncoder.NewVpxEncoder(width, height, 20, 1200, 5)
if err != nil {
return "", err
}
w.encoder = encoder
log.Println("=== StartClient ===")
w.connection, err = webrtc.NewPeerConnection(webrtcconfig)
if err != nil {
@ -128,19 +123,28 @@ func (w *WebRTC) StartClient(remoteSession string, iceCandidates []string, width
}
// audio track
dfalse := false
dtrue := true
var d0 uint16 = 0
var d1 uint16 = 1
audioTrack, err := w.connection.CreateDataChannel("b", &webrtc.DataChannelInit{
Ordered: &dfalse,
MaxRetransmits: &d0,
Negotiated: &dtrue,
ID: &d1,
})
opusTrack, err := w.connection.NewTrack(webrtc.DefaultPayloadTypeOpus, rand.Uint32(), "audio", "pion2b")
if err != nil {
return "", err
}
_, err = w.connection.AddTrack(opusTrack)
if err != nil {
return "", err
}
dfalse := false
dtrue := true
var d0 uint16 = 0
//var d1 uint16 = 1
//audioTrack, err := w.connection.CreateDataChannel("b", &webrtc.DataChannelInit{
//Ordered: &dfalse,
//MaxRetransmits: &d0,
//Negotiated: &dtrue,
//ID: &d1,
//})
//if err != nil {
//return "", err
//}
// input channel
inputTrack, err := w.connection.CreateDataChannel("a", &webrtc.DataChannelInit{
@ -171,7 +175,8 @@ func (w *WebRTC) StartClient(remoteSession string, iceCandidates []string, width
go func() {
w.isConnected = true
log.Println("ConnectionStateConnected")
w.startStreaming(vp8Track, audioTrack)
//w.startStreaming(vp8Track, audioTrack)
w.startStreaming(vp8Track, opusTrack)
}()
}
@ -264,27 +269,8 @@ func (w *WebRTC) IsConnected() bool {
}
// func (w *WebRTC) startStreaming(vp8Track *webrtc.Track, opusTrack *webrtc.Track) {
func (w *WebRTC) startStreaming(vp8Track *webrtc.Track, audioTrack *webrtc.DataChannel) {
func (w *WebRTC) startStreaming(vp8Track *webrtc.Track, opusTrack *webrtc.Track) {
log.Println("Start streaming")
// send screenshot
go func() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered when sent to close Image Channel")
}
}()
// TODO: Use same yuv
for yuv := range w.ImageChannel {
if !w.isConnected {
return
}
if len(w.encoder.Input) < cap(w.encoder.Input) {
w.encoder.Input <- yuv
}
}
}()
// receive frame buffer
go func() {
defer func() {
@ -293,11 +279,11 @@ func (w *WebRTC) startStreaming(vp8Track *webrtc.Track, audioTrack *webrtc.DataC
}
}()
for bs := range w.encoder.Output {
for data := range w.ImageChannel {
if *config.IsMonitor {
log.Println("Encoding FPS : ", w.calculateFPS())
}
err := vp8Track.WriteSample(media.Sample{Data: bs, Samples: 1})
err := vp8Track.WriteSample(media.Sample{Data: data, Samples: 1})
if err != nil {
log.Println("Warn: Err write sample: ", err)
}
@ -316,7 +302,10 @@ func (w *WebRTC) startStreaming(vp8Track *webrtc.Track, audioTrack *webrtc.DataC
if !w.isConnected {
return
}
audioTrack.Send(data)
err := opusTrack.WriteSample(media.Sample{Data: data, Samples: uint32(len(data))})
if err != nil {
log.Println("Warn: Err write sample: ", err)
}
}
}()

View file

@ -3,7 +3,6 @@ package worker
import (
"log"
"github.com/giongto35/cloud-game/config"
"github.com/giongto35/cloud-game/cws"
"github.com/giongto35/cloud-game/webrtc"
"github.com/giongto35/cloud-game/worker/room"
@ -50,7 +49,7 @@ func (h *Handler) RouteOverlord() {
func(resp cws.WSPacket) (req cws.WSPacket) {
log.Println("Received relay SDP of a browser from overlord")
peerconnection := webrtc.NewWebRTC()
localSession, err := peerconnection.StartClient(resp.Data, iceCandidates[resp.SessionID], config.Width, config.Height)
localSession, err := peerconnection.StartClient(resp.Data, iceCandidates[resp.SessionID])
//h.peerconnections[resp.SessionID] = peerconnection
// Create new sessions when we have new peerconnection initialized

View file

@ -1,31 +1,65 @@
package room
import (
"fmt"
"log"
"github.com/giongto35/cloud-game/config"
"github.com/giongto35/cloud-game/emulator"
"github.com/giongto35/cloud-game/util"
vpxEncoder "github.com/giongto35/cloud-game/vpx-encoder"
"gopkg.in/hraban/opus.v2"
)
func (r *Room) startAudio() {
func resample(pcm []float32, targetSize int, srcSampleRate int, dstSampleRate int) []float32 {
newPCML := make([]float32, targetSize/2)
newPCMR := make([]float32, targetSize/2)
newPCM := make([]float32, targetSize)
for i := 0; i+1 < len(pcm); i += 2 {
newPCML[(i/2)*dstSampleRate/srcSampleRate] = pcm[i]
newPCMR[(i/2)*dstSampleRate/srcSampleRate] = pcm[i+1]
}
for i := 1; i < len(newPCML); i++ {
if newPCML[i] == 0 {
newPCML[i] = newPCML[i-1]
}
}
for i := 1; i < len(newPCMR); i++ {
if newPCMR[i] == 0 {
newPCMR[i] = newPCMR[i-1]
}
}
for i := 0; i+1 < targetSize; i += 2 {
newPCM[i] = newPCML[i/2]
newPCM[i+1] = newPCMR[i/2]
}
return newPCM
}
func (r *Room) startAudio(sampleRate int) {
log.Println("Enter fan audio")
//srcSampleRate := 32768
srcSampleRate := sampleRate
dstSampleRate := 48000
enc, err := opus.NewEncoder(emulator.SampleRate, emulator.Channels, opus.AppAudio)
enc, err := opus.NewEncoder(dstSampleRate, 2, opus.AppVoIP)
maxBufferSize := emulator.TimeFrame * emulator.SampleRate / 1000
pcm := make([]float32, maxBufferSize) // 640 * 1000 / 16000 == 40 ms
enc.SetMaxBandwidth(opus.Fullband)
enc.SetBitrateToAuto()
enc.SetComplexity(10)
dstBufferSize := 240
srcBufferSize := dstBufferSize * srcSampleRate / dstSampleRate
fmt.Println("src BufferSize", srcBufferSize)
pcm := make([]float32, srcBufferSize) // 640 * 1000 / 16000 == 40 ms
idx := 0
if err != nil {
log.Println("[!] Cannot create audio encoder")
log.Println("[!] Cannot create audio encoder", err)
return
}
var count byte = 0
// fanout Audio
fmt.Println("listening audiochanel", r.IsRunning)
for sample := range r.audioChannel {
if !r.IsRunning {
log.Println("Room ", r.ID, " audio channel closed")
@ -36,68 +70,71 @@ func (r *Room) startAudio() {
pcm[idx] = sample
idx++
if idx == len(pcm) {
data := make([]byte, 640)
n, err := enc.EncodeFloat32(pcm, data)
data := make([]byte, dstBufferSize)
dstpcm := resample(pcm, dstBufferSize, srcSampleRate, dstSampleRate)
n, err := enc.EncodeFloat32(dstpcm, data)
if err != nil {
log.Println("[!] Failed to decode")
log.Println("[!] Failed to decode", err)
idx = 0
continue
}
data = data[:n]
data = append(data, count)
// TODO: r.rtcSessions is rarely updated. Lock will hold down perf
//r.sessionsLock.Lock()
for _, webRTC := range r.rtcSessions {
// Client stopped
//if !webRTC.IsClosed() {
//continue
//}
// encode frame
// fanout audioChannel
if webRTC.IsConnected() {
// NOTE: can block here
webRTC.AudioChannel <- data
}
//isRoomRunning = true
}
//r.sessionsLock.Unlock()
idx = 0
count = (count + 1) & 0xff
}
}
}
func (r *Room) startVideo() {
size := int(float32(config.Width*config.Height) * 1.5)
func (r *Room) startVideo(width, height int) {
size := int(float32(width*height) * 1.5)
yuv := make([]byte, size, size)
// fanout Screen
for image := range r.imageChannel {
if !r.IsRunning {
log.Println("Room ", r.ID, " video channel closed")
return
encoder, err := vpxEncoder.NewVpxEncoder(width, height, 20, 1200, 5)
if err != nil {
return
}
// send screenshot
go func() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered when sent to close Image Channel")
}
}()
for image := range r.imageChannel {
if !r.IsRunning {
log.Println("Room ", r.ID, " video channel closed")
return
}
if len(encoder.Input) < cap(encoder.Input) {
util.RgbaToYuvInplace(image, yuv, width, height)
encoder.Input <- yuv
}
}
}()
// TODO: Use worker pool for encoding
util.RgbaToYuvInplace(image, yuv)
// fanout Screen
for data := range encoder.Output {
// TODO: r.rtcSessions is rarely updated. Lock will hold down perf
//r.sessionsLock.Lock()
for _, webRTC := range r.rtcSessions {
// Client stopped
//if webRTC.IsClosed() {
//continue
//}
// encode frame
// fanout imageChannel
if webRTC.IsConnected() {
// NOTE: can block here
webRTC.ImageChannel <- yuv
webRTC.ImageChannel <- data
}
}
//r.sessionsLock.Unlock()
}
}

View file

@ -10,9 +10,10 @@ import (
"strconv"
"strings"
"sync"
"time"
emulator "github.com/giongto35/cloud-game/emulator"
"github.com/giongto35/cloud-game/emulator"
"github.com/giongto35/cloud-game/libretro/nanoarch"
"github.com/giongto35/cloud-game/util"
"github.com/giongto35/cloud-game/webrtc"
storage "github.com/giongto35/cloud-game/worker/cloud-storage"
)
@ -37,11 +38,13 @@ type Room struct {
// NOTE: Not in use, lock rtcSessions
sessionsLock *sync.Mutex
// Director is emulator
director *emulator.Director
director emulator.CloudEmulator
// Cloud storage to store room state online
onlineStorage *storage.Client
// GameName
gameName string
// Meta of game
//meta emulator.Meta
}
// NewRoom creates a new room
@ -55,9 +58,6 @@ func NewRoom(roomID, gamePath, gameName string, onlineStorage *storage.Client) *
audioChannel := make(chan float32, 30)
inputChannel := make(chan int, 100)
// create director
director := emulator.NewDirector(roomID, imageChannel, audioChannel, inputChannel)
room := &Room{
ID: roomID,
@ -66,20 +66,16 @@ func NewRoom(roomID, gamePath, gameName string, onlineStorage *storage.Client) *
inputChannel: inputChannel,
rtcSessions: []*webrtc.WebRTC{},
sessionsLock: &sync.Mutex{},
director: director,
IsRunning: true,
onlineStorage: onlineStorage,
Done: make(chan struct{}, 1),
}
go room.startVideo()
go room.startAudio()
// Check if room is on local storage, if not, pull from GCS to local storage
go func(gamePath, gameName, roomID string) {
// Check room is on local or fetch from server
savepath := emulator.GetSavePath(roomID)
savepath := util.GetSavePath(roomID)
log.Println("Check ", savepath, " on local : ", room.isGameOnLocal(savepath))
if !room.isGameOnLocal(savepath) {
// Fetch room from GCP to server
@ -92,30 +88,66 @@ func NewRoom(roomID, gamePath, gameName string, onlineStorage *storage.Client) *
if roomID != "" {
gameName = getGameNameFromRoomID(roomID)
}
log.Printf("Room %s started. GameName: %s", roomID, gameName)
log.Printf("Room %s started. GamePath: %s, GameName: %s", roomID, gamePath, gameName)
// Spawn new emulator based on gameName and plug-in all channels
room.director = getEmulator(gameName, roomID, imageChannel, audioChannel, inputChannel)
path := gamePath + "/" + gameName
director.Start(path)
meta := room.director.LoadMeta(path)
log.Printf("Load with Meta %+v", meta)
go room.startVideo(meta.Width, meta.Height)
go room.startAudio(meta.AudioSampleRate)
room.director.Start()
log.Printf("Room %s ended", roomID)
start := time.Now()
// TODO: do we need GC, we can remove it
runtime.GC()
log.Printf("GC takes %s\n", time.Since(start))
}(gamePath, gameName, roomID)
return room
}
// create director
func getEmulator(gameName string, roomID string, imageChannel chan<- *image.RGBA, audioChannel chan<- float32, inputChannel <-chan int) emulator.CloudEmulator {
gameType := getGameType(gameName)
switch gameType {
case "nes":
return emulator.NewDirector(roomID, imageChannel, audioChannel, inputChannel)
case "gba":
nanoarch.Init("gba", roomID, imageChannel, audioChannel, inputChannel)
return nanoarch.NAEmulator
case "bin":
nanoarch.Init("pcsx", roomID, imageChannel, audioChannel, inputChannel)
return nanoarch.NAEmulator
}
return nil
}
// getGameNameFromRoomID parse roomID to get roomID and gameName
func getGameNameFromRoomID(roomID string) string {
parts := strings.Split(roomID, "-")
parts := strings.Split(roomID, "|")
if len(parts) <= 1 {
return ""
}
return parts[1]
}
func getGameType(gameName string) string {
parts := strings.Split(gameName, ".")
if len(parts) <= 1 {
return ""
}
return parts[len(parts)-1]
}
// generateRoomID generate a unique room ID containing 16 digits
func generateRoomID(gameName string) string {
roomID := strconv.FormatInt(rand.Int63(), 16) + "-" + gameName
roomID := strconv.FormatInt(rand.Int63(), 16) + "|" + gameName
log.Println("Generate Room ID", roomID)
//roomID := uuid.Must(uuid.NewV4()).String()
return roomID
@ -196,7 +228,8 @@ func (r *Room) Close() {
r.IsRunning = false
log.Println("Closing room", r.ID)
log.Println("Closing director of room ", r.ID)
close(r.director.Done)
r.director.Close()
//close(r.director.Done)
log.Println("Closing input of room ", r.ID)
close(r.inputChannel)
close(r.Done)