From aca3e2e3ad1fbbf8a4a0eaa43e9978254a67df51 Mon Sep 17 00:00:00 2001 From: spddl Date: Mon, 30 May 2022 12:54:14 +0200 Subject: [PATCH] init --- .gitignore | 8 + GoSysLat.code-workspace | 8 + GoSysLat.manifest | 15 + Icon.ico | Bin 0 -> 131290 bytes LICENSE | 4 +- RTSSClient/RTSSClient.go | 224 ++++++++++++ RTSSClient/RTSSClient.h.go | 226 ++++++++++++ RTSSClient/go.mod | 5 + RTSSClient/go.sum | 2 + RTSSClient/kernel32dll.go | 28 ++ ReadingQueue.go | 344 ++++++++++++++++++ SaveToFile.go | 280 +++++++++++++++ TargetWindow_D3D9/d3d9.go | 294 +++++++++++++++ TargetWindow_D3D9/go.mod | 10 + TargetWindow_D3D9/go.sum | 17 + TargetWindow_OpenGL/go.mod | 8 + TargetWindow_OpenGL/go.sum | 4 + TargetWindow_OpenGL/opengl.go | 258 +++++++++++++ TestCase_BCDStore.ps1 | 89 +++++ TestCase_Freestyle.ps1 | 10 + TestCase_GameMode.ps1 | 51 +++ TestCase_HAGS.ps1 | 51 +++ TestCase_NVidia.ps1 | 101 ++++++ TestCase_PresentMode.ps1 | 47 +++ TestCase_RTSS_print_3.bat | 1 + TestCase_RTSS_print_4.bat | 1 + TestCase_VRR.ps1 | 50 +++ TestCase_win32ps.ps1 | 65 ++++ USBController/USBController.go | 137 +++++++ USBController/go.mod | 5 + USBController/go.sum | 2 + USBController/kernel32dll.go | 129 +++++++ USBController/setupapi_windows.go | 497 ++++++++++++++++++++++++++ USBController/setupapidll.go | 87 +++++ USBController/types_windows.go | 556 +++++++++++++++++++++++++++++ USBController/zsetupapi_windows.go | 432 ++++++++++++++++++++++ build.bat | 9 + cliFlags.go | 78 ++++ config.go | 122 +++++++ example.png | Bin 0 -> 34887 bytes go.mod | 33 ++ go.sum | 42 +++ gui.go | 337 +++++++++++++++++ gui_SettingsDialog.go | 367 +++++++++++++++++++ gui_logs.go | 216 +++++++++++ helper.go | 52 +++ helper_test.go | 134 +++++++ main.go | 228 ++++++++++++ readme.md | 14 + rsrc.syso | Bin 0 -> 132818 bytes run.bat | 24 ++ 51 files changed, 5700 insertions(+), 2 deletions(-) create mode 100644 .gitignore create mode 100644 GoSysLat.code-workspace create mode 100644 GoSysLat.manifest create mode 100644 Icon.ico create mode 100644 RTSSClient/RTSSClient.go create mode 100644 RTSSClient/RTSSClient.h.go create mode 100644 RTSSClient/go.mod create mode 100644 RTSSClient/go.sum create mode 100644 RTSSClient/kernel32dll.go create mode 100644 ReadingQueue.go create mode 100644 SaveToFile.go create mode 100644 TargetWindow_D3D9/d3d9.go create mode 100644 TargetWindow_D3D9/go.mod create mode 100644 TargetWindow_D3D9/go.sum create mode 100644 TargetWindow_OpenGL/go.mod create mode 100644 TargetWindow_OpenGL/go.sum create mode 100644 TargetWindow_OpenGL/opengl.go create mode 100644 TestCase_BCDStore.ps1 create mode 100644 TestCase_Freestyle.ps1 create mode 100644 TestCase_GameMode.ps1 create mode 100644 TestCase_HAGS.ps1 create mode 100644 TestCase_NVidia.ps1 create mode 100644 TestCase_PresentMode.ps1 create mode 100644 TestCase_RTSS_print_3.bat create mode 100644 TestCase_RTSS_print_4.bat create mode 100644 TestCase_VRR.ps1 create mode 100644 TestCase_win32ps.ps1 create mode 100644 USBController/USBController.go create mode 100644 USBController/go.mod create mode 100644 USBController/go.sum create mode 100644 USBController/kernel32dll.go create mode 100644 USBController/setupapi_windows.go create mode 100644 USBController/setupapidll.go create mode 100644 USBController/types_windows.go create mode 100644 USBController/zsetupapi_windows.go create mode 100644 build.bat create mode 100644 cliFlags.go create mode 100644 config.go create mode 100644 example.png create mode 100644 go.mod create mode 100644 go.sum create mode 100644 gui.go create mode 100644 gui_SettingsDialog.go create mode 100644 gui_logs.go create mode 100644 helper.go create mode 100644 helper_test.go create mode 100644 main.go create mode 100644 readme.md create mode 100644 rsrc.syso create mode 100644 run.bat diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f863235 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +GoSysLat.exe +LogFolder/ +TestCases/ +TargetWindow_D3D11/ +*.csv +*.html +*.txt +*.log \ No newline at end of file diff --git a/GoSysLat.code-workspace b/GoSysLat.code-workspace new file mode 100644 index 0000000..876a149 --- /dev/null +++ b/GoSysLat.code-workspace @@ -0,0 +1,8 @@ +{ + "folders": [ + { + "path": "." + } + ], + "settings": {} +} \ No newline at end of file diff --git a/GoSysLat.manifest b/GoSysLat.manifest new file mode 100644 index 0000000..99bcacd --- /dev/null +++ b/GoSysLat.manifest @@ -0,0 +1,15 @@ + + + + + + + + + + + PerMonitorV2, PerMonitor + True + + + \ No newline at end of file diff --git a/Icon.ico b/Icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..e16ece6b44b006eb4898b9189158ec6bb5499dc9 GIT binary patch literal 131290 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 W_%42n&#E}_v-S61QSRYh @@ -671,4 +671,4 @@ into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read -. +. \ No newline at end of file diff --git a/RTSSClient/RTSSClient.go b/RTSSClient/RTSSClient.go new file mode 100644 index 0000000..5b069f1 --- /dev/null +++ b/RTSSClient/RTSSClient.go @@ -0,0 +1,224 @@ +package RTSSClient + +import ( + "log" + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +const ( + slotOwnerOSD = "GoSyslat______________" + clientPriority = 1 + + Black = "" + White = "" +) + +type unsafeSlice struct { + Data unsafe.Pointer + Len int + Cap int +} + +const ( + STANDARD_RIGHTS_REQUIRED = 0x000F0000 + SECTION_QUERY = 0x0001 + SECTION_MAP_WRITE = 0x0002 + SECTION_MAP_READ = 0x0004 + SECTION_MAP_EXECUTE = 0x0008 + SECTION_EXTEND_SIZE = 0x0010 + + SECTION_ALL_ACCESS = STANDARD_RIGHTS_REQUIRED | SECTION_QUERY | SECTION_MAP_WRITE | SECTION_MAP_READ | SECTION_MAP_EXECUTE | SECTION_EXTEND_SIZE + FILE_MAP_ALL_ACCESS = SECTION_ALL_ACCESS +) + +func UpdateOSD(lpText string) { + var bResult bool + hMapFile, _ := OpenFileMapping(FILE_MAP_ALL_ACCESS, false, "RTSSSharedMemoryV2") + if hMapFile == 0 { + // when RTSS has not been started + // log.Println("could not open RTSSSharedMemoryV2") + // log.Println(err) + return + } + defer syscall.CloseHandle(hMapFile) + + if hMapFile != 0 { + pMapAddr, err := syscall.MapViewOfFile(hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, 0) + if pMapAddr == 0 || err != nil { + log.Println(err) + return + } + defer syscall.UnmapViewOfFile(pMapAddr) + + pMem := (*RTSS_SHARED_MEMORY)(unsafe.Pointer(pMapAddr)) + // log.Println("pMem", prettyPrint(pMem)) + if (pMem.DwSignature == 1381258067 /*'RTSS'*/) && (pMem.DwVersion >= 0x00020000) { + for dwPass := 0; dwPass < 2; dwPass++ { + // 1st pass : find previously captured OSD slot + // 2nd pass : otherwise find the first unused OSD slot and capture it + + // If the caller is "SysLat" allow it to take over the first OSD slot + var dwEntry DWORD + if clientPriority == 0 { + dwEntry = 0 + } else { + dwEntry = 1 + } + + for ; dwEntry < pMem.DwOSDArrSize; dwEntry++ { + // allow primary OSD clients (e.g. EVGA Precision / MSI Afterburner) to use the first slot exclusively, so third party + // applications start scanning the slots from the second one - CHANGED THIS TO 0 SO I CAN BE PRIMARY BECAUSE I NEED THE CORNERS + + pEntry := (*RTSS_SHARED_MEMORY_OSD_ENTRY)(unsafe.Pointer(pMapAddr + uintptr(pMem.DwOSDArrOffset) + uintptr(dwEntry)*uintptr(pMem.DwOSDEntrySize))) + if dwPass != 0 { + if pEntry.SzOSDOwner != [256]byte{} { + strcpy(&pEntry.SzOSDOwner, slotOwnerOSD, len(slotOwnerOSD)) + } + } + + // remember that strcmp returns 0 if the strings match... so the following if statement basically says if the strings match + pEntry_szOSDOwner := windows.ByteSliceToString(pEntry.SzOSDOwner[:]) + if pEntry_szOSDOwner == slotOwnerOSD { + lpTextPtr, _ := windows.BytePtrFromString(lpText) + if pMem.DwVersion >= 0x00020007 { + // use extended text slot for v2.7 and higher shared memory, it allows displaying 4096 symbols + // instead of 256 for regular text slot + if pMem.DwVersion >= 0x0002000e { + // OSD locking is supported on v2.14 and higher shared memory + + DwBusy := (*LONG)(unsafe.Pointer(&pMem.DwBusy)) + if *DwBusy != 0 { + // bit 0 of this variable will be set if OSD is locked by renderer and cannot be refreshed + // at the moment + *DwBusy = 0 + } else { + // maybe strncpy_s is better, but it does not seem to be necessary until now + strncpy(&pEntry.SzOSDEx[0], lpTextPtr, int32(unsafe.Sizeof(pEntry.SzOSDEx))) + } + + // DWORD dwBusy = _interlockedbittestandset(&pMem.dwBusy, 0); // also not necessary + // https://cpp.hotexamples.com/de/examples/-/-/_interlockedbittestandset/cpp-_interlockedbittestandset-function-examples.html + // https://docs.microsoft.com/en-us/windows/win32/api/winnt/nf-winnt-_interlockedbittestandset + } else { + strncpy(&pEntry.SzOSDEx[0], lpTextPtr, int32(unsafe.Sizeof(pEntry.SzOSDEx))) + } + + } else { + strncpy(&pEntry.SzOSDEx[0], lpTextPtr, int32(unsafe.Sizeof(pEntry.SzOSDEx))) + } + + pMem.DwOSDFrame++ + bResult = true + break + } + } + if bResult { + break + } + } + } + } +} + +func BytePtrToString(p *byte) string { + if p == nil { + log.Println("p == nil") + return "" + } + if *p == 0 { + log.Println("*p == 0") + return "" + } + + // Find NUL terminator. + n := 0 + for ptr := unsafe.Pointer(p); *(*byte)(ptr) != 0; n++ { + ptr = unsafe.Pointer(uintptr(ptr) + 1) + } + + n += 100 + log.Println("n", n) + + var s []byte + h := (*unsafeSlice)(unsafe.Pointer(&s)) + h.Data = unsafe.Pointer(p) + h.Len = n + h.Cap = n + + return string(s) +} + +// BytePtrToString converts byte pointer to a Go string. +func BytePtrToStringg(p *byte) string { + a := (*[10000]uint8)(unsafe.Pointer(p)) + i := 0 + for a[i] != 0 { + i++ + } + return string(a[:i]) +} + +func strcpy(dest *[256]byte, src string, n int) { + maxn := len(dest) - 1 + if n > maxn { + n = maxn + } + + for i := 0; i < n; i++ { + dest[i] = src[i] + } +} + +func strncpy(dest, src *byte, len int32) *byte { + // Copy up to the len or first NULL bytes - whichever comes first. + var ( + pSrc = src + pDest = dest + i int32 + ) + for i < len && *pSrc != 0 { + *pDest = *pSrc + i++ + pSrc = (*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(src)) + uintptr(i))) + pDest = (*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(dest)) + uintptr(i))) + } + + // The rest of the dest will be padded with zeros to the len. + for i < len { + *pDest = 0 + i++ + pDest = (*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(dest)) + uintptr(i))) + } + + return dest +} + +func GetSharedMemoryVersion() DWORD { + hMapFile, err := OpenFileMapping(FILE_MAP_ALL_ACCESS, false, "RTSSSharedMemoryV2") + if hMapFile == 0 { // when RTSS has not been started + log.Println("could not open RTSSSharedMemoryV2") + log.Println(err) + return DWORD(0) + } + defer syscall.CloseHandle(hMapFile) + + if hMapFile != 0 { + pMapAddr, err := syscall.MapViewOfFile(hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, 0) + if pMapAddr == 0 || err != nil { + log.Println(err) + return DWORD(0) + } + defer syscall.UnmapViewOfFile(pMapAddr) + + pMem := (*RTSS_SHARED_MEMORY)(unsafe.Pointer(pMapAddr)) + if (pMem.DwSignature == 1381258067 /*'RTSS'*/) && (pMem.DwVersion >= 0x00020000) { + return pMem.DwVersion + } else { + log.Printf("RTSS DwSignature %#v, DwVersion %#v\n", pMem.DwSignature, pMem.DwVersion) + } + } + return DWORD(0) +} diff --git a/RTSSClient/RTSSClient.h.go b/RTSSClient/RTSSClient.h.go new file mode 100644 index 0000000..fc713ef --- /dev/null +++ b/RTSSClient/RTSSClient.h.go @@ -0,0 +1,226 @@ +package RTSSClient + +const ( + MAX_PATH = 260 +) + +type ( + DWORD uint32 + LONG int32 + LARGE_INTEGER int64 + LPBYTE *byte +) + +type RTSS_SHARED_MEMORY struct { + DwSignature DWORD + // signature allows applications to verify status of shared memory + + // The signature can be set to: + // 'RTSS' - statistics server's memory is initialized and contains + // valid data + // 0xDEAD - statistics server's memory is marked for deallocation and + // no longer contain valid data + // otherwise the memory is not initialized + DwVersion DWORD + // structure version ((major<<16) + minor) + // must be set to 0x0002xxxx for v2.x structure + + DwAppEntrySize DWORD + // size of RTSS_SHARED_MEMORY_OSD_ENTRY for compatibility with future versions + DwAppArrOffset DWORD + // offset of arrOSD array for compatibility with future versions + DwAppArrSize DWORD + // size of arrOSD array for compatibility with future versions + + DwOSDEntrySize DWORD + // size of RTSS_SHARED_MEMORY_APP_ENTRY for compatibility with future versions + DwOSDArrOffset DWORD + // offset of arrApp array for compatibility with future versions + DwOSDArrSize DWORD + // size of arrOSD array for compatibility with future versions + + DwOSDFrame DWORD + // Global OSD frame ID. Increment it to force the server to update OSD for all currently active 3D + // applications. + + // next fields are valid for v2.14 and newer shared memory format only + + DwBusy LONG // long? int32 or int64 + // set bit 0 when you're writing to shared memory and reset it when done + + // WARNING: do not forget to reset it, otherwise you'll completely lock OSD updates for all clients + + // next fields are valid for v2.15 and newer shared memory format only + + DwDesktopVideoCaptureFlags DWORD + dwDesktopVideoCaptureStat DWORD // DWORD dwDesktopVideoCaptureStat[5]; + // shared copy of desktop video capture flags and performance stats for 64-bit applications + + // next fields are valid for v2.16 and newer shared memory format only + + DwLastForegroundApp DWORD + // last foreground application entry index + DwLastForegroundAppProcessID DWORD + // last foreground application process ID + + // OSD slot descriptor structure + + // WARNING: next fields should never (!!!) be accessed directly, use the offsets to access them in order to provide + // compatibility with future versions + + arrOSD [8]RTSS_SHARED_MEMORY_OSD_ENTRY + // array of OSD slots + arrApp [256]RTSS_SHARED_MEMORY_APP_ENTRY + // array of application descriptors + + // next fields are valid for v2.9 and newer shared memory format only + + // WARNING: due to design flaw there is no offset available for this field, so it must be calculated manually as + // dwAppArrOffset + dwAppArrSize * dwAppEntrySize + + // VIDEO_CAPTURE_PARAM autoVideoCaptureParam; +} + +type RTSS_SHARED_MEMORY_OSD_ENTRY struct { + SzOSD [256]byte // char szOSD[256]; + // OSD slot text + SzOSDOwner [256]byte // char szOSDOwner[256]; + // OSD slot owner ID + + // next fields are valid for v2.7 and newer shared memory format only + + SzOSDEx [4096]byte // char szOSDEx[4096]; + // extended OSD slot text + + // next fields are valid for v2.12 and newer shared memory format only + + Buffer [262144]byte // BYTE buffer[262144]; + // OSD slot data buffer +} + +type RTSS_SHARED_MEMORY_APP_ENTRY struct { + // application identification related fields + + DwProcessID DWORD + // process ID + SzName [MAX_PATH]byte + // process executable name + DwFlags DWORD + // application specific flags + + // instantaneous framerate related fields + + DwTime0 DWORD + // start time of framerate measurement period (in milliseconds) + + // Take a note that this field must contain non-zero value to calculate + // framerate properly! + DwTime1 DWORD + // end time of framerate measurement period (in milliseconds) + DwFrames DWORD + // amount of frames rendered during (dwTime1 - dwTime0) period + DwFrameTime DWORD + // frame time (in microseconds) + + // to calculate framerate use the following formulas: + + // 1000.0f * dwFrames / (dwTime1 - dwTime0) for framerate calculated once per second + // or + // 1000000.0f / dwFrameTime for framerate calculated once per frame + + // framerate statistics related fields + + DwStatFlags DWORD + // bitmask containing combination of STATFLAG_... flags + DwStatTime0 DWORD + // statistics record period start time + DwStatTime1 DWORD + // statistics record period end time + DwStatFrames DWORD + // total amount of frames rendered during statistics record period + DwStatCount DWORD + // amount of min/avg/max measurements during statistics record period + DwStatFramerateMin DWORD + // minimum instantaneous framerate measured during statistics record period + DwStatFramerateAvg DWORD + // average instantaneous framerate measured during statistics record period + DwStatFramerateMax DWORD + // maximum instantaneous framerate measured during statistics record period + + // OSD related fields + + DwOSDX DWORD + // OSD X-coordinate (coordinate wrapping is allowed, i.e. -5 defines 5 + // pixel offset from the right side of the screen) + DwOSDY DWORD + // OSD Y-coordinate (coordinate wrapping is allowed, i.e. -5 defines 5 + // pixel offset from the bottom side of the screen) + DwOSDPixel DWORD + // OSD pixel zooming ratio + DwOSDColor DWORD + // OSD color in RGB format + DwOSDFrame DWORD + // application specific OSD frame ID. Don't change it directly! + + DwScreenCaptureFlags DWORD + SzScreenCapturePath [MAX_PATH]byte // szScreenCapturePath[MAX_PATH] string + + // next fields are valid for v2.1 and newer shared memory format only + + DwOSDBgndColor DWORD + // OSD background color in RGB format + + // next fields are valid for v2.2 and newer shared memory format only + + DwVideoCaptureFlags DWORD + SzVideoCapturePath [MAX_PATH]byte // szVideoCapturePath[MAX_PATH] string + DwVideoFramerate DWORD + DwVideoFramesize DWORD + DwVideoFormat DWORD + DwVideoQuality DWORD + DwVideoCaptureThreads DWORD + + DwScreenCaptureQuality DWORD + DwScreenCaptureThreads DWORD + + // next fields are valid for v2.3 and newer shared memory format only + + DwAudioCaptureFlags DWORD + + // next fields are valid for v2.4 and newer shared memory format only + + DwVideoCaptureFlagsEx DWORD + + // next fields are valid for v2.5 and newer shared memory format only + + DwAudioCaptureFlags2 DWORD + + DwStatFrameTimeMin DWORD + DwStatFrameTimeAvg DWORD + DwStatFrameTimeMax DWORD + DwStatFrameTimeCount DWORD + + DwStatFrameTimeBuf DWORD // dwStatFrameTimeBuf[1024] DWORD + DwStatFrameTimeBufPos DWORD + DwStatFrameTimeBufFramerate DWORD + + // next fields are valid for v2.6 and newer shared memory format only + + QwAudioCapturePTTEventPush LARGE_INTEGER + QwAudioCapturePTTEventRelease LARGE_INTEGER + + QwAudioCapturePTTEventPush2 LARGE_INTEGER + QwAudioCapturePTTEventRelease2 LARGE_INTEGER + + // next fields are valid for v2.8 and newer shared memory format only + + DwPrerecordSizeLimit DWORD + DwPrerecordTimeLimit DWORD + + // next fields are valid for v2.13 and newer shared memory format only + + QwStatTotalTime LARGE_INTEGER + DwStatFrameTimeLowBuf [1024]DWORD // dwStatFrameTimeLowBuf[1024] DWORD + DwStatFramerate1Dot0PercentLow DWORD + DwStatFramerate0Dot1PercentLow DWORD +} diff --git a/RTSSClient/go.mod b/RTSSClient/go.mod new file mode 100644 index 0000000..987b7aa --- /dev/null +++ b/RTSSClient/go.mod @@ -0,0 +1,5 @@ +module RTSSClient + +go 1.17 + +require golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 diff --git a/RTSSClient/go.sum b/RTSSClient/go.sum new file mode 100644 index 0000000..768bd3f --- /dev/null +++ b/RTSSClient/go.sum @@ -0,0 +1,2 @@ +golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 h1:nhht2DYV/Sn3qOayu8lM+cU1ii9sTLUeBQwQQfUHtrs= +golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/RTSSClient/kernel32dll.go b/RTSSClient/kernel32dll.go new file mode 100644 index 0000000..593d0ff --- /dev/null +++ b/RTSSClient/kernel32dll.go @@ -0,0 +1,28 @@ +package RTSSClient + +import ( + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +var ( + // Library + kernel32 = syscall.NewLazyDLL("kernel32.dll") + + // Functions + openFileMapping = kernel32.NewProc("OpenFileMappingW") +) + +func OpenFileMapping(dwDesiredAccess uint32, bInheritHandle bool, lpName string) (syscall.Handle, error) { + namep, _ := windows.UTF16PtrFromString(lpName) + var inheritHandle uint32 + if bInheritHandle { + inheritHandle = 1 + } + + ret, _, err := openFileMapping.Call(uintptr(dwDesiredAccess), uintptr(inheritHandle), uintptr(unsafe.Pointer(namep))) + + return syscall.Handle(ret), err +} diff --git a/ReadingQueue.go b/ReadingQueue.go new file mode 100644 index 0000000..6aab936 --- /dev/null +++ b/ReadingQueue.go @@ -0,0 +1,344 @@ +package main + +import ( + "context" + "fmt" + "os" + "sync" + "syscall" + "time" + + "github.com/spddl/RTSSClient" + "github.com/spddl/USBController" +) + +type Queue struct { + ctx context.Context + cancel context.CancelFunc + wg *sync.WaitGroup + ticker *time.Ticker + sendch chan int32 +} + +func (q *Queue) StartQueue() { + q.ctx, q.cancel = context.WithCancel(context.Background()) + q.wg = &sync.WaitGroup{} + q.wg.Add(2) // Reading & (Ticker || TickerCli) + q.ticker = time.NewTicker(time.Second) + q.sendch = make(chan int32) +} + +func (q *Queue) Reading(hPort *syscall.Handle) { + defer q.wg.Done() + + sysLatResultsBytes := []byte{} + + // timerA := time.Now() + // timerB := time.Now() + // timerC := time.Now() + + for { + select { + case <-q.ctx.Done(): + return + default: + } + + serialReadData, ok := USBController.ReadByte(hPort) // the function runs every 100ms on timeout + if !ok { // and starts from the beginning if no data was received + continue + } + + // log.Printf("serialReadData - %d\n", serialReadData) + + switch serialReadData { + case 1: // A + // fmt.Printf("- From C to A => %s\n", time.Since(timerC)) + // timerA = time.Now() + q.sendch <- -1 + case 2: // B + // fmt.Printf("- From A to B => \t\t%s\n", time.Since(timerA)) + // timerB = time.Now() + q.sendch <- -2 + case 3: // C + // log.Println("C") + // fmt.Printf("- From B to C => \t\t\t\t%s\n", time.Since(timerB)) + // timerC = time.Now() + + // log.Println("len(sysLatResultsBytes)", len(sysLatResultsBytes), sysLatResultsBytes, string(sysLatResultsBytes)) + // sysLatResultsBytes = []byte{} + + if len(sysLatResultsBytes) != 0 { + data := make([]byte, 8) + copy(data, sysLatResultsBytes) + intVar := ByteArrayToInt(data) + q.sendch <- int32(intVar) + sysLatResultsBytes = []byte{} + } else { + if db.Count != 0 { + dbBacklog = append(dbBacklog, db) + gui.ResetData() + } + db.Countdown = time.Time{} + } + default: + sysLatResultsBytes = append(sysLatResultsBytes, serialReadData) + } + } +} + +func (q *Queue) Ticker() { + defer q.wg.Done() + for { + select { + case <-q.ticker.C: + countCurrent := len(db.All.Current) + if countCurrent != 0 { + // log.Println(countCurrent, "records in one sec") + + // calculates the average of all data accumulated within one second + var sum float64 = 0 + for i := 0; i < countCurrent; i++ { + sum += db.All.Current[i].Value + } + SecValue := sum / float64(countCurrent) + + // and adds it to the "Second.Current" + db.Second.Backlog = append(db.Second.Backlog, Data{ + Timestamp: time.Now(), + Value: SecValue, + }) + gui.SetValue(gui.SecValue, float64ToString(Round(SecValue))) + lenSecondBacklog := len(db.Second.Backlog) + if lenSecondBacklog > 1 { + gui.SetDeltaValue(gui.SecValueDelta, db.Second.Backlog[lenSecondBacklog-2].Value, SecValue) + } + + // All "Current" data is pushed into the "Backlog" for later use + db.All.Backlog = append(db.All.Backlog, db.All.Current...) + db.All.Current = []Data{} + + countSecond := len(db.Second.Backlog) + if countSecond >= 60 { // one minute has passed and we can start calculating the average in the last minute + last60Values := db.Second.Backlog[countSecond-60:] + var sum float64 = 0 + for i := 0; i < 60; i++ { + sum += last60Values[i].Value + } + MinuteValue := sum / float64(len(last60Values)) + + // and adds it to the "Minute.Current" + db.Minute.Current = append(db.Minute.Current, Data{ + Timestamp: time.Now(), + Value: MinuteValue, + }) + gui.SetValue(gui.MinuteValue, float64ToString(Round(MinuteValue))) + + lenMinuteCurrent := len(db.Minute.Current) + if lenMinuteCurrent > 1 { + gui.SetDeltaValue(gui.MinuteValueDelta, db.Minute.Current[lenMinuteCurrent-2].Value, MinuteValue) + } + + db.Second.Backlog = append(db.Second.Backlog, db.Second.Current...) + db.Second.Current = []Data{} + } + } + case <-q.ctx.Done(): + q.ticker.Stop() + return + } + } +} + +func (q *Queue) TickerCli() { + defer q.wg.Done() + for { + select { + case <-q.ticker.C: + countCurrent := len(db.All.Current) + if countCurrent != 0 { + // calculates the average of all data accumulated within one second + var sum float64 = 0 + for i := 0; i < countCurrent; i++ { + sum += db.All.Current[i].Value + } + SecValue := sum / float64(countCurrent) + + // and adds it to the "Second.Current" + db.Second.Backlog = append(db.Second.Backlog, Data{ + Timestamp: time.Now(), + Value: SecValue, + }) + + lenSecondBacklog := len(db.Second.Backlog) + if lenSecondBacklog > 1 { + gui.SetDeltaValue(gui.SecValueDelta, db.Second.Backlog[lenSecondBacklog-2].Value, SecValue) + } + + // All "Current" data is pushed into the "Backlog" for later use + db.All.Backlog = append(db.All.Backlog, db.All.Current...) + db.All.Current = []Data{} + + countSecond := len(db.Second.Backlog) + if countSecond >= 60 { // one minute has passed and we can start calculating the average in the last minute + last60Values := db.Second.Backlog[countSecond-60:] + var sum float64 = 0 + for i := 0; i < 60; i++ { + sum += last60Values[i].Value + } + MinuteValue := sum / float64(len(last60Values)) + + // and adds it to the "Minute.Current" + db.Minute.Current = append(db.Minute.Current, Data{ + Timestamp: time.Now(), + Value: MinuteValue, + }) + + lenMinuteCurrent := len(db.Minute.Current) + if lenMinuteCurrent > 1 { + gui.SetDeltaValue(gui.MinuteValueDelta, db.Minute.Current[lenMinuteCurrent-2].Value, MinuteValue) + } + + db.Second.Backlog = append(db.Second.Backlog, db.Second.Current...) + db.Second.Current = []Data{} + } + } + case <-q.ctx.Done(): + q.ticker.Stop() + return + } + } +} + +func (q *Queue) DataProcessing() { + for sysLatResult := range q.sendch { + // log.Println("sysLatResult", sysLatResult, c.DetectLight) + + if !c.DetectLight { + switch sysLatResult { + case -1: + sysLatResult = -2 + case -2: + sysLatResult = -1 + } + } + + switch sysLatResult { + case -1: // White + if targetOGL.IsActive { + targetOGL.SetWhite() + } + if targetD3D9.IsActive { + targetD3D9.SetWhite() + } + if !cliMode { + RTSSClient.UpdateOSD(RTSSOSDWhite) + } else if f.RTSS { + RTSSClient.UpdateOSD(RTSSOSDWhite) + } + case -2: // Black + if targetOGL.IsActive { + targetOGL.SetBlack() + } + if targetD3D9.IsActive { + targetD3D9.SetBlack() + } + if !cliMode { + RTSSClient.UpdateOSD(RTSSOSDBlack) + } else if f.RTSS { + RTSSClient.UpdateOSD(RTSSOSDBlack) + } + + default: // Data in µs + // fmt.Println(sysLatResult) + + timestamp := time.Now() + valueFloat := float64(sysLatResult) / 1000 + + if cliMode { + db.Count++ + if db.Countdown.IsZero() { + db.Countdown = timestamp + queue.ticker.Reset(time.Second) + } + + db.All.Current = append(db.All.Current, Data{ + Timestamp: timestamp, + Value: valueFloat, + }) + + if f.Print { + db.e.Add(valueFloat) + // https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences + // fmt.Printf("\033c%f", valueFloat) + fmt.Printf("\033c%f", db.e.Value()) + } + + SinceCountdown := time.Since(db.Countdown) + if f.Count != -1 && f.Count < db.Count || time.Duration(0) != f.Delay && SinceCountdown > f.Delay { + dbBacklog = append(dbBacklog, db) + var dbTemp = []Database{db} + dbt := NewDatabaseTableModel(&dbTemp) + for _, values := range dbt.items { + if len(values.Data) != 0 { + SaveToFile(values) + os.Exit(0) + } + } + } + + } else { + oldEwmaValue := db.e.Value() + db.e.Add(valueFloat) + newEwmaValue := db.e.Value() + gui.SetValue(gui.EwmaValue, float64ToString(Round(newEwmaValue))) + gui.SetDeltaValue(gui.EwmaValueDelta, oldEwmaValue, newEwmaValue) + + db.Count++ + if db.Countdown.IsZero() { + gui.ResetGUI() + db.Countdown = timestamp + queue.ticker.Reset(time.Second) + if gui.FileName != nil && gui.FileName.Text() == "" { + gui.Synchronize(func() { + gui.FileName.SetText(timestamp.Format("2006-01-02T15-04-05")) + }) + } + } + + gui.SetValue(gui.Count, IntToString(db.Count)) + + db.All.Current = append(db.All.Current, Data{ + Timestamp: timestamp, + Value: valueFloat, + }) + + if valueFloat > db.All.Max { + db.All.Max = valueFloat + gui.SetValue(gui.ValueMax, float64ToString(valueFloat)) + } + + if valueFloat < db.All.Min || db.All.Min == 0 { + db.All.Min = valueFloat + gui.SetValue(gui.ValueMin, float64ToString(valueFloat)) + } + + gui.SetValue(gui.Value, float64ToString(valueFloat)) + SinceCountdown := time.Since(db.Countdown) + gui.SetValue(gui.Countdown, SinceCountdown.String()) + if cliMode { + if f.Count >= db.Count || SinceCountdown >= f.Delay { + dbBacklog = append(dbBacklog, db) + var dbTemp = []Database{db} + dbt := NewDatabaseTableModel(&dbTemp) + for _, values := range dbt.items { + if len(values.Data) != 0 { + SaveToFile(values) + } + } + } + } + } + } + } +} diff --git a/SaveToFile.go b/SaveToFile.go new file mode 100644 index 0000000..8aca588 --- /dev/null +++ b/SaveToFile.go @@ -0,0 +1,280 @@ +package main + +import ( + "errors" + "fmt" + "io" + "log" + "math" + "os" + "path/filepath" + "strconv" + + "github.com/VividCortex/ewma" + "github.com/eclesh/welford" + "github.com/go-echarts/go-echarts/v2/charts" + "github.com/go-echarts/go-echarts/v2/components" + "github.com/go-echarts/go-echarts/v2/opts" + "github.com/go-echarts/go-echarts/v2/types" +) + +func SaveToFile(dbt *DatabaseTable) { + stats := welford.New() + + // deletes the first high values in descending order + i := 0 + var lastValue float64 + for ; i < len(dbt.Data); i++ { + if lastValue == 0.0 || lastValue > dbt.Data[i].Value { + lastValue = dbt.Data[i].Value + } else { + break + } + } + + var filename = dbt.Data[0].Timestamp.Format("2006-01-02T15-04-05") + var projectName string + if cliMode { + // filename = dbt.Data[0].Timestamp.Format("2006-01-02T15-04-05") + if f.Name != "" { + filename = f.Name + "_" + filename + projectName = f.Name + } else { + projectName = filename + } + } else { + guiFileName := gui.FileName.Text() + projectName = guiFileName + // filename = dbt.Data[0].Timestamp.Format("2006-01-02T15-04-05") + if guiFileName != "" && guiFileName != filename { + filename = gui.FileName.Text() + "_" + filename + } + } + // CSV + csvFile, err := os.Create(filepath.Join(f.LogFolder, filename+".csv")) + if err != nil { + log.Println(err) + } + _, err = csvFile.Write(append([]byte("time,latency"), []byte{13, 10}...)) + if err != nil { + log.Println(err) + } + + // https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average + SimpleEWMA := ewma.NewMovingAverage() + + var highestLatencyRound5 int + round5Map := make(map[int]int) + var XValuesLineChartTime = []string{} + var YValuesLineChartTime = []opts.LineData{} + var benchmarkTime float64 + var rawData []float64 + for _, v := range dbt.Data { + // CSV + temp := []byte(v.Timestamp.Format("2006-01-02T15:04:05.999999999") + ",") + temp = append(temp, []byte(float64ToString(v.Value))...) // in ms + temp = append(temp, []byte{13, 10}...) // \r\n + _, err := csvFile.Write(temp) + if err != nil { + log.Println(err) + } + + // summary + rawData = append(rawData, v.Value) + benchmarkTime += v.Value + SimpleEWMA.Add(v.Value) + stats.Add(v.Value) + + // chart, bar + r5 := int(round5(v.Value)) + val, ok := round5Map[r5] + if ok { + round5Map[r5] = val + 1 + } else { + round5Map[r5] = 1 + if highestLatencyRound5 < r5 { + highestLatencyRound5 = r5 + } + } + + // chart, line + XValuesLineChartTime = append(XValuesLineChartTime, v.Timestamp.Sub(dbt.Data[0].Timestamp).String()) + YValuesLineChartTime = append(YValuesLineChartTime, opts.LineData{ + Value: v.Value, + }) + } + csvFile.Close() + + var newFile bool + if _, err := os.Stat(filepath.Join(f.LogFolder, "summary.csv")); errors.Is(err, os.ErrNotExist) { + newFile = true + } + + header := "name,start,duration,count,max,min,average,ewma average,stdev\n" + summary := fmt.Sprintf(`%s,%s,%s,%d,%.3f,%.3f,%.3f,%.3f,%.3f`, + projectName, + dbt.Data[0].Timestamp.Format("2006-01-02T15:04:05.999999999"), + dbt.Data[len(dbt.Data)-1].Timestamp.Sub(dbt.Data[0].Timestamp).String(), + len(dbt.Data), + stats.Max(), + stats.Min(), + stats.Mean(), + SimpleEWMA.Value(), + stats.Stddev(), + ) + + summaryFile, err := os.OpenFile(filepath.Join(f.LogFolder, "summary.csv"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) + if err != nil { + log.Println(err) + } + + if newFile { + if _, err := summaryFile.WriteString(header); err != nil { + log.Println(err) + } + } + + if _, err := summaryFile.WriteString(summary + "\n"); err != nil { + log.Println(err) + } + summaryFile.Close() + + // Chart + chartXValues := []string{} + for i := 5; i <= highestLatencyRound5; i += 5 { + if i == 5 { + chartXValues = append(chartXValues, "5ms") + } else { + chartXValues = append(chartXValues, strconv.Itoa(i)) + } + } + + BarItems := make([]opts.BarData, highestLatencyRound5/5+1) + for i := 1; i < len(BarItems); i++ { + val, ok := round5Map[i*5] + if !ok { + BarItems[i-1] = opts.BarData{ + Value: 0, + } + } else { + BarItems[i-1] = opts.BarData{ + Value: val, + } + } + } + + page := components.NewPage(). + SetLayout(components.PageFlexLayout). + AddCharts( + createBarChartRound5( + filename, + fmt.Sprintf("start: %s, count: %d, max: %.3f, min: %.3f, average: %.3f, ewma average: %.3f", dbt.Data[0].Timestamp.Format("2006-01-02T15:04:05"), stats.Count(), stats.Max(), stats.Min(), stats.Mean(), SimpleEWMA.Value()), + chartXValues, + BarItems, + ), + createLineChartTime( + "time series", + fmt.Sprintf("duration: %s, max: %.3f, min: %.3f, average: %.3f, ewma average: %.3f", dbt.Data[len(dbt.Data)-1].Timestamp.Sub(dbt.Data[0].Timestamp).String(), stats.Max(), stats.Min(), stats.Mean(), SimpleEWMA.Value()), + XValuesLineChartTime, + YValuesLineChartTime, + ), + ) + + chartFile, err := os.Create(filepath.Join(f.LogFolder, filename+".html")) + if err != nil { + log.Println(err) + } + page.Render(io.MultiWriter(chartFile)) + chartFile.Close() +} + +func createBarChartRound5(title, subtitle string, xValues []string, yValues []opts.BarData) *charts.Bar { + bar := charts.NewBar() + bar.SetGlobalOptions( + charts.WithInitializationOpts(opts.Initialization{ + PageTitle: "GoSysLat", + Theme: types.ThemeWesteros, + }), + charts.WithTitleOpts(opts.Title{ + Title: title, + Subtitle: subtitle, + }), + charts.WithDataZoomOpts(opts.DataZoom{ + Type: "inside", + Start: 0, + End: 100, + XAxisIndex: []int{0}, + }), + charts.WithDataZoomOpts(opts.DataZoom{ + Type: "slider", + Start: 0, + End: 100, + XAxisIndex: []int{0}, + }), + ) + bar.SetXAxis(xValues). + AddSeries("Data", yValues) + + return bar +} + +func createLineChartTime(title, subtitle string, xValues []string, yValues []opts.LineData) *charts.Line { + line := charts.NewLine() + line.SetGlobalOptions( + charts.WithInitializationOpts(opts.Initialization{ + Theme: types.ThemeWesteros, + }), + charts.WithTitleOpts(opts.Title{ + Title: title, + Subtitle: subtitle, + }), + charts.WithDataZoomOpts(opts.DataZoom{ + Type: "inside", + Start: 0, + End: 100, + XAxisIndex: []int{0}, + }), + charts.WithDataZoomOpts(opts.DataZoom{ + Type: "slider", + Start: 0, + End: 100, + XAxisIndex: []int{0}, + }), + ) + + line.SetXAxis(xValues). + AddSeries("Data", yValues). + SetSeriesOptions( + charts.WithLabelOpts( + opts.Label{ + Show: true, + }, + ), + charts.WithAreaStyleOpts( + opts.AreaStyle{ + Opacity: 0.2, + }, + ), + charts.WithLineChartOpts( + opts.LineChart{ + Smooth: true, + }, + ), + ) + + return line +} + +func round5(x float64) float64 { + // x = 1 => 5 + // x = 6 => 10 + return math.Ceil(x/5) * 5 +} + +func round(val float64) float64 { + return math.Round(val*100) / 100 +} + +func roundInt(val float64) int { + return int(math.Round(val)) +} diff --git a/TargetWindow_D3D9/d3d9.go b/TargetWindow_D3D9/d3d9.go new file mode 100644 index 0000000..95e736d --- /dev/null +++ b/TargetWindow_D3D9/d3d9.go @@ -0,0 +1,294 @@ +package TargetWindow_D3D9 + +import ( + "log" + "runtime" + "runtime/debug" + "syscall" + "time" + + "github.com/gonutz/d3d9" + "github.com/gonutz/w32/v2" +) + +type Target struct { + device *d3d9.Device + verticesLen uint + IsActive bool + WhiteBox bool +} + +var ( + colorWhite = d3d9.ColorRGB(255, 255, 255) + colorBlack = d3d9.ColorRGB(0, 0, 0) + previousPlacement w32.WINDOWPLACEMENT +) + +func (t *Target) Start(currentFileName string, fullscreen bool, fWidth, fHeight int, pushMode bool) { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + classNamePtr, _ := syscall.UTF16PtrFromString("fullscreen_window_class") + w32.RegisterClassEx(&w32.WNDCLASSEX{ + Cursor: w32.LoadCursor(0, w32.MakeIntResource(w32.IDC_ARROW)), + WndProc: syscall.NewCallback(func(window w32.HWND, msg uint32, w, l uintptr) uintptr { + switch msg { + case w32.WM_KEYDOWN: + switch w { + case w32.VK_ESCAPE: + w32.SendMessage(window, w32.WM_CLOSE, 0, 0) + w32.PostQuitMessage(0) + case w32.VK_F11: + toggleFullscreen(window) // BUG: stays, Composed: Copy with GPU GDI + } + return 1 + case w32.WM_DESTROY: + w32.PostQuitMessage(0) + return 0 + default: + return w32.DefWindowProc(window, msg, w, l) + } + }), + ClassName: classNamePtr, + Icon: w32.ExtractIcon(currentFileName, 0), + }) + + windowNamePtr, _ := syscall.UTF16PtrFromString("TargetWindow D3D9") + windowHandle := w32.CreateWindow( + classNamePtr, + windowNamePtr, + w32.WS_OVERLAPPEDWINDOW|w32.WS_VISIBLE, + w32.CW_USEDEFAULT, + w32.CW_USEDEFAULT, + 640, + 480, + 0, + 0, + 0, + nil, + ) + + var err error + d3d, err := d3d9.Create(d3d9.SDK_VERSION) + defer d3d.Release() + check(err) + + if fullscreen { // Hardware: Legacy Flip + var width, height uint32 + if fWidth == 0 || fHeight == 0 { + MaxWidth, MaxHeight, _ := checkMaxRes() + if fWidth == 0 { + width = MaxWidth + } else { + width = uint32(fWidth) + } + if fHeight == 0 { + height = MaxHeight + } else { + height = uint32(fHeight) + } + } else { + width = uint32(fWidth) + height = uint32(fHeight) + } + for { + t.device, _, err = d3d.CreateDevice( + d3d9.ADAPTER_DEFAULT, + d3d9.DEVTYPE_HAL, + d3d9.HWND(windowHandle), + d3d9.CREATE_SOFTWARE_VERTEXPROCESSING, + d3d9.PRESENT_PARAMETERS{ + Windowed: 0, + HDeviceWindow: d3d9.HWND(windowHandle), + SwapEffect: d3d9.SWAPEFFECT_DISCARD, + BackBufferFormat: d3d9.FMT_X8R8G8B8, + BackBufferWidth: width, + BackBufferHeight: height, + }, + ) + if err == nil { + break + } + time.Sleep(time.Second) + } + } else { // window mode // Composed: Copy with GPU GDI + t.device, _, err = d3d.CreateDevice( + d3d9.ADAPTER_DEFAULT, + d3d9.DEVTYPE_HAL, + d3d9.HWND(windowHandle), + d3d9.CREATE_HARDWARE_VERTEXPROCESSING, + d3d9.PRESENT_PARAMETERS{ + Windowed: 1, + HDeviceWindow: d3d9.HWND(windowHandle), + SwapEffect: d3d9.SWAPEFFECT_DISCARD, + }, + ) + check(err) + } + defer t.device.Release() + check(t.device.SetRenderState(d3d9.RS_CULLMODE, uint32(d3d9.CULL_NONE))) + + decl, err := t.device.CreateVertexDeclaration([]d3d9.VERTEXELEMENT{ + { + Stream: 0, + Offset: 0, + Type: d3d9.DECLTYPE_FLOAT2, + Method: d3d9.DECLMETHOD_DEFAULT, + Usage: d3d9.DECLUSAGE_POSITION, + UsageIndex: 0, + }, + d3d9.DeclEnd(), + }) + check(err) + defer decl.Release() + check(t.device.SetVertexDeclaration(decl)) + + vertices := []float32{ + -1, -0.8, + -1, 0.8, + 1, -0.8, + + 1, 0.8, + 1, -0.8, + -1, 0.8, + + -0.8, -1, + -0.8, 1, + 0.8, 1, + + 0.8, 1, + 0.8, -1, + -0.8, -1, + } + + t.verticesLen = uint(len(vertices) / 3) + vb, err := t.device.CreateVertexBuffer(uint(len(vertices)*4), d3d9.USAGE_WRITEONLY, 0, d3d9.POOL_DEFAULT, 0) + check(err) + defer vb.Release() + vbMem, err := vb.Lock(0, 0, d3d9.LOCK_DISCARD) + check(err) + vbMem.SetFloat32s(0, vertices) + check(vb.Unlock()) + + check(t.device.SetStreamSource(0, vb, 0, 2*4)) + + if !pushMode { // the best I can achieve + // create a timer that ticks every 10ms and register a callback for it + // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-settimer + w32.SetTimer(windowHandle, 1, 10, 0) // USER_TIMER_MINIMUM + } + + var msg w32.MSG + for w32.GetMessage(&msg, 0, 0, 0) != 0 { + if msg.Message == 0x0012 { // WM_QUIT + t.IsActive = false + } else { + w32.TranslateMessage(&msg) + t.IsActive = true + w32.DispatchMessage(&msg) + } + } +} + +func (t *Target) Close() { + t.IsActive = false +} + +func (t *Target) SetWhite() { + t.WhiteBox = true + t.draw() +} +func (t *Target) SetBlack() { + t.WhiteBox = false + t.draw() +} + +func (t *Target) draw() { + if !t.IsActive { + return + } + + if t.WhiteBox { + t.device.Clear(nil, d3d9.CLEAR_TARGET, colorWhite, 0, 0) + } else { + t.device.Clear(nil, d3d9.CLEAR_TARGET, colorBlack, 0, 0) + } + check(t.device.BeginScene()) // invalid call + check(t.device.DrawPrimitive(d3d9.PT_TRIANGLELIST, 0, t.verticesLen)) + check(t.device.EndScene()) + t.device.Present(nil, nil, 0, nil) +} + +func checkMaxRes() (uint32, uint32, uint32) { + var maxScreenW uint32 + var maxScreenH uint32 + var maxRefreshRate uint32 + d3d, err := d3d9.Create(d3d9.SDK_VERSION) + check(err) + defer d3d.Release() + + adapterCount := d3d.GetAdapterCount() + for adapter := uint(0); adapter < adapterCount; adapter++ { + displayMode, err := d3d.GetAdapterDisplayMode(adapter) + check(err) + + if maxRefreshRate < displayMode.RefreshRate { + maxRefreshRate = displayMode.RefreshRate + if displayMode.Width > maxScreenW { + maxScreenW = displayMode.Width + } + if displayMode.Height > maxScreenH { + maxScreenH = displayMode.Height + } + } + } + return maxScreenW, maxScreenH, maxRefreshRate +} + +func toggleFullscreen(window w32.HWND) { + style := w32.GetWindowLong(window, w32.GWL_STYLE) + if style&w32.WS_OVERLAPPEDWINDOW != 0 { + // go into full-screen + var monitorInfo w32.MONITORINFO + monitor := w32.MonitorFromWindow(window, w32.MONITOR_DEFAULTTOPRIMARY) + if w32.GetWindowPlacement(window, &previousPlacement) && + w32.GetMonitorInfo(monitor, &monitorInfo) { + w32.SetWindowLong( + window, + w32.GWL_STYLE, + style & ^w32.WS_OVERLAPPEDWINDOW, + ) + w32.SetWindowPos( + window, + 0, + int(monitorInfo.RcMonitor.Left), + int(monitorInfo.RcMonitor.Top), + int(monitorInfo.RcMonitor.Right-monitorInfo.RcMonitor.Left), + int(monitorInfo.RcMonitor.Bottom-monitorInfo.RcMonitor.Top), + w32.SWP_NOOWNERZORDER|w32.SWP_FRAMECHANGED, + ) + } + w32.ShowCursor(false) + } else { + // go into windowed mode + w32.SetWindowLong( + window, + w32.GWL_STYLE, + style|w32.WS_OVERLAPPEDWINDOW, + ) + w32.SetWindowPlacement(window, &previousPlacement) + w32.SetWindowPos(window, 0, 0, 0, 0, 0, + w32.SWP_NOMOVE|w32.SWP_NOSIZE|w32.SWP_NOZORDER| + w32.SWP_NOOWNERZORDER|w32.SWP_FRAMECHANGED, + ) + w32.ShowCursor(true) + } +} + +func check(err error) { + if err != nil { + debug.PrintStack() + log.Fatalln(err) + panic(err) + } +} diff --git a/TargetWindow_D3D9/go.mod b/TargetWindow_D3D9/go.mod new file mode 100644 index 0000000..e25f5e1 --- /dev/null +++ b/TargetWindow_D3D9/go.mod @@ -0,0 +1,10 @@ +module github.com/spddl/TargetWindow_D3D9 + +go 1.17 + +require ( + github.com/gonutz/d3d9 v1.2.1 + github.com/gonutz/w32/v2 v2.4.0 +) + +require github.com/go-echarts/go-echarts/v2 v2.2.4 // indirect diff --git a/TargetWindow_D3D9/go.sum b/TargetWindow_D3D9/go.sum new file mode 100644 index 0000000..0db28bf --- /dev/null +++ b/TargetWindow_D3D9/go.sum @@ -0,0 +1,17 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-echarts/go-echarts/v2 v2.2.4 h1:SKJpdyNIyD65XjbUZjzg6SwccTNXEgmh+PlaO23g2H0= +github.com/go-echarts/go-echarts/v2 v2.2.4/go.mod h1:6TOomEztzGDVDkOSCFBq3ed7xOYfbOqhaBzD0YV771A= +github.com/gonutz/d3d9 v1.2.1 h1:xcQLMKrAf+/hvFtsyc9LuLJ+qs6TPtbzuKYONRERpYo= +github.com/gonutz/d3d9 v1.2.1/go.mod h1:q74g3QbR280b+qYauwEV0N9SVadszWPLZ4l/wHiD/AA= +github.com/gonutz/w32/v2 v2.4.0 h1:k+R8/ddsnb9dwVDsTDWvkJGFmYJDI3ZMgcKgdpLuMpw= +github.com/gonutz/w32/v2 v2.4.0/go.mod h1:MgtHx0AScDVNKyB+kjyPder4xIi3XAcHS6LDDU2DmdE= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/TargetWindow_OpenGL/go.mod b/TargetWindow_OpenGL/go.mod new file mode 100644 index 0000000..45c2b32 --- /dev/null +++ b/TargetWindow_OpenGL/go.mod @@ -0,0 +1,8 @@ +module TargetWindow_OpenGL + +go 1.18 + +require ( + github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 + github.com/go-gl/glfw v0.0.0-20220320163800-277f93cfa958 +) diff --git a/TargetWindow_OpenGL/go.sum b/TargetWindow_OpenGL/go.sum new file mode 100644 index 0000000..f255e23 --- /dev/null +++ b/TargetWindow_OpenGL/go.sum @@ -0,0 +1,4 @@ +github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 h1:zDw5v7qm4yH7N8C8uWd+8Ii9rROdgWxQuGoJ9WDXxfk= +github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw= +github.com/go-gl/glfw v0.0.0-20220320163800-277f93cfa958 h1:aQjQrLKagKRNl/GGy16WNwsiYAVs2Kfwvg5pq07O7Ns= +github.com/go-gl/glfw v0.0.0-20220320163800-277f93cfa958/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= diff --git a/TargetWindow_OpenGL/opengl.go b/TargetWindow_OpenGL/opengl.go new file mode 100644 index 0000000..a05b574 --- /dev/null +++ b/TargetWindow_OpenGL/opengl.go @@ -0,0 +1,258 @@ +package TargetWindow_OpenGL + +import ( + "fmt" + "runtime" + "strings" + + "github.com/go-gl/gl/v4.1-core/gl" // OR: github.com/go-gl/gl/v2.1/gl + "github.com/go-gl/glfw/v3.2/glfw" +) + +type Target struct { + WhiteBox bool + vao uint32 + program uint32 + window *glfw.Window + IsActive bool +} + +const title = "TargetWindow OpenGL" + +var ( + vertices = []float32{ + -1, 0.8, 0, // Oben Links + -1, 1, 0, + -0.8, 0.8, 0, + -0.8, 1, 0, // Oben Links + -1, 1, 0, + -0.8, 0.8, 0, + + 1, 0.8, 0, // Oben Rechts + 1, 1, 0, + 0.8, 0.8, 0, + 0.8, 1, 0, // Oben Rechts + 1, 1, 0, + 0.8, 0.8, 0, + + 1, -0.8, 0, // Unten Rechts + 1, -1, 0, + 0.8, -0.8, 0, + 0.8, -1, 0, // Unten Rechts + 1, -1, 0, + 0.8, -0.8, 0, + + -1, -0.8, 0, // Unten Links + -1, -1, 0, + -0.8, -0.8, 0, + -0.8, -1, 0, // Unten Links + -1, -1, 0, + -0.8, -0.8, 0, + } + + fragmentShaderSource = ` + #version 410 + out vec4 frag_colour; + void main() { + frag_colour = vec4(1, 1, 1, 1); + } +` + "\x00" +) + +// initGlfw initializes glfw and returns a Window to use. +func initGlfw(fullscreen bool, width, height int) *glfw.Window { + if err := glfw.Init(); err != nil { + panic(err) + } + + glfw.WindowHint(glfw.Resizable, glfw.False) + glfw.WindowHint(glfw.ContextVersionMajor, 4) + glfw.WindowHint(glfw.ContextVersionMinor, 1) + glfw.WindowHint(glfw.OpenGLProfile, glfw.OpenGLCoreProfile) + glfw.WindowHint(glfw.OpenGLForwardCompatible, glfw.True) + + var window *glfw.Window + var err error + if fullscreen { // fullscreen + monitor := glfw.GetPrimaryMonitor() + mode := monitor.GetVideoMode() + window, err = glfw.CreateWindow(mode.Width, mode.Height, title, monitor, nil) + } else { + window, err = glfw.CreateWindow(width, height, title, nil, nil) + } + if err != nil { + panic(err) + } + + window.MakeContextCurrent() + + // glfw.SwapInterval(1) + + window.SetKeyCallback(func(w *glfw.Window, key glfw.Key, scancode int, action glfw.Action, mods glfw.ModifierKey) { + switch key { + case glfw.KeyEscape: + w.SetShouldClose(true) + + case glfw.KeyF11: + // https://gist.github.com/pwaller/73593ae93d4f252bfb85 + } + }) + return window +} + +// initOpenGL initializes OpenGL and returns an intiialized program. +func initOpenGL() uint32 { + if err := gl.Init(); err != nil { + panic(err) + } + // log.Println("OpenGL version", gl.GoStr(gl.GetString(gl.VERSION))) + + fragmentShader, err := compileShader(fragmentShaderSource, gl.FRAGMENT_SHADER) + if err != nil { + panic(err) + } + + prog := gl.CreateProgram() + + gl.AttachShader(prog, fragmentShader) + gl.LinkProgram(prog) + return prog +} + +func (t *Target) Start(fullscreen bool, fWidth, fHeight int, pushMode bool) { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + if fWidth == 0 { + fWidth = 640 + } + if fHeight == 0 { + fHeight = 480 + } + + t.window = initGlfw(fullscreen, fWidth, fHeight) + defer glfw.Terminate() + + t.program = initOpenGL() + t.vao = makeVao(vertices) + + if !pushMode { + go func(t *Target) { + for !t.window.ShouldClose() { + do(func() { + t.draw() + }) + } + }(t) + } + + t.IsActive = true + for fn := range mainfunc { + fn() + if t.window.ShouldClose() { + break + } + } + + t.Close() +} + +func (t *Target) draw() { + if !t.IsActive { + return + } + + gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT) + + if t.WhiteBox { + gl.UseProgram(t.program) + gl.DrawArrays(gl.TRIANGLES, 0, int32(len(vertices)/3)) + } + + glfw.PollEvents() + t.window.SwapBuffers() +} + +func (t *Target) SetWhite() { + do(func() { + t.WhiteBox = true + t.draw() + }) +} +func (t *Target) SetBlack() { + do(func() { + t.WhiteBox = false + t.draw() + }) +} + +// makeVao initializes and returns a vertex array from the points provided. +func makeVao(points []float32) uint32 { + var vbo uint32 + gl.GenBuffers(1, &vbo) + gl.BindBuffer(gl.ARRAY_BUFFER, vbo) + gl.BufferData(gl.ARRAY_BUFFER, 4*len(points), gl.Ptr(points), gl.STATIC_DRAW) + + var vao uint32 + gl.GenVertexArrays(1, &vao) + gl.BindVertexArray(vao) + gl.EnableVertexAttribArray(0) + gl.BindBuffer(gl.ARRAY_BUFFER, vbo) + gl.VertexAttribPointer(0, 3, gl.FLOAT, false, 0, nil) + + return vao +} + +func compileShader(source string, shaderType uint32) (uint32, error) { + shader := gl.CreateShader(shaderType) + + csources, free := gl.Strs(source) + gl.ShaderSource(shader, 1, csources, nil) + free() + gl.CompileShader(shader) + + var status int32 + gl.GetShaderiv(shader, gl.COMPILE_STATUS, &status) + if status == gl.FALSE { + var logLength int32 + gl.GetShaderiv(shader, gl.INFO_LOG_LENGTH, &logLength) + + log := strings.Repeat("\x00", int(logLength+1)) + gl.GetShaderInfoLog(shader, logLength, nil, gl.Str(log)) + + return 0, fmt.Errorf("failed to compile %v: %v", source, log) + } + + return shader, nil +} + +func (t *Target) Close() { + t.IsActive = false + glfw.Terminate() +} + +// func getGID() uint64 { +// b := make([]byte, 64) +// b = b[:runtime.Stack(b, false)] +// b = bytes.TrimPrefix(b, []byte("goroutine ")) +// b = b[:bytes.IndexByte(b, ' ')] +// n, _ := strconv.ParseUint(string(b), 10, 64) +// return n +// } + +// queue of work to run in main thread. +var mainfunc = make(chan func()) + +// do runs f on the main thread. +func do(f func()) { + done := make(chan bool, 1) + mainfunc <- func() { + f() + done <- true + } + <-done + + // mainfunc <- func() { + // f() + // } +} diff --git a/TestCase_BCDStore.ps1 b/TestCase_BCDStore.ps1 new file mode 100644 index 0000000..385bbd6 --- /dev/null +++ b/TestCase_BCDStore.ps1 @@ -0,0 +1,89 @@ +Param( + [int]$Id +) + +# first time call: .\TestCase.ps1 -Id -1 + +$ComPort = 3 +$Duration = "60s" +$RestartNeeded = $true + +$GoSysLatPath = "$PSScriptRoot/GoSysLat.exe" +$LogFolder = "LogFolder" +$BatchFolder = "$PSScriptRoot/TestCases/" +Start-Transcript -Path "$PSScriptRoot\TestCase.log" -Append | Out-Null +Set-Location "$PSScriptRoot" + +$TestCases = @() +for ($i = 0; $i -lt 15; $i++) { # 15 times + # https://docs.microsoft.com/en-us/windows-hardware/drivers/devtest/bcdedit--set + <# $TestCases += @{ + Name = 'tscsyncpolicy Enhanced' + Batch = 'tscsyncpolicy_enhanced.bat' + } + $TestCases += @{ + Name = 'tscsyncpolicy Legacy' + Batch = 'tscsyncpolicy_legacy.bat' + } + $TestCases += @{ + Name = 'tscsyncpolicy default' + Batch = 'tscsyncpolicy_default.bat' + } #> + $TestCases += @{ + Name = 'disabledynamictick yes' + Batch = 'disabledynamictick_yes.bat' + } + $TestCases += @{ + Name = 'disabledynamictick no' + Batch = 'disabledynamictick_no.bat' + } + <# $TestCases += @{ + Name = 'useplatformtick yes' + Batch = 'useplatformtick_yes.bat' + } + $TestCases += @{ + Name = 'useplatformtick deletevalue' + Batch = 'useplatformtick_deletevalue.bat' + } #> + <# $TestCases += @{ + Name = 'Services Disable' + Batch = 'Services_Disable.bat' + } + $TestCases += @{ + Name = 'Services Enable' + Batch = 'Services_Enable.bat' + } #> +} + +Write-Host "$id/$($TestCases.Count) => $($TestCases[$id].Name)" +Write-Host "Start-Process -FilePath $GoSysLatPath -ArgumentList `"-d3d9`", `"-fullscreen`", `"-port $ComPort`", `"-time $Duration`", `"-name $($TestCases[$id].Name)`" , `"-logs $LogFolder`" -Wait" + +if ($id -ne -1) { + Start-Sleep -Seconds 45 + # Write-Host -NoNewLine 'Press any key to continue...'; + # $null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown'); + + # Test + Start-Process -FilePath $GoSysLatPath -ArgumentList "-d3d9", "-fullscreen", "-port $ComPort", "-time $Duration", "-name `"$($TestCases[$id].Name)`"" , "-logs `"$LogFolder`"" -Wait +} + +$Id += 1 # check if there is a test case next to it + +if ($null -ne $TestCases[$id]) { + # new test environment is being prepared + $TestCase = Join-Path -Path $BatchFolder -ChildPath $TestCases[$id].Batch + if (Test-Path -Path $TestCase -PathType Leaf) { + Start-Process -FilePath $TestCase -Wait # Test environment is created + } + + if ($RestartNeeded) { + New-ItemProperty -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\RunOnce" -Name "GoSysLat" -Value "Powershell -File $PSScriptRoot\$($MyInvocation.MyCommand.Name) -Id $Id" + + Start-Process -FilePath shutdown -ArgumentList "/r", "/t 0" -Wait + } else { + . $MyInvocation.MyCommand.Path -Id $Id + } +} + +Stop-Transcript | Out-Null +Start-Process -FilePath shutdown -ArgumentList "/s", "/t 0" -Wait \ No newline at end of file diff --git a/TestCase_Freestyle.ps1 b/TestCase_Freestyle.ps1 new file mode 100644 index 0000000..34eaf06 --- /dev/null +++ b/TestCase_Freestyle.ps1 @@ -0,0 +1,10 @@ +$ComPort = 3 +$Duration = "60s" + +$GoSysLatPath = "$PSScriptRoot/GoSysLat.exe" +$LogFolder = "LogFolder" +Set-Location "$PSScriptRoot" + +$TestCaseName = Read-Host "TestCase Name?" +# Start-Process -FilePath $GoSysLatPath -ArgumentList "-d3d9", "-fullscreen", "-port $ComPort", "-time $Duration", "-name `"$TestCaseName`"" , "-logs `"$LogFolder`"" -Wait +Start-Process -FilePath $GoSysLatPath -ArgumentList "-ogl", "-fullscreen", "-port $ComPort", "-time $Duration", "-name `"$TestCaseName`"" , "-logs `"$LogFolder`"" -Wait diff --git a/TestCase_GameMode.ps1 b/TestCase_GameMode.ps1 new file mode 100644 index 0000000..b132418 --- /dev/null +++ b/TestCase_GameMode.ps1 @@ -0,0 +1,51 @@ +Param( + [int]$Id +) + +# first time call: .\TestCase.ps1 -Id -1 + +$ComPort = 3 +$Duration = "30s" +$RestartNeeded = $false + +$GoSysLatPath = "$PSScriptRoot/GoSysLat.exe" +$LogFolder = "LogFolder" +# $BatchFolder = "$PSScriptRoot/TestCases/" +Start-Transcript -Path "$PSScriptRoot\TestCase.log" -Append | Out-Null +Set-Location "$PSScriptRoot" + +$TestCases = @() +for ($i = 0; $i -lt 15; $i++) { # 15 times + $TestCases += @{ Value = 0x0; Name = 'GameMode Off' } + $TestCases += @{ Value = 0x1; Name = 'GameMode On' } +} + +Write-Host "$id/$($TestCases.Count) => $($TestCases[$id].Name)" + +if ($id -ne -1) { + Start-Sleep -Seconds 5 + # Write-Host -NoNewLine 'Press any key to continue...'; + # $null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown'); + + # Test + Start-Process -FilePath $GoSysLatPath -ArgumentList "-d3d9", "-fullscreen", "-port $ComPort", "-time $Duration", "-name `"$($TestCases[$id].Name)`"" , "-logs `"$LogFolder`"" -Wait +} + +$Id += 1 # check if there is a test case next to it + +if ($null -ne $TestCases[$id]) { + # new test environment is being prepared + Set-ItemProperty -Path "HKCU:\Software\Microsoft\GameBar" -Name "AutoGameModeEnabled" -Value $TestCases[$id].Value -Type DWord + + if ($RestartNeeded) { + # Autostart + New-ItemProperty -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\RunOnce" -Name "GoSysLat" -Value "Powershell -File $PSScriptRoot\$($MyInvocation.MyCommand.Name) -Id $Id" + + Start-Process -FilePath shutdown -ArgumentList "/r", "/t 0" -Wait + } else { + . $MyInvocation.MyCommand.Path -Id $Id + } +} + +Stop-Transcript | Out-Null +Start-Process -FilePath shutdown -ArgumentList "/s", "/t 0" -Wait \ No newline at end of file diff --git a/TestCase_HAGS.ps1 b/TestCase_HAGS.ps1 new file mode 100644 index 0000000..03a6e82 --- /dev/null +++ b/TestCase_HAGS.ps1 @@ -0,0 +1,51 @@ +Param( + [int]$Id +) + +# first time call: .\TestCase.ps1 -Id -1 + +$ComPort = 3 +$Duration = "60s" +$RestartNeeded = $true + +$GoSysLatPath = "$PSScriptRoot/GoSysLat.exe" +$LogFolder = "LogFolder" +# $BatchFolder = "$PSScriptRoot/TestCases/" +Start-Transcript -Path "$PSScriptRoot\TestCase.log" -Append | Out-Null +Set-Location "$PSScriptRoot" + +$TestCases = @() +for ($i = 0; $i -lt 5; $i++) { # 5 times + $TestCases += @{ Value = 0x1; Name = 'HwSchMode Off' } + $TestCases += @{ Value = 0x2; Name = 'HwSchMode On' } +} + +Write-Host "$id/$($TestCases.Count) => $($TestCases[$id].Name)" + +if ($id -ne -1) { + Start-Sleep -Seconds 45 + # Write-Host -NoNewLine 'Press any key to continue...'; + # $null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown'); + + # Test + Start-Process -FilePath $GoSysLatPath -ArgumentList "-d3d9", "-fullscreen", "-port $ComPort", "-time $Duration", "-name `"$($TestCases[$id].Name)`"" , "-logs `"$LogFolder`"" -Wait +} + +$Id += 1 # check if there is a test case next to it + +if ($null -ne $TestCases[$id]) { + # new test environment is being prepared + Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\GraphicsDrivers" -Name "HwSchMode" -Value $TestCases[$id].Value -Type DWord + + if ($RestartNeeded) { + # Autostart + New-ItemProperty -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\RunOnce" -Name "GoSysLat" -Value "Powershell -File $PSScriptRoot\$($MyInvocation.MyCommand.Name) -Id $Id" + + Start-Process -FilePath shutdown -ArgumentList "/r", "/t 0" -Wait + } else { + . $MyInvocation.MyCommand.Path -Id $Id + } +} + +Stop-Transcript | Out-Null +Start-Process -FilePath shutdown -ArgumentList "/s", "/t 0" -Wait \ No newline at end of file diff --git a/TestCase_NVidia.ps1 b/TestCase_NVidia.ps1 new file mode 100644 index 0000000..c18008b --- /dev/null +++ b/TestCase_NVidia.ps1 @@ -0,0 +1,101 @@ +Param( + [int]$Id +) + +# first time call: .\TestCase.ps1 -Id -1 +# 511.79 + +$ComPort = 3 +$Duration = "45s" +$RestartNeeded = $false + +$GoSysLatPath = "$PSScriptRoot/GoSysLat.exe" +$LogFolder = "LogFolder" +# $BatchFolder = "$PSScriptRoot/TestCases/" +# $ApplyPath = Join-Path -Path $BatchFolder -ChildPath "0_Apply.exe" +Start-Transcript -Path "$PSScriptRoot\TestCase.log" -Append | Out-Null +Set-Location "$PSScriptRoot" + +$width = 1280 +$height = 720 + +# $One = @('1_Aspect_ratio.exe', '1_Fullscreen.exe', '1_Integer_scaling.exe', '1_no_scaling.exe') +$One = @('1_Aspect_ratio.exe', '1_Fullscreen.exe', '1_no_scaling.exe') +$Two = @('2_Perform_scaling_on_Display.exe', '2_Perform_scaling_on_GPU.exe') +$Three = @('3_Override_the_scaling_mode_set_by_games_and_programs_OFF.exe', '3_Override_the_scaling_mode_set_by_games_and_programs_ON.exe') + +$TestCases = @() + +for ($i = 0; $i -lt 3; $i++) { + # 5 times + foreach ($1 in $One) { + foreach ($2 in $Two) { + foreach ($3 in $Three) { + $TestCases += @{ + Name = "$($1.substring(2, $1.Length-6)), $($2.substring(2, $2.Length-6)), $($3.substring(2, $3.Length-6))" + Test = @($1, $2, $3) + } + } + } + } +} + +if ($id -ne -1) { + # Write-Host "$id/$($TestCases.Count) => $($TestCases[$id].Name)" + if ($TestCases[$id].Name -eq "no_scaling, Perform_scaling_on_Display, Override_the_scaling_mode_set_by_games_and_programs_OFF" -or + $TestCases[$id].Name -eq "Aspect_ratio, Perform_scaling_on_Display, Override_the_scaling_mode_set_by_games_and_programs_OFF" + ) { + Write-Host 'reposition the sensor'; + $null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown'); + } + # Start-Sleep -Seconds 3 + + # .\GoSysLat.exe -d3d9 -fullscreen -port 4 -width 800 -height 600 + + #Write-Host 'press any key to start'; + #$null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown'); + + # Test + Start-Process -FilePath $GoSysLatPath -ArgumentList "-d3d9", "-fullscreen", "-port $ComPort", "-time $Duration", "-width $width", "-height $height", "-name `"$($TestCases[$id].Name)`"" , "-logs `"$LogFolder`"" -Wait +} else { + #Start-Process -FilePath "C:\Program Files\WindowsApps\NVIDIACorp.NVIDIAControlPanel_8.1.962.0_x64__56jybvy8sckqj\nvcplui.exe" +} + +$Id += 1 # check if there is a test case next to it + +if ($null -ne $TestCases[$id]) { + # new test environment is being prepared + Write-Host "$id/$($TestCases.Count) => $($TestCases[$id].Name)" + + $nvcplui = Start-Process -FilePath "C:\Program Files\WindowsApps\NVIDIACorp.NVIDIAControlPanel_8.1.962.0_x64__56jybvy8sckqj\nvcplui.exe" -passthru + $nvcplui.WaitForExit() + + <# + Start-Sleep -Seconds 6 + foreach ($testFiles in $TestCases[$id].Test) { + $testFile = Join-Path -Path $BatchFolder -ChildPath $testFiles + if (Test-Path -Path $testFile -PathType Leaf) { + Write-Host $testFile + Start-Process -FilePath $testFile -Wait -NoNewWindow + Start-Sleep -Seconds 2 + } + } + Start-Sleep -Seconds 2 + Write-Host $ApplyPath + Start-Process -FilePath $ApplyPath -Wait -NoNewWindow + Start-Sleep -Seconds 2 + #> + #Stop-Process -Name "nvcplui" + + if ($RestartNeeded) { + New-ItemProperty -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\RunOnce" -Name "GoSysLat" -Value "Powershell -File $PSScriptRoot\$($MyInvocation.MyCommand.Name) -Id $Id" + + Start-Process -FilePath shutdown -ArgumentList "/r", "/t 0" -Wait + } + else { + . $MyInvocation.MyCommand.Path -Id $Id + } +} + +Stop-Transcript | Out-Null +# Start-Process -FilePath shutdown -ArgumentList "/s", "/t 0" -Wait \ No newline at end of file diff --git a/TestCase_PresentMode.ps1 b/TestCase_PresentMode.ps1 new file mode 100644 index 0000000..7f9a15b --- /dev/null +++ b/TestCase_PresentMode.ps1 @@ -0,0 +1,47 @@ + +$ComPort = 3 +# $Duration = "30s" +$Duration = "10m" + +$GoSysLatPath = "$PSScriptRoot/GoSysLat.exe" +$LogFolder = "LogFolder" +Set-Location "$PSScriptRoot" + +$path = "HKCU:\System\GameConfigStore" + +Function Set-RegistryData { + param( + [string]$p, + [string]$n, + [int]$v + ) + if (!(Test-Path -Path $p)) { + New-item -Path $p -Force + } + Set-ItemProperty -Path $p -Name $n -Value $v -Force +} + +for ($i = 0; $i -lt 2; $i++) { + Start-Sleep -s 3 + if($i % 2 -eq 0) { # FSO / Hardware: Independent Flip + Write-Host "# FSO / Hardware: Independent Flip" + Set-RegistryData -p $path -n "GameDVR_FSEBehaviorMode" -v 0 + Set-RegistryData -p $path -n "GameDVR_FSEBehavior" -v 0 + Set-RegistryData -p $path -n "GameDVR_HonorUserFSEBehaviorMode" -v 0 + Set-RegistryData -p $path -n "GameDVR_DXGIHonorFSEWindowsCompatible" -v 0 + Start-Sleep -s 1 + Start-Process -FilePath $GoSysLatPath -ArgumentList "-d3d9", "-fullscreen", "-port $ComPort", "-time $Duration", "-name FSO" , "-logs `"$LogFolder`"" -Wait + } else { # FSE / Hardware: Legacy Flip + Write-Host "# FSE / Hardware: Legacy Flip" + Set-RegistryData -p $path -n "GameDVR_FSEBehaviorMode" -v 2 + Set-RegistryData -p $path -n "GameDVR_FSEBehavior" -v 2 + Set-RegistryData -p $path -n "GameDVR_HonorUserFSEBehaviorMode" -v 1 + Set-RegistryData -p $path -n "GameDVR_DXGIHonorFSEWindowsCompatible" -v 1 + Start-Sleep -s 1 + Start-Process -FilePath $GoSysLatPath -ArgumentList "-d3d9", "-fullscreen", "-port $ComPort", "-time $Duration", "-name FSE" , "-logs `"$LogFolder`"" -Wait + } +} + + + + diff --git a/TestCase_RTSS_print_3.bat b/TestCase_RTSS_print_3.bat new file mode 100644 index 0000000..4c1a188 --- /dev/null +++ b/TestCase_RTSS_print_3.bat @@ -0,0 +1 @@ +.\GoSysLat.exe -rtss -port 3 -print -rtssosdwidth 100 -rtssosdheight 100 \ No newline at end of file diff --git a/TestCase_RTSS_print_4.bat b/TestCase_RTSS_print_4.bat new file mode 100644 index 0000000..4c55e5a --- /dev/null +++ b/TestCase_RTSS_print_4.bat @@ -0,0 +1 @@ +.\GoSysLat.exe -rtss -port 4 -print -rtssosdwidth 100 -rtssosdheight 100 \ No newline at end of file diff --git a/TestCase_VRR.ps1 b/TestCase_VRR.ps1 new file mode 100644 index 0000000..a712150 --- /dev/null +++ b/TestCase_VRR.ps1 @@ -0,0 +1,50 @@ +Param( + [int]$Id +) + +# first time call: .\TestCase.ps1 -Id -1 + +$ComPort = 3 +$Duration = "60s" +$RestartNeeded = $true + +$GoSysLatPath = "$PSScriptRoot/GoSysLat.exe" +$LogFolder = "LogFolder" +# $BatchFolder = "$PSScriptRoot/TestCases/" +Start-Transcript -Path "$PSScriptRoot\TestCase.log" -Append | Out-Null +Set-Location "$PSScriptRoot" + +$TestCases = @() +for ($i = 0; $i -lt 5; $i++) { # 5 times + $TestCases += @{ Value = "VRROptimizeEnable=0"; Name = 'VRROptimizeEnable Off' } + $TestCases += @{ Value = "VRROptimizeEnable=1"; Name = 'VRROptimizeEnable On' } +} + +if ($id -ne -1) { + Write-Host "$id/$($TestCases.Count) => $($TestCases[$id].Name)" + Start-Sleep -Seconds 45 + # Write-Host -NoNewLine 'Press any key to continue...'; + # $null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown'); + + # Test + Start-Process -FilePath $GoSysLatPath -ArgumentList "-d3d9", "-fullscreen", "-port $ComPort", "-time $Duration", "-name `"$($TestCases[$id].Name)`"" , "-logs `"$LogFolder`"" -Wait +} + +$Id += 1 # check if there is a test case next to it + +if ($null -ne $TestCases[$id]) { + # new test environment is being prepared + Set-ItemProperty -Path "HKCU:\Software\Microsoft\DirectX\UserGpuPreferences" -Name "DirectXUserGlobalSettings" -Value $TestCases[$id].Value + + if ($RestartNeeded) { + # Autostart + New-ItemProperty -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\RunOnce" -Name "GoSysLat" -Value "Powershell -File $PSScriptRoot\$($MyInvocation.MyCommand.Name) -Id $Id" + + Start-Process -FilePath shutdown -ArgumentList "/r", "/t 0" -Wait + } else { + . $MyInvocation.MyCommand.Path -Id $Id + } +} + +Stop-Transcript | Out-Null +Start-Process -FilePath shutdown -ArgumentList "/s", "/t 0" -Wait \ No newline at end of file diff --git a/TestCase_win32ps.ps1 b/TestCase_win32ps.ps1 new file mode 100644 index 0000000..2e062aa --- /dev/null +++ b/TestCase_win32ps.ps1 @@ -0,0 +1,65 @@ +Param( + [int]$Id +) + +# first time call: .\TestCase.ps1 -Id -1 + +$ComPort = 3 +$Duration = "15s" +$RestartNeeded = $true + +$GoSysLatPath = "$PSScriptRoot/GoSysLat.exe" +$LogFolder = "LogFolder" +# $BatchFolder = "$PSScriptRoot/TestCases/" +Start-Transcript -Path "$PSScriptRoot\TestCase.log" -Append | Out-Null +Set-Location "$PSScriptRoot" + +$TestCases = @() +for ($i = 0; $i -lt 5; $i++) { + # 5 times + $TestCases += @{ Value = 0x2A; Name = '0x2A, Short, Fixed, High foreground boost' } + $TestCases += @{ Value = 0x29; Name = '0x29, Short, Fixed, Medium foreground boost.' } + $TestCases += @{ Value = 0x28; Name = '0x28, Short, Fixed, No foreground boost.' } + + $TestCases += @{ Value = 0x26; Name = '0x26, Short, Variable, High foreground boost.' } + $TestCases += @{ Value = 0x25; Name = '0x25, Short, Variable, Medium foreground boost.' } + $TestCases += @{ Value = 0x24; Name = '0x24, Short, Variable, No foreground boost.' } + + $TestCases += @{ Value = 0x1A; Name = '0x1A, Long, Fixed, High foreground boost.' } + $TestCases += @{ Value = 0x19; Name = '0x19, Long, Fixed, Medium foreground boost.' } + $TestCases += @{ Value = 0x18; Name = '0x18, Long, Fixed, No foreground boost.' } + + $TestCases += @{ Value = 0x16; Name = '0x16, Long, Variable, High foreground boost.' } + $TestCases += @{ Value = 0x15; Name = '0x15, Long, Variable, Medium foreground boost.' } + $TestCases += @{ Value = 0x14; Name = '0x14, Long, Variable, No foreground boost.' } +} + +if ($id -ne -1) { + Write-Host "$id/$($TestCases.Count) => $($TestCases[$id].Name)" + Start-Sleep -Seconds 35 + + # Write-Host -NoNewLine 'Press any key to continue...' + # $null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown') + + # Test + Start-Process -FilePath $GoSysLatPath -ArgumentList "-d3d9", "-fullscreen", "-port $ComPort", "-time $Duration", "-name `"$($TestCases[$id].Name)`"" , "-logs `"$LogFolder`"" -Wait +} + +$Id += 1 # check if there is a test case next to it + +if ($null -ne $TestCases[$id]) { + # new test environment is being prepared + Set-ItemProperty -Path "HKLM:\SYSTEM\ControlSet001\Control\PriorityControl" -Name "Win32PrioritySeparation" -Value $TestCases[$id].Value -Type DWord + + if ($RestartNeeded) { + New-ItemProperty -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\RunOnce" -Name "GoSysLat" -Value "Powershell -File $PSScriptRoot\$($MyInvocation.MyCommand.Name) -Id $Id" + + Start-Process -FilePath shutdown -ArgumentList "/r", "/t 0" -Wait # restart + } + else { + . $MyInvocation.MyCommand.Path -Id $Id + } +} + +Stop-Transcript | Out-Null +Start-Process -FilePath shutdown -ArgumentList "/s", "/t 0" -Wait # shutdown \ No newline at end of file diff --git a/USBController/USBController.go b/USBController/USBController.go new file mode 100644 index 0000000..39838b9 --- /dev/null +++ b/USBController/USBController.go @@ -0,0 +1,137 @@ +package USBController + +import ( + "log" + "strings" + "syscall" + "unicode" +) + +const ( + EV_RXCHAR = 0x0001 + EV_ERR = 0x0080 +) + +var COMDevices []Device + +// var HIDDevices []Device + +func init() { + var comHandle DevInfo + COMDevices, comHandle = FindDevices(GUID_DEVINTERFACE_COMPORT) + SetupDiDestroyDeviceInfoList(comHandle) + for i := range COMDevices { + COMIndex := strings.Index(COMDevices[i].FriendlyName, "COM") + COMDevices[i].COMid = ReturnNumber(COMDevices[i].FriendlyName[COMIndex+3:]) + } + + // var hidHandle DevInfo + // HIDDevices, hidHandle = FindDevices(GUID_DEVINTERFACE_MOUSE) + // SetupDiDestroyDeviceInfoList(hidHandle) + // for i := range HIDDevices { + // log.Printf("%#v\n", HIDDevices[i]) + // } +} + +func ReturnNumber(s string) string { + r := []rune{} + for _, c := range s { + if unicode.IsDigit(c) { + r = append(r, c) + } else { + return string(r) + } + } + return string(r) +} + +func OpenComPort(portSpecifier string) syscall.Handle { + portSpec, _ := syscall.UTF16PtrFromString(portSpecifier) + hPort, err := syscall.CreateFile( + portSpec, + syscall.GENERIC_READ|syscall.GENERIC_WRITE, + syscall.FILE_SHARE_WRITE|syscall.FILE_SHARE_READ, + nil, + syscall.OPEN_EXISTING, + 0, 0) + if err != nil { + log.Printf("0x%X, %s", hPort, err) + } + + var dcb c_DCB + if err = GetCommState(hPort, &dcb); err != nil { + log.Println(err) + syscall.CloseHandle(hPort) + return hPort // INVALID_HANDLE_VALUE + } + + dcb.BaudRate = 9600 + + dcb.ByteSize = 8 + dcb.Parity = 0 // NOPARITY + dcb.StopBits = 0 // ONESTOPBIT + if err = SetCommState(hPort, &dcb); err != nil { + log.Println(err) + syscall.CloseHandle(hPort) + return hPort + } + + // Read this carefully because timeouts are important + // https://docs.microsoft.com/en-us/windows/win32/api/winbase/ns-winbase-commtimeouts + var timeouts c_COMMTIMEOUTS // https://docs.microsoft.com/en-us/windows/win32/api/winbase/ns-winbase-commtimeouts + err = GetCommTimeouts(hPort, &timeouts) + if err != nil { + log.Println(err) + } + + timeouts.ReadIntervalTimeout = 100 + timeouts.ReadTotalTimeoutConstant = 100 + timeouts.WriteTotalTimeoutConstant = 100 + err = SetCommTimeouts(hPort, &timeouts) + if err != nil { + log.Println(err) + } + + err = SetCommMask(hPort, EV_RXCHAR|EV_ERR) // receive character event + if err != nil { + log.Println(err) + return hPort + } + + return hPort +} + +func CloseComPort(hPort syscall.Handle) { + if hPort != 0 && hPort != 0xFFFFFFFFFFFFFFFF { + if err := PurgeComm(hPort, PURGE_RXABORT); err != nil { + log.Println(err) + return + } + + if err := syscall.CloseHandle(hPort); err != nil { + log.Println(err) + } + } +} + +func ReadByte(hPort *syscall.Handle) (byte, bool) { + var nNumberOfBytesToRead uint32 = 0 + buf := make([]byte, 1) + if err := syscall.ReadFile(*hPort, buf, &nNumberOfBytesToRead, nil); err != nil { + log.Println(err) + panic(err) + } + return buf[0], nNumberOfBytesToRead != 0 + // return buf[:nNumberOfBytesToRead], nNumberOfBytesToRead != 0 + +} + +func WriteByte(hPort *syscall.Handle, buf []byte) bool { + // log.Println("WriteByte", buf[0]) + var done uint32 + err := syscall.WriteFile(*hPort, buf, &done, nil) + if err != nil { + log.Println(err) + } + return err != nil +} diff --git a/USBController/go.mod b/USBController/go.mod new file mode 100644 index 0000000..f895cb3 --- /dev/null +++ b/USBController/go.mod @@ -0,0 +1,5 @@ +module USBController + +go 1.17 + +require golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 diff --git a/USBController/go.sum b/USBController/go.sum new file mode 100644 index 0000000..529b1e6 --- /dev/null +++ b/USBController/go.sum @@ -0,0 +1,2 @@ +golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 h1:xHms4gcpe1YE7A3yIllJXP16CMAGuqwO2lX1mTyyRRc= +golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/USBController/kernel32dll.go b/USBController/kernel32dll.go new file mode 100644 index 0000000..2e881a3 --- /dev/null +++ b/USBController/kernel32dll.go @@ -0,0 +1,129 @@ +package USBController + +import ( + "syscall" + "unsafe" +) + +var ( + // Library + modkernel32 = syscall.NewLazyDLL("kernel32.dll") + + // Functions + procPurgeComm = modkernel32.NewProc("PurgeComm") + procSetCommMask = modkernel32.NewProc("SetCommMask") + procGetCommState = modkernel32.NewProc("GetCommState") + procSetCommState = modkernel32.NewProc("SetCommState") + procGetCommTimeouts = modkernel32.NewProc("GetCommTimeouts") + procSetCommTimeouts = modkernel32.NewProc("SetCommTimeouts") +) + +type c_COMMTIMEOUTS struct { + ReadIntervalTimeout uint32 + ReadTotalTimeoutMultiplier uint32 + ReadTotalTimeoutConstant uint32 + WriteTotalTimeoutMultiplier uint32 + WriteTotalTimeoutConstant uint32 +} + +type purgeFlag int + +const ( + PURGE_TXABORT purgeFlag = 0x01 + PURGE_RXABORT purgeFlag = 0x02 + PURGE_TXCLEAR purgeFlag = 0x04 + PURGE_RXCLEAR purgeFlag = 0x08 +) + +type c_DCB struct { + DCBlength uint32 + BaudRate uint32 + Pad_cgo_0 [4]byte + WReserved uint16 + XonLim uint16 + XoffLim uint16 + ByteSize uint8 + Parity uint8 + StopBits uint8 + XonChar int8 + XoffChar int8 + ErrorChar int8 + EofChar int8 + EvtChar int8 + WReserved1 uint16 +} + +func PurgeComm(handle syscall.Handle, purge purgeFlag) error { + // BOOL PurgeComm( HANDLE hFile, DWORD dwFlags ) + var err error + r0, _, e1 := syscall.Syscall(procPurgeComm.Addr(), 2, uintptr(handle), uintptr(purge), 0) + if r0 == 0 { + if e1 != 0 { + return error(e1) + } else { + return syscall.EINVAL + } + } + return err +} + +func GetCommState(handle syscall.Handle, dcb *c_DCB) (err error) { + r1, _, e1 := syscall.Syscall(procGetCommState.Addr(), 2, uintptr(handle), uintptr(unsafe.Pointer(dcb)), 0) + if r1 == 0 { + if e1 != 0 { + err = error(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +func SetCommState(handle syscall.Handle, dcb *c_DCB) (err error) { + r1, _, e1 := syscall.Syscall(procSetCommState.Addr(), 2, uintptr(handle), uintptr(unsafe.Pointer(dcb)), 0) + if r1 == 0 { + if e1 != 0 { + err = error(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +func SetCommMask(handle syscall.Handle, mask uint32) (err error) { + r0, _, e1 := syscall.Syscall(procSetCommMask.Addr(), 2, uintptr(handle), uintptr(mask), 0) + if r0 == 0 { + if e1 != 0 { + err = error(e1) + } else { + err = syscall.EINVAL + } + } + + return +} + +func GetCommTimeouts(handle syscall.Handle, timeouts *c_COMMTIMEOUTS) (err error) { + r1, _, e1 := syscall.Syscall(procGetCommTimeouts.Addr(), 2, uintptr(handle), uintptr(unsafe.Pointer(timeouts)), 0) + if r1 == 0 { + if e1 != 0 { + err = error(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +func SetCommTimeouts(handle syscall.Handle, timeouts *c_COMMTIMEOUTS) (err error) { + r1, _, e1 := syscall.Syscall(procSetCommTimeouts.Addr(), 2, uintptr(handle), uintptr(unsafe.Pointer(timeouts)), 0) + if r1 == 0 { + if e1 != 0 { + err = error(e1) + } else { + err = syscall.EINVAL + } + } + return +} diff --git a/USBController/setupapi_windows.go b/USBController/setupapi_windows.go new file mode 100644 index 0000000..5f82ecf --- /dev/null +++ b/USBController/setupapi_windows.go @@ -0,0 +1,497 @@ +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2019 WireGuard LLC. All Rights Reserved. + */ + +package USBController + +import ( + "encoding/binary" + "errors" + "fmt" + "syscall" + "unsafe" + + "golang.org/x/sys/windows" + "golang.org/x/sys/windows/registry" +) + +// sys setupDiCreateDeviceInfoListEx(classGUID *windows.GUID, hwndParent uintptr, machineName *uint16, reserved uintptr) (handle DevInfo, err error) [failretval==DevInfo(windows.InvalidHandle)] = setupapi.SetupDiCreateDeviceInfoListExW + +// SetupDiCreateDeviceInfoListEx function creates an empty device information set on a remote or a local computer and optionally associates the set with a device setup class. +func SetupDiCreateDeviceInfoListEx(classGUID *windows.GUID, hwndParent uintptr, machineName string) (deviceInfoSet DevInfo, err error) { + var machineNameUTF16 *uint16 + if machineName != "" { + machineNameUTF16, err = syscall.UTF16PtrFromString(machineName) + if err != nil { + return + } + } + return setupDiCreateDeviceInfoListEx(classGUID, hwndParent, machineNameUTF16, 0) +} + +// sys setupDiGetDeviceInfoListDetail(deviceInfoSet DevInfo, deviceInfoSetDetailData *DevInfoListDetailData) (err error) = setupapi.SetupDiGetDeviceInfoListDetailW + +// SetupDiGetDeviceInfoListDetail function retrieves information associated with a device information set including the class GUID, remote computer handle, and remote computer name. +func SetupDiGetDeviceInfoListDetail(deviceInfoSet DevInfo) (deviceInfoSetDetailData *DevInfoListDetailData, err error) { + data := &DevInfoListDetailData{} + data.size = uint32(unsafe.Sizeof(*data)) + + return data, setupDiGetDeviceInfoListDetail(deviceInfoSet, data) +} + +// GetDeviceInfoListDetail method retrieves information associated with a device information set including the class GUID, remote computer handle, and remote computer name. +func (deviceInfoSet DevInfo) GetDeviceInfoListDetail() (*DevInfoListDetailData, error) { + return SetupDiGetDeviceInfoListDetail(deviceInfoSet) +} + +// sys setupDiCreateDeviceInfo(deviceInfoSet DevInfo, DeviceName *uint16, classGUID *windows.GUID, DeviceDescription *uint16, hwndParent uintptr, CreationFlags DICD, deviceInfoData *DevInfoData) (err error) = setupapi.SetupDiCreateDeviceInfoW + +// SetupDiCreateDeviceInfo function creates a new device information element and adds it as a new member to the specified device information set. +func SetupDiCreateDeviceInfo(deviceInfoSet DevInfo, deviceName string, classGUID *windows.GUID, deviceDescription string, hwndParent uintptr, creationFlags DICD) (deviceInfoData *DevInfoData, err error) { + deviceNameUTF16, err := syscall.UTF16PtrFromString(deviceName) + if err != nil { + return + } + + var deviceDescriptionUTF16 *uint16 + if deviceDescription != "" { + deviceDescriptionUTF16, err = syscall.UTF16PtrFromString(deviceDescription) + if err != nil { + return + } + } + + data := &DevInfoData{} + data.size = uint32(unsafe.Sizeof(*data)) + + return data, setupDiCreateDeviceInfo(deviceInfoSet, deviceNameUTF16, classGUID, deviceDescriptionUTF16, hwndParent, creationFlags, data) +} + +// CreateDeviceInfo method creates a new device information element and adds it as a new member to the specified device information set. +func (deviceInfoSet DevInfo) CreateDeviceInfo(deviceName string, classGUID *windows.GUID, deviceDescription string, hwndParent uintptr, creationFlags DICD) (*DevInfoData, error) { + return SetupDiCreateDeviceInfo(deviceInfoSet, deviceName, classGUID, deviceDescription, hwndParent, creationFlags) +} + +// sys setupDiEnumDeviceInfo(deviceInfoSet DevInfo, memberIndex uint32, deviceInfoData *DevInfoData) (err error) = setupapi.SetupDiEnumDeviceInfo + +// SetupDiEnumDeviceInfo function returns a DevInfoData structure that specifies a device information element in a device information set. +func SetupDiEnumDeviceInfo(deviceInfoSet DevInfo, memberIndex int) (*DevInfoData, error) { + data := &DevInfoData{} + data.size = uint32(unsafe.Sizeof(*data)) + + return data, setupDiEnumDeviceInfo(deviceInfoSet, uint32(memberIndex), data) +} + +// EnumDeviceInfo method returns a DevInfoData structure that specifies a device information element in a device information set. +func (deviceInfoSet DevInfo) EnumDeviceInfo(memberIndex int) (*DevInfoData, error) { + return SetupDiEnumDeviceInfo(deviceInfoSet, memberIndex) +} + +// SetupDiDestroyDeviceInfoList function deletes a device information set and frees all associated memory. +// sys SetupDiDestroyDeviceInfoList(deviceInfoSet DevInfo) (err error) = setupapi.SetupDiDestroyDeviceInfoList + +// Close method deletes a device information set and frees all associated memory. +func (deviceInfoSet DevInfo) Close() error { + return SetupDiDestroyDeviceInfoList(deviceInfoSet) +} + +// sys SetupDiBuildDriverInfoList(deviceInfoSet DevInfo, deviceInfoData *DevInfoData, driverType SPDIT) (err error) = setupapi.SetupDiBuildDriverInfoList + +// BuildDriverInfoList method builds a list of drivers that is associated with a specific device or with the global class driver list for a device information set. +func (deviceInfoSet DevInfo) BuildDriverInfoList(deviceInfoData *DevInfoData, driverType SPDIT) error { + return SetupDiBuildDriverInfoList(deviceInfoSet, deviceInfoData, driverType) +} + +// sys SetupDiCancelDriverInfoSearch(deviceInfoSet DevInfo) (err error) = setupapi.SetupDiCancelDriverInfoSearch + +// CancelDriverInfoSearch method cancels a driver list search that is currently in progress in a different thread. +func (deviceInfoSet DevInfo) CancelDriverInfoSearch() error { + return SetupDiCancelDriverInfoSearch(deviceInfoSet) +} + +// sys setupDiEnumDriverInfo(deviceInfoSet DevInfo, deviceInfoData *DevInfoData, driverType SPDIT, memberIndex uint32, driverInfoData *DrvInfoData) (err error) = setupapi.SetupDiEnumDriverInfoW + +// SetupDiEnumDriverInfo function enumerates the members of a driver list. +func SetupDiEnumDriverInfo(deviceInfoSet DevInfo, deviceInfoData *DevInfoData, driverType SPDIT, memberIndex int) (*DrvInfoData, error) { + data := &DrvInfoData{} + data.size = uint32(unsafe.Sizeof(*data)) + + return data, setupDiEnumDriverInfo(deviceInfoSet, deviceInfoData, driverType, uint32(memberIndex), data) +} + +// EnumDriverInfo method enumerates the members of a driver list. +func (deviceInfoSet DevInfo) EnumDriverInfo(deviceInfoData *DevInfoData, driverType SPDIT, memberIndex int) (*DrvInfoData, error) { + return SetupDiEnumDriverInfo(deviceInfoSet, deviceInfoData, driverType, memberIndex) +} + +// sys setupDiGetSelectedDriver(deviceInfoSet DevInfo, deviceInfoData *DevInfoData, driverInfoData *DrvInfoData) (err error) = setupapi.SetupDiGetSelectedDriverW + +// SetupDiGetSelectedDriver function retrieves the selected driver for a device information set or a particular device information element. +func SetupDiGetSelectedDriver(deviceInfoSet DevInfo, deviceInfoData *DevInfoData) (*DrvInfoData, error) { + data := &DrvInfoData{} + data.size = uint32(unsafe.Sizeof(*data)) + + return data, setupDiGetSelectedDriver(deviceInfoSet, deviceInfoData, data) +} + +// GetSelectedDriver method retrieves the selected driver for a device information set or a particular device information element. +func (deviceInfoSet DevInfo) GetSelectedDriver(deviceInfoData *DevInfoData) (*DrvInfoData, error) { + return SetupDiGetSelectedDriver(deviceInfoSet, deviceInfoData) +} + +// sys SetupDiSetSelectedDriver(deviceInfoSet DevInfo, deviceInfoData *DevInfoData, driverInfoData *DrvInfoData) (err error) = setupapi.SetupDiSetSelectedDriverW + +// SetSelectedDriver method sets, or resets, the selected driver for a device information element or the selected class driver for a device information set. +func (deviceInfoSet DevInfo) SetSelectedDriver(deviceInfoData *DevInfoData, driverInfoData *DrvInfoData) error { + return SetupDiSetSelectedDriver(deviceInfoSet, deviceInfoData, driverInfoData) +} + +// sys setupDiGetDriverInfoDetail(deviceInfoSet DevInfo, deviceInfoData *DevInfoData, driverInfoData *DrvInfoData, driverInfoDetailData *DrvInfoDetailData, driverInfoDetailDataSize uint32, requiredSize *uint32) (err error) = setupapi.SetupDiGetDriverInfoDetailW + +// SetupDiGetDriverInfoDetail function retrieves driver information detail for a device information set or a particular device information element in the device information set. +func SetupDiGetDriverInfoDetail(deviceInfoSet DevInfo, deviceInfoData *DevInfoData, driverInfoData *DrvInfoData) (*DrvInfoDetailData, error) { + const bufCapacity = 0x800 + buf := [bufCapacity]byte{} + var bufLen uint32 + + data := (*DrvInfoDetailData)(unsafe.Pointer(&buf[0])) + data.size = uint32(unsafe.Sizeof(*data)) + + err := setupDiGetDriverInfoDetail(deviceInfoSet, deviceInfoData, driverInfoData, data, bufCapacity, &bufLen) + if err == nil { + // The buffer was was sufficiently big. + data.size = bufLen + return data, nil + } + + if errWin, ok := err.(syscall.Errno); ok && errWin == windows.ERROR_INSUFFICIENT_BUFFER { + // The buffer was too small. Now that we got the required size, create another one big enough and retry. + buf := make([]byte, bufLen) + data := (*DrvInfoDetailData)(unsafe.Pointer(&buf[0])) + data.size = uint32(unsafe.Sizeof(*data)) + + err = setupDiGetDriverInfoDetail(deviceInfoSet, deviceInfoData, driverInfoData, data, bufLen, &bufLen) + if err == nil { + data.size = bufLen + return data, nil + } + } + + return nil, err +} + +// GetDriverInfoDetail method retrieves driver information detail for a device information set or a particular device information element in the device information set. +func (deviceInfoSet DevInfo) GetDriverInfoDetail(deviceInfoData *DevInfoData, driverInfoData *DrvInfoData) (*DrvInfoDetailData, error) { + return SetupDiGetDriverInfoDetail(deviceInfoSet, deviceInfoData, driverInfoData) +} + +// sys SetupDiDestroyDriverInfoList(deviceInfoSet DevInfo, deviceInfoData *DevInfoData, driverType SPDIT) (err error) = setupapi.SetupDiDestroyDriverInfoList + +// DestroyDriverInfoList method deletes a driver list. +func (deviceInfoSet DevInfo) DestroyDriverInfoList(deviceInfoData *DevInfoData, driverType SPDIT) error { + return SetupDiDestroyDriverInfoList(deviceInfoSet, deviceInfoData, driverType) +} + +// sys setupDiGetClassDevsEx(classGUID *windows.GUID, Enumerator *uint16, hwndParent uintptr, Flags DIGCF, deviceInfoSet DevInfo, machineName *uint16, reserved uintptr) (handle DevInfo, err error) [failretval==DevInfo(windows.InvalidHandle)] = setupapi.SetupDiGetClassDevsExW + +// SetupDiGetClassDevsEx function returns a handle to a device information set that contains requested device information elements for a local or a remote computer. +func SetupDiGetClassDevsEx(classGUID *windows.GUID, enumerator string, hwndParent uintptr, flags DIGCF, deviceInfoSet DevInfo, machineName string) (handle DevInfo, err error) { + var enumeratorUTF16 *uint16 + if enumerator != "" { + enumeratorUTF16, err = syscall.UTF16PtrFromString(enumerator) + if err != nil { + return + } + } + var machineNameUTF16 *uint16 + if machineName != "" { + machineNameUTF16, err = syscall.UTF16PtrFromString(machineName) + if err != nil { + return + } + } + return setupDiGetClassDevsEx(classGUID, enumeratorUTF16, hwndParent, flags, deviceInfoSet, machineNameUTF16, 0) +} + +// SetupDiCallClassInstaller function calls the appropriate class installer, and any registered co-installers, with the specified installation request (DIF code). +// sys SetupDiCallClassInstaller(installFunction DI_FUNCTION, deviceInfoSet DevInfo, deviceInfoData *DevInfoData) (err error) = setupapi.SetupDiCallClassInstaller + +// CallClassInstaller member calls the appropriate class installer, and any registered co-installers, with the specified installation request (DIF code). +func (deviceInfoSet DevInfo) CallClassInstaller(installFunction DI_FUNCTION, deviceInfoData *DevInfoData) error { + return SetupDiCallClassInstaller(installFunction, deviceInfoSet, deviceInfoData) +} + +// sys setupDiOpenDevRegKey(deviceInfoSet DevInfo, deviceInfoData *DevInfoData, Scope DICS_FLAG, HwProfile uint32, KeyType DIREG, samDesired uint32) (key windows.Handle, err error) [failretval==windows.InvalidHandle] = setupapi.SetupDiOpenDevRegKey + +// SetupDiOpenDevRegKey function opens a registry key for device-specific configuration information. +func SetupDiOpenDevRegKey(deviceInfoSet DevInfo, deviceInfoData *DevInfoData, scope DICS_FLAG, hwProfile uint32, keyType DIREG, samDesired uint32) (registry.Key, error) { + handle, err := setupDiOpenDevRegKey(deviceInfoSet, deviceInfoData, scope, hwProfile, keyType, samDesired) + return registry.Key(handle), err +} + +// OpenDevRegKey method opens a registry key for device-specific configuration information. +func (deviceInfoSet DevInfo) OpenDevRegKey(deviceInfoData *DevInfoData, scope DICS_FLAG, hwProfile uint32, keyType DIREG, samDesired uint32) (registry.Key, error) { + return SetupDiOpenDevRegKey(deviceInfoSet, deviceInfoData, scope, hwProfile, keyType, samDesired) +} + +// GetInterfaceID method returns network interface ID. +func (deviceInfoSet DevInfo) GetInterfaceID(deviceInfoData *DevInfoData) (*windows.GUID, error) { + // Open HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Class\\ registry key. + key, err := deviceInfoSet.OpenDevRegKey(deviceInfoData, DICS_FLAG_GLOBAL, 0, DIREG_DRV, registry.READ) + if err != nil { + return nil, errors.New("Device-specific registry key open failed: " + err.Error()) + } + defer key.Close() + + // Read the NetCfgInstanceId value. + value, valueType, err := key.GetStringValue("NetCfgInstanceId") + if err != nil { + return nil, errors.New("RegQueryStringValue(\"NetCfgInstanceId\") failed: " + err.Error()) + } + if valueType != registry.SZ { + return nil, fmt.Errorf("NetCfgInstanceId registry value is not REG_SZ (expected: %v, provided: %v)", registry.SZ, valueType) + } + + // Convert to windows.GUID. + ifid, err := windows.GUIDFromString(value) + // ifid, err := FromString(value) + if err != nil { + return nil, fmt.Errorf("NetCfgInstanceId registry value is not a GUID (expected: \"{...}\", provided: %q)", value) + } + + return &ifid, nil +} + +// sys setupDiGetDeviceRegistryProperty(deviceInfoSet DevInfo, deviceInfoData *DevInfoData, property SPDRP, propertyRegDataType *uint32, propertyBuffer *byte, propertyBufferSize uint32, requiredSize *uint32) (err error) = setupapi.SetupDiGetDeviceRegistryPropertyW + +// SetupDiGetDeviceRegistryProperty function retrieves a specified Plug and Play device property. +func SetupDiGetDeviceRegistryProperty(deviceInfoSet DevInfo, deviceInfoData *DevInfoData, property SPDRP) (value interface{}, err error) { + buf := make([]byte, 0x100) + var dataType, bufLen uint32 + err = setupDiGetDeviceRegistryProperty(deviceInfoSet, deviceInfoData, property, &dataType, &buf[0], uint32(cap(buf)), &bufLen) + if err == nil { + // The buffer was sufficiently big. + return getRegistryValue(buf[:bufLen], dataType) + } + + if errWin, ok := err.(syscall.Errno); ok && errWin == windows.ERROR_INSUFFICIENT_BUFFER { + // The buffer was too small. Now that we got the required size, create another one big enough and retry. + buf = make([]byte, bufLen) + err = setupDiGetDeviceRegistryProperty(deviceInfoSet, deviceInfoData, property, &dataType, &buf[0], uint32(cap(buf)), &bufLen) + if err == nil { + return getRegistryValue(buf[:bufLen], dataType) + } + } + + return +} + +func getRegistryValue(buf []byte, dataType uint32) (interface{}, error) { + switch dataType { + case windows.REG_SZ: + return windows.UTF16ToString(BufToUTF16(buf)), nil + case windows.REG_EXPAND_SZ: + return registry.ExpandString(windows.UTF16ToString(BufToUTF16(buf))) + case windows.REG_BINARY: + return buf, nil + case windows.REG_DWORD_LITTLE_ENDIAN: + return binary.LittleEndian.Uint32(buf), nil + case windows.REG_DWORD_BIG_ENDIAN: + return binary.BigEndian.Uint32(buf), nil + case windows.REG_MULTI_SZ: + bufW := BufToUTF16(buf) + a := []string{} + for i := 0; i < len(bufW); { + j := i + wcslen(bufW[i:]) + if i < j { + a = append(a, windows.UTF16ToString(bufW[i:j])) + } + i = j + 1 + } + return a, nil + case windows.REG_QWORD_LITTLE_ENDIAN: + return binary.LittleEndian.Uint64(buf), nil + default: + return nil, fmt.Errorf("unsupported registry value type: %v", dataType) + } +} + +// BufToUTF16 function reinterprets []byte buffer as []uint16 +func BufToUTF16(buf []byte) []uint16 { + sl := struct { + addr *uint16 + len int + cap int + }{(*uint16)(unsafe.Pointer(&buf[0])), len(buf) / 2, cap(buf) / 2} + return *(*[]uint16)(unsafe.Pointer(&sl)) +} + +// UTF16ToBuf function reinterprets []uint16 as []byte +func UTF16ToBuf(buf []uint16) []byte { + sl := struct { + addr *byte + len int + cap int + }{(*byte)(unsafe.Pointer(&buf[0])), len(buf) * 2, cap(buf) * 2} + return *(*[]byte)(unsafe.Pointer(&sl)) +} + +func wcslen(str []uint16) int { + for i := 0; i < len(str); i++ { + if str[i] == 0 { + return i + } + } + return len(str) +} + +// GetDeviceRegistryProperty method retrieves a specified Plug and Play device property. +func (deviceInfoSet DevInfo) GetDeviceRegistryProperty(deviceInfoData *DevInfoData, property SPDRP) (interface{}, error) { + return SetupDiGetDeviceRegistryProperty(deviceInfoSet, deviceInfoData, property) +} + +// sys setupDiSetDeviceRegistryProperty(deviceInfoSet DevInfo, deviceInfoData *DevInfoData, property SPDRP, propertyBuffer *byte, propertyBufferSize uint32) (err error) = setupapi.SetupDiSetDeviceRegistryPropertyW + +// SetupDiSetDeviceRegistryProperty function sets a Plug and Play device property for a device. +func SetupDiSetDeviceRegistryProperty(deviceInfoSet DevInfo, deviceInfoData *DevInfoData, property SPDRP, propertyBuffers []byte) error { + return setupDiSetDeviceRegistryProperty(deviceInfoSet, deviceInfoData, property, &propertyBuffers[0], uint32(len(propertyBuffers))) +} + +// SetDeviceRegistryProperty function sets a Plug and Play device property for a device. +func (deviceInfoSet DevInfo) SetDeviceRegistryProperty(deviceInfoData *DevInfoData, property SPDRP, propertyBuffers []byte) error { + return SetupDiSetDeviceRegistryProperty(deviceInfoSet, deviceInfoData, property, propertyBuffers) +} + +// sys setupDiGetDeviceInstallParams(deviceInfoSet DevInfo, deviceInfoData *DevInfoData, deviceInstallParams *DevInstallParams) (err error) = setupapi.SetupDiGetDeviceInstallParamsW + +// SetupDiGetDeviceInstallParams function retrieves device installation parameters for a device information set or a particular device information element. +func SetupDiGetDeviceInstallParams(deviceInfoSet DevInfo, deviceInfoData *DevInfoData) (*DevInstallParams, error) { + params := &DevInstallParams{} + params.size = uint32(unsafe.Sizeof(*params)) + + return params, setupDiGetDeviceInstallParams(deviceInfoSet, deviceInfoData, params) +} + +// GetDeviceInstallParams method retrieves device installation parameters for a device information set or a particular device information element. +func (deviceInfoSet DevInfo) GetDeviceInstallParams(deviceInfoData *DevInfoData) (*DevInstallParams, error) { + return SetupDiGetDeviceInstallParams(deviceInfoSet, deviceInfoData) +} + +// SetupDiGetClassInstallParams function retrieves class installation parameters for a device information set or a particular device information element. +// sys SetupDiGetClassInstallParams(deviceInfoSet DevInfo, deviceInfoData *DevInfoData, classInstallParams *ClassInstallHeader, classInstallParamsSize uint32, requiredSize *uint32) (err error) = setupapi.SetupDiGetClassInstallParamsW + +// GetClassInstallParams method retrieves class installation parameters for a device information set or a particular device information element. +func (deviceInfoSet DevInfo) GetClassInstallParams(deviceInfoData *DevInfoData, classInstallParams *ClassInstallHeader, classInstallParamsSize uint32, requiredSize *uint32) error { + return SetupDiGetClassInstallParams(deviceInfoSet, deviceInfoData, classInstallParams, classInstallParamsSize, requiredSize) +} + +// sys SetupDiSetDeviceInstallParams(deviceInfoSet DevInfo, deviceInfoData *DevInfoData, deviceInstallParams *DevInstallParams) (err error) = setupapi.SetupDiSetDeviceInstallParamsW + +// SetDeviceInstallParams member sets device installation parameters for a device information set or a particular device information element. +func (deviceInfoSet DevInfo) SetDeviceInstallParams(deviceInfoData *DevInfoData, deviceInstallParams *DevInstallParams) error { + return SetupDiSetDeviceInstallParams(deviceInfoSet, deviceInfoData, deviceInstallParams) +} + +// SetupDiSetClassInstallParams function sets or clears class install parameters for a device information set or a particular device information element. +// sys SetupDiSetClassInstallParams(deviceInfoSet DevInfo, deviceInfoData *DevInfoData, classInstallParams *ClassInstallHeader, classInstallParamsSize uint32) (err error) = setupapi.SetupDiSetClassInstallParamsW + +// SetClassInstallParams method sets or clears class install parameters for a device information set or a particular device information element. +func (deviceInfoSet DevInfo) SetClassInstallParams(deviceInfoData *DevInfoData, classInstallParams *ClassInstallHeader, classInstallParamsSize uint32) error { + return SetupDiSetClassInstallParams(deviceInfoSet, deviceInfoData, classInstallParams, classInstallParamsSize) +} + +// sys setupDiClassNameFromGuidEx(classGUID *windows.GUID, className *uint16, classNameSize uint32, requiredSize *uint32, machineName *uint16, reserved uintptr) (err error) = setupapi.SetupDiClassNameFromGuidExW + +// SetupDiClassNameFromGuidEx function retrieves the class name associated with a class GUID. The class can be installed on a local or remote computer. +func SetupDiClassNameFromGuidEx(classGUID *windows.GUID, machineName string) (className string, err error) { + var classNameUTF16 [MAX_CLASS_NAME_LEN]uint16 + + var machineNameUTF16 *uint16 + if machineName != "" { + machineNameUTF16, err = syscall.UTF16PtrFromString(machineName) + if err != nil { + return + } + } + + err = setupDiClassNameFromGuidEx(classGUID, &classNameUTF16[0], MAX_CLASS_NAME_LEN, nil, machineNameUTF16, 0) + if err != nil { + return + } + + className = windows.UTF16ToString(classNameUTF16[:]) + return +} + +// sys setupDiClassGuidsFromNameEx(className *uint16, classGuidList *windows.GUID, classGuidListSize uint32, requiredSize *uint32, machineName *uint16, reserved uintptr) (err error) = setupapi.SetupDiClassGuidsFromNameExW + +// SetupDiClassGuidsFromNameEx function retrieves the GUIDs associated with the specified class name. This resulting list contains the classes currently installed on a local or remote computer. +func SetupDiClassGuidsFromNameEx(className, machineName string) ([]windows.GUID, error) { + classNameUTF16, err := syscall.UTF16PtrFromString(className) + if err != nil { + return nil, err + } + + const bufCapacity = 4 + var buf [bufCapacity]windows.GUID + var bufLen uint32 + + var machineNameUTF16 *uint16 + if machineName != "" { + machineNameUTF16, err = syscall.UTF16PtrFromString(machineName) + if err != nil { + return nil, err + } + } + + err = setupDiClassGuidsFromNameEx(classNameUTF16, &buf[0], bufCapacity, &bufLen, machineNameUTF16, 0) + if err == nil { + // The GUID array was sufficiently big. Return its slice. + return buf[:bufLen], nil + } + + if errWin, ok := err.(syscall.Errno); ok && errWin == windows.ERROR_INSUFFICIENT_BUFFER { + // The GUID array was too small. Now that we got the required size, create another one big enough and retry. + buf := make([]windows.GUID, bufLen) + err = setupDiClassGuidsFromNameEx(classNameUTF16, &buf[0], bufLen, &bufLen, machineNameUTF16, 0) + if err == nil { + return buf[:bufLen], nil + } + } + + return nil, err +} + +// sys setupDiGetSelectedDevice(deviceInfoSet DevInfo, deviceInfoData *DevInfoData) (err error) = setupapi.SetupDiGetSelectedDevice + +// SetupDiGetSelectedDevice function retrieves the selected device information element in a device information set. +func SetupDiGetSelectedDevice(deviceInfoSet DevInfo) (*DevInfoData, error) { + data := &DevInfoData{} + data.size = uint32(unsafe.Sizeof(*data)) + + return data, setupDiGetSelectedDevice(deviceInfoSet, data) +} + +// GetSelectedDevice method retrieves the selected device information element in a device information set. +func (deviceInfoSet DevInfo) GetSelectedDevice() (*DevInfoData, error) { + return SetupDiGetSelectedDevice(deviceInfoSet) +} + +// SetupDiSetSelectedDevice function sets a device information element as the selected member of a device information set. This function is typically used by an installation wizard. +// sys SetupDiSetSelectedDevice(deviceInfoSet DevInfo, deviceInfoData *DevInfoData) (err error) = setupapi.SetupDiSetSelectedDevice + +// SetSelectedDevice method sets a device information element as the selected member of a device information set. This function is typically used by an installation wizard. +func (deviceInfoSet DevInfo) SetSelectedDevice(deviceInfoData *DevInfoData) error { + return SetupDiSetSelectedDevice(deviceInfoSet, deviceInfoData) +} + +// sys cm_Get_DevNode_Status(status *uint32, problemNumber *uint32, devInst DEVINST, flags uint32) (ret CONFIGRET) = CfgMgr32.CM_Get_DevNode_Status + +func CM_Get_DevNode_Status(status *uint32, problemNumber *uint32, devInst uint32, flags uint32) uint32 { + ret := cm_Get_DevNode_Status(status, problemNumber, devInst, flags) + // if ret == 0 { + // return nil + // } + return ret +} diff --git a/USBController/setupapidll.go b/USBController/setupapidll.go new file mode 100644 index 0000000..e96fb42 --- /dev/null +++ b/USBController/setupapidll.go @@ -0,0 +1,87 @@ +package USBController + +import ( + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +var ( + // Library + modSetupapi = windows.NewLazyDLL("setupapi.dll") + + // Functions + procSetupDiGetClassDevsW = modSetupapi.NewProc("SetupDiGetClassDevsW") +) + +type Device struct { + Idata DevInfoData + FriendlyName string + COMid string +} + +var GUID_DEVINTERFACE_COMPORT, _ = windows.GUIDFromString("{86E0D1E0-8089-11D0-9CE4-08003E301F73}") +var GUID_DEVINTERFACE_MOUSE, _ = windows.GUIDFromString("{378DE44C-56EF-11D1-BC8C-00A0C91405DD}") + +func FindDevices(classGuid windows.GUID) ([]Device, DevInfo) { + var allDevices []Device + handle, err := SetupDiGetClassDevs(&classGuid, nil, 0, uint32(DIGCF_PRESENT|DIGCF_DEVICEINTERFACE)) + + if err != nil { + panic(err) + } + + var index = 0 + for { + idata, err := SetupDiEnumDeviceInfo(handle, index) + if err != nil { // ERROR_NO_MORE_ITEMS + break + } + index++ + + dev := Device{ + Idata: *idata, + } + + val, err := SetupDiGetDeviceRegistryProperty(handle, idata, SPDRP_FRIENDLYNAME) + if err == nil { + dev.FriendlyName = val.(string) + } + + allDevices = append(allDevices, dev) + } + return allDevices, handle +} + +func SetupDiGetClassDevs(classGuid *windows.GUID, enumerator *uint16, hwndParent uintptr, flags uint32) (handle DevInfo, err error) { + r0, _, e1 := syscall.Syscall6(procSetupDiGetClassDevsW.Addr(), 4, uintptr(unsafe.Pointer(classGuid)), uintptr(unsafe.Pointer(enumerator)), uintptr(hwndParent), uintptr(flags), 0, 0) + handle = DevInfo(r0) + if handle == DevInfo(windows.InvalidHandle) { + if e1 != 0 { + err = error(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +func GetDeviceProperty(dis DevInfo, devInfoData *DevInfoData, devPropKey DEVPROPKEY) ([]byte, error) { + var propt, size uint32 + buf := make([]byte, 16) + run := true + for run { + err := SetupDiGetDeviceProperty(dis, devInfoData, &devPropKey, &propt, &buf[0], uint32(len(buf)), &size, 0) + switch { + case size > uint32(len(buf)): + buf = make([]byte, size+16) + case err != nil: + return buf, err + default: + run = false + } + } + + return buf, nil +} diff --git a/USBController/types_windows.go b/USBController/types_windows.go new file mode 100644 index 0000000..7eb12f3 --- /dev/null +++ b/USBController/types_windows.go @@ -0,0 +1,556 @@ +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2019 WireGuard LLC. All Rights Reserved. + */ + +package USBController + +import ( + "strings" + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +const ( + MAX_DEVICE_ID_LEN = 200 + MAX_DEVNODE_ID_LEN = MAX_DEVICE_ID_LEN + MAX_GUID_STRING_LEN = 39 // 38 chars + terminator null + MAX_CLASS_NAME_LEN = 32 + MAX_PROFILE_LEN = 80 + MAX_CONFIG_VALUE = 9999 + MAX_INSTANCE_VALUE = 9999 + CONFIGMG_VERSION = 0x0400 +) + +// +// Define maximum string length constants +// +const ( + LINE_LEN = 256 // Windows 9x-compatible maximum for displayable strings coming from a device INF. + MAX_INF_STRING_LENGTH = 4096 // Actual maximum size of an INF string (including string substitutions). + MAX_INF_SECTION_NAME_LENGTH = 255 // For Windows 9x compatibility, INF section names should be constrained to 32 characters. + MAX_TITLE_LEN = 60 + MAX_INSTRUCTION_LEN = 256 + MAX_LABEL_LEN = 30 + MAX_SERVICE_NAME_LEN = 256 + MAX_SUBTITLE_LEN = 256 +) + +const ( + // SP_MAX_MACHINENAME_LENGTH defines maximum length of a machine name in the format expected by ConfigMgr32 CM_Connect_Machine (i.e., "\\\\MachineName\0"). + SP_MAX_MACHINENAME_LENGTH = windows.MAX_PATH + 3 +) + +// HSPFILEQ is type for setup file queue +type HSPFILEQ uintptr + +// DevInfo holds reference to device information set +type DevInfo windows.Handle + +// DevInfoData is a device information structure (references a device instance that is a member of a device information set) +type DevInfoData struct { + size uint32 + ClassGUID windows.GUID + DevInst uint32 // DEVINST handle + _ uintptr +} + +// DevInfoListDetailData is a structure for detailed information on a device information set (used for SetupDiGetDeviceInfoListDetail which supercedes the functionality of SetupDiGetDeviceInfoListClass). +type DevInfoListDetailData struct { + size uint32 + ClassGUID windows.GUID + RemoteMachineHandle windows.Handle + remoteMachineName [SP_MAX_MACHINENAME_LENGTH]uint16 +} + +func (data *DevInfoListDetailData) GetRemoteMachineName() string { + return windows.UTF16ToString(data.remoteMachineName[:]) +} + +func (data *DevInfoListDetailData) SetRemoteMachineName(remoteMachineName string) error { + str, err := syscall.UTF16FromString(remoteMachineName) + if err != nil { + return err + } + copy(data.remoteMachineName[:], str) + return nil +} + +// DI_FUNCTION is function type for device installer +type DI_FUNCTION uint32 + +const ( + DIF_SELECTDEVICE DI_FUNCTION = 0x00000001 + DIF_INSTALLDEVICE DI_FUNCTION = 0x00000002 + DIF_ASSIGNRESOURCES DI_FUNCTION = 0x00000003 + DIF_PROPERTIES DI_FUNCTION = 0x00000004 + DIF_REMOVE DI_FUNCTION = 0x00000005 + DIF_FIRSTTIMESETUP DI_FUNCTION = 0x00000006 + DIF_FOUNDDEVICE DI_FUNCTION = 0x00000007 + DIF_SELECTCLASSDRIVERS DI_FUNCTION = 0x00000008 + DIF_VALIDATECLASSDRIVERS DI_FUNCTION = 0x00000009 + DIF_INSTALLCLASSDRIVERS DI_FUNCTION = 0x0000000A + DIF_CALCDISKSPACE DI_FUNCTION = 0x0000000B + DIF_DESTROYPRIVATEDATA DI_FUNCTION = 0x0000000C + DIF_VALIDATEDRIVER DI_FUNCTION = 0x0000000D + DIF_DETECT DI_FUNCTION = 0x0000000F + DIF_INSTALLWIZARD DI_FUNCTION = 0x00000010 + DIF_DESTROYWIZARDDATA DI_FUNCTION = 0x00000011 + DIF_PROPERTYCHANGE DI_FUNCTION = 0x00000012 + DIF_ENABLECLASS DI_FUNCTION = 0x00000013 + DIF_DETECTVERIFY DI_FUNCTION = 0x00000014 + DIF_INSTALLDEVICEFILES DI_FUNCTION = 0x00000015 + DIF_UNREMOVE DI_FUNCTION = 0x00000016 + DIF_SELECTBESTCOMPATDRV DI_FUNCTION = 0x00000017 + DIF_ALLOW_INSTALL DI_FUNCTION = 0x00000018 + DIF_REGISTERDEVICE DI_FUNCTION = 0x00000019 + DIF_NEWDEVICEWIZARD_PRESELECT DI_FUNCTION = 0x0000001A + DIF_NEWDEVICEWIZARD_SELECT DI_FUNCTION = 0x0000001B + DIF_NEWDEVICEWIZARD_PREANALYZE DI_FUNCTION = 0x0000001C + DIF_NEWDEVICEWIZARD_POSTANALYZE DI_FUNCTION = 0x0000001D + DIF_NEWDEVICEWIZARD_FINISHINSTALL DI_FUNCTION = 0x0000001E + DIF_INSTALLINTERFACES DI_FUNCTION = 0x00000020 + DIF_DETECTCANCEL DI_FUNCTION = 0x00000021 + DIF_REGISTER_COINSTALLERS DI_FUNCTION = 0x00000022 + DIF_ADDPROPERTYPAGE_ADVANCED DI_FUNCTION = 0x00000023 + DIF_ADDPROPERTYPAGE_BASIC DI_FUNCTION = 0x00000024 + DIF_TROUBLESHOOTER DI_FUNCTION = 0x00000026 + DIF_POWERMESSAGEWAKE DI_FUNCTION = 0x00000027 + DIF_ADDREMOTEPROPERTYPAGE_ADVANCED DI_FUNCTION = 0x00000028 + DIF_UPDATEDRIVER_UI DI_FUNCTION = 0x00000029 + DIF_FINISHINSTALL_ACTION DI_FUNCTION = 0x0000002A +) + +// DevInstallParams is device installation parameters structure (associated with a particular device information element, or globally with a device information set) +type DevInstallParams struct { + size uint32 + Flags DI_FLAGS + FlagsEx DI_FLAGSEX + hwndParent uintptr + InstallMsgHandler uintptr + InstallMsgHandlerContext uintptr + FileQueue HSPFILEQ + _ uintptr + _ uint32 + driverPath [windows.MAX_PATH]uint16 +} + +func (params *DevInstallParams) GetDriverPath() string { + return windows.UTF16ToString(params.driverPath[:]) +} + +func (params *DevInstallParams) SetDriverPath(driverPath string) error { + str, err := syscall.UTF16FromString(driverPath) + if err != nil { + return err + } + copy(params.driverPath[:], str) + return nil +} + +// DI_FLAGS is SP_DEVINSTALL_PARAMS.Flags values +type DI_FLAGS uint32 + +const ( + // Flags for choosing a device + DI_SHOWOEM DI_FLAGS = 0x00000001 // support Other... button + DI_SHOWCOMPAT DI_FLAGS = 0x00000002 // show compatibility list + DI_SHOWCLASS DI_FLAGS = 0x00000004 // show class list + DI_SHOWALL DI_FLAGS = 0x00000007 // both class & compat list shown + DI_NOVCP DI_FLAGS = 0x00000008 // don't create a new copy queue--use caller-supplied FileQueue + DI_DIDCOMPAT DI_FLAGS = 0x00000010 // Searched for compatible devices + DI_DIDCLASS DI_FLAGS = 0x00000020 // Searched for class devices + DI_AUTOASSIGNRES DI_FLAGS = 0x00000040 // No UI for resources if possible + + // Flags returned by DiInstallDevice to indicate need to reboot/restart + DI_NEEDRESTART DI_FLAGS = 0x00000080 // Reboot required to take effect + DI_NEEDREBOOT DI_FLAGS = 0x00000100 // "" + + // Flags for device installation + DI_NOBROWSE DI_FLAGS = 0x00000200 // no Browse... in InsertDisk + + // Flags set by DiBuildDriverInfoList + DI_MULTMFGS DI_FLAGS = 0x00000400 // Set if multiple manufacturers in class driver list + + // Flag indicates that device is disabled + DI_DISABLED DI_FLAGS = 0x00000800 // Set if device disabled + + // Flags for Device/Class Properties + DI_GENERALPAGE_ADDED DI_FLAGS = 0x00001000 + DI_RESOURCEPAGE_ADDED DI_FLAGS = 0x00002000 + + // Flag to indicate the setting properties for this Device (or class) caused a change so the Dev Mgr UI probably needs to be updated. + DI_PROPERTIES_CHANGE DI_FLAGS = 0x00004000 + + // Flag to indicate that the sorting from the INF file should be used. + DI_INF_IS_SORTED DI_FLAGS = 0x00008000 + + // Flag to indicate that only the the INF specified by SP_DEVINSTALL_PARAMS.DriverPath should be searched. + DI_ENUMSINGLEINF DI_FLAGS = 0x00010000 + + // Flag that prevents ConfigMgr from removing/re-enumerating devices during device + // registration, installation, and deletion. + DI_DONOTCALLCONFIGMG DI_FLAGS = 0x00020000 + + // The following flag can be used to install a device disabled + DI_INSTALLDISABLED DI_FLAGS = 0x00040000 + + // Flag that causes SetupDiBuildDriverInfoList to build a device's compatible driver + // list from its existing class driver list, instead of the normal INF search. + DI_COMPAT_FROM_CLASS DI_FLAGS = 0x00080000 + + // This flag is set if the Class Install params should be used. + DI_CLASSINSTALLPARAMS DI_FLAGS = 0x00100000 + + // This flag is set if the caller of DiCallClassInstaller does NOT want the internal default action performed if the Class installer returns ERROR_DI_DO_DEFAULT. + DI_NODI_DEFAULTACTION DI_FLAGS = 0x00200000 + + // Flags for device installation + DI_QUIETINSTALL DI_FLAGS = 0x00800000 // don't confuse the user with questions or excess info + DI_NOFILECOPY DI_FLAGS = 0x01000000 // No file Copy necessary + DI_FORCECOPY DI_FLAGS = 0x02000000 // Force files to be copied from install path + DI_DRIVERPAGE_ADDED DI_FLAGS = 0x04000000 // Prop provider added Driver page. + DI_USECI_SELECTSTRINGS DI_FLAGS = 0x08000000 // Use Class Installer Provided strings in the Select Device Dlg + DI_OVERRIDE_INFFLAGS DI_FLAGS = 0x10000000 // Override INF flags + DI_PROPS_NOCHANGEUSAGE DI_FLAGS = 0x20000000 // No Enable/Disable in General Props + + DI_NOSELECTICONS DI_FLAGS = 0x40000000 // No small icons in select device dialogs + + DI_NOWRITE_IDS DI_FLAGS = 0x80000000 // Don't write HW & Compat IDs on install +) + +// DI_FLAGSEX is SP_DEVINSTALL_PARAMS.FlagsEx values +type DI_FLAGSEX uint32 + +const ( + DI_FLAGSEX_CI_FAILED DI_FLAGSEX = 0x00000004 // Failed to Load/Call class installer + DI_FLAGSEX_FINISHINSTALL_ACTION DI_FLAGSEX = 0x00000008 // Class/co-installer wants to get a DIF_FINISH_INSTALL action in client context. + DI_FLAGSEX_DIDINFOLIST DI_FLAGSEX = 0x00000010 // Did the Class Info List + DI_FLAGSEX_DIDCOMPATINFO DI_FLAGSEX = 0x00000020 // Did the Compat Info List + DI_FLAGSEX_FILTERCLASSES DI_FLAGSEX = 0x00000040 + DI_FLAGSEX_SETFAILEDINSTALL DI_FLAGSEX = 0x00000080 + DI_FLAGSEX_DEVICECHANGE DI_FLAGSEX = 0x00000100 + DI_FLAGSEX_ALWAYSWRITEIDS DI_FLAGSEX = 0x00000200 + DI_FLAGSEX_PROPCHANGE_PENDING DI_FLAGSEX = 0x00000400 // One or more device property sheets have had changes made to them, and need to have a DIF_PROPERTYCHANGE occur. + DI_FLAGSEX_ALLOWEXCLUDEDDRVS DI_FLAGSEX = 0x00000800 + DI_FLAGSEX_NOUIONQUERYREMOVE DI_FLAGSEX = 0x00001000 + DI_FLAGSEX_USECLASSFORCOMPAT DI_FLAGSEX = 0x00002000 // Use the device's class when building compat drv list. (Ignored if DI_COMPAT_FROM_CLASS flag is specified.) + DI_FLAGSEX_NO_DRVREG_MODIFY DI_FLAGSEX = 0x00008000 // Don't run AddReg and DelReg for device's software (driver) key. + DI_FLAGSEX_IN_SYSTEM_SETUP DI_FLAGSEX = 0x00010000 // Installation is occurring during initial system setup. + DI_FLAGSEX_INET_DRIVER DI_FLAGSEX = 0x00020000 // Driver came from Windows Update + DI_FLAGSEX_APPENDDRIVERLIST DI_FLAGSEX = 0x00040000 // Cause SetupDiBuildDriverInfoList to append a new driver list to an existing list. + DI_FLAGSEX_PREINSTALLBACKUP DI_FLAGSEX = 0x00080000 // not used + DI_FLAGSEX_BACKUPONREPLACE DI_FLAGSEX = 0x00100000 // not used + DI_FLAGSEX_DRIVERLIST_FROM_URL DI_FLAGSEX = 0x00200000 // build driver list from INF(s) retrieved from URL specified in SP_DEVINSTALL_PARAMS.DriverPath (empty string means Windows Update website) + DI_FLAGSEX_EXCLUDE_OLD_INET_DRIVERS DI_FLAGSEX = 0x00800000 // Don't include old Internet drivers when building a driver list. Ignored on Windows Vista and later. + DI_FLAGSEX_POWERPAGE_ADDED DI_FLAGSEX = 0x01000000 // class installer added their own power page + DI_FLAGSEX_FILTERSIMILARDRIVERS DI_FLAGSEX = 0x02000000 // only include similar drivers in class list + DI_FLAGSEX_INSTALLEDDRIVER DI_FLAGSEX = 0x04000000 // only add the installed driver to the class or compat driver list. Used in calls to SetupDiBuildDriverInfoList + DI_FLAGSEX_NO_CLASSLIST_NODE_MERGE DI_FLAGSEX = 0x08000000 // Don't remove identical driver nodes from the class list + DI_FLAGSEX_ALTPLATFORM_DRVSEARCH DI_FLAGSEX = 0x10000000 // Build driver list based on alternate platform information specified in associated file queue + DI_FLAGSEX_RESTART_DEVICE_ONLY DI_FLAGSEX = 0x20000000 // only restart the device drivers are being installed on as opposed to restarting all devices using those drivers. + DI_FLAGSEX_RECURSIVESEARCH DI_FLAGSEX = 0x40000000 // Tell SetupDiBuildDriverInfoList to do a recursive search + DI_FLAGSEX_SEARCH_PUBLISHED_INFS DI_FLAGSEX = 0x80000000 // Tell SetupDiBuildDriverInfoList to do a "published INF" search +) + +// ClassInstallHeader is the first member of any class install parameters structure. It contains the device installation request code that defines the format of the rest of the install parameters structure. +type ClassInstallHeader struct { + size uint32 + InstallFunction DI_FUNCTION +} + +func MakeClassInstallHeader(installFunction DI_FUNCTION) *ClassInstallHeader { + hdr := &ClassInstallHeader{InstallFunction: installFunction} + hdr.size = uint32(unsafe.Sizeof(*hdr)) + return hdr +} + +// DICS_FLAG specifies the scope of a device property change +type DICS_FLAG uint32 + +const ( + DICS_FLAG_GLOBAL DICS_FLAG = 0x00000001 // make change in all hardware profiles + DICS_FLAG_CONFIGSPECIFIC DICS_FLAG = 0x00000002 // make change in specified profile only + DICS_FLAG_CONFIGGENERAL DICS_FLAG = 0x00000004 // 1 or more hardware profile-specific changes to follow +) + +// DI_REMOVEDEVICE specifies the scope of the device removal +type DI_REMOVEDEVICE uint32 + +const ( + DI_REMOVEDEVICE_GLOBAL DI_REMOVEDEVICE = 0x00000001 // Make this change in all hardware profiles. Remove information about the device from the registry. + DI_REMOVEDEVICE_CONFIGSPECIFIC DI_REMOVEDEVICE = 0x00000002 // Make this change to only the hardware profile specified by HwProfile. this flag only applies to root-enumerated devices. When Windows removes the device from the last hardware profile in which it was configured, Windows performs a global removal. +) + +// RemoveDeviceParams is a structure corresponding to a DIF_REMOVE install function. +type RemoveDeviceParams struct { + ClassInstallHeader ClassInstallHeader + Scope DI_REMOVEDEVICE + HwProfile uint32 +} + +// DrvInfoData is driver information structure (member of a driver info list that may be associated with a particular device instance, or (globally) with a device information set) +type DrvInfoData struct { + size uint32 + DriverType uint32 + _ uintptr + description [LINE_LEN]uint16 + mfgName [LINE_LEN]uint16 + providerName [LINE_LEN]uint16 + DriverDate windows.Filetime + DriverVersion uint64 +} + +func (data *DrvInfoData) GetDescription() string { + return windows.UTF16ToString(data.description[:]) +} + +func (data *DrvInfoData) SetDescription(description string) error { + str, err := syscall.UTF16FromString(description) + if err != nil { + return err + } + copy(data.description[:], str) + return nil +} + +func (data *DrvInfoData) GetMfgName() string { + return windows.UTF16ToString(data.mfgName[:]) +} + +func (data *DrvInfoData) SetMfgName(mfgName string) error { + str, err := syscall.UTF16FromString(mfgName) + if err != nil { + return err + } + copy(data.mfgName[:], str) + return nil +} + +func (data *DrvInfoData) GetProviderName() string { + return windows.UTF16ToString(data.providerName[:]) +} + +func (data *DrvInfoData) SetProviderName(providerName string) error { + str, err := syscall.UTF16FromString(providerName) + if err != nil { + return err + } + copy(data.providerName[:], str) + return nil +} + +// IsNewer method returns true if DrvInfoData date and version is newer than supplied parameters. +func (data *DrvInfoData) IsNewer(driverDate windows.Filetime, driverVersion uint64) bool { + if data.DriverDate.HighDateTime > driverDate.HighDateTime { + return true + } + if data.DriverDate.HighDateTime < driverDate.HighDateTime { + return false + } + + if data.DriverDate.LowDateTime > driverDate.LowDateTime { + return true + } + if data.DriverDate.LowDateTime < driverDate.LowDateTime { + return false + } + + if data.DriverVersion > driverVersion { + return true + } + if data.DriverVersion < driverVersion { + return false + } + + return false +} + +// DrvInfoDetailData is driver information details structure (provides detailed information about a particular driver information structure) +type DrvInfoDetailData struct { + size uint32 // On input, this must be exactly the sizeof(DrvInfoDetailData). On output, we set this member to the actual size of structure data. + InfDate windows.Filetime + compatIDsOffset uint32 + compatIDsLength uint32 + _ uintptr + sectionName [LINE_LEN]uint16 + infFileName [windows.MAX_PATH]uint16 + drvDescription [LINE_LEN]uint16 + hardwareID [1]uint16 +} + +func (data *DrvInfoDetailData) GetSectionName() string { + return windows.UTF16ToString(data.sectionName[:]) +} + +func (data *DrvInfoDetailData) GetInfFileName() string { + return windows.UTF16ToString(data.infFileName[:]) +} + +func (data *DrvInfoDetailData) GetDrvDescription() string { + return windows.UTF16ToString(data.drvDescription[:]) +} + +func (data *DrvInfoDetailData) GetHardwareID() string { + if data.compatIDsOffset > 1 { + bufW := data.getBuf() + return windows.UTF16ToString(bufW[:wcslen(bufW)]) + } + + return "" +} + +func (data *DrvInfoDetailData) GetCompatIDs() []string { + a := make([]string, 0) + + if data.compatIDsLength > 0 { + bufW := data.getBuf() + bufW = bufW[data.compatIDsOffset : data.compatIDsOffset+data.compatIDsLength] + for i := 0; i < len(bufW); { + j := i + wcslen(bufW[i:]) + if i < j { + a = append(a, windows.UTF16ToString(bufW[i:j])) + } + i = j + 1 + } + } + + return a +} + +func (data *DrvInfoDetailData) getBuf() []uint16 { + length := (data.size - uint32(unsafe.Offsetof(data.hardwareID))) / 2 + sl := struct { + addr *uint16 + len int + cap int + }{&data.hardwareID[0], int(length), int(length)} + return *(*[]uint16)(unsafe.Pointer(&sl)) +} + +// IsCompatible method tests if given hardware ID matches the driver or is listed on the compatible ID list. +func (data *DrvInfoDetailData) IsCompatible(hwid string) bool { + hwidLC := strings.ToLower(hwid) + if strings.EqualFold(data.GetHardwareID(), hwidLC) { + return true + } + a := data.GetCompatIDs() + for i := range a { + if strings.EqualFold(a[i], hwidLC) { + return true + } + } + + return false +} + +// DICD flags control SetupDiCreateDeviceInfo +type DICD uint32 + +const ( + DICD_GENERATE_ID DICD = 0x00000001 + DICD_INHERIT_CLASSDRVS DICD = 0x00000002 +) + +// +// SPDIT flags to distinguish between class drivers and +// device drivers. +// (Passed in 'DriverType' parameter of driver information list APIs) +// +type SPDIT uint32 + +const ( + SPDIT_NODRIVER SPDIT = 0x00000000 + SPDIT_CLASSDRIVER SPDIT = 0x00000001 + SPDIT_COMPATDRIVER SPDIT = 0x00000002 +) + +// DIGCF flags control what is included in the device information set built by SetupDiGetClassDevs +type DIGCF uint32 + +const ( + DIGCF_DEFAULT DIGCF = 0x00000001 // only valid with DIGCF_DEVICEINTERFACE + DIGCF_PRESENT DIGCF = 0x00000002 + DIGCF_ALLCLASSES DIGCF = 0x00000004 + DIGCF_PROFILE DIGCF = 0x00000008 + DIGCF_DEVICEINTERFACE DIGCF = 0x00000010 +) + +// DIREG specifies values for SetupDiCreateDevRegKey, SetupDiOpenDevRegKey, and SetupDiDeleteDevRegKey. +type DIREG uint32 + +const ( + DIREG_DEV DIREG = 0x00000001 // Open/Create/Delete device key + DIREG_DRV DIREG = 0x00000002 // Open/Create/Delete driver key + DIREG_BOTH DIREG = 0x00000004 // Delete both driver and Device key +) + +// +// SPDRP specifies device registry property codes +// (Codes marked as read-only (R) may only be used for +// SetupDiGetDeviceRegistryProperty) +// +// These values should cover the same set of registry properties +// as defined by the CM_DRP codes in cfgmgr32.h. +// +// Note that SPDRP codes are zero based while CM_DRP codes are one based! +// +type SPDRP uint32 + +const ( + SPDRP_DEVICEDESC SPDRP = 0x00000000 // DeviceDesc (R/W) + SPDRP_HARDWAREID SPDRP = 0x00000001 // HardwareID (R/W) + SPDRP_COMPATIBLEIDS SPDRP = 0x00000002 // CompatibleIDs (R/W) + SPDRP_SERVICE SPDRP = 0x00000004 // Service (R/W) + SPDRP_CLASS SPDRP = 0x00000007 // Class (R--tied to ClassGUID) + SPDRP_CLASSGUID SPDRP = 0x00000008 // ClassGUID (R/W) + SPDRP_DRIVER SPDRP = 0x00000009 // Driver (R/W) + SPDRP_CONFIGFLAGS SPDRP = 0x0000000A // ConfigFlags (R/W) + SPDRP_MFG SPDRP = 0x0000000B // Mfg (R/W) + SPDRP_FRIENDLYNAME SPDRP = 0x0000000C // FriendlyName (R/W) + SPDRP_LOCATION_INFORMATION SPDRP = 0x0000000D // LocationInformation (R/W) + SPDRP_PHYSICAL_DEVICE_OBJECT_NAME SPDRP = 0x0000000E // PhysicalDeviceObjectName (R) + SPDRP_CAPABILITIES SPDRP = 0x0000000F // Capabilities (R) + SPDRP_UI_NUMBER SPDRP = 0x00000010 // UiNumber (R) + SPDRP_UPPERFILTERS SPDRP = 0x00000011 // UpperFilters (R/W) + SPDRP_LOWERFILTERS SPDRP = 0x00000012 // LowerFilters (R/W) + SPDRP_BUSTYPEGUID SPDRP = 0x00000013 // BusTypeGUID (R) + SPDRP_LEGACYBUSTYPE SPDRP = 0x00000014 // LegacyBusType (R) + SPDRP_BUSNUMBER SPDRP = 0x00000015 // BusNumber (R) + SPDRP_ENUMERATOR_NAME SPDRP = 0x00000016 // Enumerator Name (R) + SPDRP_SECURITY SPDRP = 0x00000017 // Security (R/W, binary form) + SPDRP_SECURITY_SDS SPDRP = 0x00000018 // Security (W, SDS form) + SPDRP_DEVTYPE SPDRP = 0x00000019 // Device Type (R/W) + SPDRP_EXCLUSIVE SPDRP = 0x0000001A // Device is exclusive-access (R/W) + SPDRP_CHARACTERISTICS SPDRP = 0x0000001B // Device Characteristics (R/W) + SPDRP_ADDRESS SPDRP = 0x0000001C // Device Address (R) + SPDRP_UI_NUMBER_DESC_FORMAT SPDRP = 0x0000001D // UiNumberDescFormat (R/W) + SPDRP_DEVICE_POWER_DATA SPDRP = 0x0000001E // Device Power Data (R) + SPDRP_REMOVAL_POLICY SPDRP = 0x0000001F // Removal Policy (R) + SPDRP_REMOVAL_POLICY_HW_DEFAULT SPDRP = 0x00000020 // Hardware Removal Policy (R) + SPDRP_REMOVAL_POLICY_OVERRIDE SPDRP = 0x00000021 // Removal Policy Override (RW) + SPDRP_INSTALL_STATE SPDRP = 0x00000022 // Device Install State (R) + SPDRP_LOCATION_PATHS SPDRP = 0x00000023 // Device Location Paths (R) + SPDRP_BASE_CONTAINERID SPDRP = 0x00000024 // Base ContainerID (R) + + SPDRP_MAXIMUM_PROPERTY SPDRP = 0x00000025 // Upper bound on ordinals +) + +type DEVPROPKEY struct { + fmtid windows.GUID + pid uint32 +} + +var DEVPKEY_PciRootBus_PCIExpressNativePMEControl = DEVPROPKEY{ + // windows.GUID{0x3ab22e31, 0x8264, 0x4b4e, [8]byte{0x9a, 0xf5, 0xa8, 0xd2, 0xd8, 0xe3, 0x3e, 0x62}}, + windows.GUID{Data1: 0x3ab22e31, Data2: 0x8264, Data3: 0x4b4e, Data4: [8]byte{0x9a, 0xf5, 0xa8, 0xd2, 0xd8, 0xe3, 0x3e, 0x62}}, + 14, +} + +var DEVPKEY_PciRootBus_MaxMSILimit = DEVPROPKEY{ + // windows.GUID{0x3ab22e31, 0x8264, 0x4b4e, [8]byte{0x9a, 0xf5, 0xa8, 0xd2, 0xd8, 0xe3, 0x3e, 0x62}}, + windows.GUID{Data1: 0x3ab22e31, Data2: 0x8264, Data3: 0x4b4e, Data4: [8]byte{0x9a, 0xf5, 0xa8, 0xd2, 0xd8, 0xe3, 0x3e, 0x62}}, + 15, +} diff --git a/USBController/zsetupapi_windows.go b/USBController/zsetupapi_windows.go new file mode 100644 index 0000000..96a1b9c --- /dev/null +++ b/USBController/zsetupapi_windows.go @@ -0,0 +1,432 @@ +// Code generated by 'go generate'; DO NOT EDIT. + +package USBController + +import ( + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +var _ unsafe.Pointer + +// Do the interface allocations only once for common +// Errno values. +const ( + errnoERROR_IO_PENDING = 997 +) + +var ( + errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING) +) + +// errnoErr returns common boxed Errno values, to prevent +// allocations at runtime. +func errnoErr(e syscall.Errno) error { + switch e { + case 0: + return nil + case errnoERROR_IO_PENDING: + return errERROR_IO_PENDING + } + // add more here, after collecting data on the common + // error values see on Windows. (perhaps when running + // all.bat?) + return e +} + +var ( + // Library + modsetupapi = windows.NewLazySystemDLL("setupapi.dll") + + // Functions + procSetupDiCreateDeviceInfoListExW = modsetupapi.NewProc("SetupDiCreateDeviceInfoListExW") + procSetupDiGetDeviceInfoListDetailW = modsetupapi.NewProc("SetupDiGetDeviceInfoListDetailW") + procSetupDiCreateDeviceInfoW = modsetupapi.NewProc("SetupDiCreateDeviceInfoW") + procSetupDiEnumDeviceInfo = modsetupapi.NewProc("SetupDiEnumDeviceInfo") + procSetupDiDestroyDeviceInfoList = modsetupapi.NewProc("SetupDiDestroyDeviceInfoList") + procSetupDiBuildDriverInfoList = modsetupapi.NewProc("SetupDiBuildDriverInfoList") + procSetupDiCancelDriverInfoSearch = modsetupapi.NewProc("SetupDiCancelDriverInfoSearch") + procSetupDiEnumDriverInfoW = modsetupapi.NewProc("SetupDiEnumDriverInfoW") + procSetupDiGetSelectedDriverW = modsetupapi.NewProc("SetupDiGetSelectedDriverW") + procSetupDiSetSelectedDriverW = modsetupapi.NewProc("SetupDiSetSelectedDriverW") + procSetupDiGetDriverInfoDetailW = modsetupapi.NewProc("SetupDiGetDriverInfoDetailW") + procSetupDiDestroyDriverInfoList = modsetupapi.NewProc("SetupDiDestroyDriverInfoList") + procSetupDiGetClassDevsExW = modsetupapi.NewProc("SetupDiGetClassDevsExW") + procSetupDiCallClassInstaller = modsetupapi.NewProc("SetupDiCallClassInstaller") + procSetupDiOpenDevRegKey = modsetupapi.NewProc("SetupDiOpenDevRegKey") + procSetupDiGetDeviceRegistryPropertyW = modsetupapi.NewProc("SetupDiGetDeviceRegistryPropertyW") + procSetupDiSetDeviceRegistryPropertyW = modsetupapi.NewProc("SetupDiSetDeviceRegistryPropertyW") + procSetupDiGetDeviceInstallParamsW = modsetupapi.NewProc("SetupDiGetDeviceInstallParamsW") + procSetupDiGetClassInstallParamsW = modsetupapi.NewProc("SetupDiGetClassInstallParamsW") + procSetupDiSetDeviceInstallParamsW = modsetupapi.NewProc("SetupDiSetDeviceInstallParamsW") + procSetupDiSetClassInstallParamsW = modsetupapi.NewProc("SetupDiSetClassInstallParamsW") + procSetupDiClassNameFromGuidExW = modsetupapi.NewProc("SetupDiClassNameFromGuidExW") + procSetupDiClassGuidsFromNameExW = modsetupapi.NewProc("SetupDiClassGuidsFromNameExW") + procSetupDiGetSelectedDevice = modsetupapi.NewProc("SetupDiGetSelectedDevice") + procSetupDiSetSelectedDevice = modsetupapi.NewProc("SetupDiSetSelectedDevice") + procSetupDiGetDevicePropertyW = modsetupapi.NewProc("SetupDiGetDevicePropertyW") + procSetupDiRestartDevices = modsetupapi.NewProc("SetupDiRestartDevices") + + modCfgMgr32 = windows.NewLazySystemDLL("CfgMgr32.dll") + procCM_Get_DevNode_Status = modCfgMgr32.NewProc("CM_Get_DevNode_Status") +) + +func setupDiCreateDeviceInfoListEx(classGUID *windows.GUID, hwndParent uintptr, machineName *uint16, reserved uintptr) (handle DevInfo, err error) { + r0, _, e1 := syscall.Syscall6(procSetupDiCreateDeviceInfoListExW.Addr(), 4, uintptr(unsafe.Pointer(classGUID)), uintptr(hwndParent), uintptr(unsafe.Pointer(machineName)), uintptr(reserved), 0, 0) + handle = DevInfo(r0) + if handle == DevInfo(windows.InvalidHandle) { + if e1 != 0 { + err = errnoErr(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +func setupDiGetDeviceInfoListDetail(deviceInfoSet DevInfo, deviceInfoSetDetailData *DevInfoListDetailData) (err error) { + r1, _, e1 := syscall.Syscall(procSetupDiGetDeviceInfoListDetailW.Addr(), 2, uintptr(deviceInfoSet), uintptr(unsafe.Pointer(deviceInfoSetDetailData)), 0) + if r1 == 0 { + if e1 != 0 { + err = errnoErr(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +func setupDiCreateDeviceInfo(deviceInfoSet DevInfo, DeviceName *uint16, classGUID *windows.GUID, DeviceDescription *uint16, hwndParent uintptr, CreationFlags DICD, deviceInfoData *DevInfoData) (err error) { + r1, _, e1 := syscall.Syscall9(procSetupDiCreateDeviceInfoW.Addr(), 7, uintptr(deviceInfoSet), uintptr(unsafe.Pointer(DeviceName)), uintptr(unsafe.Pointer(classGUID)), uintptr(unsafe.Pointer(DeviceDescription)), uintptr(hwndParent), uintptr(CreationFlags), uintptr(unsafe.Pointer(deviceInfoData)), 0, 0) + if r1 == 0 { + if e1 != 0 { + err = errnoErr(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +func setupDiEnumDeviceInfo(deviceInfoSet DevInfo, memberIndex uint32, deviceInfoData *DevInfoData) (err error) { + r1, _, e1 := syscall.Syscall(procSetupDiEnumDeviceInfo.Addr(), 3, uintptr(deviceInfoSet), uintptr(memberIndex), uintptr(unsafe.Pointer(deviceInfoData))) + if r1 == 0 { + if e1 != 0 { + err = errnoErr(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +func SetupDiDestroyDeviceInfoList(deviceInfoSet DevInfo) (err error) { + r1, _, e1 := syscall.Syscall(procSetupDiDestroyDeviceInfoList.Addr(), 1, uintptr(deviceInfoSet), 0, 0) + if r1 == 0 { + if e1 != 0 { + err = errnoErr(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +func SetupDiBuildDriverInfoList(deviceInfoSet DevInfo, deviceInfoData *DevInfoData, driverType SPDIT) (err error) { + r1, _, e1 := syscall.Syscall(procSetupDiBuildDriverInfoList.Addr(), 3, uintptr(deviceInfoSet), uintptr(unsafe.Pointer(deviceInfoData)), uintptr(driverType)) + if r1 == 0 { + if e1 != 0 { + err = errnoErr(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +func SetupDiCancelDriverInfoSearch(deviceInfoSet DevInfo) (err error) { + r1, _, e1 := syscall.Syscall(procSetupDiCancelDriverInfoSearch.Addr(), 1, uintptr(deviceInfoSet), 0, 0) + if r1 == 0 { + if e1 != 0 { + err = errnoErr(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +func setupDiEnumDriverInfo(deviceInfoSet DevInfo, deviceInfoData *DevInfoData, driverType SPDIT, memberIndex uint32, driverInfoData *DrvInfoData) (err error) { + r1, _, e1 := syscall.Syscall6(procSetupDiEnumDriverInfoW.Addr(), 5, uintptr(deviceInfoSet), uintptr(unsafe.Pointer(deviceInfoData)), uintptr(driverType), uintptr(memberIndex), uintptr(unsafe.Pointer(driverInfoData)), 0) + if r1 == 0 { + if e1 != 0 { + err = errnoErr(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +func setupDiGetSelectedDriver(deviceInfoSet DevInfo, deviceInfoData *DevInfoData, driverInfoData *DrvInfoData) (err error) { + r1, _, e1 := syscall.Syscall(procSetupDiGetSelectedDriverW.Addr(), 3, uintptr(deviceInfoSet), uintptr(unsafe.Pointer(deviceInfoData)), uintptr(unsafe.Pointer(driverInfoData))) + if r1 == 0 { + if e1 != 0 { + err = errnoErr(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +func SetupDiSetSelectedDriver(deviceInfoSet DevInfo, deviceInfoData *DevInfoData, driverInfoData *DrvInfoData) (err error) { + r1, _, e1 := syscall.Syscall(procSetupDiSetSelectedDriverW.Addr(), 3, uintptr(deviceInfoSet), uintptr(unsafe.Pointer(deviceInfoData)), uintptr(unsafe.Pointer(driverInfoData))) + if r1 == 0 { + if e1 != 0 { + err = errnoErr(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +func setupDiGetDriverInfoDetail(deviceInfoSet DevInfo, deviceInfoData *DevInfoData, driverInfoData *DrvInfoData, driverInfoDetailData *DrvInfoDetailData, driverInfoDetailDataSize uint32, requiredSize *uint32) (err error) { + r1, _, e1 := syscall.Syscall6(procSetupDiGetDriverInfoDetailW.Addr(), 6, uintptr(deviceInfoSet), uintptr(unsafe.Pointer(deviceInfoData)), uintptr(unsafe.Pointer(driverInfoData)), uintptr(unsafe.Pointer(driverInfoDetailData)), uintptr(driverInfoDetailDataSize), uintptr(unsafe.Pointer(requiredSize))) + if r1 == 0 { + if e1 != 0 { + err = errnoErr(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +func SetupDiDestroyDriverInfoList(deviceInfoSet DevInfo, deviceInfoData *DevInfoData, driverType SPDIT) (err error) { + r1, _, e1 := syscall.Syscall(procSetupDiDestroyDriverInfoList.Addr(), 3, uintptr(deviceInfoSet), uintptr(unsafe.Pointer(deviceInfoData)), uintptr(driverType)) + if r1 == 0 { + if e1 != 0 { + err = errnoErr(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +func setupDiGetClassDevsEx(classGUID *windows.GUID, Enumerator *uint16, hwndParent uintptr, Flags DIGCF, deviceInfoSet DevInfo, machineName *uint16, reserved uintptr) (handle DevInfo, err error) { + r0, _, e1 := syscall.Syscall9(procSetupDiGetClassDevsExW.Addr(), 7, uintptr(unsafe.Pointer(classGUID)), uintptr(unsafe.Pointer(Enumerator)), uintptr(hwndParent), uintptr(Flags), uintptr(deviceInfoSet), uintptr(unsafe.Pointer(machineName)), uintptr(reserved), 0, 0) + handle = DevInfo(r0) + if handle == DevInfo(windows.InvalidHandle) { + if e1 != 0 { + err = errnoErr(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +func SetupDiCallClassInstaller(installFunction DI_FUNCTION, deviceInfoSet DevInfo, deviceInfoData *DevInfoData) (err error) { + r1, _, e1 := syscall.Syscall( + procSetupDiCallClassInstaller.Addr(), + 3, + uintptr(installFunction), + uintptr(deviceInfoSet), + uintptr(unsafe.Pointer(deviceInfoData)), + ) + if r1 == 0 { + if e1 != 0 { + err = errnoErr(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +func setupDiOpenDevRegKey(deviceInfoSet DevInfo, deviceInfoData *DevInfoData, Scope DICS_FLAG, HwProfile uint32, KeyType DIREG, samDesired uint32) (key windows.Handle, err error) { + r0, _, e1 := syscall.Syscall6(procSetupDiOpenDevRegKey.Addr(), 6, uintptr(deviceInfoSet), uintptr(unsafe.Pointer(deviceInfoData)), uintptr(Scope), uintptr(HwProfile), uintptr(KeyType), uintptr(samDesired)) + key = windows.Handle(r0) + if key == windows.InvalidHandle { + if e1 != 0 { + err = errnoErr(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +func setupDiGetDeviceRegistryProperty(deviceInfoSet DevInfo, deviceInfoData *DevInfoData, property SPDRP, propertyRegDataType *uint32, propertyBuffer *byte, propertyBufferSize uint32, requiredSize *uint32) (err error) { + r1, _, e1 := syscall.Syscall9(procSetupDiGetDeviceRegistryPropertyW.Addr(), 7, uintptr(deviceInfoSet), uintptr(unsafe.Pointer(deviceInfoData)), uintptr(property), uintptr(unsafe.Pointer(propertyRegDataType)), uintptr(unsafe.Pointer(propertyBuffer)), uintptr(propertyBufferSize), uintptr(unsafe.Pointer(requiredSize)), 0, 0) + if r1 == 0 { + if e1 != 0 { + err = errnoErr(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +func setupDiSetDeviceRegistryProperty(deviceInfoSet DevInfo, deviceInfoData *DevInfoData, property SPDRP, propertyBuffer *byte, propertyBufferSize uint32) (err error) { + r1, _, e1 := syscall.Syscall6(procSetupDiSetDeviceRegistryPropertyW.Addr(), 5, uintptr(deviceInfoSet), uintptr(unsafe.Pointer(deviceInfoData)), uintptr(property), uintptr(unsafe.Pointer(propertyBuffer)), uintptr(propertyBufferSize), 0) + if r1 == 0 { + if e1 != 0 { + err = errnoErr(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +func setupDiGetDeviceInstallParams(deviceInfoSet DevInfo, deviceInfoData *DevInfoData, deviceInstallParams *DevInstallParams) (err error) { + r1, _, e1 := syscall.Syscall(procSetupDiGetDeviceInstallParamsW.Addr(), 3, uintptr(deviceInfoSet), uintptr(unsafe.Pointer(deviceInfoData)), uintptr(unsafe.Pointer(deviceInstallParams))) + if r1 == 0 { + if e1 != 0 { + err = errnoErr(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +func SetupDiGetClassInstallParams(deviceInfoSet DevInfo, deviceInfoData *DevInfoData, classInstallParams *ClassInstallHeader, classInstallParamsSize uint32, requiredSize *uint32) (err error) { + r1, _, e1 := syscall.Syscall6(procSetupDiGetClassInstallParamsW.Addr(), 5, uintptr(deviceInfoSet), uintptr(unsafe.Pointer(deviceInfoData)), uintptr(unsafe.Pointer(classInstallParams)), uintptr(classInstallParamsSize), uintptr(unsafe.Pointer(requiredSize)), 0) + if r1 == 0 { + if e1 != 0 { + err = errnoErr(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +func SetupDiSetDeviceInstallParams(deviceInfoSet DevInfo, deviceInfoData *DevInfoData, deviceInstallParams *DevInstallParams) (err error) { + r1, _, e1 := syscall.Syscall(procSetupDiSetDeviceInstallParamsW.Addr(), 3, uintptr(deviceInfoSet), uintptr(unsafe.Pointer(deviceInfoData)), uintptr(unsafe.Pointer(deviceInstallParams))) + if r1 == 0 { + if e1 != 0 { + err = errnoErr(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +// WINSETUPAPI BOOL SetupDiRestartDevices( +// [in] HDEVINFO DeviceInfoSet, +// [in, out] PSP_DEVINFO_DATA DeviceInfoData +// ); +func SetupDiRestartDevices(deviceInfoSet DevInfo, deviceInfoData *DevInfoData) (err error) { + r1, _, e1 := syscall.Syscall( + procSetupDiRestartDevices.Addr(), 2, + uintptr(deviceInfoSet), + uintptr(unsafe.Pointer(deviceInfoData)), + 0, + ) + if r1 == 0 { + if e1 != 0 { + err = errnoErr(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +func SetupDiSetClassInstallParams(deviceInfoSet DevInfo, deviceInfoData *DevInfoData, classInstallParams *ClassInstallHeader, classInstallParamsSize uint32) (err error) { + r1, _, e1 := syscall.Syscall6(procSetupDiSetClassInstallParamsW.Addr(), + 4, + uintptr(deviceInfoSet), + uintptr(unsafe.Pointer(deviceInfoData)), + uintptr(unsafe.Pointer(classInstallParams)), + uintptr(classInstallParamsSize), + 0, + 0, + ) + if r1 == 0 { + if e1 != 0 { + err = errnoErr(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +func setupDiClassNameFromGuidEx(classGUID *windows.GUID, className *uint16, classNameSize uint32, requiredSize *uint32, machineName *uint16, reserved uintptr) (err error) { + r1, _, e1 := syscall.Syscall6(procSetupDiClassNameFromGuidExW.Addr(), 6, uintptr(unsafe.Pointer(classGUID)), uintptr(unsafe.Pointer(className)), uintptr(classNameSize), uintptr(unsafe.Pointer(requiredSize)), uintptr(unsafe.Pointer(machineName)), uintptr(reserved)) + if r1 == 0 { + if e1 != 0 { + err = errnoErr(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +func setupDiClassGuidsFromNameEx(className *uint16, classGuidList *windows.GUID, classGuidListSize uint32, requiredSize *uint32, machineName *uint16, reserved uintptr) (err error) { + r1, _, e1 := syscall.Syscall6(procSetupDiClassGuidsFromNameExW.Addr(), 6, uintptr(unsafe.Pointer(className)), uintptr(unsafe.Pointer(classGuidList)), uintptr(classGuidListSize), uintptr(unsafe.Pointer(requiredSize)), uintptr(unsafe.Pointer(machineName)), uintptr(reserved)) + if r1 == 0 { + if e1 != 0 { + err = errnoErr(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +func setupDiGetSelectedDevice(deviceInfoSet DevInfo, deviceInfoData *DevInfoData) (err error) { + r1, _, e1 := syscall.Syscall(procSetupDiGetSelectedDevice.Addr(), 2, uintptr(deviceInfoSet), uintptr(unsafe.Pointer(deviceInfoData)), 0) + if r1 == 0 { + if e1 != 0 { + err = errnoErr(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +func SetupDiSetSelectedDevice(deviceInfoSet DevInfo, deviceInfoData *DevInfoData) (err error) { + r1, _, e1 := syscall.Syscall(procSetupDiSetSelectedDevice.Addr(), 2, uintptr(deviceInfoSet), uintptr(unsafe.Pointer(deviceInfoData)), 0) + if r1 == 0 { + if e1 != 0 { + err = errnoErr(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +// https://github.com/tajtiattila/hid/blob/2bd63ffd4c8c0b81e5999c7b183cc4325f773527/platform/zsys_windows.go +func SetupDiGetDeviceProperty(devInfoSet DevInfo, devInfoData *DevInfoData, propKey *DEVPROPKEY, propType *uint32, propBuf *byte, propBufSize uint32, reqsize *uint32, flags uint32) (err error) { + r1, _, e1 := syscall.Syscall9(procSetupDiGetDevicePropertyW.Addr(), 8, uintptr(devInfoSet), uintptr(unsafe.Pointer(devInfoData)), uintptr(unsafe.Pointer(propKey)), uintptr(unsafe.Pointer(propType)), uintptr(unsafe.Pointer(propBuf)), uintptr(propBufSize), uintptr(unsafe.Pointer(reqsize)), uintptr(flags), 0) + if r1 == 0 { + if e1 != 0 { + err = error(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +func cm_Get_DevNode_Status(status *uint32, problemNumber *uint32, devInst uint32, flags uint32) uint32 { + r0, r1, er := syscall.Syscall6(procCM_Get_DevNode_Status.Addr(), 4, uintptr(unsafe.Pointer(status)), uintptr(unsafe.Pointer(problemNumber)), uintptr(devInst), uintptr(flags), 0, 0) + println(r1) + println(er) + return uint32(r0) +} \ No newline at end of file diff --git a/build.bat b/build.bat new file mode 100644 index 0000000..db652fa --- /dev/null +++ b/build.bat @@ -0,0 +1,9 @@ +cls +@REM broken in 1.18.0 +@REM gocritic check -enableAll -disable="#experimental,#opinionated,#commentedOutCode" ./... +@REM gocritic check -enableAll ./... + +@REM go build +::go build -ldflags="-s -w -H windowsgui" +go build -ldflags="-s -w" +pause \ No newline at end of file diff --git a/cliFlags.go b/cliFlags.go new file mode 100644 index 0000000..a86ca05 --- /dev/null +++ b/cliFlags.go @@ -0,0 +1,78 @@ +package main + +import ( + "flag" + "fmt" + "log" + "time" +) + +var cliMode bool + +type Flags struct { + Name string + LogFolder string + Port int + Delay time.Duration + Count int + RTSS bool + RTSSOSDX int + RTSSOSDY int + RTSSOSDWidth int + RTSSOSDHeight int + D3D9 bool + OGL bool + Fullscreen bool + Width int + Height int + Print bool +} + +var f Flags + +// go build; .\GoSysLat.exe -d3d9 -port 4 -count 10000 +// go build; .\GoSysLat.exe -d3d9 -fullscreen -port 4 -time 30s -name Test +func init() { + flag.StringVar(&f.Name, "name", "", "will be used later for the name") + flag.StringVar(&f.LogFolder, "logs", "", "Log folder") + flag.IntVar(&f.Port, "port", 0, "") + flag.IntVar(&f.Count, "count", -1, "after which amount should be canceled") + timePtr := flag.String("time", "", "time span: 5h30m40s") + flag.BoolVar(&f.RTSS, "rtss", false, "RTSS enable support") + flag.IntVar(&f.RTSSOSDX, "rtssosdx", 0, "RTSSOSDX") + flag.IntVar(&f.RTSSOSDY, "rtssosdy", 0, "RTSSOSDY") + flag.IntVar(&f.RTSSOSDWidth, "rtssosdwidth", 0, "RTSSOSDWidth") + flag.IntVar(&f.RTSSOSDHeight, "rtssosdheight", 0, "RTSSOSDHeight") + flag.BoolVar(&f.D3D9, "d3d9", false, "D3D9 enable support") + flag.BoolVar(&f.OGL, "ogl", false, "OpenGL enable support") + flag.BoolVar(&f.Fullscreen, "fullscreen", false, "activate fullscreen") + flag.IntVar(&f.Width, "width", 0, "the width of the D3D9 application") + flag.IntVar(&f.Height, "height", 0, "the height of the D3D9 application") + flag.BoolVar(&f.Print, "print", false, "show the values in the console") + flag.Parse() + + if f.Name != "" || + f.Port != 0 || + f.Count != -1 || + *timePtr != "" || + f.RTSS || f.D3D9 || f.OGL { + // f.D3D9Fullscreen, f.LogFolder, f.Width & f.Height is optional + + if !f.RTSS && !f.D3D9 && !f.OGL { + panic("use -rtss, -d3d9 or -ogl") + } + if f.Count == -1 && *timePtr == "" { + fmt.Println("with -count or -time you can define an end so that a log is created") + } + + var err error + f.Delay, err = time.ParseDuration(*timePtr) // Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". + if err != nil { + log.Println(err) + } + + // log.Println(prettyPrint(f)) + + cliMode = true + } +} diff --git a/config.go b/config.go new file mode 100644 index 0000000..ab3eeee --- /dev/null +++ b/config.go @@ -0,0 +1,122 @@ +package main + +import ( + "encoding/json" + "log" + "os" + "path/filepath" + "strconv" + "syscall" +) + +type Config struct { + ComPort int `json:"ComPort"` + OSDX int `json:"OSD_x"` + OSDY int `json:"OSD_y"` + OSDHeight int `json:"OSD_height"` + OSDWidth int `json:"OSD_width"` + PushMode bool `json:"Push_Mode"` + Display bool `json:"Display"` + DetectLight bool `json:"Detect_Light"` +} + +var ( + executable string + c Config +) + +func init() { + ex, err := os.Executable() + if err != nil { + panic(err) + } + executable = filepath.Base(ex) + c.Load() +} + +func (c *Config) read() []byte { + stream, _ := syscall.UTF16PtrFromString(executable + ":Stream") + hStream, err := syscall.CreateFile(stream, syscall.GENERIC_READ, syscall.FILE_SHARE_READ, nil, syscall.OPEN_EXISTING, 0, 0) + if err != nil { // config not found + return []byte{} + } + defer syscall.CloseHandle(hStream) + + var done uint32 = 0 + buf := make([]byte, 128) + err = syscall.ReadFile(hStream, buf, &done, nil) + if err != nil { + log.Println(err) + } + return buf[:done] +} + +func (c *Config) write(data []byte) { + stream, _ := syscall.UTF16PtrFromString(executable + ":Stream") + syscall.DeleteFile(stream) + hStream, err := syscall.CreateFile(stream, syscall.GENERIC_WRITE, syscall.FILE_SHARE_WRITE, nil, syscall.OPEN_ALWAYS, 0, 0) + if err != nil { + log.Println(err) + } + defer syscall.CloseHandle(hStream) + + var done uint32 + err = syscall.WriteFile(hStream, data, &done, nil) + if err != nil { + log.Println(err) + } +} + +func (c *Config) Load() { + // more < GoSysLat.exe:Stream:$DATA + data := c.read() + if len(data) != 0 { + err := json.Unmarshal(data, c) + if err != nil { + log.Println(err) + } + } else { + c.Reset() + } + + if cliMode { + log.Printf("f %#v\n", f) + if f.RTSSOSDX != 0 { + c.OSDX = f.RTSSOSDX + } + if f.RTSSOSDY != 0 { + c.OSDY = f.RTSSOSDY + } + if f.RTSSOSDHeight != 0 { + c.OSDHeight = f.RTSSOSDHeight + } + if f.RTSSOSDWidth != 0 { + c.OSDWidth = f.RTSSOSDWidth + } + log.Printf("c %#v\n", c) + } +} + +func (c *Config) Save() { + data, err := json.Marshal(c) + if err != nil { + log.Println(err) + } + c.write(data) +} + +func (c *Config) Reset() { + *c = Config{ + // default settings + ComPort: 1, + Display: true, + DetectLight: true, + OSDHeight: 10, + OSDWidth: 10, + } +} + +func (c *Config) RefreshOSDStrings() { + RTSSOSDBlack = "" + RTSSOSDWhite = "" +} diff --git a/example.png b/example.png new file mode 100644 index 0000000000000000000000000000000000000000..5f44db37ce0a4fc7955faad3d236de05dfbb6eaf GIT binary patch literal 34887 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 literal 0 HcmV?d00001 diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..0a01746 --- /dev/null +++ b/go.mod @@ -0,0 +1,33 @@ +module GoSysLat + +go 1.17 + +replace github.com/spddl/RTSSClient => ./RTSSClient + +replace github.com/spddl/USBController => ./USBController + +replace github.com/spddl/TargetWindow_D3D9 => ./TargetWindow_D3D9 + +replace github.com/spddl/TargetWindow_OpenGL => ./TargetWindow_OpenGL + +require ( + github.com/MakeNowJust/hotkey v0.0.0-20200628032113-41fa0caa507a + github.com/VividCortex/ewma v1.2.0 + github.com/eclesh/welford v0.0.0-20150116075914-eec62615b1f0 + github.com/go-echarts/go-echarts/v2 v2.2.4 + github.com/lxn/walk v0.0.0-20210112085537-c389da54e794 + github.com/spddl/RTSSClient v0.0.0-00010101000000-000000000000 + github.com/spddl/TargetWindow_D3D9 v0.0.0-00010101000000-000000000000 + github.com/spddl/TargetWindow_OpenGL v0.0.0-00010101000000-000000000000 + github.com/spddl/USBController v0.0.0-00010101000000-000000000000 + golang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a +) + +require ( + github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 // indirect + github.com/go-gl/glfw v0.0.0-20220320163800-277f93cfa958 // indirect + github.com/gonutz/d3d9 v1.2.1 // indirect + github.com/gonutz/w32/v2 v2.4.0 // indirect + github.com/lxn/win v0.0.0-20210218163916-a377121e959e // indirect + gopkg.in/Knetic/govaluate.v3 v3.0.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..8a2bdcd --- /dev/null +++ b/go.sum @@ -0,0 +1,42 @@ +github.com/MakeNowJust/hotkey v0.0.0-20200628032113-41fa0caa507a h1:VuNOuPqMVzE4/RJI7PGkXBkjF4cuCnSNKXZl9WNVA6g= +github.com/MakeNowJust/hotkey v0.0.0-20200628032113-41fa0caa507a/go.mod h1:0rsyU1Bd2nX15GGdZ/ieVlKRED0HSnusBaKyULAJUYQ= +github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= +github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= +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/eclesh/welford v0.0.0-20150116075914-eec62615b1f0 h1:kQ0PoUSzoqBCdOXbrrZtBGHZOs46U8Aj0BG1Xa+tq7w= +github.com/eclesh/welford v0.0.0-20150116075914-eec62615b1f0/go.mod h1:Lt9Nv8E2/1kmk2nD0nTZTae7qFRCXPJV6q7Debf1An8= +github.com/go-echarts/go-echarts/v2 v2.2.4 h1:SKJpdyNIyD65XjbUZjzg6SwccTNXEgmh+PlaO23g2H0= +github.com/go-echarts/go-echarts/v2 v2.2.4/go.mod h1:6TOomEztzGDVDkOSCFBq3ed7xOYfbOqhaBzD0YV771A= +github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 h1:zDw5v7qm4yH7N8C8uWd+8Ii9rROdgWxQuGoJ9WDXxfk= +github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw= +github.com/go-gl/glfw v0.0.0-20220320163800-277f93cfa958 h1:aQjQrLKagKRNl/GGy16WNwsiYAVs2Kfwvg5pq07O7Ns= +github.com/go-gl/glfw v0.0.0-20220320163800-277f93cfa958/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/gonutz/d3d9 v1.2.1 h1:xcQLMKrAf+/hvFtsyc9LuLJ+qs6TPtbzuKYONRERpYo= +github.com/gonutz/d3d9 v1.2.1/go.mod h1:q74g3QbR280b+qYauwEV0N9SVadszWPLZ4l/wHiD/AA= +github.com/gonutz/w32/v2 v2.4.0 h1:k+R8/ddsnb9dwVDsTDWvkJGFmYJDI3ZMgcKgdpLuMpw= +github.com/gonutz/w32/v2 v2.4.0/go.mod h1:MgtHx0AScDVNKyB+kjyPder4xIi3XAcHS6LDDU2DmdE= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lxn/walk v0.0.0-20210112085537-c389da54e794 h1:NVRJ0Uy0SOFcXSKLsS65OmI1sgCCfiDUPj+cwnH7GZw= +github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ= +github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc= +github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk= +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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.0 h1:jlIyCplCJFULU/01vCkhKuTyc3OorI3bJFuw6obfgho= +github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a h1:N2T1jUrTQE9Re6TFF5PhvEHXHCguynGhKjWVsIUt5cY= +golang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/Knetic/govaluate.v3 v3.0.0 h1:18mUyIt4ZlRlFZAAfVetz4/rzlJs9yhN+U02F4u1AOc= +gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/gui.go b/gui.go new file mode 100644 index 0000000..4fbf74b --- /dev/null +++ b/gui.go @@ -0,0 +1,337 @@ +package main + +import ( + "log" + "time" + + "github.com/lxn/walk" + "github.com/spddl/TargetWindow_D3D9" + "github.com/spddl/TargetWindow_OpenGL" + + //lint:ignore ST1001 standard behavior lxn/walk + . "github.com/lxn/walk/declarative" +) + +type GUI struct { + *walk.MainWindow + Data *Config + FileName *walk.LineEdit + Count *walk.Label + Countdown *walk.Label + ValueMin *walk.Label + ValueMax *walk.Label + Value *walk.Label + ValueDelta *walk.Label + SecValue *walk.Label + SecValueDelta *walk.Label + MinuteValue *walk.Label + MinuteValueDelta *walk.Label + EwmaValue *walk.Label + EwmaValueDelta *walk.Label +} + +var ( + greenColor = walk.RGB(0, 0xff, 0) + redColor = walk.RGB(0xff, 0, 0) + backgroundColor = walk.RGB(0xf0, 0xf0, 0xf0) +) + +var ( + targetD3D9 TargetWindow_D3D9.Target + targetOGL TargetWindow_OpenGL.Target +) + +func (g *GUI) Start() { + g.Data = &c + + var openSettings *walk.Action + + if err := (MainWindow{ + AssignTo: &g.MainWindow, + Title: "GoSysLat", + Icon: 2, + Size: Size{ + Width: 250, + Height: 1, + }, + DoubleBuffering: true, + MenuItems: []MenuItem{ + Action{ + AssignTo: &openSettings, + Text: "&Settings", + OnTriggered: func() { + RunSettingsDialog(g) + }, + }, + Menu{ + Text: "&Target Window", + Items: []MenuItem{ + Action{ + AssignTo: &openSettings, + Text: "Open in Window (D3D9)", + OnTriggered: func() { + targetD3D9 = TargetWindow_D3D9.Target{} + go func() { + targetD3D9.Start(currentFileName, false, f.Width, f.Height, c.PushMode) + targetD3D9.Close() + }() + }, + }, + Action{ + AssignTo: &openSettings, + Text: "Open in Fullscreen (D3D9)", + OnTriggered: func() { + targetD3D9 = TargetWindow_D3D9.Target{} + go func() { + targetD3D9.Start(currentFileName, true, f.Width, f.Height, c.PushMode) + targetD3D9.Close() + }() + }, + }, + Separator{}, + Action{ + AssignTo: &openSettings, + Text: "Open in Window (OGL)", + OnTriggered: func() { + targetOGL = TargetWindow_OpenGL.Target{} + go targetOGL.Start(false, f.Width, f.Height, c.PushMode) + }, + }, + Action{ + AssignTo: &openSettings, + Text: "Open in Fullscreen (OGL)", + OnTriggered: func() { + targetOGL = TargetWindow_OpenGL.Target{} + go targetOGL.Start(true, f.Width, f.Height, c.PushMode) + }, + }, + }, + }, + Menu{ + Text: "&Help", + Items: []MenuItem{ + Action{ + Text: "How to start?", + OnTriggered: g.howToStart_Triggered, + }, + Action{ + Text: "About", + OnTriggered: g.aboutAction_Triggered, + }, + }, + }, + }, + Layout: VBox{ + Alignment: AlignHNearVNear, + }, + Children: []Widget{ + Composite{ + Layout: HBox{}, + Children: []Widget{ + Label{ + Text: "Project name", + }, + LineEdit{ + AssignTo: &g.FileName, + Text: "", + }, + }, + }, + Composite{ + Layout: Grid{Columns: 3}, + Children: []Widget{ + + Label{ + Text: "Count:", + }, + Label{ + ColumnSpan: 2, + Text: "-", + AssignTo: &g.Count, + Font: Font{Family: "Segoe UI", PointSize: 20}, + }, + + Label{ + Text: "Current Value (in ms):", + }, + Label{ + Text: "-", + AssignTo: &g.Value, + Font: Font{Family: "Segoe UI", PointSize: 20}, + MinSize: Size{ + Width: 150, + }, + }, + Label{ + Text: "", + AssignTo: &g.ValueDelta, + TextColor: backgroundColor, + Background: SolidColorBrush{Color: backgroundColor}, + }, + + Label{ + Text: "Average seconds:", + }, + Label{ + Text: "-", + AssignTo: &g.SecValue, + Font: Font{PointSize: 20}, + }, + Label{ + Text: "", + AssignTo: &g.SecValueDelta, + TextColor: backgroundColor, + Background: SolidColorBrush{Color: backgroundColor}, + }, + + Label{ + Text: "Average minute:", + }, + Label{ + Text: "-", + AssignTo: &g.MinuteValue, + Font: Font{PointSize: 20}, + }, + Label{ + Text: "", + AssignTo: &g.MinuteValueDelta, + TextColor: backgroundColor, + Background: SolidColorBrush{Color: backgroundColor}, + }, + + Label{ + Text: "EWMA Value:", + ToolTipText: "SimpleEWMA", + }, + Label{ + Text: "-", + AssignTo: &g.EwmaValue, + Font: Font{PointSize: 20}, + }, + Label{ + Text: "", + AssignTo: &g.EwmaValueDelta, + TextColor: backgroundColor, + Background: SolidColorBrush{Color: backgroundColor}, + }, + }, + }, + + Composite{ + Layout: Grid{Columns: 2}, + Children: []Widget{ + Label{ + Text: "Countdown:", + }, + Label{ + Text: "", + AssignTo: &g.Countdown, + }, + + Label{ + Text: "Min:", + }, + Label{ + Text: "-", + AssignTo: &g.ValueMin, + }, + + Label{ + Text: "Max:", + }, + Label{ + Text: "-", + AssignTo: &g.ValueMax, + }, + }, + }, + VSpacer{}, + PushButton{ + Text: "Reset (Ctrl + Del)", + OnClicked: func() { + g.ResetData() + g.ResetGUI() + }, + }, + PushButton{ + Text: "Save Logs (Ctrl + Return)", + OnClicked: func() { + SaveLogs(&gui) + }, + }, + }, + }).Create(); err != nil { + log.Fatal(err) + } +} + +func (g *GUI) aboutAction_Triggered() { + walk.MsgBox(g, "About", "I started with GoSysLat to better understand how the SysLat works. When I had rebuilt the basic framework I tried to improve the system further, faster with alternative methods to RTSS, e.g. OpenGL and DirectX9. The goal was to get more accurate and better visualized results.", walk.MsgBoxIconInformation) +} + +func (g *GUI) howToStart_Triggered() { + walk.MsgBox(g, "How to start", `First you need a different firmware than the factory one. "github.com/spddl/SysLat_Firmware" + +After that you have to select the correct COM port in the settings. + +Now you can create a "Target Window" or open it in a game with RTSS. If an area now changes between black and white, the communication with your SysLat works. +Now you can hold the sensor against this area on the monitor and the software will do the rest.`, walk.MsgBoxIconInformation) +} + +func (g *GUI) SetValue(value *walk.Label, text string) { + if g == nil || value == nil { + return + } + g.Synchronize(func() { + if err := value.SetText(text); err != nil { + log.Println(err) + } + }) +} + +func (g *GUI) SetDeltaValue(value *walk.Label, oldValue, newValue float64) { + if g == nil || value == nil { + return + } + deltafloat64 := Round(newValue - oldValue) + g.Synchronize(func() { + switch { + case deltafloat64 == 0: + value.SetTextColor(backgroundColor) + case newValue > oldValue: + value.SetText("( +" + float64ToString(deltafloat64) + " )") + value.SetTextColor(redColor) + case newValue < oldValue: + value.SetText("( " + float64ToString(deltafloat64) + " )") + value.SetTextColor(greenColor) + default: + // will never happen but makes the compiler happy + value.SetTextColor(backgroundColor) + } + }) +} + +func (g *GUI) ResetData() { + db.Count = 0 + db.Countdown = time.Time{} + db.All = Dataset{} + db.Second = Dataset{} + db.Minute = Dataset{} +} + +func (g *GUI) ResetGUI() { + g.Synchronize(func() { + g.Count.SetText("-") + g.Value.SetText("-") + g.ValueDelta.SetText("") + g.SecValue.SetText("-") + g.SecValueDelta.SetText("") + g.MinuteValue.SetText("-") + g.MinuteValueDelta.SetText("") + g.EwmaValue.SetText("-") + g.EwmaValueDelta.SetText("") + g.Countdown.SetText("") + g.ValueMin.SetText("-") + g.ValueMax.SetText("-") + }) +} diff --git a/gui_SettingsDialog.go b/gui_SettingsDialog.go new file mode 100644 index 0000000..de6b381 --- /dev/null +++ b/gui_SettingsDialog.go @@ -0,0 +1,367 @@ +package main + +import ( + "fmt" + "log" + + "github.com/lxn/walk" + "github.com/spddl/USBController" + + //lint:ignore ST1001 standard behavior lxn/walk + . "github.com/lxn/walk/declarative" +) + +type Port struct { + Id int + Name string + COMName string +} + +func KnownPorts() []*Port { + var result []*Port + for _, v := range USBController.COMDevices { + p := new(Port) + p.Name = v.FriendlyName + p.Id = StringToInt(v.COMid) + p.COMName = "COM" + v.COMid + result = append(result, p) + } + return result +} + +func RunSettingsDialog(owner *GUI) (int, error) { + var dlg *walk.Dialog + var db *walk.DataBinder + var ComPortCB *walk.ComboBox + var acceptPB, cancelPB *walk.PushButton + var OSDXS, OSDYS, OSDWidthS, OSDHeightS *walk.Slider + var OSDXNE, OSDYNE, OSDWidthNE, OSDHeightNE *walk.NumberEdit + var DetectLightL *walk.Label + var DisplayCB, DetectLightCB *walk.CheckBox + + return Dialog{ + AssignTo: &dlg, + Title: "Settings", + Icon: 2, + DefaultButton: &acceptPB, + CancelButton: &cancelPB, + DataBinder: DataBinder{ + AssignTo: &db, + Name: "data", + DataSource: owner.Data, + ErrorPresenter: ToolTipErrorPresenter{}, + }, + MinSize: Size{ + Width: 400, + Height: 300, + }, + Layout: VBox{}, + Children: []Widget{ + Composite{ + Layout: Grid{Columns: 2}, + Children: []Widget{ + Label{ + Text: "COM Port:", + }, + ComboBox{ + AssignTo: &ComPortCB, + Value: c.ComPort, + BindingMember: "Id", + DisplayMember: "Name", + Model: KnownPorts(), + OnCurrentIndexChanged: func() { // BUG: called 3 times + newCOM := KnownPorts()[ComPortCB.CurrentIndex()].Id + if c.ComPort != newCOM { + queue.cancel() + queue.wg.Wait() + USBController.CloseComPort(hPort) // close handle to ComPort + + log.Println("OpenComPort", KnownPorts()[ComPortCB.CurrentIndex()].COMName) + hPort = USBController.OpenComPort(KnownPorts()[ComPortCB.CurrentIndex()].COMName) + if hPort == 0 || hPort == 0xFFFFFFFFFFFFFFFF { + log.Printf("Can't Open COM%d Port. %x", c.ComPort, hPort) + walk.MsgBox(nil, "Error", fmt.Sprintf("Can't Open COM%d port\nchange the COM port under settings and restart the tool", c.ComPort), walk.MsgBoxIconError) + return + } + + if c.Display { + USBController.WriteByte(&hPort, []byte{1}) + } else { + USBController.WriteByte(&hPort, []byte{2}) + } + if c.DetectLight { + USBController.WriteByte(&hPort, []byte{3}) + } else { + USBController.WriteByte(&hPort, []byte{4}) + } + + queue.StartQueue() + go queue.Reading(&hPort) + go queue.Ticker() + + c.ComPort = KnownPorts()[ComPortCB.CurrentIndex()].Id + c.Save() + go queue.DataProcessing() + } + }, + }, + + GroupBox{ + Title: "RTSS OSD", + ColumnSpan: 2, + Layout: Grid{Columns: 3}, + Children: []Widget{ + Label{ + Text: "X:", + ToolTipText: "X Coordinate", + }, + Slider{ + AssignTo: &OSDXS, + MinValue: 0, + MaxValue: 100, + Value: Bind("OSDX"), + OnValueChanged: func() { + value := OSDXS.Value() + OSDXNE.SetValue(float64(value)) + c.OSDX = value + c.RefreshOSDStrings() + c.Save() + }, + }, + NumberEdit{ + AssignTo: &OSDXNE, + MaxSize: Size{ + Width: 60, + }, + Value: Bind("OSDX", Range{ + Min: 0.0, + Max: 100.0, + }), + SpinButtonsVisible: true, + Decimals: 0, + OnValueChanged: func() { + value := int(OSDXNE.Value()) + OSDXS.SetValue(value) + c.OSDX = value + c.RefreshOSDStrings() + c.Save() + }, + }, + + Label{ + Text: "Y:", + ToolTipText: "Y Coordinate", + }, + Slider{ + AssignTo: &OSDYS, + MinValue: 0, + MaxValue: 100, + Value: Bind("OSDY"), + OnValueChanged: func() { + value := OSDYS.Value() + OSDYNE.SetValue(float64(value)) + c.OSDY = value + c.RefreshOSDStrings() + c.Save() + }, + }, + NumberEdit{ + AssignTo: &OSDYNE, + MaxSize: Size{ + Width: 60, + }, + Value: Bind("OSDY", Range{ + Min: 0.0, + Max: 100.0, + }), + SpinButtonsVisible: true, + Decimals: 0, + OnValueChanged: func() { + value := int(OSDYNE.Value()) + OSDYS.SetValue(value) + c.OSDY = value + c.RefreshOSDStrings() + c.Save() + }, + }, + + Label{ + Text: "Width:", + ToolTipText: "Width of the OSD's square", + }, + Slider{ + AssignTo: &OSDWidthS, + MinValue: 0, + MaxValue: 100, + Value: Bind("OSDWidth"), + OnValueChanged: func() { + value := OSDWidthS.Value() + OSDWidthNE.SetValue(float64(value)) + c.OSDWidth = value + c.RefreshOSDStrings() + c.Save() + }, + }, + NumberEdit{ + AssignTo: &OSDWidthNE, + MaxSize: Size{ + Width: 60, + }, + Value: Bind("OSDWidth", Range{ + Min: 0.0, + Max: 100.0, + }), + SpinButtonsVisible: true, + Decimals: 0, + OnValueChanged: func() { + value := int(OSDWidthNE.Value()) + OSDWidthS.SetValue(value) + c.OSDWidth = value + c.RefreshOSDStrings() + c.Save() + }, + }, + + Label{ + Text: "Height:", + ToolTipText: "Height of the OSD's square", + }, + Slider{ + AssignTo: &OSDHeightS, + MinValue: 0, + MaxValue: 100, + Value: Bind("OSDWidth"), + OnValueChanged: func() { + value := OSDHeightS.Value() + OSDHeightNE.SetValue(float64(value)) + c.OSDHeight = value + c.RefreshOSDStrings() + c.Save() + }, + }, + NumberEdit{ + AssignTo: &OSDHeightNE, + MaxSize: Size{ + Width: 60, + }, + Value: Bind("OSDHeight", Range{ + Min: 0.0, + Max: 100.0, + }), + SpinButtonsVisible: true, + Decimals: 0, + OnValueChanged: func() { + value := int(OSDHeightNE.Value()) + OSDHeightS.SetValue(value) + c.OSDHeight = value + c.RefreshOSDStrings() + c.Save() + }, + }, + }, + }, + + GroupBox{ + Title: "D3D9 / OpenGL", + Layout: VBox{ + Alignment: AlignHNearVNear, + }, + Children: []Widget{ + RadioButtonGroup{ + DataMember: "PushMode", + Buttons: []RadioButton{ + { + ToolTipText: "Here the images are updated as repetitively as possible", + Text: "Benchmark Mode", + Value: false, + OnClicked: func() { + c.PushMode = false + c.Save() + }, + }, + { + ToolTipText: "Here the images are only updated when the syslat sends a signal", + Text: "Push Mode", + Value: true, + OnClicked: func() { + c.PushMode = true + c.Save() + }, + }, + }, + }, + }, + }, + GroupBox{ + Title: "Device Settings", + // Layout: VBox{ + // Alignment: AlignHNearVNear, + // }, + Layout: Grid{ + Alignment: AlignHNearVNear, + Columns: 2, + }, + + Children: []Widget{ + + Label{ + Text: "Display:", + }, + CheckBox{ + AssignTo: &DisplayCB, + Checked: Bind("Display"), + OnCheckedChanged: func() { + c.Display = DisplayCB.Checked() + c.Save() + + if DisplayCB.Checked() { + USBController.WriteByte(&hPort, []byte{1}) + } else { + USBController.WriteByte(&hPort, []byte{2}) + } + }, + }, + + Label{ + AssignTo: &DetectLightL, + Text: "Detect Light:", + }, + CheckBox{ + AssignTo: &DetectLightCB, + Checked: Bind("DetectLight"), + OnCheckedChanged: func() { + c.DetectLight = DetectLightCB.Checked() + c.Save() + + var result bool + if DetectLightCB.Checked() { + DetectLightL.SetText("Detect Light: (Black > White)") + result = USBController.WriteByte(&hPort, []byte{3}) + } else { + DetectLightL.SetText("Detect no Light: (White > Black)") + result = USBController.WriteByte(&hPort, []byte{4}) + } + log.Println(result) + }, + }, + }, + }, + VSpacer{}, + }, + }, + Composite{ + Layout: HBox{}, + Children: []Widget{ + HSpacer{}, + PushButton{ + AssignTo: &acceptPB, + Text: "OK", + OnClicked: func() { + dlg.Accept() + }, + }, + }, + }, + }, + }.Run(*owner) +} diff --git a/gui_logs.go b/gui_logs.go new file mode 100644 index 0000000..e7b52d2 --- /dev/null +++ b/gui_logs.go @@ -0,0 +1,216 @@ +package main + +import ( + "sort" + "time" + + "github.com/VividCortex/ewma" + "github.com/lxn/walk" + + //lint:ignore ST1001 standard behavior lxn/walk + . "github.com/lxn/walk/declarative" +) + +type Data struct { + Timestamp time.Time + Value float64 +} + +type Dataset struct { + Current []Data + Backlog []Data + Max float64 + Min float64 +} + +type DatabaseTable struct { + checked bool + Index int + Count int + Countdown time.Time + Max float64 + Min float64 + MovingAverage ewma.MovingAverage + Data []Data +} + +type DatabaseTableModel struct { + walk.TableModelBase + walk.SorterBase + sortColumn int + sortOrder walk.SortOrder + items []*DatabaseTable +} + +func NewDatabaseTableModel(dbBacklog *[]Database) *DatabaseTableModel { + m := new(DatabaseTableModel) + m.ResetRows(dbBacklog) + return m +} + +// Called by the TableView from SetModel and every time the model publishes a +// RowsReset event. +func (m *DatabaseTableModel) RowCount() int { + return len(m.items) +} + +// Called by the TableView when it needs the text to display for a given cell. +func (m *DatabaseTableModel) Value(row, col int) interface{} { + item := m.items[row] + + switch col { + case 0: + return item.Index + case 1: + return item.Count + case 2: + return item.Countdown + case 3: + return item.Min + case 4: + return item.Max + } + + panic("unexpected col") +} + +// Called by the TableView to retrieve if a given row is checked. +func (m *DatabaseTableModel) Checked(row int) bool { + return m.items[row].checked +} + +// Called by the TableView when the user toggled the check box of a given row. +func (m *DatabaseTableModel) SetChecked(row int, checked bool) error { + m.items[row].checked = checked + + return nil +} + +// Called by the TableView to sort the model. +func (m *DatabaseTableModel) Sort(col int, order walk.SortOrder) error { + m.sortColumn, m.sortOrder = col, order + + sort.SliceStable(m.items, func(i, j int) bool { + a, b := m.items[i], m.items[j] + + c := func(ls bool) bool { + if m.sortOrder == walk.SortAscending { + return ls + } + + return !ls + } + + switch m.sortColumn { + case 0: + return c(a.Index < b.Index) + + case 1: + return c(a.Count < b.Count) + + case 2: + return c(a.Countdown.After(b.Countdown)) + + case 3: + return c(a.Min < b.Min) + + case 4: + return c(a.Max < b.Max) + } + + panic("unreachable") + }) + + return m.SorterBase.Sort(col, order) +} + +func (m *DatabaseTableModel) ResetRows(dbBacklog *[]Database) { + dbb := *dbBacklog + for i := 0; i < len(dbb); i++ { + temp := DatabaseTable{ + Index: i, + Count: dbb[i].Count, + Countdown: dbb[i].Countdown, + Max: dbb[i].All.Max, + Min: dbb[i].All.Min, + Data: dbb[i].All.Backlog, + MovingAverage: dbb[i].e, + } + m.items = append(m.items, &temp) + } + + // Notify TableView and other interested parties about the reset. + m.PublishRowsReset() + + m.Sort(m.sortColumn, m.sortOrder) +} + +func SaveLogs(owner *GUI) (int, error) { + var dlg *walk.Dialog + model := NewDatabaseTableModel(&dbBacklog) + var tv *walk.TableView + + // find the maximum + var maxCount int + for _, v := range model.items { + if maxCount < v.Count { + maxCount = v.Count + } + } + // set the checkmark for the maximum + for _, v := range model.items { + if maxCount == v.Count { + v.checked = true + } + } + + return Dialog{ + AssignTo: &dlg, + Title: "Which logs should be saved", + Icon: 2, + MinSize: Size{ + Width: 400, + Height: 300, + }, + Layout: VBox{}, + Children: []Widget{ + Label{ + Text: "Which data sets should be saved?", + }, + TableView{ + AssignTo: &tv, + AlternatingRowBG: true, + CheckBoxes: true, + ColumnsOrderable: true, + Columns: []TableViewColumn{ + {Title: "#", Width: 40}, + {Title: "entries"}, + {Title: "started", Format: "2006-01-02 15:04:05"}, + {Title: "min"}, + {Title: "max"}, + }, + StyleCell: func(style *walk.CellStyle) { + if model.items[style.Row()].checked { + if style.Row()%2 == 0 { + style.BackgroundColor = walk.RGB(159, 215, 255) + } else { + style.BackgroundColor = walk.RGB(143, 199, 239) + } + } + }, + Model: model, + }, + PushButton{ + Text: "Save", + OnClicked: func() { + for _, dbt := range model.items { + if dbt.checked { + SaveToFile(dbt) + walk.MsgBox(nil, "", "File saved", walk.MsgBoxIconInformation) + } + } + }, + }, + }, + }.Run(*owner) +} diff --git a/helper.go b/helper.go new file mode 100644 index 0000000..f4a854f --- /dev/null +++ b/helper.go @@ -0,0 +1,52 @@ +package main + +import ( + "encoding/binary" + "fmt" + "math" + "strconv" +) + +func ByteToInt(value []byte) (int, error) { + return strconv.Atoi(string(value)) +} + +// func float32ToString(value float32) string { +// return strconv.FormatFloat(float64(value), 'g', -1, 64) +// } + +func float64ToString(value float64) string { + return strconv.FormatFloat(value, 'g', -1, 64) +} + +func UintToString(value uint) string { + return fmt.Sprintf("%d", db.Count) +} + +func IntToString(value int) string { + return strconv.Itoa(value) +} + +func StringToFloat64(value string) float64 { + f, err := strconv.ParseFloat(value, 64) + if err != nil { + return 0.0 + } + return f +} + +func Round(f float64) float64 { + return (math.Round(f*100) / 100) +} + +func StringToInt(s string) int { + i, err := strconv.Atoi(s) + if err != nil { + return -1 + } + return i +} + +func ByteArrayToInt(data []byte) int { + return int(binary.LittleEndian.Uint64(data)) +} diff --git a/helper_test.go b/helper_test.go new file mode 100644 index 0000000..4c00531 --- /dev/null +++ b/helper_test.go @@ -0,0 +1,134 @@ +package main + +import ( + "fmt" + "math" + "strconv" + "testing" +) + +/////////////////// +// Float64ToString +/////////////////// + +func BenchmarkFloat64ToString_fmtSprintf(b *testing.B) { + var i float64 + for i = 0; i < float64(b.N); i++ { + val := fmt.Sprintf("%f", i) + _ = val + } +} + +func BenchmarkFloat64ToString_strconvFormatFloat(b *testing.B) { + var i float64 + for i = 0; i < float64(b.N); i++ { + val := strconv.FormatFloat(i, 'g', -1, 64) + _ = val + } +} + +/////////////////// +// Float32ToString +/////////////////// + +func BenchmarkFloat32ToString_fmtSprintf(b *testing.B) { + var i float32 + for i = 0; i < float32(b.N); i++ { + val := fmt.Sprintf("%f", i) + _ = val + } +} + +func BenchmarkFloat32ToString_strconvFormatFloat(b *testing.B) { + var i float64 + for i = 0; i < float64(b.N); i++ { + val := strconv.FormatFloat(float64(i), 'g', -1, 32) + _ = val + } +} + +/////////////////// +// IntToString +/////////////////// + +func BenchmarkIntToString_strconvItoa(b *testing.B) { + for i := 0; i < b.N; i++ { + val := strconv.Itoa(i) + _ = val + } +} + +func BenchmarkIntToString_FormatInt(b *testing.B) { + for i := 0; i < b.N; i++ { + val := strconv.FormatInt(int64(i), 10) + _ = val + } +} + +/////////////////// +// Float64Round +/////////////////// + +func BenchmarkFloat64Round_mathRound(b *testing.B) { + var i float64 + for i = 0; i < float64(b.N); i++ { + val := math.Round(i*100) / 100 + _ = val + } +} + +func BenchmarkFloat64Round_fmtSprintf(b *testing.B) { + var i float64 + for i = 0; i < float64(b.N); i++ { + val := fmt.Sprintf("%.2f", i) + _ = val + } +} + +/////////////////// +// Float32Round +/////////////////// + +// func BenchmarkFloat32Round_mathRound(b *testing.B) { +// var i float32 +// for i = 0; i < float32(b.N); i++ { +// val := math.Round(float64(i*100)) / 100 +// _ = val +// } +// } + +func BenchmarkFloat32Round_fmtSprintf(b *testing.B) { + var i float32 + for i = 0; i < float32(b.N); i++ { + val := fmt.Sprintf("%.2f", i) + _ = val + } +} + +// func BenchmarkFloat32Round_intFloat(b *testing.B) { +// var i float32 +// for i = 0; i < float32(b.N); i++ { +// val := float32(int(i*100)) / 100 +// _ = val +// } +// } + +/////////////////// +// StringsConcat +/////////////////// + +// fmt.Sprintf("", c.OSDX, c.OSDY, c.OSDWidth, c.OSDHeight) + +func BenchmarkStringsConcat_fmtSprintf(b *testing.B) { + for i := 0; i < b.N; i++ { + val := fmt.Sprintf("", i, i*2, i*3, i*4) + _ = val + } +} + +func BenchmarkStringsConcat_strconvItoa(b *testing.B) { + for i := 0; i < b.N; i++ { + val := "" + _ = val + } +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..0925940 --- /dev/null +++ b/main.go @@ -0,0 +1,228 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + "os" + "os/signal" + "path/filepath" + "syscall" + "time" + + "github.com/MakeNowJust/hotkey" + "github.com/VividCortex/ewma" + "github.com/lxn/walk" + "golang.org/x/sys/windows" + + "github.com/spddl/RTSSClient" + "github.com/spddl/TargetWindow_D3D9" + "github.com/spddl/TargetWindow_OpenGL" + + "github.com/spddl/USBController" +) + +type Database struct { + Count int + Countdown time.Time + All Dataset + Second Dataset + Minute Dataset + e ewma.MovingAverage +} + +var ( + db Database + dbBacklog []Database + hPort syscall.Handle + gui GUI + RTSSOSDBlack string + RTSSOSDWhite string + sigs chan os.Signal + queue Queue + currentFileName string + toggle chan bool + trigger bool +) + +func init() { + log.SetFlags(log.Lmicroseconds | log.Lshortfile) // https://ispycode.com/GO/Logging/Setting-output-flags + + // https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getpriorityclass + windows.SetPriorityClass(windows.CurrentProcess(), 0x00000100) // REALTIME_PRIORITY_CLASS, if it is started as admin otherwise it is only high + + // https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average + db.e = ewma.NewMovingAverage(60) + + e, err := os.Executable() + if err != nil { + log.Fatal(err) + } + currentFileName = filepath.Base(e) +} + +func cleanUp() { + RTSSClient.UpdateOSD("") // needs testing, maybe you can hide the rectangle like this + if targetOGL.IsActive { + targetOGL.Close() + } + if targetD3D9.IsActive { + targetD3D9.Close() + } + if trigger { + USBController.WriteByte(&hPort, []byte{5}) + } + queue.cancel() + log.Println("queue.wg.Wait() // block") + queue.wg.Wait() // block + + USBController.CloseComPort(hPort) // close handle to ComPort +} + +func prettyPrint(i interface{}) string { + s, _ := json.MarshalIndent(i, "", "\t") + return string(s) +} + +func main() { + sigs = make(chan os.Signal, 1) + signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) + go func() { + <-sigs + cleanUp() + os.Exit(0) + }() + + c.RefreshOSDStrings() + if !cliMode { + gui.Start() + gui.Show() + + hkey := hotkey.New() + hkey.Register(hotkey.Ctrl, hotkey.DELETE, func() { + gui.ResetData() + gui.ResetGUI() + }) + hkey.Register(hotkey.Ctrl, hotkey.RETURN, func() { + var dbTemp = []Database{db} + gui.ResetData() + gui.ResetGUI() + dbt := NewDatabaseTableModel(&dbTemp) + for _, values := range dbt.items { + if len(values.Data) != 0 { + SaveToFile(values) + } + } + }) + + /// the syslat is not designed for this. + // var quit chan struct{} + // hkey.Register(hotkey.Ctrl, 0x52, func() { + // USBController.WriteByte(&hPort, []byte{5}) + // trigger = !trigger + // if trigger { + // mouseHook = SetWindowsHookEx(WH_MOUSE_LL, (HOOKPROC)(func(nCode int, wparam WPARAM, lparam LPARAM) LRESULT { + // if nCode == 0 && wparam == WM_LBUTTONDOWN { + // log.Println("WM_LBUTTONDOWN", trigger) + // if trigger { + // go USBController.WriteByte(&hPort, []byte{0}) + // } + // } + // return CallNextHookEx(mouseHook, nCode, wparam, lparam) + // }), 0, 0) + // quit = make(chan struct{}) + // go func() { + // var msg MSG + // for { + // select { + // case <-quit: // BUG: the loop will not be killed + // return + // default: + // // if GetMessage(&msg, 0, 0, 0) != 0 { + // // TranslateMessage(&msg) + // // DispatchMessage(&msg) + // // } + // for PeekMessage(&msg, 0, 0, 0, PM_NOREMOVE|PM_QS_INPUT) { + + // } + // } + // } + // }() + // } else { + // close(quit) + // UnhookWindowsHookEx(mouseHook) + // mouseHook = 0 + // } + // }) + } + + if cliMode { + c.ComPort = f.Port + } + + // the first valid port as default port + if c.ComPort == 0 || !TestValidComPort(IntToString(c.ComPort)) { + c.Reset() + c.ComPort = StringToInt(USBController.COMDevices[0].COMid) + c.Save() + } + + hPort = USBController.OpenComPort(fmt.Sprintf("COM%d", c.ComPort)) + if hPort == 0 || hPort == 0xFFFFFFFFFFFFFFFF { + walk.MsgBox(nil, "Error", fmt.Sprintf("Can't Open COM%d port", c.ComPort), walk.MsgBoxIconError) + c.Reset() + // the first valid port as default port + c.ComPort = StringToInt(USBController.COMDevices[0].COMid) + c.Save() + } + if c.Display { + USBController.WriteByte(&hPort, []byte{1}) + } else { + USBController.WriteByte(&hPort, []byte{2}) + } + if c.DetectLight { + USBController.WriteByte(&hPort, []byte{3}) + } else { + USBController.WriteByte(&hPort, []byte{4}) + } + + queue.StartQueue() + go queue.Reading(&hPort) + if cliMode { + go queue.TickerCli() + } else { + go queue.Ticker() + } + go queue.DataProcessing() + + if cliMode { + if f.D3D9 { + targetD3D9 = TargetWindow_D3D9.Target{} + go func() { + targetD3D9.Start(currentFileName, f.Fullscreen, f.Width, f.Height, c.PushMode) + sigs <- syscall.SIGINT + }() + } else if f.OGL { + targetOGL = TargetWindow_OpenGL.Target{} + go func() { + targetOGL.Start(f.Fullscreen, f.Width, f.Height, c.PushMode) + sigs <- syscall.SIGINT + }() + } + } else { + gui.Run() + sigs <- syscall.SIGINT + } + + <-sigs + cleanUp() +} + +func TestValidComPort(port string) bool { + for _, com := range USBController.COMDevices { + if com.COMid == port { + return true + } + } + return false +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..c67112b --- /dev/null +++ b/readme.md @@ -0,0 +1,14 @@ +# GoSysLat + +I didn't understand the original SysLat software and how it interacted with the device at first, so I wanted to rebuild it to understand it better. This allowed me to add some functions afterwards and increase the speed of the query. + +To use this GoSysLat client you need the device [SysLat](https://syslat.com) and a customized [firmware](https://github.com/spddl/SysLat_Firmware) + +[Here are some of my test results.](https://bit.ly/gosyslat) These results are of course only momentary recordings from my system. +It should be clear that the latency measurement is only a polling of different hardware and software. That means you will never get the same results if you compare 2 settings that do not change the latency. + +In this repo are also the TestCase's I used if someone wants to recreate this. + +Thanks for the support and help to [timecard](https://github.com/djdallmann), [Skew](https://github.com/Skewjo) and [henmill](https://github.com/henmill) + +![example](/example.png) \ No newline at end of file diff --git a/rsrc.syso b/rsrc.syso new file mode 100644 index 0000000000000000000000000000000000000000..d09dd6344557d8b4838f062036226aa341df6def GIT binary patch literal 132818 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 JA+BBf{{ie=;`#sp literal 0 HcmV?d00001 diff --git a/run.bat b/run.bat new file mode 100644 index 0000000..2ab6d3f --- /dev/null +++ b/run.bat @@ -0,0 +1,24 @@ +@echo off + +SET filename=GoSysLat + +:loop +cls + +@REM gocritic check -enable="#performance" ./... +@REM gocritic check -enableAll -disable="#experimental,#opinionated,#commentedOutCode" ./... + +go build +@REM go build -race -o %filename%.exe +@REM go build -tags debug -o %filename%.exe + +IF %ERRORLEVEL% EQU 0 %filename%.exe +@REM IF %ERRORLEVEL% EQU 0 %filename%.exe -ogl -port 4 -time 30s -print +@REM IF %ERRORLEVEL% EQU 0 %filename%.exe -ogl -fullscreen -port 4 -print +@REM IF %ERRORLEVEL% EQU 0 %filename%.exe -d3d9 -fullscreen -port 4 -print +@REM .\GoSysLat.exe +@REM .\GoSysLat.exe -ogl -port 4 -time 30s +@REM .\GoSysLat.exe -ogl -fullscreen -port 4 -time 30s + +pause +goto loop \ No newline at end of file