# Copyright 2004-2026 Tom Rothamel <pytom@bishoujo.us>
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

from typing import NotRequired, TypedDict, Any

import sys
import os
import time
import io
import threading
import gc
import atexit
import platform

import renpy
import renpy.pygame as pygame

# Imports for backward compatibility.
from renpy.display.position import absolute as absolute, position as position
from renpy.display.displayable import Displayable, DisplayableArguments as DisplayableArguments, place as place
from renpy.display.scenelists import SceneListEntry as SceneListEntry, SceneLists as SceneLists

import_time = time.time()

try:
    import android

    android.init  # Check to see if we have the right module.
except Exception:
    android = None

if renpy.emscripten:
    import emscripten
else:
    emscripten = None

TIMEEVENT = pygame.event.register("TIMEEVENT")
PERIODIC = pygame.event.register("PERIODIC")
REDRAW = pygame.event.register("REDRAW")
EVENTNAME = pygame.event.register("EVENTNAME")

# All events except for TIMEEVENT and REDRAW
ALL_EVENTS: set[int] = set(pygame.event.get_standard_events())
ALL_EVENTS.add(PERIODIC)
ALL_EVENTS.add(EVENTNAME)

enabled_events: set[int] = {
    pygame.QUIT,
    pygame.APP_TERMINATING,
    pygame.APP_LOWMEMORY,
    pygame.APP_WILLENTERBACKGROUND,
    pygame.APP_DIDENTERBACKGROUND,
    pygame.APP_WILLENTERFOREGROUND,
    pygame.APP_DIDENTERFOREGROUND,
    pygame.WINDOWEVENT,
    pygame.SYSWMEVENT,
    pygame.KEYDOWN,
    pygame.KEYUP,
    pygame.TEXTEDITING,
    pygame.TEXTINPUT,
    pygame.KEYMAPCHANGED,
    pygame.MOUSEMOTION,
    pygame.MOUSEBUTTONDOWN,
    pygame.MOUSEBUTTONUP,
    pygame.MOUSEWHEEL,
    pygame.JOYAXISMOTION,
    pygame.JOYHATMOTION,
    pygame.JOYBALLMOTION,
    pygame.JOYBUTTONDOWN,
    pygame.JOYBUTTONUP,
    pygame.JOYDEVICEADDED,
    pygame.JOYDEVICEREMOVED,
    pygame.CONTROLLERAXISMOTION,
    pygame.CONTROLLERBUTTONDOWN,
    pygame.CONTROLLERBUTTONUP,
    pygame.CONTROLLERDEVICEADDED,
    pygame.CONTROLLERDEVICEREMOVED,
    pygame.RENDER_TARGETS_RESET,
    TIMEEVENT,
    PERIODIC,
    REDRAW,
    EVENTNAME,
}

input_events: set[int] = {
    pygame.KEYDOWN,
    pygame.KEYUP,
    pygame.TEXTEDITING,
    pygame.TEXTINPUT,
    pygame.MOUSEMOTION,
    pygame.MOUSEBUTTONDOWN,
    pygame.MOUSEBUTTONUP,
    pygame.MOUSEWHEEL,
    pygame.JOYAXISMOTION,
    pygame.JOYHATMOTION,
    pygame.JOYBALLMOTION,
    pygame.JOYBUTTONDOWN,
    pygame.JOYBUTTONUP,
    pygame.CONTROLLERAXISMOTION,
    pygame.CONTROLLERBUTTONDOWN,
    pygame.CONTROLLERBUTTONUP,
}


# The number of msec between periodic events.
PERIODIC_INTERVAL = 50

null: "renpy.display.layout.Null | None" = None

# Time management.
time_base = 0.0
time_mult = 1.0

# Mouse management.
relx = 0
rely = 0


def init_time():
    warp = os.environ.get("RENPY_TIMEWARP", "1.0")

    global time_base
    global time_mult

    time_base = time.time()
    time_mult = float(warp)


def get_time() -> float:
    t = time.time()
    return time_base + (t - time_base) * time_mult


def get_size() -> tuple[int, int]:
    """
    Returns the screen size. Always returns at least (256, 256), to make sure
    that we don't divide by zero.
    """

    size = pygame.display.get_size()

    if not size:
        return size

    if size[0] >= 256 and size[1] >= 256:
        return size

    return (max(size[0], 256), max(size[1], 256))


def displayable_by_tag(layer: str, tag: str) -> Displayable | None:
    """
    Get the displayable on the given layer with the given tag.
    """

    return renpy.game.context().scene_lists.get_displayable_by_tag(layer, tag)


class IgnoreEvent(Exception):
    """
    Exception that is raised when we want to ignore an event, but
    also don't want to return anything.
    """


class EndInteraction(Exception):
    """
    Exception that can be raised (for example, during the render method of
    a displayable) to end the current interaction immediately.
    """

    def __init__(self, value: Any):
        self.value = value


class MouseMove:
    """
    This contains information about the current mouse move.
    """

    def __init__(self, x: int, y: int, duration: float | None):
        self.start = get_time()

        if duration is not None:
            self.duration = duration
            self.start_x, self.start_y = renpy.display.draw.get_mouse_pos()
        else:
            self.duration = 4 / 60.0
            self.start_x, self.start_y = x, y

        self.end_x = x
        self.end_y = y

    def perform(self) -> bool:
        """
        Performs the mouse move. Returns True if this should be called
        again, or False if the move has finished.
        """

        elapsed = get_time() - self.start

        if elapsed >= self.duration:
            renpy.display.draw.set_mouse_pos(self.end_x, self.end_y)
            return False

        done = 1.0 * elapsed / self.duration

        x = int(self.start_x + done * (self.end_x - self.start_x))
        y = int(self.start_y + done * (self.end_y - self.start_y))

        renpy.display.draw.set_mouse_pos(x, y)
        return True


def get_safe_mode() -> bool:
    """
    Returns true if we should go into safe mode.
    """

    if renpy.safe_mode_checked:
        return False

    if getattr(renpy.game.args, "safe_mode", False):
        return True

    try:
        if renpy.windows:
            import ctypes

            VK_SHIFT = 0x10

            ctypes.windll.user32.GetKeyState.restype = ctypes.c_ushort
            if ctypes.windll.user32.GetKeyState(VK_SHIFT) & 0x8000:
                return True
            else:
                return False

        # Safe mode doesn't work on other platforms.
        return False

    except Exception:
        return False


class RendererInfo(TypedDict):
    renderer: str
    resizable: bool
    additive: bool
    models: bool

    gpu_vendor: NotRequired[str]
    gpu_name: NotRequired[str]
    gpu_driver_version: NotRequired[str]


class Renderer:
    """
    A Renderer (also known as a draw object) is responsible for drawing a
    tree of displayables to the window. It also provides other services that
    involved drawing and the SDL main window, as documented here.

    A Renderer is responsible for updating the renpy.game.preferences.fullscreen
    and renpy.game.preferences.physical_size preferences, when these are
    changed from outside the game.

    A renderer has an info dict, that contains the keys from pygame.display.Info(),
    and then:
    - "renderer", the name of the Renderer.
    - "resizable", true if the window can be resized.
    - "additive", true if additive blending is supported.
    - "models", true if model-based rendering is being used.
    """

    texture_cache = {}

    info: RendererInfo

    def get_texture_size(self):
        """
        This returns a pair contining the total amount of memory consumed by
        textures, and a count of the number of textures that exist.
        """

    def update(self, force=False):
        """
        This is called before a draw operation to check to see if the state of
        the draw objects needs to be updated after an external event has occurred.
        Things that require draw updates might be:

        * The window has changed its size.
        * The window has changed full-screen status.
        * `force` is given, which generally means that it's likely the GL
          context has become invalid.

        After this has been called, the system should be in a good state for
        rendering.

        Returns True if a redraw is required, False otherwise.
        """

    def init(self, virtual_size):
        """
        This creates a renderer with the given `virtual_size`. It returns
        True of the renderer initialized correctly, False otherwise.
        """

    def quit(self):
        """
        This shuts down the renderer until the next call to ``init``.
        """

    def resize(self):
        """
        This is called to implement resizing and changing the fullscreen
        mode. It is expected to determine the size to use using
        renpy.game.preferences.physical_size, and the fullscreen mode
        using renpy.game.preferences.fullscreen.
        """

    def can_block(self):
        """
        Returns True if we can block to wait for input, False if the screen
        needs to be immediately redrawn.
        """

    def should_redraw(self, needs_redraw, first_pass, can_block):
        """
        Determines if the screen needs to be redrawn. Returns True if it
        does.

        `needs_redraw`
            True if the needs_redraw flag is set.

        `first_pass`
            True if this is the first pass through the interact loop.

        `can_block`
            The value of self.can_block, from above.
        """

    def mutated_surface(self, surf):
        """
        Called to indicated that `surf` has changed and textures based on
        it should not be used.
        """

        if surf in self.texture_cache:
            del self.texture_cache[surf]

    def load_texture(self, surf, transient=False):
        """
        Loads a surface into a texture.

        `surf`
            The pygame.Surface to load.

        `transient`
            True if the texture is unlikely to be used for more than a single
            frame.
        """

    def ready_one_texture(self):
        """
        This is called in the main thread to indicate that one texture
        should be loaded into the GPU.
        """

    def kill_textures(self):
        """
        Removes all cached textures, to free memory.
        """

    def solid_texture(self, w, h, color):
        """
        This is called to create a (`w` x `h`) texture of a single
        color.

        Returns the texture.
        """

    def draw_screen(self, surftree, flip=True):
        """
        This draw the screen.

        `surftree`
            A Render object (the root of a tree of Render objects) that
            will be drawn to the screen.

        `flip`
            If True, the drawing will be presented to the user.
        """

    def render_to_texture(self, what, alpha):
        """
        Converts `what`, a tree of Renders, to a texture of the same size.

        `alpha`
            A hint as to if the texture should have an alpha channel.
        """

    def is_pixel_opaque(self, what, x, y):
        """
        Returns true if the pixel is not 100% transparent.

        `what`
            A tree of renders.

        `x`, `y`
            The coordinates of the pixels to check.
        """

    def get_half(self, what):
        """
        Gets a texture that is half the size of `what`, which may be
        a texture or a tree of Renders.
        """

    def translate_point(self, x, y):
        """
        Translates (`x`, `y`) from physical to virtual coordinates.
        """

        return (0, 0)

    def untranslate_point(self, x, y):
        """
        Untranslates (`x`, `y`) from virtual to physical coordinates.
        """

        return (0, 0)

    def mouse_event(self, ev):
        """
        This translates the .pos field of `ev` from physical coordinates to
        virtual coordinates. Returns an (x, y) pair of virtual coordinates.
        """

        return ev

    def get_mouse_pos(self):
        """
        Returns the x and y coordinates of the mouse, in virtual coordinates.
        """

        return (0, 0)

    def set_mouse_pos(self, x, y):
        """
        Moves the mouse to the virtual coordinates `x` and `y`.
        """

        x, y = self.untranslate_point(x, y)
        pygame.mouse.set_pos([x, y])

    def screenshot(self, surftree):
        """
        This returns a pygame.Surface that is the result of rendering
        `surftree`, a tree of Renders.
        """

    def event_peek_sleep(self):
        """
        On platforms where CPU usage is gated by the need to redraw, sleeps
        a short amount of time to keep the CPU idle.
        """

    def get_physical_size(self):
        """
        Returns the physical size of the window, in physical pixels.
        """


# How long should we be in maximum framerate mode at the start of the game?
initial_maximum_framerate = 0.0


class Interface:
    """
    This represents the user interface that interacts with the user.
    It manages the Display objects that display things to the user, and
    also handles accepting and responding to user input.

    @ivar display: The display that we used to display the screen.

    @ivar profile_time: The time of the last profiling.

    @ivar screenshot: A screenshot, or None if no screenshot has been
    taken.

    @ivar old_scene: The last thing that was displayed to the screen.

    @ivar transition: A map from layer name to the transition that will
    be applied the next time interact restarts.

    @ivar transition_time: A map from layer name to the time the transition
    involving that layer started.

    @ivar transition_from: A map from layer name to the scene that we're
    transitioning from on that layer.

    @ivar suppress_transition: If True, then the next transition will not
    happen.

    @ivar force_redraw: If True, a redraw is forced.

    @ivar restart_interaction: If True, the current interaction will
    be restarted.

    @ivar pushed_event: If not None, an event that was pushed back
    onto the stack.

    @ivar mouse: The name of the mouse cursor to use during the current
    interaction.

    @ivar ticks: The number of 20hz ticks.

    @ivar frame_time: The time at which we began drawing this frame.

    @ivar interact_time: The time of the start of the first frame of the current interact_core.

    @ivar time_event: A singleton ignored event.

    @ivar event_time: The time of the current event.

    @ivar timeout_time: The time at which the timeout will occur.
    """

    def __init__(self):
        # PNG data and the surface for the current file screenshot.
        self.screenshot = None
        self.screenshot_surface = None

        self.old_scene = {}
        self.transition = {}
        self.suppress_transition = False
        self.quick_quit = False
        self.force_redraw = False
        self.restart_interaction = False
        self.pushed_event = None
        self.ticks = 0
        self.mouse = "default"
        self.timeout_time = None
        self.last_event = None
        self.current_context = None
        self.roll_forward = None

        # Are we in fullscreen mode?
        self.fullscreen = False

        # Things to be preloaded.
        self.preloads = []

        # The time at which this object was initialized.
        self.init_time = get_time()

        # The time at which this draw occurs.
        self.frame_time = 0

        # The time when this interaction occurred.
        self.interact_time = None

        # The time we last tried to quit.
        self.quit_time = 0

        # Are we currently processing the quit event?
        self.in_quit_event = False

        self.time_event = pygame.event.Event(TIMEEVENT, {"modal": False})
        self.redraw_event = pygame.event.Event(REDRAW)

        # Are we focused?
        self.mouse_focused = True
        self.keyboard_focused = True

        # Properties for each layer.
        self.layer_properties = {}

        # Have we shown the window this interaction?
        self.shown_window = False

        # Should we ignore the rest of the current touch? Used to ignore the
        # rest of a mousepress after a longpress occurs.
        self.ignore_touch = False

        # Should we clear the screenshot at the start of the next interaction?
        self.clear_screenshot = False

        # Is our audio paused?
        self.audio_paused = False

        # Transition-related:

        # A map from layer (or None for the root) to the uninstantiate
        # transition object that's on that layer.
        self.ongoing_transition = {}

        # The same, but for the transition after it's been called with the
        # old and new displayables.
        self.instantiated_transition = {}

        # The time an ongoing transition started.
        self.transition_time = {}

        # The displayable that an ongoing transition is transitioning from.
        self.transition_from = {}

        # The previous root transform.
        self.old_root_transform = None

        # Init layers.
        renpy.display.scenelists.init_layers()

        global null
        null = renpy.display.layout.Null()

        for layer in renpy.display.scenelists.layers:
            if layer in renpy.config.layer_clipping:
                x, y, w, h = renpy.config.layer_clipping[layer]
                self.layer_properties[layer] = dict(
                    xpos=x,
                    xanchor=0,
                    ypos=y,
                    yanchor=0,
                    xmaximum=w,
                    ymaximum=h,
                    xminimum=w,
                    yminimum=h,
                    clipping=True,
                )

            else:
                self.layer_properties[layer] = {}

        # A stack giving the values of self.transition and self.transition_time
        # for contexts outside the current one. This is used to restore those
        # in the case where nothing has changed in the new context.
        self.transition_info_stack = []

        # The time when the event was dispatched.
        self.event_time = 0

        # The time we saw the last mouse event.
        self.mouse_event_time = None

        # Should we show the mouse?
        self.show_mouse = True

        # Should we reset the display?
        self.display_reset = False

        # Should we profile the next frame?
        self.profile_once = False

        # The thread that can do display operations.
        self.thread = threading.current_thread()

        # Init timing.
        init_time()

        # The time of the last input event (mouse, keyboard, gamepad).
        self.input_event_time = get_time()
        self.mouse_event_time = (
            self.input_event_time - 1.0
        )  # Setting it 1s before input_event_time allows the first interaction to use default focus.

        # The current window caption.
        self.window_caption = None

        renpy.game.interface = self
        renpy.display.interface = self

        # Are we in safe mode, from holding down shift at start?
        self.safe_mode = False

        # Do we need a background screenshot?
        self.bgscreenshot_needed = False

        # Event used to signal background screenshot taken.
        self.bgscreenshot_event = threading.Event()

        # The background screenshot surface.
        self.bgscreenshot_surface = None

        # Mouse move. If not None, information about the current mouse
        # move.
        self.mouse_move = None

        # If in text editing mode, the current text editing event.
        self.text_editing = None

        # The text rectangle after the current draw.
        self.text_rect = None

        # The text rectangle after the previous draw.
        self.old_text_rect = None

        # Are we a touchscreen?
        self.touch = renpy.exports.variant("touch")

        # Should we use the touch keyboard?
        self.touch_keyboard = (self.touch and renpy.emscripten) or renpy.config.touch_keyboard

        # Should we restart the interaction?
        self.restart_interaction = True

        # For compatibility with older code.
        if renpy.config.periodic_callback:
            renpy.config.periodic_callbacks.append(renpy.config.periodic_callback)

        # Has start been called?
        self.started = False

        # Are we in fullscreen video mode?
        self.fullscreen_video = False

        self.safe_mode = get_safe_mode()
        renpy.safe_mode_checked = True

        # A scale factor used to compensate for the system DPI.
        self.dpi_scale = self.setup_dpi_scaling()

        renpy.display.log.write("DPI scale factor: %f", self.dpi_scale)

        # A time until which we should draw at maximum framerate.
        self.maximum_framerate_time = 0.0
        self.maximum_framerate(initial_maximum_framerate)

        # True if this is the first interact.
        self.start_interact = True

        # The time of each frame.
        self.frame_times = []

        # The duration of each frame, in seconds.
        self.frame_duration = 1.0 / 60.0

        # The cursor cache.
        self.cursor_cache: dict | None = None

        # The old mouse.
        self.old_mouse = None

        # A map from a layer to the duration of the current transition on that
        # layer.
        self.transition_delay = {}

        # Is this the first frame?
        self.first_frame = True

        # Should prediction be forced? This causes the prediction coroutine to
        # be prioritized, and is set to False when it's done, when preloading
        # is done, or at the end of the interaction.
        self.force_prediction = False

        # The number of interactions that have happened without processing an event.
        self.interaction_counter = 0

        # This caches the mod field of the last event that has one, allowing keyboard
        # modifiers to be used with mouse and other events.
        self.mod = 0

        # A queue of functions to invoke at the start of the next interaction.
        # Set by renpy.exports.invoke_in_main_thread.
        self.invoke_queue = []

        # The previous state of the screensaver.
        self.last_screensaver = None

        self.last_emscripten_preload_time: float = get_time()
        """The last time an idle frame allowed an emscripten preload pass to run without stuttering."""

        try:
            self.setup_nvdrs()
        except Exception:
            pass

    def setup_nvdrs(self):
        if renpy.session.get("_reload", False):
            return

        from ctypes import cdll, c_char_p

        nvdrs = cdll.nvdrs

        disable_thread_optimization = nvdrs.disable_thread_optimization
        restore_thread_optimization = nvdrs.restore_thread_optimization
        get_nvdrs_error = nvdrs.get_nvdrs_error
        get_nvdrs_error.restype = c_char_p

        renpy.display.log.write("nvdrs: Loaded, about to disable thread optimizations.")

        disable_thread_optimization()

        if error := get_nvdrs_error():
            renpy.display.log.write("nvdrs: %r (can be ignored)", error)
        else:
            renpy.display.log.write("nvdrs: Disabled thread optimizations.")

        atexit.register(restore_thread_optimization)

    def setup_dpi_scaling(self):
        if "RENPY_HIGHDPI" in os.environ:
            return float(os.environ["RENPY_HIGHDPI"])

        if not renpy.windows:
            return 1.0

        try:
            import ctypes
            from ctypes import c_void_p, c_int

            ctypes.windll.user32.SetProcessDPIAware()

            GetDC = ctypes.windll.user32.GetDC
            GetDC.restype = c_void_p
            GetDC.argtypes = [c_void_p]

            ReleaseDC = ctypes.windll.user32.ReleaseDC
            ReleaseDC.argtypes = [c_void_p, c_void_p]

            GetDeviceCaps = ctypes.windll.gdi32.GetDeviceCaps
            GetDeviceCaps.restype = c_int
            GetDeviceCaps.argtypes = [c_void_p, c_int]

            LOGPIXELSX = 88

            dc = GetDC(None)
            rv = GetDeviceCaps(dc, LOGPIXELSX) / 96.0
            ReleaseDC(None, dc)

            if rv < renpy.config.de_minimus_dpi_scale:
                renpy.display.log.write("De minimus DPI scale, was %r", rv)
                rv = 1.0

            return rv

        except Exception:
            renpy.display.log.write("Could not determine DPI scale factor:")
            renpy.display.log.exception()
            return 1.0

    def get_display_layout(self):
        """
        Get the display layout. A list of rectangles that have monitors in them.
        """

        rv = []
        for i in range(pygame.display.get_num_video_displays()):
            rv.append(pygame.display.get_display_bounds(i))

        return tuple(rv)

    def on_move(self, pos):
        """
        Called when the player moves the window.
        """

        if not (renpy.windows or renpy.macintosh or renpy.linux):
            return

        if renpy.game.preferences.fullscreen or renpy.game.preferences.maximized:
            return

        renpy.game.preferences.window_position = pos
        renpy.game.preferences.window_position_layout = self.get_display_layout()

    def start(self):
        """
        Starts the interface, by opening a window and setting the mode.
        """

        if self.started:
            return

        # Avoid starting on Android if we don't have focus.
        if renpy.android:
            self.check_android_start()

        gc.collect()

        if gc.garbage:
            del gc.garbage[:]

        # Kill off the presplash.
        renpy.display.presplash.end()

        # Initialize audio.
        pygame.display.hint("SDL_APP_NAME", (renpy.config.name or "Ren'Py Game").encode("utf-8"))
        pygame.display.hint("SDL_AUDIO_DEVICE_APP_NAME", (renpy.config.name or "Ren'Py Game").encode("utf-8"))

        renpy.audio.audio.init()

        # Initialize pygame.
        try:
            pygame.display.init()
            pygame.mouse.init()
        except Exception:
            pass

        self.post_init()

        renpy.display.emulator.init_emulator()

        renpy.display.render.render_ready()

        # If we are on the web browser, start preloading the browser cache.
        if renpy.emscripten and renpy.game.preferences.web_cache_preload:
            emscripten.run_script("loadCache()")

        renpy.main.log_clock("Interface start")

        self.started = True

        self.set_mode()

        # Load the image fonts.
        renpy.text.font.load_fonts()

        # Setup periodic event.
        pygame.time.set_timer(PERIODIC, PERIODIC_INTERVAL)

        # Don't grab the screen.
        pygame.event.set_grab(False)

        if not self.safe_mode:
            renpy.display.controller.init()

        pygame.event.get([pygame.MOUSEMOTION, pygame.MOUSEBUTTONDOWN, pygame.MOUSEBUTTONUP])

        # Create a cache of the mouse information.
        if renpy.config.mouse:
            self.cursor_cache = {}

            cursors = {}

            for key, cursor_list in renpy.config.mouse.items():  # type: ignore
                color_cursor_list = []

                for i in cursor_list:
                    if i not in cursors:
                        fn, x, y = i
                        surf = renpy.display.im.load_surface(fn)
                        cursors[i] = pygame.mouse.ColorCursor(surf, x, y)

                    color_cursor_list.append(cursors[i])

                self.cursor_cache[key] = color_cursor_list

            if ("default" not in self.cursor_cache) and (None in self.cursor_cache):
                self.cursor_cache["default"] = self.cursor_cache[None]

        s = "Total time until interface ready: {}s.".format(time.time() - import_time)

        if renpy.android and not renpy.config.log_to_stdout:
            print(s)

        # Clear out any pending events.
        pygame.event.get()

        for i in renpy.config.display_start_callbacks:
            i()

    def post_init(self):
        """
        This is called after display init, but before the window is created.
        """

        pygame.display.hint("SDL_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR", "0")
        pygame.display.hint("SDL_VIDEO_MINIMIZE_ON_FOCUS_LOSS", "0")
        pygame.display.hint("SDL_TOUCH_MOUSE_EVENTS", "1")
        pygame.display.hint("SDL_MOUSE_TOUCH_EVENTS", "0")
        pygame.display.hint("SDL_EMSCRIPTEN_ASYNCIFY", "0")
        pygame.display.hint("SDL_IME_SHOW_UI", "1")

        if renpy.config.mouse_focus_clickthrough:
            pygame.display.hint("SDL_MOUSE_FOCUS_CLICKTHROUGH", "1")

        # Needed for Ubuntu Unity.
        wmclass = renpy.config.save_directory or os.path.basename(sys.argv[0])
        os.environ["SDL_VIDEO_X11_WMCLASS"] = wmclass

        self.set_window_caption(force=True)
        self.set_icon()

        if renpy.android:
            android.wakelock(True)
            android.activity.armOnStop()

        # Block events we don't use.
        for i in pygame.event.get_standard_events():
            if i in enabled_events:
                continue

            if i in renpy.config.pygame_events:
                continue

            pygame.event.set_blocked(i)

        # Fix a problem with fullscreen and maximized.
        if renpy.game.preferences.fullscreen:
            renpy.game.preferences.maximized = False

    def after_first_frame(self):
        """
        Called after the first frame has been drawn.
        """

        if renpy.android:
            from jnius import autoclass

            PythonSDLActivity = autoclass("org.renpy.android.PythonSDLActivity")
            PythonSDLActivity.mActivity.hidePresplash()

            print("Hid presplash.")

    def set_screensaver(self):
        new_screensaver = renpy.config.allow_screensaver

        if renpy.game.preferences.afm_enable:
            new_screensaver = False

        if new_screensaver != self.last_screensaver:
            pygame.display.set_screensaver(new_screensaver)
            self.last_screensaver = new_screensaver

    def set_icon(self):
        """
        This is called to set up the window icon.
        """

        # Window icon.
        icon = renpy.config.window_icon

        if icon:
            try:
                with renpy.loader.load(icon, directory="images") as f:
                    im = renpy.display.scale.image_load_unscaled(
                        f,
                        icon,
                    )

                # Convert the aspect ratio to be square.
                iw, ih = im.get_size()
                imax = max(iw, ih)
                square_im = renpy.display.pgrender.surface_unscaled((imax, imax), True)
                square_im.blit(im, ((imax - iw) // 2, (imax - ih) // 2))
                im = square_im

                if im.get_size()[0] > 1024:
                    im = renpy.display.scale.smoothscale(im, (1024, 1024))

                pygame.display.set_icon(im)
            except renpy.webloader.DownloadNeeded:
                pass

    def set_window_caption(self, force=False):
        window_title = renpy.config.window_title

        if window_title is None:
            window_title = "A Ren'Py Game"

        caption = renpy.translation.translate_string(window_title) + renpy.store._window_subtitle

        if renpy.exports.get_autoreload():
            caption += " - autoreload"

        if not force and caption == self.window_caption:
            return

        self.window_caption = caption
        pygame.display.set_caption(caption.encode("utf-8"))

    def iconify(self):
        pygame.display.iconify()

    def get_draw_constructors(self):
        """
        Figures out the list of draw constructors to try.
        """

        renderer = renpy.game.preferences.renderer
        renderer = os.environ.get("RENPY_RENDERER", renderer)
        renderer = renpy.session.get("renderer", renderer)

        renpy.config.renderer = renderer

        if renpy.android or renpy.ios or renpy.emscripten:
            renderers = ["gles2"]
        elif renpy.windows:
            renderers = ["gl2", "angle2", "gles2"]
        elif renpy.linux and platform.machine() == "aarch64":
            renderers = ["gles2", "gl2"]
        else:
            renderers = ["gl2", "gles2"]

        # Prevent a performance warning if the renderer
        # is taken from old persistent data.
        if renderer not in renderers:
            renderer = "auto"

        # Software renderer is the last hope for PC .
        if not (renpy.android or renpy.ios or renpy.emscripten):
            renderers = renderers + ["sw"]

        if renderer in renderers:
            renderers = [renderer, "sw"]

        if renderer == "sw":
            renderers = ["sw"]

        if self.safe_mode:
            renderers = ["sw"]

        draw_objects = {}

        def make_draw(name, mod, cls, *args):
            if name not in renderers:
                return False

            try:
                __import__(mod)
                module = sys.modules[mod]
                draw_class = getattr(module, cls)
                draw_objects[name] = draw_class(*args)
                return True

            except Exception:
                renpy.display.log.write("Couldn't import {0} renderer:".format(name))
                renpy.display.log.exception()

                return False

        make_draw("gl2", "renpy.gl2.gl2draw", "GL2Draw", "gl2")
        make_draw("angle2", "renpy.gl2.gl2draw", "GL2Draw", "angle2")
        make_draw("gles2", "renpy.gl2.gl2draw", "GL2Draw", "gles2")

        make_draw("sw", "renpy.display.swdraw", "SWDraw")

        rv = []

        def append_draw(name):
            if name in draw_objects:
                rv.append((name, draw_objects[name]))
            else:
                renpy.display.log.write("Unknown renderer: {0}".format(name))

        for i in renderers:
            append_draw(i)

        return rv

    def kill_textures(self, keep_const_size=False):
        """
        Kills all textures that have been loaded.
        """

        if renpy.display.draw is None:
            return

        if keep_const_size and not (renpy.android or renpy.ios):
            renpy.display.im.cache.clear_variable_size()
        else:
            renpy.display.im.cache.clear()

        renpy.gl2.assimp.free_memory()
        renpy.text.text.layout_cache_clear()
        renpy.display.video.texture.clear()
        renpy.display.render.free_memory()

        renpy.display.draw.kill_textures()

    def kill_surfaces(self):
        """
        Kills all surfaces that have been loaded.
        """

        if renpy.display.draw is None:
            return

        renpy.display.im.cache.clear()
        renpy.display.module.bo_cache = None

    def before_resize(self):
        """
        This is called when the window has been resized.
        """

        self.kill_textures(keep_const_size=not self.display_reset)

        if not renpy.mobile:
            pygame.key.stop_text_input()
            pygame.key.set_text_input_rect(None)
            self.text_rect = None
            self.old_text_rect = None

        self.display_reset = False

        self.force_redraw = True

        # Assume we have focus until told otherwise.
        self.mouse_focused = True
        self.keyboard_focused = True

        # Assume we're not minimized.
        self.minimized = False

        # Force an interaction restart.
        self.restart_interaction = True

        # True if we're doing a one-time profile.
        self.profile_once = False

        # Clear the frame times.
        self.frame_times = []

    def set_mode(self):
        """
        This constructs the draw object and sets the initial size of the
        window.
        """

        if renpy.session.get("_keep_renderer", False):
            renpy.display.render.models = renpy.display.draw.info.get("models", False)
            return

        virtual_size = (renpy.config.screen_width, renpy.config.screen_height)

        if renpy.display.draw:
            draws = [("current", renpy.display.draw)]
        else:
            draws = self.get_draw_constructors()

        for name, draw in draws:
            renpy.display.log.write("")
            renpy.display.log.write("Initializing {0} renderer:".format(name))
            if draw.init(virtual_size):
                renpy.display.draw = draw
                renpy.display.render.models = draw.info.get("models", False)
                break
            else:
                pygame.display.destroy()

        else:
            # Ensure we don't get stuck in fullscreen.
            renpy.game.preferences.fullscreen = False
            raise Exception("Could not set video mode.")

        renpy.session["renderer"] = draw.info["renderer"]

        if renpy.android:
            android.init()
            pygame.event.get()

    def draw_screen(self, root_widget, fullscreen_video, draw):
        try:
            renpy.display.render.per_frame = True
            renpy.display.screen.per_frame()
        finally:
            renpy.display.render.per_frame = False

        surftree = renpy.display.render.render_screen(
            root_widget,
            renpy.config.screen_width,
            renpy.config.screen_height,
        )

        if draw:
            renpy.display.draw.draw_screen(surftree)

        if renpy.emscripten:
            emscripten.sleep(0)

        now = time.time()

        self.frame_times.append(now)

        while (now - self.frame_times[0]) > renpy.config.performance_window:
            self.frame_times.pop(0)

        renpy.display.render.mark_sweep()
        renpy.display.focus.take_focuses()

        self.surftree = surftree
        self.fullscreen_video = fullscreen_video

        if self.first_frame:
            self.after_first_frame()
            self.first_frame = False

    def take_screenshot(self, scale, background=False, keep_existing=False):
        """
        This takes a screenshot of the current screen, and stores it so
        that it can gotten using get_screenshot()

        `background`
           If true, we're in a background thread. So queue the request
           until it can be handled by the main thread.

        `keep_existing`
            If true, the existing screenshot is kept.
        """

        if self.screenshot and keep_existing:
            return

        self.clear_screenshot = False

        # Do nothing before the first interaction.
        if not self.started:
            return

        if background and not renpy.emscripten:
            self.bgscreenshot_event.clear()
            self.bgscreenshot_needed = True

            if not self.bgscreenshot_event.wait(1.0):
                raise Exception("Screenshot timed out.")

            surf = self.bgscreenshot_surface
            self.bgscreenshot_surface = None

        else:
            surf = renpy.display.draw.screenshot(self.surftree)

        surf = renpy.display.scale.smoothscale(surf, scale)

        renpy.display.render.mutated_surface(surf)

        self.screenshot_surface = surf

        with io.BytesIO() as sio:
            renpy.display.module.save_png(surf, sio, 0)
            self.screenshot = sio.getvalue()

    def check_background_screenshot(self):
        """
        Handles requests for a background screenshot.
        """

        if self.bgscreenshot_needed:
            self.bgscreenshot_needed = False
            self.bgscreenshot_surface = renpy.display.draw.screenshot(self.surftree)
            self.bgscreenshot_event.set()

    def get_screenshot(self):
        """
        Gets the current screenshot, as a string. Returns None if there isn't
        a current screenshot.
        """

        if not self.started:
            return None

        rv = self.screenshot

        if not rv:
            self.take_screenshot(
                (renpy.config.thumbnail_width, renpy.config.thumbnail_height),
                background=(threading.current_thread() is not self.thread),
            )
            rv = self.screenshot
            self.lose_screenshot()

        return rv

    def lose_screenshot(self):
        """
        This deallocates the saved screenshot.
        """

        self.screenshot = None
        self.screenshot_surface = None

    def save_screenshot(self, filename):
        """
        Saves a full-size screenshot in the given filename.
        """

        window = renpy.display.draw.screenshot(self.surftree)

        if renpy.config.screenshot_crop:
            window = renpy.display.scale.smoothscale(window, (renpy.config.screen_width, renpy.config.screen_height))
            window = window.subsurface(renpy.config.screenshot_crop)

        try:
            renpy.display.scale.image_save_unscaled(window, filename)
            if renpy.emscripten:
                emscripten.run_script(r"""FSDownload('%s');""" % filename)
            return True
        except Exception:
            if renpy.config.debug:
                raise

            return False

    def screenshot_to_bytes(self, size=None):
        """
        This takes a screenshot of the last thing drawn, and returns it.
        """

        self.clear_screenshot = False

        # Do nothing before the first interaction.
        if not self.started:
            return

        surf = renpy.display.draw.screenshot(self.surftree)

        if size is not None:
            surf = renpy.display.scale.smoothscale(surf, size)

        renpy.display.render.mutated_surface(surf)

        self.screenshot_surface = surf

        with io.BytesIO() as sio:
            renpy.display.module.save_png(surf, sio, 0)
            return sio.getvalue()

    def show_window(self):
        if not renpy.store._window:
            return

        if not renpy.game.preferences.show_empty_window:
            return

        if renpy.game.context().scene_lists.shown_window:
            return

        if renpy.config.empty_window:
            old_history = renpy.store._history
            renpy.store._history = False

            renpy.performance.PPP("empty window")

            old_say_attributes = renpy.game.context().say_attributes
            old_temporary_attributes = renpy.game.context().temporary_attributes

            try:
                renpy.game.context().say_attributes = None
                renpy.game.context().temporary_attributes = None

                renpy.config.empty_window()

            finally:
                renpy.store._history = old_history

                renpy.game.context().say_attributes = old_say_attributes
                renpy.game.context().temporary_attributes = old_temporary_attributes

    def do_with(self, trans, paired, clear=False):
        if renpy.config.with_callback:
            trans = renpy.config.with_callback(trans, paired)

        if (not trans) or self.suppress_transition:
            self.with_none()
            return False
        else:
            self.set_transition(trans)
            return self.interact(
                trans_pause=True, suppress_overlay=not renpy.config.overlay_during_with, mouse="with", clear=clear
            )

    def with_none(self, overlay=True):
        """
        Implements the with None command, which sets the scene we will
        be transitioning from.
        """

        renpy.performance.PPP("start of with none")

        # Show the window, if that's necessary.
        self.show_window()

        # Compute the overlay.
        if overlay:
            self.compute_overlay()

        scene_lists = renpy.game.context().scene_lists

        # Compute the scene.
        for layer, d in self.compute_scene(scene_lists).items():
            if layer is None:
                if not self.transition:
                    self.old_scene[layer] = d
            elif layer not in self.transition:
                self.old_scene[layer] = d

        # Get rid of transient things.
        for i in renpy.config.overlay_layers:
            scene_lists.clear(i)

        scene_lists.replace_transient()
        scene_lists.shown_window = False

        if renpy.store._side_image_attributes_reset:
            renpy.store._side_image_attributes = None
            renpy.store._side_image_attributes_reset = False

    def end_transitions(self):
        """
        This runs at the end of each interaction to remove the transitions
        that have run their course.
        """

        layers = list(self.ongoing_transition)

        for l in layers:  # noqa: E741
            if l is None:
                self.ongoing_transition.pop(None, None)
                self.instantiated_transition.pop(None, None)
                self.transition_time.pop(None, None)
                self.transition_from.pop(None, None)
                continue

            start = self.transition_time.get(l, self.frame_time) or 0
            delay = self.transition_delay.get(l, None) or 0

            if (self.frame_time - start) >= delay:
                self.ongoing_transition.pop(l, None)
                self.instantiated_transition.pop(l, None)
                self.transition_time.pop(l, None)
                self.transition_from.pop(l, None)

    def get_ongoing_transition(self, layer=None):
        """
        Gets the transition that is currently operating on `layer`, or at the top level
        if `layer` is None.
        """

        rv = self.ongoing_transition.get(layer, None)
        transition = self.instantiated_transition.get(layer, None)

        if transition is None:
            return None

        start = self.transition_time.get(layer, self.frame_time) or self.frame_time
        delay = getattr(transition, "delay", 0)

        if (self.frame_time - start) < delay:
            return rv

        return None

    def set_transition(self, transition, layer=None, force=False):
        """
        Sets the transition that will be performed as part of the next
        interaction.
        """

        if self.suppress_transition and not force:
            return

        if transition is None:
            self.transition.pop(layer, None)
        else:
            self.transition[layer] = transition

    def event_peek(self, sleep=True):
        """
        This peeks the next event. It returns None if no event exists.
        """

        if renpy.emscripten and sleep:
            emscripten.sleep(0)

        if self.pushed_event:
            return self.pushed_event

        ev = pygame.event.poll()

        if ev.type == pygame.NOEVENT:
            self.check_background_screenshot()
            # Seems to prevent the CPU from speeding up.
            renpy.display.draw.event_peek_sleep()
            return None

        self.pushed_event = ev

        return ev

    def event_poll(self):
        """
        Called to busy-wait for an event while we're waiting to
        redraw a frame.
        """

        if renpy.emscripten:
            emscripten.sleep(0)

        if self.pushed_event:
            rv = self.pushed_event
            self.pushed_event = None
        else:
            rv = pygame.event.poll()

        self.last_event = rv

        return rv

    def event_wait(self):
        """
        This is in its own function so that we can track in the
        profiler how much time is spent in interact.
        """

        if self.pushed_event:
            rv = self.pushed_event
            self.pushed_event = None
            self.last_event = rv
            return rv

        self.check_background_screenshot()

        if renpy.emscripten:
            emscripten.sleep(0)

            while True:
                ev = pygame.event.poll()
                if ev.type != pygame.NOEVENT:
                    break

                emscripten.sleep(1)

        else:
            ev = pygame.event.wait()

        self.last_event = ev

        return ev

    def compute_overlay(self):
        if renpy.store.suppress_overlay:
            return

        # Figure out what the overlay layer should look like.
        renpy.ui.layer("overlay")

        for i in renpy.config.overlay_functions:
            i()

        if renpy.game.context().scene_lists.shown_window:
            for i in renpy.config.window_overlay_functions:
                i()

        renpy.ui.close()

    def compute_scene(self, scene_lists):
        """
        This converts scene lists into a dictionary mapping layer
        name to a Fixed containing that layer.
        """

        rv = {}

        for layer in renpy.display.scenelists.layers:
            d = scene_lists.make_layer(layer, self.layer_properties[layer])
            rv[layer] = scene_lists.transform_layer(layer, d)

        root = renpy.display.layout.MultiBox(layout="fixed")
        root.layers = {}

        for layer in renpy.config.layers:
            root.layers[layer] = rv[layer]
            root.add(rv[layer])

        rv[None] = root

        return rv

    def quit_event(self):
        """
        This is called to handle the user invoking a quit.
        """

        if self.screenshot is None:
            renpy.exports.take_screenshot()

        if self.quit_time > (time.time() - 0.75):
            renpy.exports.quit(save=True)

        if self.in_quit_event:
            renpy.exports.quit(save=True)

        if renpy.config.quit_action is not None:
            self.quit_time = time.time()

            # Make the screen more suitable for interactions.
            renpy.exports.movie_stop(only_fullscreen=True)
            renpy.store.mouse_visible = True

            try:
                self.in_quit_event = True
                renpy.display.behavior.run(renpy.config.quit_action)
            finally:
                self.in_quit_event = False

        else:
            renpy.exports.quit(save=True)

    def set_mouse(self, cursor):
        """
        Sets the current mouse cursor.

        True sets a visible system cursor. False hides the cursor. A ColorCursor
        object sets a cursor image.
        """

        if cursor is self.old_mouse:
            return

        self.old_mouse = cursor

        if cursor is True:
            pygame.mouse.reset()
            pygame.mouse.set_visible(True)
        elif cursor is False:
            pygame.mouse.reset()
            pygame.mouse.set_visible(False)
        else:
            pygame.mouse.set_visible(True)
            cursor.activate()

    def hide_mouse(self):
        """
        Called from the controller to hide the mouse when a controller
        event happens.
        """

        self.mouse_event_time = 0

    def is_mouse_visible(self):
        # Figure out if the mouse visibility algorithm is hiding the mouse.
        if (renpy.config.mouse_hide_time is not None) and (
            self.mouse_event_time + renpy.config.mouse_hide_time < renpy.display.core.get_time()
        ):
            visible = False
        else:
            visible = renpy.store.mouse_visible and (not renpy.game.less_mouse)

        visible = visible and self.show_mouse and not (renpy.display.video.fullscreen)

        return visible

    def get_mouse_names(self):
        """
        Return a list of possible mice that can be displayed, in priority order.
        """

        rv = [ ]

        mouse_kind = renpy.display.focus.get_mouse()
        if mouse_kind:
            rv.append(mouse_kind)

        if self.mouse:
            rv.append(self.mouse)

        rv.append(getattr(renpy.store, "default_mouse", "default"))

        if pygame.mouse.get_pressed()[0]:
            new_rv = [ ]
            for i in rv:
                new_rv.extend([ "pressed_" + i, i ])

            rv = new_rv

        return rv

    def get_mouse_name(self, cache_only=False, interaction=True):
        """
        This is now legacy, used to support a public api but not by Ren'Py.
        """

        mouse_kind = renpy.display.focus.get_mouse()

        if interaction and (mouse_kind is None):
            mouse_kind = self.mouse

        if mouse_kind is None:
            mouse_kind = "default"

        if pygame.mouse.get_pressed()[0]:
            mouse_kind = "pressed_" + mouse_kind

        if cache_only and (mouse_kind not in self.cursor_cache):  # type: ignore
            # if the mouse_kind cursor is not in cache, use a replacement
            # if pressed_ is in the cursor name, we'll try to use pressed_default
            # or the non-pressed cursor if we have it in cache
            # otherwise we'll use the default cursor
            if mouse_kind.startswith("pressed_") and ("pressed_default" in self.cursor_cache):  # type: ignore
                # if a generic pressed_default cursor is defined, use it
                mouse_kind = "pressed_default"
            elif mouse_kind.startswith("pressed_") and (mouse_kind[8:] in self.cursor_cache):  # type: ignore
                # otherwise use the non-pressed cursor if we have it in cache
                mouse_kind = mouse_kind[8:]
            else:
                mouse_kind = "default"

        if mouse_kind == "default":
            mouse_kind = getattr(renpy.store, "default_mouse", "default")

        return mouse_kind


    def update_mouse(self, mouse_displayable):
        visible = self.is_mouse_visible()

        if mouse_displayable is not None:

            x, y = renpy.exports.get_mouse_pos()

            cursor_function = getattr(mouse_displayable, "_has_mouse_cursor", None)
            if cursor_function is not None:
                has_cursor = cursor_function()
            else:
                has_cursor = True

            if (0 <= x < renpy.config.screen_width) and (0 <= y < renpy.config.screen_height):
                visible = not has_cursor

        # If not visible, hide the mouse.
        if not visible:
            self.set_mouse(False)
            return

        # Deal with a hardware mouse, the easy way.
        if self.cursor_cache is None:
            self.set_mouse(True)
            return

        # Use hardware mouse if the preferences force.
        if renpy.game.preferences.system_cursor:
            if isinstance(self.old_mouse, pygame.mouse.ColorCursor):
                pygame.mouse.reset()
            self.set_mouse(True)
            return

        for mouse_kind in self.get_mouse_names():
            if mouse_kind in self.cursor_cache:
                anim = self.cursor_cache[mouse_kind]
                cursor = anim[self.ticks % len(anim)]
                break
        else:
            cursor = True

        self.set_mouse(cursor)

    def set_mouse_pos(self, x, y, duration):
        """
        Sets the mouse position. Duration can be a number of seconds or
        None.
        """

        self.mouse_move = MouseMove(x, y, duration)
        self.force_redraw = True

    def drawn_since(self, seconds_ago):
        """
        Returns true if the screen has been drawn in the last `seconds_ago`,
        and false otherwise.
        """

        return (get_time() - self.frame_time) <= seconds_ago

    def mobile_save(self):
        """
        Create a mobile reload file.
        """

        should_skip_save = renpy.store.main_menu or renpy.store._in_replay
        if renpy.config.save_on_mobile_background and not should_skip_save:
            renpy.loadsave.save("_reload-1", include_screenshot=False)

        renpy.persistent.update(True)
        renpy.persistent.save_on_quit_MP()

    def mobile_unlink(self):
        """
        Delete an unused mobile reload file.
        """

        # Since we came back to life, we can get rid of the
        # auto-reload.
        renpy.loadsave.unlink_save("_reload-1")

    def check_android_start(self):
        """
        Delays until the android screen is visible, to ensure the
        GL context is created properly.
        """

        from jnius import autoclass

        SDLActivity = autoclass("org.libsdl.app.SDLActivity")

        if SDLActivity.mHasFocus:
            return

        renpy.display.log.write("App not focused at interface start, shutting down early.")

        self.mobile_save()

        os._exit(1)

    def check_suspend(self, ev):
        """
        Handles the SDL2 suspend process.
        """

        if ev.type != pygame.APP_WILLENTERBACKGROUND:
            return False

        print("Pausing audio.")

        renpy.audio.audio.pause_all()

        print("Disabling periodic events.")

        pygame.time.set_timer(PERIODIC, 0)
        pygame.time.set_timer(REDRAW, 0)
        pygame.time.set_timer(TIMEEVENT, 0)

        print("Saving.")

        try:
            self.mobile_save()
        except Exception:
            import traceback

            traceback.print_exc()

        if renpy.config.quit_on_mobile_background:
            if renpy.android:
                try:
                    android.activity.finishAndRemoveTask()
                except Exception:
                    pass

                from jnius import autoclass

                System = autoclass("java.lang.System")
                System.exit(0)

            sys.exit(0)

        print("Freeing memory.")

        renpy.exports.free_memory()

        print("Releasing wakelock.")

        if renpy.android:
            android.wakelock(False)

            # Tell Android to end the onStop method.
            android.activity.finishOnStop()

        print("Entered background. --------------------------------------------")

        while True:
            ev = pygame.event.wait()

            if ev.type == pygame.APP_TERMINATING:
                sys.exit(0)

            if ev.type == pygame.APP_DIDENTERFOREGROUND:
                break

        print("Entering foreground. -------------------------------------------")

        self.mobile_unlink()

        pygame.time.set_timer(PERIODIC, PERIODIC_INTERVAL)

        renpy.audio.audio.unpause_all()

        if renpy.android:
            android.wakelock(True)
            android.activity.armOnStop()

        # Reset the display so we get the GL context back.
        self.display_reset = True
        self.restart_interaction = True

        return True

    def enter_context(self):
        """
        Called when we enter a new context.
        """

        # Stop ongoing transitions.
        self.ongoing_transition.clear()
        self.instantiated_transition.clear()
        self.transition_from.clear()
        self.transition_time.clear()

    def post_time_event(self):
        """
        Posts a time_event object to the queue.
        """

        try:
            pygame.event.post(self.time_event)
        except Exception:
            pass

    def after_longpress(self):
        """
        Called after a longpress, to ignore the mouse button release.
        """

        self.ignore_touch = True
        renpy.display.focus.mouse_handler(None, -1, -1, default=False)

    def text_event_in_queue(self):
        """
        Returns true if the next event in the queue is a text editing event.
        """

        ev = self.event_peek()
        if ev is None:
            return False
        else:
            return ev.type in (pygame.TEXTINPUT, pygame.TEXTEDITING)

    def update_text_rect(self):
        """
        Updates the text input state and text rectangle.
        """

        if renpy.store._text_rect is not None:
            self.text_rect = renpy.store._text_rect

        if self.text_rect is not None:
            not_shown = pygame.key.has_screen_keyboard_support() and not pygame.key.is_screen_keyboard_shown()
            if self.touch_keyboard:
                not_shown = renpy.exports.get_screen("_touch_keyboard", layer="screens") is None

            if self.old_text_rect != self.text_rect:
                x, y, w, h = self.text_rect
                x0, y0 = renpy.display.draw.untranslate_point(x, y)
                x1, y1 = renpy.display.draw.untranslate_point(x + w, y + h)
                rect = (x0, y0, x1 - x0, y1 - y0)

                pygame.key.set_text_input_rect(rect)

            if not self.old_text_rect or not_shown:
                pygame.key.start_text_input()

                if self.touch_keyboard:
                    renpy.exports.restart_interaction()  # required in mobile mode
                    renpy.exports.show_screen(
                        "_touch_keyboard",
                        _layer="screens",  # not 'transient' so as to be above other screens
                        # not 'overlay' as it conflicts with console
                        _transient=True,
                    )

        else:
            if self.old_text_rect:
                pygame.key.stop_text_input()
                pygame.key.set_text_input_rect(None)

                if self.touch_keyboard:
                    renpy.exports.hide_screen("_touch_keyboard", layer="screens")

        self.old_text_rect = self.text_rect

    def maximum_framerate(self, t):
        """
        Forces Ren'Py to draw the screen at the maximum framerate for `t` seconds.
        """

        if t is None:
            self.maximum_framerate_time = 0
        else:
            self.maximum_framerate_time = max(self.maximum_framerate_time, get_time() + t)

    def interact(self, clear=True, suppress_window=False, trans_pause=False, pause=None, pause_modal=False, **kwargs):
        """
        This handles an interaction, restarting it if necessary. All of the
        keyword arguments are passed off to interact_core.
        """

        renpy.plog(1, "start of new interaction")

        if not self.started:
            self.start()

        if self.clear_screenshot:
            self.lose_screenshot()

        self.clear_screenshot = False

        self.trans_pause = trans_pause

        context = renpy.game.context()

        if context.interacting:
            raise Exception(
                "Cannot start an interaction in the middle of an interaction, without creating a new context."
            )

        context.interacting = True

        # Show a missing window.
        if not suppress_window:
            self.show_window()

        # These things can be done once per interaction.

        preloads = self.preloads
        self.preloads = []

        try:
            self.start_interact = True

            for i in renpy.config.start_interact_callbacks:
                i()

            self.interaction_counter = 0

            repeat = True
            rv = None

            pause_start = get_time()

            while repeat:
                self.interaction_counter += 1

                if self.interaction_counter == 100 and renpy.config.developer:
                    raise Exception("renpy.restart_interaction() was called 100 times without processing any input.")

                repeat, rv = self.interact_core(
                    preloads=preloads,
                    trans_pause=trans_pause,
                    pause=pause,
                    pause_start=pause_start,
                    pause_modal=pause_modal,
                    **kwargs,
                )

                self.start_interact = False

            return rv

        finally:
            renpy.game.context().deferred_translate_identifier = None

            self.force_prediction = False

            context.interacting = False

            # Clean out transient stuff at the end of an interaction.
            if clear:
                scene_lists = renpy.game.context().scene_lists
                scene_lists.replace_transient()

            self.end_transitions()

            self.restart_interaction = True

            renpy.game.context().mark_seen()
            renpy.game.context().scene_lists.shown_window = False

            if renpy.game.log is not None:
                renpy.game.log.did_interaction = True

            if renpy.store._side_image_attributes_reset:
                renpy.store._side_image_attributes = None
                renpy.store._side_image_attributes_reset = False

    def consider_gc(self):
        """
        Considers if we should perform a garbage collection.
        """

        if not renpy.config.manage_gc:
            return

        count = gc.get_count()

        if count[0] >= renpy.config.idle_gc_count:
            renpy.plog(2, "before gc")

            if count[2] >= renpy.config.gc_thresholds[2]:
                gen = 2
            elif count[1] >= renpy.config.gc_thresholds[1]:
                gen = 1
            else:
                gen = 0

            gc.collect(gen)

            if gc.garbage:
                renpy.memory.print_garbage(gen)
                del gc.garbage[:]

            renpy.plog(2, "after gc")

    def idle_frame(self, expensive):
        """
        Tasks that are run during "idle" frames.
        """

        if expensive:
            renpy.plog(1, "start idle_frame (expensive)")
        else:
            renpy.plog(1, "start idle_frame (inexpensive)")

        # We want this to include the GC time, so we don't predict on
        # frames where we GC.
        start = get_time()

        step = 1

        while True:
            if self.event_peek(False) and not self.force_prediction:
                break

            if not expensive:
                if get_time() > (start + 0.0005):
                    break

            # Step 1: Run gc.
            if step == 1:
                self.consider_gc()
                step += 1

            # Step 2: Push textures to GPU.
            elif step == 2:
                renpy.display.draw.ready_one_texture()
                step += 1

            # Step 3: Predict more images.
            elif step == 3:
                if not self.prediction_coroutine:
                    step += 1
                    continue

                try:
                    result = self.prediction_coroutine.send(expensive)
                except ValueError:
                    # Saw this happen once during a quit, giving a
                    # ValueError: generator already executing
                    result = None

                if result is None:
                    self.prediction_coroutine = None
                    step += 1

                elif result is False:
                    if not expensive:
                        step += 1

            # Step 4: Preload images (on emscripten)
            elif step == 4:
                if renpy.emscripten:
                    if expensive:
                        allow_preload = True
                        self.last_emscripten_preload_time = get_time()
                    elif renpy.config.emscripten_preload_timeout is None:
                        allow_preload = False
                    elif get_time() - self.last_emscripten_preload_time > renpy.config.emscripten_preload_timeout:
                        allow_preload = True
                    else:
                        allow_preload = False

                    if allow_preload:
                        try:
                            renpy.display.im.cache.in_preload_pass = True
                            renpy.display.im.cache.preload_thread_pass()
                        finally:
                            renpy.display.im.cache.in_preload_pass = False

                step += 1

            # Step 5: Autosave.
            elif step == 5:
                if not self.did_autosave:
                    renpy.loadsave.autosave()
                    self.did_autosave = True

                step += 1

            # Step 6: Persistent data.
            elif step == 6:
                if not self.did_persistent:
                    if renpy.emscripten:
                        renpy.persistent.update()
                    else:
                        renpy.persistent.check_update()

                    self.did_persistent = True

                step += 1

            else:
                # Check to see if preloading has finished
                if renpy.display.im.cache.done():
                    self.force_prediction = False

                break

        if expensive:
            renpy.plog(1, "end idle_frame (expensive)")
        else:
            renpy.plog(1, "end idle_frame (inexpensive)")

    # This gets assigned below.
    take_layer_displayable = None

    def interact_core(
        self,
        show_mouse=True,
        trans_pause=False,
        suppress_overlay=False,
        suppress_underlay=False,
        mouse: str|None = None,
        preloads=[],
        roll_forward=None,
        pause=None,
        pause_start=0.0,
        pause_modal=None,
    ):
        """
        This handles one cycle of displaying an image to the user,
        and then responding to user input.

        @param show_mouse: Should the mouse be shown during this
        interaction? Only advisory, and usually doesn't work.

        @param trans_pause: If given, we must have a transition. Should we
        add a pause behavior during the transition?

        @param suppress_overlay: This suppresses the display of the overlay.
        @param suppress_underlay: This suppresses the display of the underlay.

        `pause`
            If not None, the amount of time before the interaction ends with
            False being returned.

        `pause_modal`
            If true, the pause will respect modal windows. If false, it will
            not.
        """

        renpy.plog(1, "start interact_core")

        # Check to see if the language has changed.
        renpy.translation.check_language()

        suppress_overlay = suppress_overlay or renpy.store.suppress_overlay

        # Store the various parameters.
        self.suppress_overlay = suppress_overlay
        self.suppress_underlay = suppress_underlay
        self.trans_pause = trans_pause

        # Show default screens.
        renpy.display.screen.show_overlay_screens(suppress_overlay)

        # Prepare screens, if need be.
        renpy.display.screen.prepare_screens()

        self.roll_forward = roll_forward
        self.show_mouse = show_mouse

        suppress_transition = renpy.config.skipping or renpy.game.less_updates

        # The global one.
        self.suppress_transition = False

        # Figure out transitions.
        if suppress_transition or renpy.game.after_rollback:
            self.ongoing_transition.clear()
            self.instantiated_transition.clear()
            self.transition_from.clear()
            self.transition_time.clear()

        if not suppress_transition:
            for k, t in self.transition.items():
                if k not in self.old_scene:
                    continue

                self.ongoing_transition[k] = t
                self.transition_from[k] = self.old_scene[k]._in_current_store()
                self.transition_time[k] = None

        self.transition.clear()

        # Safety condition, prevents deadlocks.
        if trans_pause:
            if not self.ongoing_transition:
                return False, None
            if None not in self.ongoing_transition:
                return False, None
            if suppress_transition:
                return False, None
            if not self.old_scene:
                return False, None

        # We just restarted.
        self.restart_interaction = False

        # Setup the mouse.
        if mouse is None:
            if getattr(renpy.store, "main_menu", False):
                mouse = "mainmenu"
            elif getattr(renpy.store, "_menu", False):
                mouse = "gamemenu"
            else:
                mouse = "default"

        self.mouse = mouse

        # The start and end times of this interaction.
        start_time = get_time()
        end_time = start_time

        self.frame_time = start_time

        for i in renpy.config.interact_callbacks:
            i()

        # Set the window caption.
        self.set_window_caption()

        # Set the screensaver.
        self.set_screensaver()

        # Tick time forward.
        renpy.display.im.cache.tick()
        renpy.text.text.text_tick()
        renpy.display.predict.reset()
        renpy.gl2.gl2shadercache.shader_part_filter_cache.clear()

        # Clear the size groups.
        renpy.display.layout.size_groups.clear()

        # Clear the set of updated screens.
        renpy.display.screen.updated_screens.clear()

        # Clear some events.
        pygame.event.clear((PERIODIC, TIMEEVENT, REDRAW))

        # Accumulate relative mouse movement from otherwise obsolete events.
        global relx, rely
        for ev in pygame.event.get(pygame.MOUSEMOTION):
            xr, yr = ev.rel
            relx += xr
            rely += yr
            self.mouse_event_time = get_time()

        # Add a single TIMEEVENT to the queue.
        self.post_time_event()

        # Figure out the scene list we want to show.
        scene_lists = renpy.game.context().scene_lists

        # Remove the now-hidden things.
        scene_lists.remove_hidden()

        # Compute the overlay.
        if not suppress_overlay:
            self.compute_overlay()

        # The root widget of everything that is displayed on the screen.
        root_widget = renpy.display.layout.MultiBox(layout="fixed")
        root_widget.layers = {}

        # A list of widgets that are roots of trees of widgets that are
        # considered for focusing.
        focus_roots = []

        # Add the underlay to the root widget.
        if not suppress_underlay:
            for i in renpy.config.underlay:
                root_widget.add(i)
                focus_roots.append(i)

            if roll_forward is not None:
                rfw = renpy.display.behavior.RollForward(roll_forward)
                root_widget.add(rfw)
                focus_roots.append(rfw)

        # Figure out the scene. (All of the layers, and the root.)
        scene = self.compute_scene(scene_lists)
        renpy.display.tts.set_root(scene[None])

        renpy.plog(1, "computed scene")

        # If necessary, load all images here.
        for w in scene.values():
            try:
                renpy.display.predict.displayable(w)
            except Exception:
                pass

        renpy.plog(1, "final predict")

        if pause is not None:
            pb = renpy.display.behavior.PauseBehavior(pause, modal=pause_modal)
            root_widget.add(pb, pause_start, pause_start)
            focus_roots.append(pb)

        # The root widget of all of the layers.
        layers_root = renpy.display.layout.MultiBox(layout="fixed")
        layers_root.layers = {}

        def instantiate_transition(layer, old_d, new_d):
            """
            Create a transition that will be used to transition `layer` (which
            can be None) to `d`.
            """

            old_trans = self.instantiated_transition.get(layer, None)

            trans = self.ongoing_transition[layer](old_widget=old_d, new_widget=new_d)

            if not isinstance(trans, Displayable):
                raise Exception("Expected transition to return a displayable, not a {!r}".format(trans))

            if isinstance(trans, renpy.display.transform.Transform) and isinstance(
                old_trans, renpy.display.transform.Transform
            ):
                trans.take_state(old_trans)
                trans.take_execution_state(old_trans)

            self.instantiated_transition[layer] = trans

            return trans

        def add_layer(where, layer):
            scene_layer = scene[layer]
            focus_roots.append(scene_layer)

            if self.ongoing_transition.get(layer, None) and not suppress_transition:
                trans = instantiate_transition(layer, self.transition_from[layer], scene_layer)

                transition_time = self.transition_time.get(layer, None)

                where.add(trans, transition_time, transition_time)
                where.layers[layer] = trans

            else:
                where.add(scene_layer)
                where.layers[layer] = scene_layer

        # Add the bottom layers to root_widget.
        for layer in renpy.config.bottom_layers:
            add_layer(root_widget, layer)

        # Add layers (perhaps with transitions) to the layers root.
        for layer in renpy.config.layers:
            add_layer(layers_root, layer)

        def add_layers_to_root(d, st=None, at=None):
            """
            This adds layers_root to root_widget, perhaps transforming it
            with config.layer_transforms[None]
            """

            at_list = renpy.config.layer_transforms.get(None, [])

            if not at_list:
                root_widget.add(d, st, at)
                return

            rv = renpy.display.layout.MultiBox(layout="fixed")
            rv.adjust_times = True
            rv.add(d, st, at)

            new_transform = None

            for a in at_list:
                if isinstance(a, renpy.display.motion.Transform):
                    rv = a(child=rv)
                else:
                    rv = a(rv)

                rv._unique()

                if isinstance(rv, renpy.display.transform.Transform):
                    new_transform = rv

            if new_transform is not None:
                scene_lists.transform_state(self.old_root_transform, new_transform, execution=True)

            self.old_root_transform = new_transform
            root_widget.add(rv, 0, 0)

        # Add layers_root to root_widget, perhaps through a transition.
        if self.ongoing_transition.get(None, None) and not suppress_transition:
            old_root = renpy.display.layout.MultiBox(layout="fixed")
            old_root.layers = {}

            for layer in renpy.config.layers:
                d = self.transition_from[None].layers[layer]
                old_root.layers[layer] = d
                old_root.add(d)

            trans = instantiate_transition(None, old_root, layers_root)

            if not isinstance(trans, Displayable):
                raise Exception("Expected transition to return a displayable, not a {!r}".format(trans))

            transition_time = self.transition_time.get(None, None)
            add_layers_to_root(trans, transition_time, transition_time)

            if (transition_time is None) and isinstance(trans, renpy.display.transform.Transform):
                trans.update_state()

            if trans_pause:
                if renpy.store._dismiss_pause:
                    sb = renpy.display.behavior.SayBehavior(dismiss=[], dismiss_unfocused="dismiss")
                else:
                    sb = renpy.display.behavior.SayBehavior(dismiss=[], dismiss_unfocused="dismiss_hard_pause")

                root_widget.add(sb)
                focus_roots.append(sb)

                pb = renpy.display.behavior.PauseBehavior(trans.delay)
                root_widget.add(pb, transition_time, transition_time)
                focus_roots.append(pb)

        else:
            add_layers_to_root(layers_root)

        # Add top_layers to the root_widget.
        for layer in renpy.config.top_layers:
            add_layer(root_widget, layer)

        for i in renpy.display.emulator.overlay:
            root_widget.add(i)

        if renpy.game.preferences.system_cursor:
            mouse_displayable = None
        else:
            mouse_displayable = renpy.config.mouse_displayable
            if mouse_displayable is not None:
                if not isinstance(mouse_displayable, Displayable):
                    mouse_displayable = mouse_displayable()

                if mouse_displayable is not None:
                    root_widget.add(mouse_displayable, 0, 0)

        self.prediction_coroutine = renpy.display.predict.prediction_coroutine(root_widget)
        self.prediction_coroutine.send(None)

        # Clean out the registered adjustments.
        renpy.display.behavior.adj_registered.clear()

        # Clean up some movie-related things.
        renpy.display.video.early_interact()

        # A dict of any active layer transitions.
        layer_transitions = {}

        # This try block is used to force cleanup even on termination
        # caused by an exception propagating through this function.
        try:
            # Call per-interaction code for all displayables.
            def take_layer_displayable(ld):
                """
                This is called by a layer displayable to add the layer's
                contents to the layer displayable.
                """

                if ld.layer in scene:
                    ld.layers = layer_transitions
                    add_layer(ld, ld.layer)
                    del ld.layers
                else:
                    ld.add(null)

            self.take_layer_displayable = take_layer_displayable

            renpy.display.behavior.input_pre_per_interact()
            root_widget.visit_all(lambda d: d.per_interact())
            renpy.display.behavior.input_post_per_interact()

            renpy.gl2.live2d.update_states()

            self.take_layer_displayable = None

            # Consolidate static and layer transitions for later processing.
            if layer_transitions:
                layer_transitions.update(layers_root.layers)
            else:
                layer_transitions = layers_root.layers

            # Now, update various things regarding scenes and transitions,
            # so we are ready for a new interaction or a restart.
            self.old_scene = scene

            # Okay, from here on we now have a single root widget (root_widget),
            # which we will try to show to the user.

            # Figure out what should be focused.
            renpy.display.behavior.WebInput.pre_find_focusable()
            renpy.display.focus.before_interact(focus_roots)
            renpy.display.behavior.WebInput.post_find_focusable()

            # We're no longer after rollback.
            renpy.game.after_rollback = False

            # Something updated the screens. Deal with it now, so the player doesn't
            # see it.
            if self.restart_interaction:
                return True, None

            # Redraw the screen.
            needs_redraw = True

            # First pass through the while loop?
            first_pass = True

            # We don't yet know when the interaction began.
            self.interact_time = None

            # We only want to do autosave once.
            self.did_autosave = False

            # We only want to save persistent once.
            self.did_persistent = False

            old_timeout_time = None
            old_redraw_time = None

            rv = None

            # Start sound.
            renpy.audio.audio.interact()

            # How long until we redraw.
            _redraw_in = 3600

            # Have we drawn a frame yet?
            video_frame_drawn = False

            # How many frames have we shown so far?
            frame = 0

            can_block = False

            while rv is None:
                renpy.plog(1, "start of interact while loop")

                renpy.execution.not_infinite_loop(10)

                # Check for autoreload.
                renpy.loader.check_autoreload()

                if renpy.emscripten or os.environ.get("RENPY_SIMULATE_DOWNLOAD", False):
                    renpy.webloader.process_downloaded_resources()

                avoid_draw = False

                if renpy.emscripten:
                    avoid_draw = emscripten.run_script_int("webglContextLost")

                    if emscripten.run_script_int("webglContextRestored"):
                        self.display_reset = True
                        emscripten.run_script("webglContextRestored = false;")

                if not avoid_draw:
                    for i in renpy.config.needs_redraw_callbacks:
                        if i():
                            needs_redraw = True

                    # Check for a fullscreen change.

                    if not renpy.display.can_fullscreen:
                        renpy.game.preferences.fullscreen = False

                    if renpy.game.preferences.fullscreen != self.fullscreen:
                        if renpy.emscripten:
                            if renpy.game.preferences.fullscreen:
                                emscripten.run_script("setFullscreen(true);")
                            else:
                                emscripten.run_script("setFullscreen(false);")

                        else:
                            renpy.display.draw.resize()

                    # Ask if the game has changed size.
                    if renpy.display.draw.update(force=self.display_reset):
                        needs_redraw = True

                    # Redraw the screen.
                    if self.force_redraw or (
                        (first_pass or not pygame.event.peek(ALL_EVENTS))
                        and renpy.display.draw.should_redraw(needs_redraw, first_pass, can_block)
                    ):
                        self.force_redraw = False

                        renpy.display.render.process_redraws()

                        # If we have a movie, start showing it.
                        fullscreen_video = renpy.display.video.interact()

                        # Clean out the redraws, if we have to.
                        # renpy.display.render.kill_redraws()

                        self.text_rect = None

                        # Draw the screen.
                        self.frame_time = get_time()

                        renpy.audio.audio.advance_time()  # Sets the time of all video frames.

                        self.draw_screen(root_widget, fullscreen_video, (not fullscreen_video) or video_frame_drawn)

                        if first_pass:
                            if not self.interact_time:
                                self.interact_time = max(self.frame_time, get_time() - self.frame_duration)

                            scene_lists.set_times(self.interact_time)

                            for k, v in self.transition_time.items():
                                if v is None:
                                    self.transition_time[k] = self.interact_time

                            renpy.display.render.adjust_render_cache_times(self.frame_time, self.interact_time)

                        frame += 1
                        renpy.config.frames += 1

                        # If profiling is enabled, report the profile time.
                        if renpy.config.profile or self.profile_once:
                            renpy.plog(0, "end frame")
                            renpy.performance.analyze()
                            renpy.performance.clear()
                            renpy.plog(0, "start frame")

                            self.profile_once = False

                        if (
                            first_pass
                            and self.last_event
                            and self.last_event.type
                            in [pygame.MOUSEBUTTONDOWN, pygame.MOUSEBUTTONUP, pygame.MOUSEMOTION]
                        ):
                            x, y = renpy.display.draw.get_mouse_pos()
                            x, y = renpy.test.testmouse.get_mouse_pos(x, y)
                            ev, x, y = renpy.display.emulator.emulator(self.last_event, x, y)

                            if self.ignore_touch:
                                x = -1
                                y = -1

                            if (self.last_event.type == pygame.MOUSEBUTTONUP) and getattr(
                                self.last_event, "touch", False
                            ):
                                x = -1
                                y = -1

                            renpy.display.focus.mouse_handler(None, x, y, default=False)

                        needs_redraw = False
                        first_pass = False

                        pygame.time.set_timer(REDRAW, 0)
                        pygame.event.clear([REDRAW])
                        old_redraw_time = None

                        self.update_text_rect()

                        renpy.test.testexecution.execute()

                # TTS.
                renpy.display.tts.tick()

                # Move the mouse, if necessary.
                if self.mouse_move is not None:
                    if not self.mouse_move.perform():
                        self.mouse_move = None

                # Check the autosave callback.
                if renpy.loadsave.did_autosave:
                    renpy.loadsave.did_autosave = False
                    renpy.exports.run(renpy.config.autosave_callback)

                # End an obsolete ongoing transition.
                if (
                    (not trans_pause)
                    and self.ongoing_transition.get(None, None)
                    and not self.get_ongoing_transition(None)
                ):
                    self.transition.pop(None, None)
                    self.ongoing_transition.pop(None, None)
                    self.transition_time.pop(None, None)
                    self.transition_from.pop(None, None)
                    self.restart_interaction = True

                # See if we want to restart the interaction entirely.
                if self.restart_interaction and not self.display_reset:
                    return True, None

                # Determine if we need a redraw. (We want to run these
                # functions, so we put them first to prevent short-circuiting.)

                if renpy.display.video.frequent():
                    needs_redraw = True
                    video_frame_drawn = True

                if renpy.display.render.check_redraws():
                    needs_redraw = True

                # How many seconds until we timeout.
                _timeout_in = 3600

                # Handle the redraw timer.
                redraw_time = renpy.display.render.redraw_time()

                # We only need to set the REDRAW timer if we can block.
                can_block = renpy.display.draw.can_block()

                if self.maximum_framerate_time > get_time():
                    can_block = False

                if (redraw_time is not None) and (not needs_redraw) and can_block:
                    if redraw_time != old_redraw_time:
                        time_left = redraw_time - get_time()
                        time_left = min(time_left, 3600)
                        _redraw_in = time_left

                        if time_left <= 0:
                            try:
                                pygame.event.post(self.redraw_event)
                            except Exception:
                                pass
                            pygame.time.set_timer(REDRAW, 0)
                        else:
                            pygame.time.set_timer(REDRAW, max(int(time_left * 1000), 1))

                        old_redraw_time = redraw_time
                else:
                    _redraw_in = 3600
                    pygame.time.set_timer(REDRAW, 0)

                # Handle the timeout timer.
                if not self.timeout_time:
                    pygame.time.set_timer(TIMEEVENT, 0)
                else:
                    time_left = self.timeout_time - get_time()
                    time_left = min(time_left, 3600)
                    _timeout_in = time_left

                    if time_left <= 0:
                        self.timeout_time = None
                        pygame.time.set_timer(TIMEEVENT, 0)
                        self.post_time_event()
                    elif self.timeout_time != old_timeout_time:
                        # Always set to at least 1ms.
                        pygame.time.set_timer(TIMEEVENT, int(time_left * 1000 + 1))
                        old_timeout_time = self.timeout_time

                # Process the invoke queue.
                while self.invoke_queue:
                    fn, args, kwargs = self.invoke_queue.pop(0)
                    rv = fn(*args, **kwargs)
                    if rv is not None:
                        return False, rv

                if can_block or (frame >= renpy.config.idle_frame) or (self.force_prediction):
                    expensive = not (
                        needs_redraw or (_redraw_in < 0.2) or (_timeout_in < 0.2) or renpy.display.video.playing()
                    )

                    if self.force_prediction:
                        expensive = True
                        can_block = True

                    self.idle_frame(expensive)

                if needs_redraw or (not can_block) or self.mouse_move or renpy.display.video.playing():
                    renpy.plog(1, "pre peek")
                    ev = self.event_poll()
                    renpy.plog(1, "post peek {!r}", ev)
                else:
                    renpy.plog(1, "pre wait")
                    ev = self.event_wait()
                    renpy.plog(1, "post wait {!r}", ev)

                self.interaction_counter = 0

                if ev.type == pygame.NOEVENT:
                    if can_block and (not needs_redraw) and (not self.prediction_coroutine) and (not self.mouse_move):
                        pygame.time.wait(1)

                    continue

                # Add the current keyboard modifiers to all events.
                if ev.type in (pygame.KEYDOWN, pygame.KEYUP):
                    self.mod = ev.mod

                ev.mod = self.mod

                # Recognize and ignore AltGr on Windows.
                if ev.type == pygame.KEYDOWN:
                    if ev.key == pygame.K_LCTRL:
                        ev2 = self.event_peek()

                        if (ev2 is not None) and (ev2.type == pygame.KEYDOWN):
                            if ev2.key == pygame.K_RALT:
                                continue

                # Check to see if the OS is asking us to suspend (on Android
                # and iOS.)
                if self.check_suspend(ev):
                    continue

                # Try to merge an TIMEEVENT with other timeevents.
                if ev.type == TIMEEVENT:
                    old_timeout_time = None
                    pygame.event.clear([TIMEEVENT])

                    # Set the modal flag to False.
                    ev.modal = False

                # On Android, where we have multiple mouse buttons, we can
                # merge a mouse down and mouse up event with its successor. This
                # prevents us from getting overwhelmed with too many events on
                # a multitouch screen.
                if renpy.android and (ev.type == pygame.MOUSEBUTTONDOWN or ev.type == pygame.MOUSEBUTTONUP):
                    pygame.event.clear(ev.type)

                # Handle redraw timeouts.
                if ev.type == REDRAW:
                    pygame.event.clear([REDRAW])
                    old_redraw_time = None
                    continue

                # Handle periodic events. This includes updating the mouse timers (and through the loop,
                # the mouse itself), and the audio system periodic calls.
                if ev.type == PERIODIC:
                    events = 1 + len(pygame.event.get([PERIODIC]))
                    self.ticks += events

                    for i in renpy.config.periodic_callbacks:
                        i()

                    renpy.audio.audio.periodic()
                    renpy.display.tts.periodic()
                    renpy.display.controller.periodic()

                    self.update_mouse(mouse_displayable)

                    if renpy.emscripten:
                        self.exec_js_cmd()

                    continue

                if renpy.config.log_events:
                    renpy.display.log.write("%r", ev)

                # Handle quit specially for now.
                if ev.type == pygame.QUIT:
                    self.quit_event()
                    continue

                # Ignore KEY-events while text is being edited (usually with an IME).
                if ev.type == pygame.TEXTEDITING:
                    if ev.text:
                        self.text_editing = ev
                    else:
                        self.text_editing = None
                elif ev.type == pygame.TEXTINPUT:
                    self.text_editing = None

                elif ev.type == pygame.KEYMAPCHANGED:
                    # Clear the mods when the keymap is changed, such as when
                    # an IME is selected. This fixes a problem on Windows 10 where
                    # super+space won't unset super.

                    # This only happens when the GUI key is down, as shift can
                    # also change the keymap.

                    if pygame.key.get_mods() & pygame.KMOD_GUI:
                        pygame.key.set_mods(pygame.key.get_mods() & (pygame.KMOD_NUM | pygame.KMOD_CAPS))

                    continue

                elif self.text_editing and ev.type in [pygame.KEYDOWN, pygame.KEYUP]:
                    continue

                if ev.type == pygame.VIDEOEXPOSE:
                    # Needed to force the display to redraw after expose in
                    # the software renderer.

                    if isinstance(renpy.display.draw, renpy.display.swdraw.SWDraw):
                        renpy.display.draw.full_redraw = True
                        renpy.game.interface.force_redraw = True

                    continue

                # Handle videoresize.
                if ev.type == pygame.VIDEORESIZE:
                    evs = pygame.event.get(pygame.VIDEORESIZE)
                    ev = evs[-1] if evs else ev
                    renpy.display.log.write("Resize event: %r", ev)

                    if isinstance(renpy.display.draw, renpy.display.swdraw.SWDraw):
                        renpy.display.draw.full_redraw = True

                    renpy.game.interface.force_redraw = True

                    continue

                # Handle window moves.
                if ev.type == pygame.WINDOWMOVED:
                    self.on_move(ev.pos)
                    continue

                # If we're ignoring touch events, and get a mouse up, stop
                # ignoring those events.
                if self.ignore_touch and ev.type == pygame.MOUSEBUTTONUP and ev.button == 1:
                    self.ignore_touch = False
                    continue

                # Merge mousemotion events.
                if ev.type == pygame.MOUSEMOTION:
                    xr, yr = ev.rel
                    relx += xr
                    rely += yr

                    for ev in pygame.event.get(pygame.MOUSEMOTION):
                        xr, yr = ev.rel
                        relx += xr
                        rely += yr

                    # Event is now the most recent mouse move due to loop.
                    ev.rel = relx, rely
                    relx = 0
                    rely = 0

                    if renpy.windows:
                        self.mouse_focused = True

                # Handle mouse event time, and ignoring touch.
                if (
                    ev.type == pygame.MOUSEMOTION
                    or ev.type == pygame.MOUSEBUTTONDOWN
                    or ev.type == pygame.MOUSEBUTTONUP
                ):
                    self.mouse_event_time = renpy.display.core.get_time()

                    if self.ignore_touch:
                        renpy.display.focus.mouse_handler(None, -1, -1, default=False)

                    if mouse_displayable:
                        renpy.display.render.redraw(mouse_displayable, 0)

                # Handle focus notifications.
                if ev.type == pygame.ACTIVEEVENT:
                    if ev.state & 1:
                        if not ev.gain:
                            renpy.display.focus.clear_focus()

                        self.mouse_focused = ev.gain

                        if mouse_displayable:
                            renpy.display.render.redraw(mouse_displayable, 0)

                    if ev.state & 2:
                        self.keyboard_focused = ev.gain

                        if not renpy.game.preferences.audio_when_unfocused and not renpy.emscripten:
                            if not ev.gain:
                                renpy.audio.audio.pause_all()
                            else:
                                renpy.audio.audio.unpause_all()

                    # If the window becomes inactive as a result of this event
                    # pause the audio according to preference
                    if not renpy.game.preferences.audio_when_minimized and not renpy.emscripten:
                        if not pygame.display.get_active() and not self.audio_paused:
                            renpy.audio.audio.pause_all()
                            self.audio_paused = True
                        # If the window had not gone inactive or has regained activity
                        # unpause the audio
                        elif pygame.display.get_active() and self.audio_paused:
                            renpy.audio.audio.unpause_all()
                            self.audio_paused = False

                    pygame.key.set_mods(pygame.key.get_mods() & (pygame.KMOD_NUM | pygame.KMOD_CAPS))

                # This returns the event location. It also updates the
                # mouse state as necessary.
                x, y = renpy.display.draw.mouse_event(ev)
                x, y = renpy.test.testmouse.get_mouse_pos(x, y)

                ev, x, y = renpy.display.emulator.emulator(ev, x, y)
                if ev is None:
                    continue

                if not self.mouse_focused or self.ignore_touch:
                    x = -1
                    y = -1

                self.event_time = end_time = get_time()

                if ev.type in input_events:
                    self.input_event_time = self.event_time

                # This can set the event to None, to ignore it.
                ev = renpy.display.controller.event(ev)
                if not ev:
                    continue

                # Handle skipping.
                renpy.display.behavior.skipping(ev)

                try:
                    if self.touch:
                        renpy.display.gesture.recognizer.event(ev, x, y)

                    renpy.plog(1, "start mouse focus handling")

                    # Handle the event normally.
                    rv = renpy.display.focus.mouse_handler(ev, x, y)

                    renpy.plog(1, "start event handling")

                    if rv is None:
                        rv = root_widget.event(ev, x, y, 0)

                    if rv is None:
                        rv = renpy.display.focus.key_handler(ev)

                    renpy.plog(1, "finish event handling")

                    if rv is not None:
                        break

                    # Handle displayable inspector.
                    if renpy.config.inspector:
                        if renpy.display.behavior.map_event(ev, "inspector"):
                            d = self.surftree.main_displayables_at_point(
                                x,
                                y,
                                renpy.config.transient_layers
                                + renpy.config.context_clear_layers
                                + renpy.config.overlay_layers,
                            )
                            renpy.game.invoke_in_new_context(renpy.config.inspector, d)
                        elif renpy.display.behavior.map_event(ev, "full_inspector"):
                            d = self.surftree.main_displayables_at_point(x, y, renpy.config.layers)
                            renpy.game.invoke_in_new_context(renpy.config.inspector, d)

                    # Handle the dismissing of non trans_pause transitions.
                    if (
                        self.ongoing_transition.get(None, None)
                        and (not suppress_transition)
                        and (not trans_pause)
                        and (renpy.config.dismiss_blocking_transitions)
                    ):
                        if renpy.store._dismiss_pause:
                            dismiss = "dismiss"
                        else:
                            dismiss = "dismiss_hard_pause"

                        if renpy.display.behavior.map_event(ev, dismiss):
                            self.transition.pop(None, None)
                            self.ongoing_transition.pop(None, None)
                            self.transition_time.pop(None, None)
                            self.transition_from.pop(None, None)
                            self.restart_interaction = True
                            raise IgnoreEvent()

                except IgnoreEvent:
                    # An ignored event can change the timeout. So we want to
                    # process an TIMEEVENT to ensure that the timeout is
                    # set correctly

                    if ev.type != TIMEEVENT:
                        self.post_time_event()

                    # On mobile, if an event originates from the touch mouse, unfocus.
                    if renpy.mobile and (ev.type == pygame.MOUSEBUTTONUP) and getattr(ev, "which", 0) == 4294967295:
                        if not self.restart_interaction:
                            renpy.display.focus.mouse_handler(None, -1, -1, default=False)

                # Check again after handling the event.
                needs_redraw |= renpy.display.render.check_redraws()

                if self.restart_interaction:
                    return True, None

            # If we were trans-paused and rv is true, suppress
            # transitions up to the next interaction.
            if trans_pause and rv:
                self.suppress_transition = True

            # But wait, there's more! The finally block runs some cleanup
            # after this.
            return False, rv

        except EndInteraction as e:
            return False, e.value

        finally:
            # Determine the transition delay for each layer.
            self.transition_delay = {k: getattr(v, "delay", 0) for k, v in layer_transitions.items()}

            # Clean out the overlay layers.
            for i in renpy.config.overlay_layers:
                scene_lists.clear(i)

            # Stop ongoing preloading.
            renpy.display.im.cache.end_tick()

            # We no longer disable periodic between interactions.
            # pygame.time.set_timer(PERIODIC, 0)

            pygame.time.set_timer(TIMEEVENT, 0)
            pygame.time.set_timer(REDRAW, 0)

            self.consider_gc()

            renpy.game.context().runtime += end_time - start_time

            # Restart the old interaction, which also causes a
            # redraw if needed.
            self.restart_interaction = True

            renpy.plog(1, "end interact_core")

            # print("It took", frames, "frames.")

    def timeout(self, offset):
        if offset < 0:
            return

        if self.timeout_time:
            self.timeout_time = min(self.event_time + offset, self.timeout_time)
        else:
            self.timeout_time = self.event_time + offset

    def finish_pending(self):
        """
        Called before a quit or restart to finish any pending work that might
        block other threads.
        """

        self.check_background_screenshot()

    def exec_js_cmd(self):
        """
        Execute a command from JS if required (emscripten only).
        """

        if not renpy.emscripten:
            return

        # Retrieve the command to be executed from a global JS variable
        # (an empty string is returned if the variable is not defined)

        cmd = emscripten.run_script_string("window._renpy_cmd")

        if len(cmd) == 0:
            return

        # Delete command variable to prevent executing the command again
        emscripten.run_script("delete window._renpy_cmd")

        # Execute the command
        try:
            renpy.python.py_exec(cmd, hide=True)
        except Exception as e:
            renpy.display.log.write(cmd)
            renpy.display.log.write("Error while executing JS command: %s" % (e,))
