From 0d5fa0fc96e33b4557144a01f274369baf6d6a74 Mon Sep 17 00:00:00 2001 From: redphx <96280+redphx@users.noreply.github.com> Date: Sun, 2 Feb 2025 17:57:46 +0700 Subject: [PATCH] Optimize WebGPU --- dist/better-xcloud.user.js | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/dist/better-xcloud.user.js b/dist/better-xcloud.user.js index ac824f9..34c224d 100755 --- a/dist/better-xcloud.user.js +++ b/dist/better-xcloud.user.js @@ -91,17 +91,13 @@ struct VertexOutput { @group(0) @binding(2) var ourParams: Params; const FILTER_UNSHARP_MASKING: f32 = 1.0; const CAS_CONTRAST_PEAK: f32 = 0.8 * -3.0 + 8.0; +const LUMINOSITY_FACTOR = vec3(0.299, 0.587, 0.114); @vertex -fn vsMain(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput { -let pos: array, 6> = array( -vec2(-1.0, -1.0), vec2(1.0, -1.0), vec2(-1.0, 1.0), -vec2(-1.0, 1.0), vec2(1.0, -1.0), vec2(1.0, 1.0) -); -let uv: array, 6> = array( -vec2(0.0, 1.0), vec2(1.0, 1.0), vec2(0.0, 0.0), -vec2(0.0, 0.0), vec2(1.0, 1.0), vec2(1.0, 0.0) -); -return VertexOutput(vec4(pos[vertexIndex], 0.0, 1.0), uv[vertexIndex]);} +fn vsMain(@location(0) pos: vec2) -> VertexOutput { +var out: VertexOutput; +out.position = vec4(pos, 0.0, 1.0); +out.uv = (vec2(pos.x, 1.0 - (pos.y + 1.0)) + vec2(1.0, 1.0)) * 0.5; +return out;} fn clarityBoost(coord: vec2, texSize: vec2, e: vec3) -> vec3 { let texelSize = 1.0 / texSize; let a = textureSampleBaseClampToEdge(ourTexture, ourSampler, coord + texelSize * vec2(-1.0, 1.0)).rgb; @@ -131,14 +127,14 @@ fn fsMain(input: VertexOutput) -> @location(0) vec4 { let texSize = vec2(textureDimensions(ourTexture)); let center = textureSampleBaseClampToEdge(ourTexture, ourSampler, input.uv); var adjustedRgb = clarityBoost(input.uv, texSize, center.rgb); -let gray = dot(adjustedRgb, vec3(0.299, 0.587, 0.114)); +let gray = dot(adjustedRgb, LUMINOSITY_FACTOR); adjustedRgb = mix(vec3(gray), adjustedRgb, ourParams.saturation); adjustedRgb = (adjustedRgb - 0.5) * ourParams.contrast + 0.5; adjustedRgb *= ourParams.brightness; return vec4(adjustedRgb, 1.0);}`; class BaseStreamPlayer {logTag;playerType;elementType;$video;options = {processing: "usm",sharpness: 0,brightness: 1,contrast: 1,saturation: 1};isStopped = !1;constructor(playerType, elementType, $video, logTag) {this.playerType = playerType, this.elementType = elementType, this.$video = $video, this.logTag = logTag;}init() {BxLogger.info(this.logTag, "Initialize");}updateOptions(newOptions, refresh = !1) {this.options = Object.assign(this.options, newOptions), refresh && this.refreshPlayer();}} class BaseCanvasPlayer extends BaseStreamPlayer {$canvas;targetFps = 60;frameInterval = 0;lastFrameTime = 0;animFrameId = null;frameCallback;boundDrawFrame;constructor(playerType, $video, logTag) {super(playerType, "canvas", $video, logTag);let $canvas = document.createElement("canvas");$canvas.width = $video.videoWidth, $canvas.height = $video.videoHeight, this.$canvas = $canvas, $video.insertAdjacentElement("afterend", this.$canvas);let frameCallback;if ("requestVideoFrameCallback" in HTMLVideoElement.prototype) {let $video2 = this.$video;frameCallback = $video2.requestVideoFrameCallback.bind($video2);} else frameCallback = requestAnimationFrame;this.frameCallback = frameCallback, this.boundDrawFrame = this.drawFrame.bind(this);}async init() {super.init(), await this.setupShaders(), this.setupRendering();}setTargetFps(target) {this.targetFps = target, this.lastFrameTime = 0, this.frameInterval = target ? Math.floor(1000 / target) : 0;}getCanvas() {return this.$canvas;}destroy() {if (BxLogger.info(this.logTag, "Destroy"), this.isStopped = !0, this.animFrameId) {if ("requestVideoFrameCallback" in HTMLVideoElement.prototype) this.$video.cancelVideoFrameCallback(this.animFrameId);else cancelAnimationFrame(this.animFrameId);this.animFrameId = null;}if (this.$canvas.isConnected) this.$canvas.remove();this.$canvas.width = 1, this.$canvas.height = 1;}toFilterId(processing) {return processing === "cas" ? 2 : 1;}shouldDraw() {if (this.targetFps >= 60) return !0;else if (this.targetFps === 0) return !1;let currentTime = performance.now();if (currentTime - this.lastFrameTime < this.frameInterval) return !1;return this.lastFrameTime = currentTime, !0;}drawFrame() {if (this.isStopped) return;if (this.animFrameId = this.frameCallback(this.boundDrawFrame), !this.shouldDraw()) return;this.updateFrame();}setupRendering() {this.animFrameId = this.frameCallback(this.boundDrawFrame);}} -class WebGPUPlayer extends BaseCanvasPlayer {static device;context;externalTexture;pipeline;sampler;bindGroup;optionsUpdated = !1;paramsBuffer;static async prepare() {if (!navigator.gpu) {BxEventBus.Script.emit("webgpu.ready", {});return;}try {let adapter = await navigator.gpu.requestAdapter({powerPreference: "low-power"});if (adapter) WebGPUPlayer.device = await adapter.requestDevice(), WebGPUPlayer.device.addEventListener("uncapturederror", (e) => {console.error(e.error.message);});} catch (ex) {alert(ex);}BxEventBus.Script.emit("webgpu.ready", {});}constructor($video) {super("webgpu", $video, "WebGPUPlayer");}setupShaders() {if (this.context = this.$canvas.getContext("webgpu"), !this.context) {alert("Can't initiate context");return;}let format = navigator.gpu.getPreferredCanvasFormat();this.context.configure({device: WebGPUPlayer.device,format,alphaMode: "opaque"});let shaderModule = WebGPUPlayer.device.createShaderModule({ code: clarity_boost_default });this.pipeline = WebGPUPlayer.device.createRenderPipeline({layout: "auto",vertex: {module: shaderModule,entryPoint: "vsMain"},fragment: {module: shaderModule,entryPoint: "fsMain",targets: [{ format }]},primitive: { topology: "triangle-list" }}), this.sampler = WebGPUPlayer.device.createSampler({ magFilter: "linear", minFilter: "linear" }), this.updateCanvas();}prepareUniformBuffer(value, classType) {let uniform = new classType(value), uniformBuffer = WebGPUPlayer.device.createBuffer({size: uniform.byteLength,usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST});return WebGPUPlayer.device.queue.writeBuffer(uniformBuffer, 0, uniform), uniformBuffer;}updateCanvas() {if (this.externalTexture = WebGPUPlayer.device.importExternalTexture({ source: this.$video }), !this.optionsUpdated) this.paramsBuffer = this.prepareUniformBuffer([this.toFilterId(this.options.processing),this.options.sharpness,this.options.brightness / 100,this.options.contrast / 100,this.options.saturation / 100], Float32Array), this.optionsUpdated = !0;this.bindGroup = WebGPUPlayer.device.createBindGroup({layout: this.pipeline.getBindGroupLayout(0),entries: [{ binding: 0, resource: this.sampler },{ binding: 1, resource: this.externalTexture },{ binding: 2, resource: { buffer: this.paramsBuffer } }]});}updateFrame() {this.updateCanvas();let commandEncoder = WebGPUPlayer.device.createCommandEncoder(), passEncoder = commandEncoder.beginRenderPass({colorAttachments: [{view: this.context.getCurrentTexture().createView(),loadOp: "clear",storeOp: "store",clearValue: [0, 0, 0, 1]}]});passEncoder.setPipeline(this.pipeline), passEncoder.setBindGroup(0, this.bindGroup), passEncoder.draw(6), passEncoder.end(), WebGPUPlayer.device.queue.submit([commandEncoder.finish()]);}refreshPlayer() {this.optionsUpdated = !1, this.updateCanvas();}destroy() {if (super.destroy(), this.isStopped = !0, this.pipeline = null, this.bindGroup = null, this.sampler = null, this.paramsBuffer?.destroy(), this.paramsBuffer = null, this.externalTexture = null, this.context) this.context.unconfigure(), this.context = null;console.log("WebGPU context successfully freed.");}} +class WebGPUPlayer extends BaseCanvasPlayer {static device;context;pipeline;sampler;bindGroup;optionsUpdated = !1;paramsBuffer;vertexBuffer;static async prepare() {if (!navigator.gpu) {BxEventBus.Script.emit("webgpu.ready", {});return;}try {let adapter = await navigator.gpu.requestAdapter({powerPreference: "low-power"});if (adapter) WebGPUPlayer.device = await adapter.requestDevice(), WebGPUPlayer.device.addEventListener("uncapturederror", (e) => {console.error(e.error.message);});} catch (ex) {alert(ex);}BxEventBus.Script.emit("webgpu.ready", {});}constructor($video) {super("webgpu", $video, "WebGPUPlayer");}setupShaders() {if (this.context = this.$canvas.getContext("webgpu"), !this.context) {alert("Can't initiate context");return;}let format = navigator.gpu.getPreferredCanvasFormat();this.context.configure({device: WebGPUPlayer.device,format,alphaMode: "opaque"}), this.vertexBuffer = WebGPUPlayer.device.createBuffer({label: "vertex buffer",size: 24,usage: GPUBufferUsage.VERTEX,mappedAtCreation: !0});let mappedRange = this.vertexBuffer.getMappedRange();new Float32Array(mappedRange).set([-1,3,-1,-1,3,-1]), this.vertexBuffer.unmap();let shaderModule = WebGPUPlayer.device.createShaderModule({ code: clarity_boost_default });this.pipeline = WebGPUPlayer.device.createRenderPipeline({layout: "auto",vertex: {module: shaderModule,entryPoint: "vsMain",buffers: [{arrayStride: 8,attributes: [{format: "float32x2",offset: 0,shaderLocation: 0}]}]},fragment: {module: shaderModule,entryPoint: "fsMain",targets: [{ format }]},primitive: { topology: "triangle-list" }}), this.sampler = WebGPUPlayer.device.createSampler({ magFilter: "linear", minFilter: "linear" }), this.updateCanvas();}prepareUniformBuffer(value, classType) {let uniform = new classType(value), uniformBuffer = WebGPUPlayer.device.createBuffer({size: uniform.byteLength,usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST});return WebGPUPlayer.device.queue.writeBuffer(uniformBuffer, 0, uniform), uniformBuffer;}updateCanvas() {let externalTexture = WebGPUPlayer.device.importExternalTexture({ source: this.$video });if (!this.optionsUpdated) this.paramsBuffer = this.prepareUniformBuffer([this.toFilterId(this.options.processing),this.options.sharpness,this.options.brightness / 100,this.options.contrast / 100,this.options.saturation / 100], Float32Array), this.optionsUpdated = !0;this.bindGroup = WebGPUPlayer.device.createBindGroup({layout: this.pipeline.getBindGroupLayout(0),entries: [{ binding: 0, resource: this.sampler },{ binding: 1, resource: externalTexture },{ binding: 2, resource: { buffer: this.paramsBuffer } }]});}updateFrame() {this.updateCanvas();let commandEncoder = WebGPUPlayer.device.createCommandEncoder(), passEncoder = commandEncoder.beginRenderPass({colorAttachments: [{view: this.context.getCurrentTexture().createView(),loadOp: "clear",storeOp: "store",clearValue: [0, 0, 0, 1]}]});passEncoder.setPipeline(this.pipeline), passEncoder.setBindGroup(0, this.bindGroup), passEncoder.setVertexBuffer(0, this.vertexBuffer), passEncoder.draw(3), passEncoder.end(), WebGPUPlayer.device.queue.submit([commandEncoder.finish()]);}refreshPlayer() {this.optionsUpdated = !1, this.updateCanvas();}destroy() {if (super.destroy(), this.isStopped = !0, this.pipeline = null, this.bindGroup = null, this.sampler = null, this.paramsBuffer?.destroy(), this.paramsBuffer = null, this.vertexBuffer?.destroy(), this.vertexBuffer = null, this.context) this.context.unconfigure(), this.context = null;console.log("WebGPU context successfully freed.");}} class StreamSettingsStorage extends BaseSettingsStorage {static DEFINITIONS = {"deviceVibration.mode": {requiredVariants: "full",label: t("device-vibration"),default: "off",options: {off: t("off"),on: t("on"),auto: t("device-vibration-not-using-gamepad")}},"deviceVibration.intensity": {requiredVariants: "full",label: t("vibration-intensity"),default: 50,min: 10,max: 100,params: {steps: 10,suffix: "%",exactTicks: 20}},"controller.pollingRate": {requiredVariants: "full",label: t("polling-rate"),default: 4,min: 4,max: 60,params: {steps: 4,exactTicks: 20,reverse: !0,customTextValue(value) {value = parseInt(value);let text = +(1000 / value).toFixed(2) + " Hz";if (value === 4) text = `${text} (${t("default")})`;return text;}}},"controller.settings": {default: {}},"nativeMkb.scroll.sensitivityX": {requiredVariants: "full",label: t("horizontal-scroll-sensitivity"),default: 0,min: 0,max: 1e4,params: {steps: 10,exactTicks: 2000,customTextValue: (value) => {if (!value) return t("default");return (value / 100).toFixed(1) + "x";}}},"nativeMkb.scroll.sensitivityY": {requiredVariants: "full",label: t("vertical-scroll-sensitivity"),default: 0,min: 0,max: 1e4,params: {steps: 10,exactTicks: 2000,customTextValue: (value) => {if (!value) return t("default");return (value / 100).toFixed(1) + "x";}}},"mkb.p1.preset.mappingId": {requiredVariants: "full",default: -1},"mkb.p1.slot": {requiredVariants: "full",default: 1,min: 1,max: 4,params: {hideSlider: !0}},"mkb.p2.preset.mappingId": {requiredVariants: "full",default: 0},"mkb.p2.slot": {requiredVariants: "full",default: 0,min: 0,max: 4,params: {hideSlider: !0,customTextValue(value) {return value = parseInt(value), value === 0 ? t("off") : value.toString();}}},"keyboardShortcuts.preset.inGameId": {requiredVariants: "full",default: -1},"video.player.type": {label: t("renderer"),default: "default",options: {default: t("default"),webgl2: t("webgl2"),webgpu: `${t("webgpu")} (${t("experimental")})`},suggest: {lowest: "default",highest: "webgl2"},ready: (setting) => {BxEventBus.Script.on("webgpu.ready", () => {if (!navigator.gpu || !WebGPUPlayer.device) delete setting.options["webgpu"];});}},"video.processing": {label: t("clarity-boost"),default: "usm",options: {usm: t("unsharp-masking"),cas: t("amd-fidelity-cas")},suggest: {lowest: "usm",highest: "cas"}},"video.player.powerPreference": {label: t("renderer-configuration"),default: "default",options: {default: t("default"),"low-power": t("battery-saving"),"high-performance": t("high-performance")},suggest: {highest: "low-power"}},"video.maxFps": {label: t("limit-fps"),default: 60,min: 10,max: 60,params: {steps: 10,exactTicks: 10,customTextValue: (value) => {return value = parseInt(value), value === 60 ? t("unlimited") : value + "fps";}}},"video.processing.sharpness": {label: t("sharpness"),default: 0,min: 0,max: 10,params: {exactTicks: 2,customTextValue: (value) => {return value = parseInt(value), value === 0 ? t("off") : value.toString();}},suggest: {lowest: 0,highest: 2}},"video.ratio": {label: t("aspect-ratio"),note: STATES.browser.capabilities.touch ? t("aspect-ratio-note") : void 0,default: "16:9",options: {"16:9": `16:9 (${t("default")})`,"18:9": "18:9","21:9": "21:9","16:10": "16:10","4:3": "4:3",fill: t("stretch")}},"video.position": {label: t("position"),note: STATES.browser.capabilities.touch ? t("aspect-ratio-note") : void 0,default: "center",options: {top: t("top"),"top-half": t("top-half"),center: `${t("center")} (${t("default")})`,"bottom-half": t("bottom-half"),bottom: t("bottom")}},"video.saturation": {label: t("saturation"),default: 100,min: 50,max: 150,params: {suffix: "%",ticks: 25}},"video.contrast": {label: t("contrast"),default: 100,min: 50,max: 150,params: {suffix: "%",ticks: 25}},"video.brightness": {label: t("brightness"),default: 100,min: 50,max: 150,params: {suffix: "%",ticks: 25}},"audio.volume": {label: t("volume"),default: 100,min: 0,max: 600,params: {steps: 10,suffix: "%",ticks: 100}},"stats.items": {label: t("stats"),default: ["ping", "fps", "btr", "dt", "pl", "fl"],multipleOptions: {time: t("clock"),play: t("playtime"),batt: t("battery"),ping: t("stat-ping"),jit: t("jitter"),fps: t("stat-fps"),btr: t("stat-bitrate"),dt: t("stat-decode-time"),pl: t("stat-packets-lost"),fl: t("stat-frames-lost"),dl: t("downloaded"),ul: t("uploaded")},params: {size: 0},ready: (setting) => {let multipleOptions = setting.multipleOptions;if (!STATES.browser.capabilities.batteryApi) delete multipleOptions["batt"];for (let key in multipleOptions)multipleOptions[key] = key.toUpperCase() + ": " + multipleOptions[key];}},"stats.showWhenPlaying": {label: t("show-stats-on-startup"),default: !1},"stats.quickGlance.enabled": {label: "👀 " + t("enable-quick-glance-mode"),default: !0},"stats.position": {label: t("position"),default: "top-right",options: {"top-left": t("top-left"),"top-center": t("top-center"),"top-right": t("top-right")}},"stats.textSize": {label: t("text-size"),default: "0.9rem",options: {"0.9rem": t("small"),"1.0rem": t("normal"),"1.1rem": t("large")}},"stats.opacity.all": {label: t("opacity"),default: 80,min: 50,max: 100,params: {steps: 10,suffix: "%",ticks: 10}},"stats.opacity.background": {label: t("background-opacity"),default: 100,min: 0,max: 100,params: {steps: 10,suffix: "%",ticks: 10}},"stats.colors": {label: t("conditional-formatting"),default: !1},"localCoOp.enabled": {requiredVariants: "full",label: t("enable-local-co-op-support"),labelIcon: BxIcon.LOCAL_CO_OP,default: !1,note: () => CE("div", !1, CE("a", {href: "https://github.com/redphx/better-xcloud/discussions/275",target: "_blank"}, t("enable-local-co-op-support-note")), CE("br"), "⚠️ " + t("unexpected-behavior"))}};gameSettings = {};xboxTitleId = -1;constructor() {super("BetterXcloud.Stream", StreamSettingsStorage.DEFINITIONS);}setGameId(id) {this.xboxTitleId = id;}getGameSettings(id) {if (id > -1) {if (!this.gameSettings[id]) {let gameStorage = new GameSettingsStorage(id);this.gameSettings[id] = gameStorage;for (let key in gameStorage.settings)this.getSettingByGame(id, key);}return this.gameSettings[id];}return null;}getSetting(key, checkUnsupported) {return this.getSettingByGame(this.xboxTitleId, key, checkUnsupported);}getSettingByGame(id, key, checkUnsupported) {let gameSettings = this.getGameSettings(id);if (gameSettings?.hasSetting(key)) {let gameValue = gameSettings.getSetting(key, checkUnsupported), globalValue = super.getSetting(key, checkUnsupported);if (globalValue === gameValue) this.deleteSettingByGame(id, key), gameValue = globalValue;return gameValue;}return super.getSetting(key, checkUnsupported);}setSetting(key, value, origin) {return this.setSettingByGame(this.xboxTitleId, key, value, origin);}setSettingByGame(id, key, value, origin) {let gameSettings = this.getGameSettings(id);if (gameSettings) return BxLogger.info("setSettingByGame", id, key, value), gameSettings.setSetting(key, value, origin);return BxLogger.info("setSettingByGame", id, key, value), super.setSetting(key, value, origin);}deleteSettingByGame(id, key) {let gameSettings = this.getGameSettings(id);if (gameSettings) return gameSettings.deleteSetting(key);return !1;}hasGameSetting(id, key) {let gameSettings = this.getGameSettings(id);return !!(gameSettings && gameSettings.hasSetting(key));}getControllerSetting(gamepadId) {let controllerSetting = this.getSetting("controller.settings")[gamepadId];if (!controllerSetting) controllerSetting = {};if (!controllerSetting.hasOwnProperty("shortcutPresetId")) controllerSetting.shortcutPresetId = -1;if (!controllerSetting.hasOwnProperty("customizationPresetId")) controllerSetting.customizationPresetId = 0;return controllerSetting;}} function migrateStreamSettings() {let storage = window.localStorage, globalSettings = JSON.parse(storage.getItem("BetterXcloud") || "{}"), streamSettings = JSON.parse(storage.getItem("BetterXcloud.Stream") || "{}"), modified2 = !1;for (let key in globalSettings)if (isStreamPref(key)) {if (!streamSettings.hasOwnProperty(key)) streamSettings[key] = globalSettings[key];delete globalSettings[key], modified2 = !0;}if (modified2) storage.setItem("BetterXcloud", JSON.stringify(globalSettings)), storage.setItem("BetterXcloud.Stream", JSON.stringify(streamSettings));} migrateStreamSettings(); @@ -279,7 +275,7 @@ class TrueAchievements {static instance;static getInstance = () => TrueAchieveme class VirtualControllerShortcut {static pressXboxButton() {let streamSession = window.BX_EXPOSED.streamSession;if (!streamSession) return;let released = generateVirtualControllerMapping(0), pressed = generateVirtualControllerMapping(0, {Nexus: 1,VirtualPhysicality: 1024});streamSession.onVirtualGamepadInput("systemMenu", performance.now(), [pressed]), setTimeout(() => {streamSession.onVirtualGamepadInput("systemMenu", performance.now(), [released]);}, 100);}} class ShortcutHandler {static runAction(action) {switch (action) {case "bx.settings.show":SettingsDialog.getInstance().show();break;case "stream.screenshot.capture":ScreenshotManager.getInstance().takeScreenshot();break;case "stream.video.toggle":RendererShortcut.toggleVisibility();break;case "stream.stats.toggle":StreamStats.getInstance().toggle();break;case "stream.microphone.toggle":MicrophoneShortcut.toggle();break;case "stream.menu.show":StreamUiShortcut.showHideStreamMenu();break;case "stream.sound.toggle":SoundShortcut.muteUnmute();break;case "stream.volume.inc":SoundShortcut.adjustGainNodeVolume(10);break;case "stream.volume.dec":SoundShortcut.adjustGainNodeVolume(-10);break;case "device.brightness.inc":case "device.brightness.dec":case "device.sound.toggle":case "device.volume.inc":case "device.volume.dec":AppInterface && AppInterface.runShortcut && AppInterface.runShortcut(action);break;case "mkb.toggle":if (STATES.currentStream.titleInfo?.details.hasMkbSupport) NativeMkbHandler.getInstance()?.toggle();else EmulatedMkbHandler.getInstance()?.toggle();break;case "ta.open":TrueAchievements.getInstance().open(!1);break;case "controller.xbox.press":VirtualControllerShortcut.pressXboxButton();break;}}} class ControllerShortcut {static buttonsCache = {};static buttonsStatus = {};static reset(index) {ControllerShortcut.buttonsCache[index] = [], ControllerShortcut.buttonsStatus[index] = [];}static handle(gamepad) {let controllerSettings = window.BX_STREAM_SETTINGS.controllers[gamepad.id];if (!controllerSettings) return !1;let actions = controllerSettings.shortcuts;if (!actions) return !1;let gamepadIndex = gamepad.index;ControllerShortcut.buttonsCache[gamepadIndex] = ControllerShortcut.buttonsStatus[gamepadIndex].slice(0), ControllerShortcut.buttonsStatus[gamepadIndex] = [];let pressed = [], otherButtonPressed = !1, entries = gamepad.buttons.entries(), index, button;for ([index, button] of entries)if (button.pressed && index !== 16) {if (otherButtonPressed = !0, pressed[index] = !0, actions[index] && !ControllerShortcut.buttonsCache[gamepadIndex][index]) {let idx = index;setTimeout(() => ShortcutHandler.runAction(actions[idx]), 0);}}return ControllerShortcut.buttonsStatus[gamepadIndex] = pressed, otherButtonPressed;}} -var FeatureGates = {PwaPrompt: !1,EnableWifiWarnings: !1,EnableUpdateRequiredPage: !1,ShowForcedUpdateScreen: !1,EnableTakControlResizing: !0,EnableLazyLoadedHome: !0}, nativeMkbMode = getGlobalPref("nativeMkb.mode"); +var FeatureGates = {PwaPrompt: !1,EnableWifiWarnings: !1,EnableUpdateRequiredPage: !1,ShowForcedUpdateScreen: !1,EnableTakControlResizing: !0,EnableLazyLoadedHome: !1}, nativeMkbMode = getGlobalPref("nativeMkb.mode"); if (nativeMkbMode !== "default") FeatureGates.EnableMouseAndKeyboard = nativeMkbMode === "on"; var blockFeatures = getGlobalPref("block.features"); if (blockFeatures.includes("chat")) FeatureGates.EnableGuideChatTab = !1; @@ -328,7 +324,7 @@ uniform sampler2D data; uniform vec2 iResolution; const int FILTER_UNSHARP_MASKING = 1; const float CAS_CONTRAST_PEAK = 0.8 * -3.0 + 8.0; -const vec3 LUMINOSITY_FACTOR = vec3(0.2126, 0.7152, 0.0722); +const vec3 LUMINOSITY_FACTOR = vec3(0.299, 0.587, 0.114); uniform int filterId; uniform float sharpenFactor; uniform float brightness; @@ -369,7 +365,7 @@ color = saturation != 1.0 ? mix(vec3(dot(color, LUMINOSITY_FACTOR)), color, satu color = contrast * (color - 0.5) + 0.5; color = brightness * color; fragColor = vec4(color, 1.0);}`; -class WebGL2Player extends BaseCanvasPlayer {gl = null;resources = [];program = null;constructor($video) {super("webgl2", $video, "WebGL2Player");}updateCanvas() {console.log("updateCanvas", this.options);let gl = this.gl, program = this.program, filterId = this.toFilterId(this.options.processing);gl.uniform2f(gl.getUniformLocation(program, "iResolution"), this.$canvas.width, this.$canvas.height), gl.uniform1i(gl.getUniformLocation(program, "filterId"), filterId), gl.uniform1f(gl.getUniformLocation(program, "sharpenFactor"), this.options.sharpness), gl.uniform1f(gl.getUniformLocation(program, "brightness"), this.options.brightness / 100), gl.uniform1f(gl.getUniformLocation(program, "contrast"), this.options.contrast / 100), gl.uniform1f(gl.getUniformLocation(program, "saturation"), this.options.saturation / 100);}updateFrame() {let gl = this.gl;gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, this.$video), gl.drawArrays(gl.TRIANGLES, 0, 6);}async setupShaders() {let gl = this.$canvas.getContext("webgl2", {isBx: !0,antialias: !0,alpha: !1,powerPreference: getStreamPref("video.player.powerPreference")});this.gl = gl, gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferWidth);let vShader = gl.createShader(gl.VERTEX_SHADER);gl.shaderSource(vShader, clarity_boost_default2), gl.compileShader(vShader);let fShader = gl.createShader(gl.FRAGMENT_SHADER);gl.shaderSource(fShader, clarity_boost_default3), gl.compileShader(fShader);let program = gl.createProgram();if (this.program = program, gl.attachShader(program, vShader), gl.attachShader(program, fShader), gl.linkProgram(program), gl.useProgram(program), !gl.getProgramParameter(program, gl.LINK_STATUS)) console.error(`Link failed: ${gl.getProgramInfoLog(program)}`), console.error(`vs info-log: ${gl.getShaderInfoLog(vShader)}`), console.error(`fs info-log: ${gl.getShaderInfoLog(fShader)}`);this.updateCanvas();let buffer = gl.createBuffer();this.resources.push(buffer), gl.bindBuffer(gl.ARRAY_BUFFER, buffer), gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]), gl.STATIC_DRAW), gl.enableVertexAttribArray(0), gl.vertexAttribPointer(0, 2, gl.FLOAT, !1, 0, 0);let texture = gl.createTexture();this.resources.push(texture), gl.bindTexture(gl.TEXTURE_2D, texture), gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, !0), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR), gl.uniform1i(gl.getUniformLocation(program, "data"), 0), gl.activeTexture(gl.TEXTURE0);}destroy() {super.destroy();let gl = this.gl;if (!gl) return;gl.getExtension("WEBGL_lose_context")?.loseContext(), gl.useProgram(null);for (let resource of this.resources)if (resource instanceof WebGLProgram) gl.deleteProgram(resource);else if (resource instanceof WebGLShader) gl.deleteShader(resource);else if (resource instanceof WebGLTexture) gl.deleteTexture(resource);else if (resource instanceof WebGLBuffer) gl.deleteBuffer(resource);this.gl = null;}refreshPlayer() {this.updateCanvas();}} +class WebGL2Player extends BaseCanvasPlayer {gl = null;resources = [];program = null;constructor($video) {super("webgl2", $video, "WebGL2Player");}updateCanvas() {console.log("updateCanvas", this.options);let gl = this.gl, program = this.program, filterId = this.toFilterId(this.options.processing);gl.uniform2f(gl.getUniformLocation(program, "iResolution"), this.$canvas.width, this.$canvas.height), gl.uniform1i(gl.getUniformLocation(program, "filterId"), filterId), gl.uniform1f(gl.getUniformLocation(program, "sharpenFactor"), this.options.sharpness), gl.uniform1f(gl.getUniformLocation(program, "brightness"), this.options.brightness / 100), gl.uniform1f(gl.getUniformLocation(program, "contrast"), this.options.contrast / 100), gl.uniform1f(gl.getUniformLocation(program, "saturation"), this.options.saturation / 100);}updateFrame() {let gl = this.gl;gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, this.$video), gl.drawArrays(gl.TRIANGLES, 0, 6);}async setupShaders() {let gl = this.$canvas.getContext("webgl2", {isBx: !0,antialias: !0,alpha: !1,depth: !1,preserveDrawingBuffer: !1,stencil: !1,powerPreference: getStreamPref("video.player.powerPreference")});this.gl = gl, gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferWidth);let vShader = gl.createShader(gl.VERTEX_SHADER);gl.shaderSource(vShader, clarity_boost_default2), gl.compileShader(vShader);let fShader = gl.createShader(gl.FRAGMENT_SHADER);gl.shaderSource(fShader, clarity_boost_default3), gl.compileShader(fShader);let program = gl.createProgram();if (this.program = program, gl.attachShader(program, vShader), gl.attachShader(program, fShader), gl.linkProgram(program), gl.useProgram(program), !gl.getProgramParameter(program, gl.LINK_STATUS)) console.error(`Link failed: ${gl.getProgramInfoLog(program)}`), console.error(`vs info-log: ${gl.getShaderInfoLog(vShader)}`), console.error(`fs info-log: ${gl.getShaderInfoLog(fShader)}`);this.updateCanvas();let buffer = gl.createBuffer();this.resources.push(buffer), gl.bindBuffer(gl.ARRAY_BUFFER, buffer), gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]), gl.STATIC_DRAW), gl.enableVertexAttribArray(0), gl.vertexAttribPointer(0, 2, gl.FLOAT, !1, 0, 0);let texture = gl.createTexture();this.resources.push(texture), gl.bindTexture(gl.TEXTURE_2D, texture), gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, !0), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR), gl.uniform1i(gl.getUniformLocation(program, "data"), 0), gl.activeTexture(gl.TEXTURE0);}destroy() {super.destroy();let gl = this.gl;if (!gl) return;gl.getExtension("WEBGL_lose_context")?.loseContext(), gl.useProgram(null);for (let resource of this.resources)if (resource instanceof WebGLProgram) gl.deleteProgram(resource);else if (resource instanceof WebGLShader) gl.deleteShader(resource);else if (resource instanceof WebGLTexture) gl.deleteTexture(resource);else if (resource instanceof WebGLBuffer) gl.deleteBuffer(resource);this.gl = null;}refreshPlayer() {this.updateCanvas();}} class VideoPlayer extends BaseStreamPlayer {$videoCss;$usmMatrix;constructor($video, logTag) {super("default", "video", $video, logTag);}init() {super.init();let xmlns = "http://www.w3.org/2000/svg", $svg = CE("svg", {id: "bx-video-filters",class: "bx-gone",xmlns}, CE("defs", { xmlns: "http://www.w3.org/2000/svg" }, CE("filter", {id: "bx-filter-usm",xmlns}, this.$usmMatrix = CE("feConvolveMatrix", {id: "bx-filter-usm-matrix",order: "3",xmlns}))));this.$videoCss = CE("style", { id: "bx-video-css" });let $fragment = document.createDocumentFragment();$fragment.append(this.$videoCss, $svg), document.documentElement.appendChild($fragment);}setupRendering() {}forceDrawFrame() {}updateCanvas() {}refreshPlayer() {let filters = this.getVideoPlayerFilterStyle(), videoCss = "";if (filters) videoCss += `filter: ${filters} !important;`;if (getGlobalPref("screenshot.applyFilters")) ScreenshotManager.getInstance().updateCanvasFilters(filters);let css = "";if (videoCss) css = `#game-stream video { ${videoCss} }`;this.$videoCss.textContent = css;}clearFilters() {this.$videoCss.textContent = "";}getVideoPlayerFilterStyle() {let filters = [], sharpness = this.options.sharpness || 0;if (this.options.processing === "usm" && sharpness != 0) {let matrix = `0 -1 0 -1 ${(7 - (sharpness / 2 - 1) * 0.5).toFixed(1)} -1 0 -1 0`;this.$usmMatrix?.setAttributeNS(null, "kernelMatrix", matrix), filters.push("url(#bx-filter-usm)");}let saturation = this.options.saturation || 100;if (saturation != 100) filters.push(`saturate(${saturation}%)`);let contrast = this.options.contrast || 100;if (contrast != 100) filters.push(`contrast(${contrast}%)`);let brightness = this.options.brightness || 100;if (brightness != 100) filters.push(`brightness(${brightness}%)`);return filters.join(" ");}} class StreamPlayerManager {static instance;static getInstance = () => StreamPlayerManager.instance ?? (StreamPlayerManager.instance = new StreamPlayerManager);$video;videoPlayer;canvasPlayer;playerType = "default";constructor() {}setVideoElement($video) {this.$video = $video, this.videoPlayer = new VideoPlayer($video, "VideoPlayer"), this.videoPlayer.init();}resizePlayer() {let PREF_RATIO = getStreamPref("video.ratio"), $video = this.$video, isNativeTouchGame = STATES.currentStream.titleInfo?.details.hasNativeTouchSupport, targetWidth, targetHeight, targetObjectFit;if (PREF_RATIO.includes(":")) {let tmp = PREF_RATIO.split(":"), videoRatio = parseFloat(tmp[0]) / parseFloat(tmp[1]), width = 0, height = 0, parentRect = $video.parentElement.getBoundingClientRect();if (parentRect.width / parentRect.height > videoRatio) height = parentRect.height, width = height * videoRatio;else width = parentRect.width, height = width / videoRatio;width = Math.ceil(Math.min(parentRect.width, width)), height = Math.ceil(Math.min(parentRect.height, height)), $video.dataset.width = width.toString(), $video.dataset.height = height.toString();let $parent = $video.parentElement, position = getStreamPref("video.position");if ($parent.style.removeProperty("padding-top"), $parent.dataset.position = position, position === "top-half" || position === "bottom-half") {let padding = Math.floor((window.innerHeight - height) / 4);if (padding > 0) {if (position === "bottom-half") padding *= 3;$parent.style.paddingTop = padding + "px";}}targetWidth = `${width}px`, targetHeight = `${height}px`, targetObjectFit = PREF_RATIO === "16:9" ? "contain" : "fill";} else targetWidth = "100%", targetHeight = "100%", targetObjectFit = PREF_RATIO, $video.dataset.width = window.innerWidth.toString(), $video.dataset.height = window.innerHeight.toString();if ($video.style.width = targetWidth, $video.style.height = targetHeight, $video.style.objectFit = targetObjectFit, this.canvasPlayer) {let $canvas = this.canvasPlayer.getCanvas();$canvas.style.width = targetWidth, $canvas.style.height = targetHeight, $canvas.style.objectFit = targetObjectFit, $video.dispatchEvent(new Event("resize"));}if (isNativeTouchGame && this.playerType !== "default") window.BX_EXPOSED.streamSession.updateDimensions();}switchPlayerType(type, refreshPlayer = !1) {if (this.playerType !== type) {let videoClass = BX_FLAGS.DeviceInfo.deviceType === "android-tv" ? "bx-pixel" : "bx-gone";if (this.cleanUpCanvasPlayer(), type === "default") this.$video.classList.remove(videoClass);else {if (type === "webgpu") this.canvasPlayer = new WebGPUPlayer(this.$video);else this.canvasPlayer = new WebGL2Player(this.$video);this.canvasPlayer.init(), this.videoPlayer.clearFilters(), this.$video.classList.add(videoClass);}this.playerType = type;}refreshPlayer && this.refreshPlayer();}updateOptions(options, refreshPlayer = !1) {(this.canvasPlayer || this.videoPlayer).updateOptions(options, refreshPlayer);}getPlayerElement(elementType) {if (typeof elementType === "undefined") elementType = this.playerType === "default" ? "video" : "canvas";if (elementType !== "video") return this.canvasPlayer?.getCanvas();return this.$video;}getCanvasPlayer() {return this.canvasPlayer;}refreshPlayer() {if (this.playerType === "default") this.videoPlayer.refreshPlayer();else ScreenshotManager.getInstance().updateCanvasFilters("none"), this.canvasPlayer?.refreshPlayer();this.resizePlayer();}getVideoPlayerFilterStyle() {throw new Error("Method not implemented.");}cleanUpCanvasPlayer() {this.canvasPlayer?.destroy(), this.canvasPlayer = null;}destroy() {this.cleanUpCanvasPlayer();}} function patchVideoApi() {let PREF_SKIP_SPLASH_VIDEO = getGlobalPref("ui.splashVideo.skip"), showFunc = function() {if (this.style.visibility = "visible", !this.videoWidth) return;let playerOptions = {processing: getStreamPref("video.processing"),sharpness: getStreamPref("video.processing.sharpness"),saturation: getStreamPref("video.saturation"),contrast: getStreamPref("video.contrast"),brightness: getStreamPref("video.brightness")}, streamPlayerManager = StreamPlayerManager.getInstance();streamPlayerManager.setVideoElement(this), streamPlayerManager.updateOptions(playerOptions, !1), streamPlayerManager.switchPlayerType(getStreamPref("video.player.type")), STATES.currentStream.streamPlayerManager = streamPlayerManager, BxEventBus.Stream.emit("state.playing", {$video: this});}, nativePlay = HTMLMediaElement.prototype.play;HTMLMediaElement.prototype.nativePlay = nativePlay, HTMLMediaElement.prototype.play = function() {if (this.className && this.className.startsWith("XboxSplashVideo")) {if (PREF_SKIP_SPLASH_VIDEO) return this.volume = 0, this.style.display = "none", this.dispatchEvent(new Event("ended")), new Promise(() => {});return nativePlay.apply(this);}let $parent = this.parentElement;if (!this.src && $parent.dataset.testid === "media-container") this.addEventListener("loadedmetadata", showFunc, { once: !0 });return nativePlay.apply(this);};}