Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

an Ebitengine application hangs when the app is launched via a Steam client #3181

Closed
1 of 11 tasks
corfe83 opened this issue Jan 12, 2025 · 21 comments
Closed
1 of 11 tasks

Comments

@corfe83
Copy link
Contributor

corfe83 commented Jan 12, 2025

Ebitengine Version

2.8.6

Operating System

  • Windows
  • macOS
  • Linux
  • FreeBSD
  • OpenBSD
  • Android
  • iOS
  • Nintendo Switch
  • PlayStation 5
  • Xbox
  • Web Browsers

Go Version (go version)

1.23.4

What steps will reproduce the problem?

  1. Build ebiten/v2/examples/noise
  2. In Steam Library screen, go to "Games" menu and click "Add a Non-Steam Game to my Library"
  3. Add noise.exe to the library
  4. Use steam to launch noise.exe, it will launch with steam overlay enabled
  5. It doesn't happen every time GC runs. But sometimes when GC runs, the whole process hangs for ~10 seconds or more.
  6. If you add this line to the source code for noise, it reproduces much more often: debug.SetGCPercent(5)

image

What is the expected result?

Noise.exe runs smoothly inside of steam, just like outside of it

What happens instead?

It hangs or freezes for 10+ seconds

Anything else you feel useful to add?

I have AMD Ryzen 5800X, 32GB of RAM, and an nvidia 3080 RTX GPU, this shouldn't be slow. This happens with both OpenGL and DirectX.

You can use environment variables to play with GOGC to either make GC run more frequently, or never runs. If you disable GC it will never happen, and if you set GOGC to something like 5 so GC runs very often, it happens very frequently. It does NOT happen every time GC runs, it only happens sometimes.

I suspect that any ebiten executable that runs GC with steam overlay will be affected.

@hajimehoshi
Copy link
Owner

This doesn't seem to be an Ebitengine specific issue
https://steamcommunity.com/groups/steamworks/discussions/0/600767061860305373/

@hajimehoshi
Copy link
Owner

By the way, did you specify -ldflags="-H=windowsgui"? https://github.com/ebitengine/hideconsole

@corfe83
Copy link
Contributor Author

corfe83 commented Jan 12, 2025

Yes, the problem also reproduces with -ldflags="-H=windowsgui"

@hajimehoshi
Copy link
Owner

Hmm, wouldn't rebooting your machine change the situation?

@corfe83
Copy link
Contributor Author

corfe83 commented Jan 13, 2025

I just tested to be sure. Unfortunately, rebooting does not change the situation. I am still able to reproduce this in the noise example with the above steps.

@hajimehoshi
Copy link
Owner

I could reproduce this by replacing an exe with noise's exe and staring the game via the Steam client!

@hajimehoshi hajimehoshi added this to the v2.8.7 milestone Jan 13, 2025
@hajimehoshi
Copy link
Owner

I saw the GC stats and apparently freezing is not counted as GC time.

diff --git a/examples/noise/main.go b/examples/noise/main.go
index 0bf33c55f..f65e4e9c0 100644
--- a/examples/noise/main.go
+++ b/examples/noise/main.go
@@ -18,6 +18,7 @@ import (
        "fmt"
        "image"
        "log"
+       "runtime/debug"

        "github.com/hajimehoshi/ebiten/v2"
        "github.com/hajimehoshi/ebiten/v2/ebitenutil"
@@ -57,12 +58,17 @@ func (g *Game) Update() error {
                g.noiseImage.Pix[4*i+2] = uint8(x >> 8)
                g.noiseImage.Pix[4*i+3] = 0xff
        }
+       _ = make([]byte, 256*1024)
        return nil
 }

 func (g *Game) Draw(screen *ebiten.Image) {
        screen.WritePixels(g.noiseImage.Pix)
        ebitenutil.DebugPrint(screen, fmt.Sprintf("TPS: %0.2f\nFPS: %0.2f", ebiten.ActualTPS(), ebiten.ActualFPS()))
+
+       var gcStats debug.GCStats
+       debug.ReadGCStats(&gcStats)
+       ebitenutil.DebugPrint(screen, fmt.Sprintf("\n\nLastGC: %s\nNumGC: %d\nPauseTotal: %s", gcStats.LastGC, gcStats.NumGC, gcStats.PauseTotal))
 }

image

@hajimehoshi
Copy link
Owner

hajimehoshi commented Jan 13, 2025

I think I could reproduce the freezing even without Ebitengine

package main

import (
	"runtime"
	"syscall"
	"unsafe"
)

const (
	WS_OVERLAPPEDWINDOW = 0x00000000 | 0x00C00000 | 0x00080000 | 0x00040000 | 0x00020000 | 0x00010000
	CW_USEDEFAULT       = ^0x7fffffff
	SW_SHOW             = 5
	WM_DESTROY          = 2
)

type (
	ATOM      uint16
	HANDLE    uintptr
	HINSTANCE HANDLE
	HICON     HANDLE
	HCURSOR   HANDLE
	HBRUSH    HANDLE
	HWND      HANDLE
	HMENU     HANDLE
)

type WNDCLASSEX struct {
	Size       uint32
	Style      uint32
	WndProc    uintptr
	ClsExtra   int32
	WndExtra   int32
	Instance   HINSTANCE
	Icon       HICON
	Cursor     HCURSOR
	Background HBRUSH
	MenuName   *uint16
	ClassName  *uint16
	IconSm     HICON
}

type RECT struct {
	Left, Top, Right, Bottom int32
}

type POINT struct {
	X, Y int32
}

type MSG struct {
	Hwnd    HWND
	Message uint32
	WParam  uintptr
	LParam  uintptr
	Time    uint32
	Pt      POINT
}

func GetModuleHandle(modulename *uint16) HINSTANCE {
	r, _, _ := syscall.SyscallN(procGetModuleHandle.Addr(), uintptr(unsafe.Pointer(modulename)))
	return HINSTANCE(r)
}

func RegisterClassEx(w *WNDCLASSEX) ATOM {
	r, _, _ := syscall.SyscallN(procRegisterClassEx.Addr(), uintptr(unsafe.Pointer(w)))
	return ATOM(r)
}

func CreateWindowEx(exStyle uint, className, windowName *uint16,
	style uint, x, y, width, height int, parent HWND, menu HMENU,
	instance HINSTANCE, param unsafe.Pointer) HWND {
	r, _, _ := syscall.SyscallN(procCreateWindowEx.Addr(), uintptr(exStyle), uintptr(unsafe.Pointer(className)),
		uintptr(unsafe.Pointer(windowName)), uintptr(style), uintptr(x), uintptr(y), uintptr(width), uintptr(height),
		uintptr(parent), uintptr(menu), uintptr(instance), uintptr(param))
	return HWND(r)
}

func AdjustWindowRect(rect *RECT, style uint, menu bool) bool {
	var iMenu uintptr
	if menu {
		iMenu = 1
	}
	r, _, _ := syscall.SyscallN(procAdjustWindowRect.Addr(), uintptr(unsafe.Pointer(rect)), uintptr(style), iMenu)
	return r != 0
}

func ShowWindow(hwnd HWND, cmdshow int) bool {
	r, _, _ := syscall.SyscallN(procShowWindow.Addr(), uintptr(hwnd), uintptr(cmdshow))
	return r != 0
}

func GetMessage(msg *MSG, hwnd HWND, msgFilterMin, msgFilterMax uint32) int {
	r, _, _ := syscall.SyscallN(procGetMessage.Addr(), uintptr(unsafe.Pointer(msg)), uintptr(hwnd), uintptr(msgFilterMin), uintptr(msgFilterMax))
	return int(r)
}

func TranslateMessage(msg *MSG) bool {
	r, _, _ := syscall.SyscallN(procTranslateMessage.Addr(), uintptr(unsafe.Pointer(msg)))
	return r != 0
}

func DispatchMessage(msg *MSG) uintptr {
	r, _, _ := syscall.SyscallN(procDispatchMessage.Addr(), uintptr(unsafe.Pointer(msg)))
	return r
}

func DefWindowProc(hwnd HWND, msg uint32, wParam, lParam uintptr) uintptr {
	r, _, _ := syscall.SyscallN(procDefWindowProc.Addr(), uintptr(hwnd), uintptr(msg), wParam, lParam)
	return r
}

func PostQuitMessage(exitCode int) {
	syscall.SyscallN(procPostQuitMessage.Addr(), uintptr(exitCode))
}

var (
	kernel32            = syscall.NewLazyDLL("kernel32.dll")
	procGetModuleHandle = kernel32.NewProc("GetModuleHandleW")

	user32               = syscall.NewLazyDLL("user32.dll")
	procRegisterClassEx  = user32.NewProc("RegisterClassExW")
	procCreateWindowEx   = user32.NewProc("CreateWindowExW")
	procAdjustWindowRect = user32.NewProc("AdjustWindowRect")
	procShowWindow       = user32.NewProc("ShowWindow")
	procGetMessage       = user32.NewProc("GetMessageW")
	procTranslateMessage = user32.NewProc("TranslateMessage")
	procDispatchMessage  = user32.NewProc("DispatchMessageW")
	procDefWindowProc    = user32.NewProc("DefWindowProcW")
	procPostQuitMessage  = user32.NewProc("PostQuitMessage")
)

func init() {
	runtime.LockOSThread()
}

func main() {
	className, err := syscall.UTF16PtrFromString("Sample Window Class")
	if err != nil {
		panic(err)
	}
	inst := GetModuleHandle(className)

	wc := WNDCLASSEX{
		Size:      uint32(unsafe.Sizeof(WNDCLASSEX{})),
		WndProc:   syscall.NewCallback(wndProc),
		Instance:  inst,
		ClassName: className,
	}

	RegisterClassEx(&wc)

	wr := RECT{
		Left:   0,
		Top:    0,
		Right:  320,
		Bottom: 240,
	}
	title, err := syscall.UTF16PtrFromString("My Title")
	if err != nil {
		panic(err)
	}
	AdjustWindowRect(&wr, WS_OVERLAPPEDWINDOW, false)
	hwnd := CreateWindowEx(
		0, className,
		title,
		WS_OVERLAPPEDWINDOW,
		CW_USEDEFAULT, CW_USEDEFAULT, int(wr.Right-wr.Left), int(wr.Bottom-wr.Top),
		0, 0, inst, nil,
	)
	if hwnd == 0 {
		panic(syscall.GetLastError())
	}

	ShowWindow(hwnd, SW_SHOW)

	go func() {
		for {
			_ = make([]byte, 256*1024)
			time.Sleep(time.Millisecond)
		}
	}()

	var msg MSG
	for GetMessage(&msg, 0, 0, 0) != 0 {
		TranslateMessage(&msg)
		DispatchMessage(&msg)
	}
}

func wndProc(hwnd HWND, msg uint32, wparam, lparam uintptr) uintptr {
	switch msg {
	case WM_DESTROY:
		PostQuitMessage(0)
	}
	return DefWindowProc(hwnd, msg, wparam, lparam)
}

@hajimehoshi
Copy link
Owner

hajimehoshi commented Jan 13, 2025

I could reproduce this even as a simple console game golang/go#71242 (comment)

@hajimehoshi
Copy link
Owner

hajimehoshi commented Jan 13, 2025

@corfe83 Could you try adding this to your game?

func init() {
	if runtime.GOOS == "windows" && os.Getenv("SteamClientLaunch") == "1" {
		runtime.GOMAXPROCS(max(1, min(2, runtime.NumCPU()-1)))
	}
}

@hajimehoshi hajimehoshi changed the title Examples/Noise Hangs when Steam Overlay Is Applied an Ebitengine application hangs when the app is launched via a Steam client Jan 13, 2025
@corfe83
Copy link
Contributor Author

corfe83 commented Jan 14, 2025

@hajimehoshi I just tested adding this code to the game. It does not help, unfortunately.

@hajimehoshi
Copy link
Owner

Hmm, what about runtime.GOMAXPROCS(1)?

@corfe83
Copy link
Contributor Author

corfe83 commented Jan 14, 2025

The problem also reproduces with GOMAXPROCS(1). I'm using ebiten single thread mode.

@hajimehoshi
Copy link
Owner

The problem also reproduces with GOMAXPROCS(1). I'm using ebiten single thread mode.

What if you do not use the single thread mode?

@corfe83
Copy link
Contributor Author

corfe83 commented Jan 14, 2025

Multi thread mode with GOMAXPROCS(1) still reproduces the problem as well.

@hajimehoshi
Copy link
Owner

Related: https://steamcommunity.com/groups/SteamClientBeta/discussions/3/4206993388791543748/

Changing the permission might resolve the issue, but this is too hacky.

@hajimehoshi
Copy link
Owner

hajimehoshi commented Jan 14, 2025

Would manual GCs causes the issue? For example:

debug.SetGCPercent(-1)
go func() {
    for {
        runtime.GC()
        time.Sleep(time.Second)
    }
}()

@hajimehoshi
Copy link
Owner

As we discussed in the Discord server, please try -ldflags="-X=runtime.godebugDefault=asyncpreemptoff=1" when buliding your app.

@corfe83
Copy link
Contributor Author

corfe83 commented Jan 15, 2025

I can confirm this resolves the issue. I'm so glad to see a workaround we can use to prevent this issue!

@hajimehoshi
Copy link
Owner

Awesome! I'll update the blog article about Steam https://ebitengine.org/en/blog/steam.html to notify this later.

After that, I'll close this since there is nothing we can do in Ebitengine for this issue unfortunately. My current understanding is that the Steam hooking does fishy things, so I have to prove this by creating a minimized test case in C.

@hajimehoshi hajimehoshi removed this from the v2.8.7 milestone Jan 15, 2025
@corfe83
Copy link
Contributor Author

corfe83 commented Jan 15, 2025

Yes, closing once documented seems reasonable to me!

hajimehoshi added a commit to ebitengine/ebitengine.org that referenced this issue Jan 16, 2025
@hajimehoshi hajimehoshi closed this as not planned Won't fix, can't repro, duplicate, stale Jan 16, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants