Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions native/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,54 @@ set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
set(BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
set(BUILD_GAMES OFF CACHE BOOL "" FORCE)
set(CUSTOMIZE_BUILD ON CACHE BOOL "" FORCE)
# raylib 6.0 reads every SUPPORT_ flag in its config.h into an option that defaults to ON, the ones
# config.h defines as 0 included (raysan5/raylib#5844, fixed after 6.0), and CUSTOMIZE_BUILD skips
# config.h's own values. So each of those is on unless it is switched off here.
# SUPPORT_CUSTOM_FRAME_CONTROL was: EndDrawing then neither puts the frame on the screen, reads
# input nor waits for the next frame, and bm_present leaves all three to it for every frame it
# draws. So the Linux window stayed black, and from the first input bm_present drew one frame
# after another with no wait between them and read nothing more. A capture draws into a texture
# and never gets that far, which is why the pixel snapshots passed throughout;
# PixelTests.PresentWaitsForTheNextFrame is what notices. The busy wait loop was on the same way,
# and WaitTime is how bm_present waits out a frame it does not draw, so a tray with its window
# hidden spun through every one of them: a seventh of a core, for as long as it ran. The rest are
# formats and features nothing here uses.
foreach(flag IN ITEMS
SUPPORT_CUSTOM_FRAME_CONTROL
SUPPORT_BUSY_WAIT_LOOP
SUPPORT_GPU_SKINNING
SUPPORT_FILEFORMAT_TGA
SUPPORT_FILEFORMAT_JPG
SUPPORT_FILEFORMAT_PSD
SUPPORT_FILEFORMAT_HDR
SUPPORT_FILEFORMAT_PIC
SUPPORT_FILEFORMAT_PNM
SUPPORT_FILEFORMAT_KTX
SUPPORT_FILEFORMAT_ASTC
SUPPORT_FILEFORMAT_PKM
SUPPORT_FILEFORMAT_PVR
SUPPORT_FILEFORMAT_BDF
SUPPORT_FILEFORMAT_FLAC)
set(${flag} OFF CACHE BOOL "" FORCE)
endforeach()
# raylib spins for the last part of every wait by default, for timing precision nothing here
# needs: most of a millisecond of a core per frame, for as long as the tray runs.
set(SUPPORT_PARTIALBUSY_WAIT_LOOP OFF CACHE BOOL "" FORCE)
# F12 saves a screenshot into the working directory, which is wherever the tray was started from.
# raylib looks for the key at the end of a frame it has drawn, so that is F12 pressed while frames
# are being drawn, as they are for as long as the pointer is crossing the window.
set(SUPPORT_SCREEN_CAPTURE OFF CACHE BOOL "" FORCE)
set(SUPPORT_MODULE_RAUDIO OFF CACHE BOOL "" FORCE)
set(SUPPORT_MODULE_RMODELS OFF CACHE BOOL "" FORCE)
# PNG is the one picture format anything here reads or writes: a row's icons arrive as PNG, and a
# capture is saved as one. The other four that config.h has on, nothing here loads or saves.
foreach(flag IN ITEMS
SUPPORT_FILEFORMAT_BMP
SUPPORT_FILEFORMAT_GIF
SUPPORT_FILEFORMAT_QOI
SUPPORT_FILEFORMAT_DDS)
set(${flag} OFF CACHE BOOL "" FORCE)
endforeach()
set(SUPPORT_FILEFORMAT_PNG ON CACHE BOOL "" FORCE)

FetchContent_Declare(
Expand Down
7 changes: 6 additions & 1 deletion native/src/bm.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1576,7 +1576,12 @@ BM_API int32_t bm_init(int32_t width, int32_t height, const char* title, const u
SetTraceLogLevel(LOG_WARNING);
// Always made hidden, and shown once it is where it was left: made visible, it would first
// appear centred and then jump.
SetConfigFlags(FLAG_WINDOW_RESIZABLE | FLAG_VSYNC_HINT | FLAG_WINDOW_HIDDEN);
// ALWAYS_RUN because PollInputEvents otherwise waits for an event for as long as the window is
// minimised, and every bm_present makes that call. The managed loop stopped there with it:
// the tray's icon and menu stood still, no notification was shown, a click on the tray went
// unread, and a show asked for over the socket, which is what starting BuildMonitor again
// sends, waited until something else brought the window back.
SetConfigFlags(FLAG_WINDOW_RESIZABLE | FLAG_VSYNC_HINT | FLAG_WINDOW_HIDDEN | FLAG_WINDOW_ALWAYS_RUN);
InitWindow(width, height, title != nullptr ? title : "BuildMonitor");
if (!IsWindowReady()) {
return 0;
Expand Down
126 changes: 92 additions & 34 deletions native/swift/Sources/Bm/Exports.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ import ImageIO
///
/// The header is imported for its struct layouts only, with BM_TYPES_ONLY, so these are the
/// definitions of those symbols rather than a second declaration of them.
///
/// Every one that touches AppKit runs inside its own autorelease pool. `NSApplication.run` drains
/// a pool per event, but this app never calls it: the managed loop calls in instead, and with no
/// pool pushed objc4 parks everything autoreleased in one it creates for the thread, which drains
/// only when the thread exits. That is the main thread, so everything a frame autoreleased - the
/// events, the attributed strings, the drawing - stayed until the process did, and a tray is a
/// process that runs for days.

@_cdecl("bm_version")
public func bmVersion() -> Int32 {
Expand All @@ -30,6 +37,20 @@ public func bmInit(
_ fontSize: Float,
_ placement: UnsafePointer<BmPlacement>?,
_ hidden: Int32) -> Int32 {
autoreleasepool {
initialise(width, height, title, fontTtf, fontLength, fontSize, placement, hidden)
}
}

private func initialise(
_ width: Int32,
_ height: Int32,
_ title: UnsafePointer<CChar>?,
_ fontTtf: UnsafePointer<UInt8>?,
_ fontLength: Int32,
_ fontSize: Float,
_ placement: UnsafePointer<BmPlacement>?,
_ hidden: Int32) -> Int32 {
var font: Data?
if let fontTtf, fontLength > 0 {
font = Data(bytes: fontTtf, count: Int(fontLength))
Expand All @@ -53,17 +74,19 @@ public func bmPresent(_ screen: UnsafePointer<BmScreen>?) -> Int32 {
return 0
}

// The managed side presents every frame, and a screen it has not rebuilt comes with the same
// generation. Decoded and drawn anyway, every string was copied and every row drawn sixty times
// a second to put the same pixels on screen.
let generation = screen.pointee.generation
if generation == runtime.presentedGeneration {
runtime.pumpUnchanged()
return 1
autoreleasepool {
// The managed side presents every frame, and a screen it has not rebuilt comes with the
// same generation. Decoded and drawn anyway, every string was copied and every row drawn
// sixty times a second to put the same pixels on screen.
let generation = screen.pointee.generation
if generation == runtime.presentedGeneration {
runtime.pumpUnchanged()
} else {
runtime.presentedGeneration = generation
runtime.present(Frame.decode(screen))
}
}

runtime.presentedGeneration = generation
runtime.present(Frame.decode(screen))
return 1
}

Expand All @@ -74,12 +97,14 @@ public func bmPollInput(_ input: UnsafeMutablePointer<BmInput>?) {
}

let runtime = Runtime.shared
if runtime.initialised {
runtime.measure()
runtime.drainEdit()
runtime.samplePlacement()
runtime.sampleMinimized()
runtime.sampleHover()
autoreleasepool {
if runtime.initialised {
runtime.measure()
runtime.drainEdit()
runtime.samplePlacement()
runtime.sampleMinimized()
runtime.sampleHover()
}
}

input.pointee = runtime.input
Expand All @@ -88,16 +113,20 @@ public func bmPollInput(_ input: UnsafeMutablePointer<BmInput>?) {

@_cdecl("bm_set_hidden")
public func bmSetHidden(_ hidden: Int32) {
if hidden == 0 {
Runtime.shared.show()
} else {
Runtime.shared.hide()
autoreleasepool {
if hidden == 0 {
Runtime.shared.show()
} else {
Runtime.shared.hide()
}
}
}

@_cdecl("bm_focus")
public func bmFocus() {
Runtime.shared.show()
autoreleasepool {
Runtime.shared.show()
}
}

@_cdecl("bm_set_clipboard")
Expand All @@ -106,9 +135,11 @@ public func bmSetClipboard(_ text: UnsafePointer<CChar>?) {
return
}

let board = NSPasteboard.general
board.clearContents()
board.setString(String(cString: text), forType: .string)
autoreleasepool {
let board = NSPasteboard.general
board.clearContents()
board.setString(String(cString: text), forType: .string)
}
}

@_cdecl("bm_pick_directory")
Expand All @@ -122,8 +153,10 @@ public func bmPickDirectory(
return 0
}

let picked = Runtime.shared.pickDirectory(start: start.map { String(cString: $0) })
return writePicked(picked, into: buffer, length: bufferLength)
return autoreleasepool { () -> Int32 in
let picked = Runtime.shared.pickDirectory(start: start.map { String(cString: $0) })
return writePicked(picked, into: buffer, length: bufferLength)
}
}

/// The answer as the managed side reads it back: the path as UTF-8 with no terminator, and how many
Expand Down Expand Up @@ -154,6 +187,12 @@ public func bmTrayAvailable() -> Int32 {

@_cdecl("bm_tray_init")
public func bmTrayInit() -> Int32 {
autoreleasepool {
initialiseTray()
}
}

private func initialiseTray() -> Int32 {
let runtime = Runtime.shared
if runtime.tray != nil {
return 1
Expand All @@ -172,42 +211,61 @@ public func bmTrayInit() -> Int32 {

@_cdecl("bm_tray_set_icon")
public func bmTraySetIcon(_ kind: Int32, _ png: UnsafePointer<UInt8>?, _ length: Int32) {
guard let png, length > 0 else {
guard let png, length > 0, let tray = Runtime.shared.tray else {
return
}

Runtime.shared.tray?.setIcon(kind: kind, png: Data(bytes: png, count: Int(length)))
autoreleasepool {
tray.setIcon(kind: kind, png: Data(bytes: png, count: Int(length)))
}
}

@_cdecl("bm_tray_set_menu_icon")
public func bmTraySetMenuIcon(_ name: UnsafePointer<CChar>?, _ png: UnsafePointer<UInt8>?, _ length: Int32) {
guard let name, let png, length > 0 else {
guard let name, let png, length > 0, let tray = Runtime.shared.tray else {
return
}

Runtime.shared.tray?.setMenuIcon(name: String(cString: name), png: Data(bytes: png, count: Int(length)))
autoreleasepool {
tray.setMenuIcon(name: String(cString: name), png: Data(bytes: png, count: Int(length)))
}
}

@_cdecl("bm_set_row_icon")
public func bmSetRowIcon(_ name: UnsafePointer<CChar>?, _ png: UnsafePointer<UInt8>?, _ length: Int32) {
guard let name, let png, length > 0,
let image = NSImage(data: Data(bytes: png, count: Int(length))) else {
guard let name, let png, length > 0 else {
return
}

image.size = NSSize(width: 16, height: 16)
RowIcons.images[String(cString: name)] = image
autoreleasepool {
if let image = NSImage(data: Data(bytes: png, count: Int(length))) {
image.size = NSSize(width: 16, height: 16)
RowIcons.images[String(cString: name)] = image
}
}
}

@_cdecl("bm_shutdown")
public func bmShutdown() {
Runtime.shared.shutdown()
autoreleasepool {
Runtime.shared.shutdown()
}
}

/// Renders into a bitmap of this side's own making, with scale, colour space and font smoothing
/// pinned, so a committed baseline matches on any display. No window is needed.
@_cdecl("bm_capture")
public func bmCapture(
_ screen: UnsafePointer<BmScreen>?,
_ width: Int32,
_ height: Int32,
_ pngPath: UnsafePointer<CChar>?) -> Int32 {
autoreleasepool {
capture(screen, width, height, pngPath)
}
}

private func capture(
_ screen: UnsafePointer<BmScreen>?,
_ width: Int32,
_ height: Int32,
Expand Down
16 changes: 14 additions & 2 deletions src/BuildMonitor.Native/Native/NativeResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,23 @@ static IEnumerable<string> Candidates()
yield return Path.Combine(root, file);
}

static IEnumerable<string> Rids()
static IEnumerable<string> Rids() =>
Rids(RuntimeInformation.RuntimeIdentifier);

public static IEnumerable<string> Rids(string runtimeIdentifier)
{
// The exact RID first. On Alpine that is linux-musl-x64, which we do not ship, so the
// probe simply misses rather than loading a glibc binary and hard crashing.
yield return RuntimeInformation.RuntimeIdentifier;
yield return runtimeIdentifier;

// And nothing else, or the synthesised RID below undoes that: linux-{arch} is the glibc
// build. A load that fails is reported and the head exits, but one that succeeded would
// leave a crash to come later, in whichever call first met a libc that is not the one the
// library was built against.
if (runtimeIdentifier.Contains("-musl-", StringComparison.Ordinal))
{
yield break;
}

var architecture = RuntimeInformation.OSArchitecture switch
{
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
30 changes: 30 additions & 0 deletions src/BuildMonitor.Tests/Native/NativeResolverTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/// <summary>
/// Which runtimes/{rid} directories the resolver probes, which is what decides whose build of the
/// native library a process loads. Nothing here loads one, so these run on every platform.
/// </summary>
public class NativeResolverTests
{
/// <summary>
/// The probe for a musl RID is that RID alone. The linux-{arch} candidate after it names the
/// glibc build, which a musl process must not load.
/// </summary>
[Test]
[Arguments("linux-musl-x64")]
[Arguments("linux-musl-arm64")]
public async Task MuslProbesItsOwnRidAndNothingElse(string runtimeIdentifier)
{
var rids = NativeResolver.Rids(runtimeIdentifier).ToList();
await Assert.That(rids).IsEquivalentTo([runtimeIdentifier]);
}

/// <summary>
/// Everywhere else the synthesised RID still follows the framework's own.
/// </summary>
[Test]
public async Task OtherRidsStillFallBackToTheSynthesisedRid()
{
var rids = NativeResolver.Rids("some-rid").ToList();
await Assert.That(rids.Count).IsGreaterThan(1);
await Assert.That(rids[0]).IsEqualTo("some-rid");
}
}
Loading
Loading