Skip to content

Commit c55e513

Browse files
committed
refactor(studio-bridge): use EncodingService:Base64Encode for screenshot
Replaces the hand-rolled buffer-based base64 encoder (~60 lines, including a 64-entry ASCII LUT and a localized hot loop) with a call to Roblox's built-in EncodingService:Base64Encode. The native implementation is faster than any Luau loop and removes a chunk of vendored encoding logic we'd otherwise have to maintain. Resolves a code review note from Quenty.
1 parent 9ad3700 commit c55e513

1 file changed

Lines changed: 7 additions & 68 deletions

File tree

tools/studio-bridge/src/commands/viewport/screenshot/capture-screenshot.luau

Lines changed: 7 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
55
Captures the Studio viewport using CaptureService, loads the result into
66
an EditableImage to read raw RGBA pixels, encodes as PNG via png-luau,
7-
base64-encodes the PNG, and sends it over the WebSocket.
7+
base64-encodes the PNG via EncodingService, and sends it over the WebSocket.
88
99
Protocol:
1010
Request: { type: "captureScreenshot", payload: { format?: "png" } }
@@ -18,70 +18,6 @@ local _disposed = false
1818

1919
local CaptureScreenshotAction = {}
2020

21-
-- ---------------------------------------------------------------------------
22-
-- Base64 encoder for buffer data
23-
-- ---------------------------------------------------------------------------
24-
25-
-- Pre-compute lookup: index 0..63 → ASCII byte
26-
local ENCODE_LUT: buffer = buffer.create(64)
27-
do
28-
local chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
29-
for i = 0, 63 do
30-
buffer.writeu8(ENCODE_LUT, i, string.byte(chars, i + 1))
31-
end
32-
end
33-
34-
-- Encode a buffer as a base64 string. Uses buffer I/O throughout for speed.
35-
local function base64Encode(buf: buffer): string
36-
local len = buffer.len(buf)
37-
local outLen = math.ceil(len / 3) * 4
38-
local out = buffer.create(outLen)
39-
local outIdx = 0
40-
41-
-- Localize for tight loop performance
42-
local readu8 = buffer.readu8
43-
local writeu8 = buffer.writeu8
44-
local lut = ENCODE_LUT
45-
local rshift = bit32.rshift
46-
local band = bit32.band
47-
48-
-- Process full 3-byte groups
49-
local fullLen = len - (len % 3)
50-
for i = 0, fullLen - 1, 3 do
51-
local b0 = readu8(buf, i)
52-
local b1 = readu8(buf, i + 1)
53-
local b2 = readu8(buf, i + 2)
54-
local n = b0 * 65536 + b1 * 256 + b2
55-
56-
writeu8(out, outIdx, readu8(lut, rshift(n, 18)))
57-
writeu8(out, outIdx + 1, readu8(lut, band(rshift(n, 12), 63)))
58-
writeu8(out, outIdx + 2, readu8(lut, band(rshift(n, 6), 63)))
59-
writeu8(out, outIdx + 3, readu8(lut, band(n, 63)))
60-
outIdx += 4
61-
end
62-
63-
-- Handle remaining 1 or 2 bytes
64-
local rem = len % 3
65-
if rem == 1 then
66-
local b0 = readu8(buf, fullLen)
67-
local n = b0 * 65536
68-
writeu8(out, outIdx, readu8(lut, rshift(n, 18)))
69-
writeu8(out, outIdx + 1, readu8(lut, band(rshift(n, 12), 63)))
70-
writeu8(out, outIdx + 2, 61) -- '='
71-
writeu8(out, outIdx + 3, 61) -- '='
72-
elseif rem == 2 then
73-
local b0 = readu8(buf, fullLen)
74-
local b1 = readu8(buf, fullLen + 1)
75-
local n = b0 * 65536 + b1 * 256
76-
writeu8(out, outIdx, readu8(lut, rshift(n, 18)))
77-
writeu8(out, outIdx + 1, readu8(lut, band(rshift(n, 12), 63)))
78-
writeu8(out, outIdx + 2, readu8(lut, band(rshift(n, 6), 63)))
79-
writeu8(out, outIdx + 3, 61) -- '='
80-
end
81-
82-
return buffer.tostring(out)
83-
end
84-
8521
-- ---------------------------------------------------------------------------
8622
-- Helpers
8723
-- ---------------------------------------------------------------------------
@@ -116,8 +52,8 @@ end
11652

11753
local function captureAsync(sendMessage: (msg: { [string]: any }) -> (), requestId: string?, sessionId: string)
11854
-- Resolve services (deferred so the module loads in Lune test context)
119-
local getServiceOk, captureServiceOrErr, assetServiceOrErr = pcall(function()
120-
return game:GetService("CaptureService"), game:GetService("AssetService")
55+
local getServiceOk, captureServiceOrErr, assetServiceOrErr, encodingServiceOrErr = pcall(function()
56+
return game:GetService("CaptureService"), game:GetService("AssetService"), game:GetService("EncodingService")
12157
end)
12258
if not getServiceOk then
12359
sendMessage(
@@ -127,6 +63,7 @@ local function captureAsync(sendMessage: (msg: { [string]: any }) -> (), request
12763
end
12864
local CaptureService = captureServiceOrErr
12965
local AssetService = assetServiceOrErr
66+
local EncodingService = encodingServiceOrErr
13067

13168
-- Step 1: Capture the viewport via CaptureService
13269
local callerThread = coroutine.running()
@@ -246,7 +183,9 @@ local function captureAsync(sendMessage: (msg: { [string]: any }) -> (), request
246183
end
247184
end
248185

249-
local encodeOk, encoded = pcall(base64Encode, dataBuf)
186+
local encodeOk, encoded = pcall(function()
187+
return EncodingService:Base64Encode(dataBuf)
188+
end)
250189
if not encodeOk or not encoded then
251190
sendMessage(
252191
buildError(

0 commit comments

Comments
 (0)