diff --git a/Core/GDCore/Project/Project.h b/Core/GDCore/Project/Project.h
index 59a300206168..6ba0ef36bca2 100644
--- a/Core/GDCore/Project/Project.h
+++ b/Core/GDCore/Project/Project.h
@@ -374,12 +374,14 @@ class GD_CORE_API Project {
   void SetVerticalSyncActivatedByDefault(bool enable) { verticalSync = enable; }
 
   /**
-   * Return the scale mode used by the game (usually "linear" or "nearest").
+   * Return the scale mode used by the game (usually "linear", "magnified" or
+   * "nearest").
    */
   const gd::String& GetScaleMode() const { return scaleMode; }
 
   /**
-   * Set the scale mode used by the game (usually "linear" or "nearest").
+   * Set the scale mode used by the game (usually "linear", "magnified" or
+   * "nearest").
    */
   void SetScaleMode(const gd::String& scaleMode_) { scaleMode = scaleMode_; }
 
diff --git a/Extensions/TextInput/textinputruntimeobject-pixi-renderer.ts b/Extensions/TextInput/textinputruntimeobject-pixi-renderer.ts
index 91f2ae3faf00..c09e697396ea 100644
--- a/Extensions/TextInput/textinputruntimeobject-pixi-renderer.ts
+++ b/Extensions/TextInput/textinputruntimeobject-pixi-renderer.ts
@@ -211,8 +211,8 @@ namespace gdjs {
       const isOutsideCanvas =
         canvasRight < 0 ||
         canvasBottom < 0 ||
-        canvasLeft > runtimeGame.getGameResolutionWidth() ||
-        canvasTop > runtimeGame.getGameResolutionHeight();
+        canvasLeft > runtimeGame.getRenderingResolutionWidth() ||
+        canvasTop > runtimeGame.getRenderingResolutionHeight();
       if (isOutsideCanvas) {
         this._form.style.display = 'none';
         return;
diff --git a/Extensions/TextObject/textruntimeobject-pixi-renderer.ts b/Extensions/TextObject/textruntimeobject-pixi-renderer.ts
index 0fe94a9b8085..1cf12ceb3deb 100644
--- a/Extensions/TextObject/textruntimeobject-pixi-renderer.ts
+++ b/Extensions/TextObject/textruntimeobject-pixi-renderer.ts
@@ -4,6 +4,7 @@ namespace gdjs {
     _fontManager: any;
     _text: PIXI.Text;
     _justCreated: boolean = true;
+    _upscaleRatio: integer = 1;
 
     constructor(
       runtimeObject: gdjs.TextRuntimeObject,
@@ -47,7 +48,7 @@ namespace gdjs {
       const style = this._text.style;
       style.fontStyle = this._object._italic ? 'italic' : 'normal';
       style.fontWeight = this._object._bold ? 'bold' : 'normal';
-      style.fontSize = this._object._characterSize;
+      style.fontSize = this._object._characterSize * this._upscaleRatio;
       style.fontFamily = fontName;
       if (this._object._useGradient) {
         style.fill = this._getGradientHex();
@@ -62,7 +63,7 @@ namespace gdjs {
       // @ts-ignore
       style.align = this._object._textAlign;
       style.wordWrap = this._object._wrapping;
-      style.wordWrapWidth = this._object._wrappingWidth;
+      style.wordWrapWidth = this._object._wrappingWidth * this._upscaleRatio;
       style.breakWords = true;
       style.stroke = gdjs.rgbToHexNumber(
         this._object._outlineColor[0],
@@ -70,7 +71,7 @@ namespace gdjs {
         this._object._outlineColor[2]
       );
       style.strokeThickness = this._object._isOutlineEnabled
-        ? this._object._outlineThickness
+        ? this._object._outlineThickness * this._upscaleRatio
         : 0;
       style.dropShadow = this._object._shadow;
       style.dropShadowColor = gdjs.rgbToHexNumber(
@@ -79,13 +80,16 @@ namespace gdjs {
         this._object._shadowColor[2]
       );
       style.dropShadowAlpha = this._object._shadowOpacity / 255;
-      style.dropShadowBlur = this._object._shadowBlur;
+      style.dropShadowBlur = this._object._shadowBlur * this._upscaleRatio;
       style.dropShadowAngle = gdjs.toRad(this._object._shadowAngle);
-      style.dropShadowDistance = this._object._shadowDistance;
+      style.dropShadowDistance =
+        this._object._shadowDistance * this._upscaleRatio;
       const extraPaddingForShadow = style.dropShadow
-        ? style.dropShadowDistance + style.dropShadowBlur
+        ? this._object._shadowDistance + this._object._shadowBlur
         : 0;
-      style.padding = Math.ceil(this._object._padding + extraPaddingForShadow);
+      style.padding =
+        Math.ceil(this._object._padding + extraPaddingForShadow) *
+        this._upscaleRatio;
 
       // Prevent spikey outlines by adding a miter limit
       style.miterLimit = 3;
@@ -117,7 +121,6 @@ namespace gdjs {
         this._text.position.x = this._object.x + this._text.width / 2;
         this._text.anchor.x = 0.5;
       }
-      this._text.position.y = this._object.y + this._text.height / 2;
 
       const alignmentY =
         this._object._verticalTextAlignment === 'bottom'
@@ -181,18 +184,30 @@ namespace gdjs {
       return gradient;
     }
 
+    /**
+     * Set the text object upscale ratio.
+     * @param upscaleRatio The new upscale ratio for the text object.
+     * @see gdjs.RuntimeGame.getZoomFactor
+     */
+    setUpscaleRatio(upscaleRatio: integer): void {
+      this._upscaleRatio = upscaleRatio;
+      this._text.scale.x = this._object.getScaleX() / this._upscaleRatio;
+      this._text.scale.y = this._object.getScaleY() / this._upscaleRatio;
+      this.updateStyle();
+    }
+
     /**
      * Get x-scale of the text.
      */
     getScaleX(): float {
-      return this._text.scale.x;
+      return this._object.getScaleX();
     }
 
     /**
      * Get y-scale of the text.
      */
     getScaleY(): float {
-      return this._text.scale.y;
+      return this._object.getScaleY();
     }
 
     /**
@@ -200,8 +215,8 @@ namespace gdjs {
      * @param newScale The new scale for the text object.
      */
     setScale(newScale: float): void {
-      this._text.scale.x = newScale;
-      this._text.scale.y = newScale;
+      this._text.scale.x = newScale / this._upscaleRatio;
+      this._text.scale.y = newScale / this._upscaleRatio;
     }
 
     /**
@@ -209,7 +224,7 @@ namespace gdjs {
      * @param newScale The new x-scale for the text object.
      */
     setScaleX(newScale: float): void {
-      this._text.scale.x = newScale;
+      this._text.scale.x = newScale / this._upscaleRatio;
     }
 
     /**
@@ -217,7 +232,7 @@ namespace gdjs {
      * @param newScale The new y-scale for the text object.
      */
     setScaleY(newScale: float): void {
-      this._text.scale.y = newScale;
+      this._text.scale.y = newScale / this._upscaleRatio;
     }
 
     destroy() {
diff --git a/Extensions/TextObject/textruntimeobject.ts b/Extensions/TextObject/textruntimeobject.ts
index abd6ea72d914..e5b6437c2539 100644
--- a/Extensions/TextObject/textruntimeobject.ts
+++ b/Extensions/TextObject/textruntimeobject.ts
@@ -149,7 +149,7 @@ namespace gdjs {
       this.onCreated();
     }
 
-    updateFromObjectData(
+    override updateFromObjectData(
       oldObjectData: TextObjectData,
       newObjectData: TextObjectData
     ): boolean {
@@ -214,7 +214,7 @@ namespace gdjs {
       return true;
     }
 
-    getNetworkSyncData(): TextObjectNetworkSyncData {
+    override getNetworkSyncData(): TextObjectNetworkSyncData {
       return {
         ...super.getNetworkSyncData(),
         str: this._str,
@@ -242,7 +242,7 @@ namespace gdjs {
       };
     }
 
-    updateFromNetworkSyncData(
+    override updateFromNetworkSyncData(
       networkSyncData: TextObjectNetworkSyncData
     ): void {
       super.updateFromNetworkSyncData(networkSyncData);
@@ -317,15 +317,15 @@ namespace gdjs {
       }
     }
 
-    getRendererObject() {
+    override getRendererObject() {
       return this._renderer.getRendererObject();
     }
 
-    update(instanceContainer: gdjs.RuntimeInstanceContainer): void {
+    override update(instanceContainer: gdjs.RuntimeInstanceContainer): void {
       this._renderer.ensureUpToDate();
     }
 
-    onDestroyed(): void {
+    override onDestroyed(): void {
       super.onDestroyed();
       this._renderer.destroy();
     }
@@ -345,6 +345,12 @@ namespace gdjs {
       }
     }
 
+    override onGameZoomFactorChanged(): void {
+      this._renderer.setUpscaleRatio(
+        this.getRuntimeScene().getGame().getZoomFactor()
+      );
+    }
+
     /**
      * Update the rendered object position.
      */
@@ -353,27 +359,17 @@ namespace gdjs {
       this._renderer.updatePosition();
     }
 
-    /**
-     * Set object position on X axis.
-     */
-    setX(x: float): void {
+    override setX(x: float): void {
       super.setX(x);
       this._updateTextPosition();
     }
 
-    /**
-     * Set object position on Y axis.
-     */
-    setY(y: float): void {
+    override setY(y: float): void {
       super.setY(y);
       this._updateTextPosition();
     }
 
-    /**
-     * Set the angle of the object.
-     * @param angle The new angle of the object
-     */
-    setAngle(angle: float): void {
+    override setAngle(angle: float): void {
       super.setAngle(angle);
       this._renderer.updateAngle();
     }
@@ -499,14 +495,14 @@ namespace gdjs {
     /**
      * Get width of the text.
      */
-    getWidth(): float {
+    override getWidth(): float {
       return this._wrapping ? this._wrappingWidth : this._renderer.getWidth();
     }
 
     /**
      * Get height of the text.
      */
-    getHeight(): float {
+    override getHeight(): float {
       return this._renderer.getHeight();
     }
 
@@ -685,11 +681,11 @@ namespace gdjs {
       }
     }
 
-    setWidth(width: float): void {
+    override setWidth(width: float): void {
       this.setWrappingWidth(width);
     }
 
-    getDrawableY(): float {
+    override getDrawableY(): float {
       return (
         this.getY() -
         (this._verticalTextAlignment === 'center'
diff --git a/GDJS/Runtime/CustomRuntimeObject.ts b/GDJS/Runtime/CustomRuntimeObject.ts
index 3c597d8ce06c..f650460d4f63 100644
--- a/GDJS/Runtime/CustomRuntimeObject.ts
+++ b/GDJS/Runtime/CustomRuntimeObject.ts
@@ -229,6 +229,10 @@ namespace gdjs {
      */
     onDestroy(parent: gdjs.RuntimeInstanceContainer) {}
 
+    override onGameZoomFactorChanged() {
+      this._instanceContainer.onGameZoomFactorChanged();
+    }
+
     override updatePreRender(parent: gdjs.RuntimeInstanceContainer): void {
       this._instanceContainer._updateObjectsPreRender();
       this.getRenderer().ensureUpToDate();
diff --git a/GDJS/Runtime/RuntimeInstanceContainer.ts b/GDJS/Runtime/RuntimeInstanceContainer.ts
index e4ef5eee2099..b33e06bddc04 100644
--- a/GDJS/Runtime/RuntimeInstanceContainer.ts
+++ b/GDJS/Runtime/RuntimeInstanceContainer.ts
@@ -547,6 +547,15 @@ namespace gdjs {
       return this._allInstancesList;
     }
 
+    /**
+     * Called when the game zoom factor is changed to adapt to a new resolution.
+     */
+    onGameZoomFactorChanged(): void {
+      for (const instance of this.getAdhocListOfAllInstances()) {
+        instance.onGameZoomFactorChanged();
+      }
+    }
+
     /**
      * Update the objects before launching the events.
      */
diff --git a/GDJS/Runtime/pixi-renderers/DebuggerPixiRenderer.ts b/GDJS/Runtime/pixi-renderers/DebuggerPixiRenderer.ts
index 1957181a9ff4..2df63a192a28 100644
--- a/GDJS/Runtime/pixi-renderers/DebuggerPixiRenderer.ts
+++ b/GDJS/Runtime/pixi-renderers/DebuggerPixiRenderer.ts
@@ -37,18 +37,10 @@ namespace gdjs {
       showPointsNames: boolean,
       showCustomPoints: boolean
     ) {
-      const pixiContainer = this._instanceContainer
-        .getRenderer()
-        .getRendererObject();
       if (!this._debugDraw || !this._debugDrawContainer) {
         this._debugDrawContainer = new PIXI.Container();
         this._debugDraw = new PIXI.Graphics();
-
-        // Add on top of all layers:
         this._debugDrawContainer.addChild(this._debugDraw);
-        if (pixiContainer) {
-          pixiContainer.addChild(this._debugDrawContainer);
-        }
       }
       const debugDraw = this._debugDraw;
 
@@ -112,7 +104,8 @@ namespace gdjs {
         const polygon: float[] = [];
         polygon.push.apply(
           polygon,
-          layer.applyLayerTransformation(
+          this.applyLayerTransformation(
+            layer,
             aabb.min[0],
             aabb.min[1],
             0,
@@ -121,7 +114,8 @@ namespace gdjs {
         );
         polygon.push.apply(
           polygon,
-          layer.applyLayerTransformation(
+          this.applyLayerTransformation(
+            layer,
             aabb.max[0],
             aabb.min[1],
             0,
@@ -130,7 +124,8 @@ namespace gdjs {
         );
         polygon.push.apply(
           polygon,
-          layer.applyLayerTransformation(
+          this.applyLayerTransformation(
+            layer,
             aabb.max[0],
             aabb.max[1],
             0,
@@ -139,7 +134,8 @@ namespace gdjs {
         );
         polygon.push.apply(
           polygon,
-          layer.applyLayerTransformation(
+          this.applyLayerTransformation(
+            layer,
             aabb.min[0],
             aabb.max[1],
             0,
@@ -185,7 +181,8 @@ namespace gdjs {
           // as this is for debug draw.
           const polygon: float[] = [];
           hitboxes[j].vertices.forEach((point) => {
-            point = layer.applyLayerTransformation(
+            point = this.applyLayerTransformation(
+              layer,
               point[0],
               point[1],
               0,
@@ -205,7 +202,8 @@ namespace gdjs {
         debugDraw.fill.alpha = 0.3;
 
         // Draw Center point
-        const centerPoint = layer.applyLayerTransformation(
+        const centerPoint = this.applyLayerTransformation(
+          layer,
           object.getCenterXInScene(),
           object.getCenterYInScene(),
           0,
@@ -221,7 +219,8 @@ namespace gdjs {
         );
 
         // Draw position point
-        const positionPoint = layer.applyLayerTransformation(
+        const positionPoint = this.applyLayerTransformation(
+          layer,
           object.getX(),
           object.getY(),
           0,
@@ -245,7 +244,8 @@ namespace gdjs {
             Math.abs(originPoint[0] - positionPoint[0]) >= 1 ||
             Math.abs(originPoint[1] - positionPoint[1]) >= 1
           ) {
-            originPoint = layer.applyLayerTransformation(
+            originPoint = this.applyLayerTransformation(
+              layer,
               originPoint[0],
               originPoint[1],
               0,
@@ -270,7 +270,8 @@ namespace gdjs {
           for (const customPointName in animationFrame.points.items) {
             let customPoint = object.getPointPosition(customPointName);
 
-            customPoint = layer.applyLayerTransformation(
+            customPoint = this.applyLayerTransformation(
+              layer,
               customPoint[0],
               customPoint[1],
               0,
@@ -303,6 +304,27 @@ namespace gdjs {
       debugDraw.endFill();
     }
 
+    private applyLayerTransformation(
+      layer: gdjs.RuntimeLayer,
+      x: float,
+      y: float,
+      cameraId: integer,
+      result: FloatPoint
+    ): FloatPoint {
+      layer.applyLayerTransformation(x, y, cameraId, result);
+      const gamePixiContainer = this._instanceContainer
+        .getRenderer()
+        .getRendererObject();
+      if (!gamePixiContainer) {
+        return result;
+      }
+      // The scale is usually near 1 unless the 'magnified' scale mode is used.
+      // See gdjs.RuntimeGame.getZoomFactor
+      result[0] *= gamePixiContainer.scale.x;
+      result[1] *= gamePixiContainer.scale.y;
+      return result;
+    }
+
     clearDebugDraw(): void {
       if (this._debugDraw) {
         this._debugDraw.clear();
diff --git a/GDJS/Runtime/pixi-renderers/layer-pixi-renderer.ts b/GDJS/Runtime/pixi-renderers/layer-pixi-renderer.ts
index bdeff384f2ee..d8709d6b2c6d 100644
--- a/GDJS/Runtime/pixi-renderers/layer-pixi-renderer.ts
+++ b/GDJS/Runtime/pixi-renderers/layer-pixi-renderer.ts
@@ -262,8 +262,8 @@ namespace gdjs {
             if (game.getAntialiasingMode() !== 'none') {
               this._threeEffectComposer.addPass(
                 new THREE_ADDONS.SMAAPass(
-                  game.getGameResolutionWidth(),
-                  game.getGameResolutionHeight()
+                  game.getRenderingResolutionWidth(),
+                  game.getRenderingResolutionHeight()
                 )
               );
             }
@@ -304,9 +304,12 @@ namespace gdjs {
 
             this._threePlaneTexture = texture;
             this._threePlaneTexture.generateMipmaps = false;
+            const scaleMode = this._layer
+              .getRuntimeScene()
+              .getGame()
+              .getScaleMode();
             const filter =
-              this._layer.getRuntimeScene().getGame().getScaleMode() ===
-              'nearest'
+              scaleMode === 'nearest' || scaleMode === 'magnified'
                 ? THREE.NearestFilter
                 : THREE.LinearFilter;
             this._threePlaneTexture.minFilter = filter;
@@ -501,8 +504,8 @@ namespace gdjs {
       if (this._threeEffectComposer) {
         const game = this._layer.getRuntimeScene().getGame();
         this._threeEffectComposer.setSize(
-          game.getGameResolutionWidth(),
-          game.getGameResolutionHeight()
+          game.getRenderingResolutionWidth(),
+          game.getRenderingResolutionHeight()
         );
       }
     }
diff --git a/GDJS/Runtime/pixi-renderers/runtimegame-pixi-renderer.ts b/GDJS/Runtime/pixi-renderers/runtimegame-pixi-renderer.ts
index 9c736c974d2d..e1d35994a2be 100644
--- a/GDJS/Runtime/pixi-renderers/runtimegame-pixi-renderer.ts
+++ b/GDJS/Runtime/pixi-renderers/runtimegame-pixi-renderer.ts
@@ -100,16 +100,16 @@ namespace gdjs {
         this._threeRenderer.useLegacyLights = true;
         this._threeRenderer.autoClear = false;
         this._threeRenderer.setSize(
-          this._game.getGameResolutionWidth(),
-          this._game.getGameResolutionHeight()
+          this._game.getRenderingResolutionWidth(),
+          this._game.getRenderingResolutionHeight()
         );
 
         // Create a PixiJS renderer that use the same GL context as Three.js
         // so that both can render to the canvas and even have PixiJS rendering
         // reused in Three.js (by using a RenderTexture and the same internal WebGL texture).
         this._pixiRenderer = new PIXI.Renderer({
-          width: this._game.getGameResolutionWidth(),
-          height: this._game.getGameResolutionHeight(),
+          width: this._game.getRenderingResolutionWidth(),
+          height: this._game.getRenderingResolutionHeight(),
           view: gameCanvas,
           // @ts-ignore - reuse the context from Three.js.
           context: this._threeRenderer.getContext(),
@@ -124,8 +124,8 @@ namespace gdjs {
         // "preserveDrawingBuffer: true" is needed to avoid flickering
         // and background issues on some mobile phones (see #585 #572 #566 #463).
         this._pixiRenderer = PIXI.autoDetectRenderer({
-          width: this._game.getGameResolutionWidth(),
-          height: this._game.getGameResolutionHeight(),
+          width: this._game.getRenderingResolutionWidth(),
+          height: this._game.getRenderingResolutionHeight(),
           view: gameCanvas,
           preserveDrawingBuffer: true,
           antialias: false,
@@ -272,20 +272,20 @@ namespace gdjs {
       // There is no "smart" resizing to be done here: the rendering of the game
       // should be done with the size set on the game.
       if (
-        this._pixiRenderer.width !== this._game.getGameResolutionWidth() ||
-        this._pixiRenderer.height !== this._game.getGameResolutionHeight()
+        this._pixiRenderer.width !== this._game.getRenderingResolutionWidth() ||
+        this._pixiRenderer.height !== this._game.getRenderingResolutionHeight()
       ) {
         // TODO (3D): It might be useful to resize pixi view in 3D depending on FOV value
         // to enable a mode where pixi always fills the whole screen.
         this._pixiRenderer.resize(
-          this._game.getGameResolutionWidth(),
-          this._game.getGameResolutionHeight()
+          this._game.getRenderingResolutionWidth(),
+          this._game.getRenderingResolutionHeight()
         );
 
         if (this._threeRenderer) {
           this._threeRenderer.setSize(
-            this._game.getGameResolutionWidth(),
-            this._game.getGameResolutionHeight()
+            this._game.getRenderingResolutionWidth(),
+            this._game.getRenderingResolutionHeight()
           );
         }
       }
@@ -295,8 +295,8 @@ namespace gdjs {
       // only, so won't create visual artifacts during the rendering.
       const isFullPage =
         this._forceFullscreen || this._isFullPage || this._isFullscreen;
-      let canvasWidth = this._game.getGameResolutionWidth();
-      let canvasHeight = this._game.getGameResolutionHeight();
+      let canvasWidth = this._game.getRenderingResolutionWidth();
+      let canvasHeight = this._game.getRenderingResolutionHeight();
       let maxWidth = window.innerWidth - this._marginLeft - this._marginRight;
       let maxHeight = window.innerHeight - this._marginTop - this._marginBottom;
       if (maxWidth < 0) {
@@ -523,10 +523,10 @@ namespace gdjs {
       // Handle the fact that the game is stretched to fill the canvas.
       pageCoords[0] =
         (canvasCoords[0] * this._canvasWidth) /
-        this._game.getGameResolutionWidth();
+        this._game.getRenderingResolutionWidth();
       pageCoords[1] =
         (canvasCoords[1] * this._canvasHeight) /
-        this._game.getGameResolutionHeight();
+        this._game.getRenderingResolutionHeight();
 
       return pageCoords;
     }
diff --git a/GDJS/Runtime/pixi-renderers/runtimescene-pixi-renderer.ts b/GDJS/Runtime/pixi-renderers/runtimescene-pixi-renderer.ts
index 603c1bb3436e..89ced94d85d0 100644
--- a/GDJS/Runtime/pixi-renderers/runtimescene-pixi-renderer.ts
+++ b/GDJS/Runtime/pixi-renderers/runtimescene-pixi-renderer.ts
@@ -275,6 +275,12 @@ namespace gdjs {
         pixiRenderer.render(this._pixiContainer, {
           clear: this._runtimeScene.getClearCanvas(),
         });
+        const debugContainer = this._runtimeScene
+          .getDebuggerRenderer()
+          .getRendererObject();
+        if (debugContainer) {
+          pixiRenderer.render(debugContainer, { clear: false });
+        }
         this._layerRenderingMetrics.rendered2DLayersCount++;
       }
 
diff --git a/GDJS/Runtime/runtimegame.ts b/GDJS/Runtime/runtimegame.ts
index 7bb9cceabd01..abbb3484e3c2 100644
--- a/GDJS/Runtime/runtimegame.ts
+++ b/GDJS/Runtime/runtimegame.ts
@@ -141,7 +141,7 @@ namespace gdjs {
     _originalHeight: float;
     _resizeMode: 'adaptWidth' | 'adaptHeight' | string;
     _adaptGameResolutionAtRuntime: boolean;
-    _scaleMode: 'linear' | 'nearest';
+    _scaleMode: ScaleMode;
     _pixelsRounding: boolean;
     _antialiasingMode: 'none' | 'MSAA';
     _isAntialisingEnabledOnMobile: boolean;
@@ -158,6 +158,11 @@ namespace gdjs {
      * When set to true, the scenes are notified that game resolution size changed.
      */
     _notifyScenesForGameResolutionResize: boolean = false;
+    /**
+     * When set to true, the scenes are notified that game zoom factor changed.
+     */
+    _notifyScenesForGameZoomFactorChange: boolean = false;
+    _zoomFactor: number = 1;
 
     /**
      * When paused, the game won't step and will be freezed. Useful for debugging.
@@ -551,21 +556,45 @@ namespace gdjs {
     }
 
     /**
-     * Get the game resolution (the size at which the game is played and rendered) width.
+     * Get the game resolution width for events.
      * @returns The game resolution width, in pixels.
      */
     getGameResolutionWidth(): float {
-      return this._gameResolutionWidth;
+      return this._gameResolutionWidth / this._zoomFactor;
     }
 
     /**
-     * Get the game resolution (the size at which the game is played and rendered) height.
+     * Get the game resolution height for events.
      * @returns The game resolution height, in pixels.
      */
     getGameResolutionHeight(): float {
+      return this._gameResolutionHeight / this._zoomFactor;
+    }
+
+    /**
+     * Get the game resolution width (the size at which the game is rendered).
+     * @returns The game resolution width, in pixels.
+     */
+    getRenderingResolutionWidth(): float {
+      return this._gameResolutionWidth;
+    }
+
+    /**
+     * Get the game resolution height (the size at which the game is rendered).
+     * @returns The game resolution height, in pixels.
+     */
+    getRenderingResolutionHeight(): float {
       return this._gameResolutionHeight;
     }
 
+    /**
+     * The scale is usually near 1 unless the 'magnified' scale mode is used.
+     * @returns the factor between game resolution size and rendering resolution size.
+     */
+    getZoomFactor() {
+      return this._zoomFactor;
+    }
+
     /**
      * Change the game resolution.
      *
@@ -577,17 +606,16 @@ namespace gdjs {
 
       this._gameResolutionWidth = width;
       this._gameResolutionHeight = height;
-      if (this._adaptGameResolutionAtRuntime) {
-        if (
-          gdjs.RuntimeGameRenderer &&
-          gdjs.RuntimeGameRenderer.getWindowInnerWidth &&
-          gdjs.RuntimeGameRenderer.getWindowInnerHeight
-        ) {
-          const windowInnerWidth =
-            gdjs.RuntimeGameRenderer.getWindowInnerWidth();
-          const windowInnerHeight =
-            gdjs.RuntimeGameRenderer.getWindowInnerHeight();
+      if (
+        gdjs.RuntimeGameRenderer &&
+        gdjs.RuntimeGameRenderer.getWindowInnerWidth &&
+        gdjs.RuntimeGameRenderer.getWindowInnerHeight
+      ) {
+        const windowInnerWidth = gdjs.RuntimeGameRenderer.getWindowInnerWidth();
+        const windowInnerHeight =
+          gdjs.RuntimeGameRenderer.getWindowInnerHeight();
 
+        if (this._adaptGameResolutionAtRuntime) {
           // Enlarge either the width or the eight to fill the inner window space.
           if (this._resizeMode === 'adaptWidth') {
             this._gameResolutionWidth =
@@ -614,6 +642,35 @@ namespace gdjs {
             }
           }
         }
+        if (
+          this._scaleMode === 'magnified' &&
+          this._gameResolutionWidth > 0 &&
+          this._gameResolutionHeight > 0 &&
+          // Fall back on linear if magnified is used on a high resolution game.
+          this._originalWidth <= 960 &&
+          this._originalHeight <= 540
+        ) {
+          const pixelSize = Math.max(
+            1,
+            Math.ceil(
+              this._zoomFactor *
+                Math.min(
+                  windowInnerWidth / this._gameResolutionWidth,
+                  windowInnerHeight / this._gameResolutionHeight
+                )
+            )
+          );
+          this._gameResolutionWidth = Math.round(
+            (this._gameResolutionWidth * pixelSize) / this._zoomFactor
+          );
+          this._gameResolutionHeight = Math.round(
+            (this._gameResolutionHeight * pixelSize) / this._zoomFactor
+          );
+          if (this._zoomFactor !== pixelSize && pixelSize >= 1) {
+            this._zoomFactor = pixelSize;
+            this._notifyScenesForGameZoomFactorChange = true;
+          }
+        }
       }
 
       // Don't alter the game resolution. The renderer
@@ -682,9 +739,9 @@ namespace gdjs {
     }
 
     /**
-     * Return the scale mode of the game ("linear" or "nearest").
+     * Return the scale mode of the game ("linear", "magnified" or "nearest").
      */
-    getScaleMode(): 'linear' | 'nearest' {
+    getScaleMode(): ScaleMode {
       return this._scaleMode;
     }
 
@@ -962,6 +1019,10 @@ namespace gdjs {
               this._sceneStack.onGameResolutionResized();
               this._notifyScenesForGameResolutionResize = false;
             }
+            if (this._notifyScenesForGameZoomFactorChange) {
+              this._sceneStack.onGameZoomFactorChanged();
+              this._notifyScenesForGameZoomFactorChange = false;
+            }
 
             // Render and step the scene.
             if (this._sceneStack.step(elapsedTime)) {
diff --git a/GDJS/Runtime/runtimeobject.ts b/GDJS/Runtime/runtimeobject.ts
index 47179079b686..0434373b14d5 100644
--- a/GDJS/Runtime/runtimeobject.ts
+++ b/GDJS/Runtime/runtimeobject.ts
@@ -658,6 +658,11 @@ namespace gdjs {
      */
     onSceneResumed(runtimeScene: gdjs.RuntimeScene): void {}
 
+    /**
+     * Called when the game zoom factor is changed to adapt to a new resolution.
+     */
+    onGameZoomFactorChanged(): void {}
+
     //Rendering:
     /**
      * @return The internal object for a 2D rendering (PIXI.DisplayObject...)
diff --git a/GDJS/Runtime/scenestack.ts b/GDJS/Runtime/scenestack.ts
index b04bd5976187..1c3831b4fb9d 100644
--- a/GDJS/Runtime/scenestack.ts
+++ b/GDJS/Runtime/scenestack.ts
@@ -34,6 +34,15 @@ namespace gdjs {
       }
     }
 
+    /**
+     * Called when the game zoom factor is changed to adapt to a new resolution.
+     */
+    onGameZoomFactorChanged(): void {
+      for (let i = 0; i < this._stack.length; ++i) {
+        this._stack[i].onGameZoomFactorChanged();
+      }
+    }
+
     step(elapsedTime: float): boolean {
       this._throwIfDisposed();
       if (this._isNextLayoutLoading || this._stack.length === 0) {
diff --git a/GDJS/Runtime/types/project-data.d.ts b/GDJS/Runtime/types/project-data.d.ts
index 8f4947b17911..024939051aed 100644
--- a/GDJS/Runtime/types/project-data.d.ts
+++ b/GDJS/Runtime/types/project-data.d.ts
@@ -329,13 +329,15 @@ declare interface EffectNetworkSyncData {
   };
 }
 
+declare type ScaleMode = 'linear' | 'magnified' | 'nearest';
+
 declare interface ProjectPropertiesData {
   adaptGameResolutionAtRuntime: boolean;
   folderProject: boolean;
   orientation: string;
   packageName: string;
   projectFile: string;
-  scaleMode: 'linear' | 'nearest';
+  scaleMode: ScaleMode;
   pixelsRounding: boolean;
   antialiasingMode: 'none' | 'MSAA';
   antialisingEnabledOnMobile: boolean;
diff --git a/newIDE/app/src/AssetStore/InstallAsset.js b/newIDE/app/src/AssetStore/InstallAsset.js
index 86bfb6e90861..5b4c674cb63d 100644
--- a/newIDE/app/src/AssetStore/InstallAsset.js
+++ b/newIDE/app/src/AssetStore/InstallAsset.js
@@ -129,7 +129,9 @@ export const installResource = (
   if (newResource.getKind() === 'image') {
     // $FlowExpectedError[prop-missing] - We know the resource is an ImageResource and has the setSmooth method.
     newResource.setSmooth(
-      project.getScaleMode() !== 'nearest' && !isPixelArt(asset)
+      project.getScaleMode() !== 'nearest' &&
+        project.getScaleMode() !== 'magnified' &&
+        !isPixelArt(asset)
     );
   }
 
diff --git a/newIDE/app/src/InstancesEditor/InstancesRenderer/LayerRenderer.js b/newIDE/app/src/InstancesEditor/InstancesRenderer/LayerRenderer.js
index 625fce456066..53321d6387c6 100644
--- a/newIDE/app/src/InstancesEditor/InstancesRenderer/LayerRenderer.js
+++ b/newIDE/app/src/InstancesEditor/InstancesRenderer/LayerRenderer.js
@@ -655,7 +655,8 @@ export default class LayerRenderer {
 
     threePlaneTexture.generateMipmaps = false;
     const filter =
-      this.project.getScaleMode() === 'nearest'
+      this.project.getScaleMode() === 'nearest' ||
+      this.project.getScaleMode() === 'magnified'
         ? THREE.NearestFilter
         : THREE.LinearFilter;
     threePlaneTexture.minFilter = filter;
diff --git a/newIDE/app/src/ProjectManager/ProjectPropertiesDialog.js b/newIDE/app/src/ProjectManager/ProjectPropertiesDialog.js
index 46a01a06b6aa..1ebd11b3bb8e 100644
--- a/newIDE/app/src/ProjectManager/ProjectPropertiesDialog.js
+++ b/newIDE/app/src/ProjectManager/ProjectPropertiesDialog.js
@@ -729,11 +729,15 @@ const ProjectPropertiesDialog = (props: Props) => {
                 >
                   <SelectOption
                     value="linear"
-                    label={t`Linear (antialiased rendering, good for most games)`}
+                    label={t`Linear (smooth, good for most games)`}
+                  />
+                  <SelectOption
+                    value="magnified"
+                    label={t`Magnified (sharp, good for modern pixel-art games)`}
                   />
                   <SelectOption
                     value="nearest"
-                    label={t`Nearest (no antialiasing, good for pixel perfect games)`}
+                    label={t`Nearest (aliased, good for pixel perfect games)`}
                   />
                 </SelectField>
                 <Checkbox
@@ -749,7 +753,7 @@ const ProjectPropertiesDialog = (props: Props) => {
                     notifyOfChange();
                   }}
                 />
-                {scaleMode === 'nearest' && (
+                {(scaleMode === 'nearest' || scaleMode === 'magnified') && (
                   <DismissableAlertMessage
                     identifier="use-non-smoothed-textures"
                     kind="info"
diff --git a/newIDE/app/src/ResourcesList/ResourceUtils.js b/newIDE/app/src/ResourcesList/ResourceUtils.js
index 8341cde9c331..d7430751e50d 100644
--- a/newIDE/app/src/ResourcesList/ResourceUtils.js
+++ b/newIDE/app/src/ResourcesList/ResourceUtils.js
@@ -154,7 +154,10 @@ export const applyResourceDefaults = (
   newResource: gdResource
 ) => {
   if (newResource instanceof gd.ImageResource) {
-    newResource.setSmooth(project.getScaleMode() !== 'nearest');
+    newResource.setSmooth(
+      project.getScaleMode() !== 'nearest' &&
+        project.getScaleMode() !== 'magnified'
+    );
   }
 };