[Scummvm-git-logs] scummvm master -> d0f71333b5d791c2361efe9ce9ed5707eccda640

lephilousophe noreply at scummvm.org
Sat Jan 24 12:27:50 UTC 2026


This automated email contains information about 1 new commit which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .

Summary:
d0f71333b5 NDS: Make some parts of ScummVM go to the secondary ROM


Commit: d0f71333b5d791c2361efe9ce9ed5707eccda640
    https://github.com/scummvm/scummvm/commit/d0f71333b5d791c2361efe9ce9ed5707eccda640
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2026-01-24T13:27:47+01:00

Commit Message:
NDS: Make some parts of ScummVM go to the secondary ROM

This fixes the build on master.

Changed paths:
  A dists/ds/make_twl.py
    backends/platform/ds/ds.mk


diff --git a/backends/platform/ds/ds.mk b/backends/platform/ds/ds.mk
index 8a2e10e72fd..3bac81ad933 100644
--- a/backends/platform/ds/ds.mk
+++ b/backends/platform/ds/ds.mk
@@ -60,6 +60,27 @@ vpath %.png $(srcdir)
 
 backends/platform/ds/ds-graphics.o: backends/platform/ds/gfx/banner.o
 
+# When building with plugins, the main binary is too big for a DS ROM size
+# We need to move some parts of it to the secondary ROM only available on DSi.
+ifeq ($(DYNAMIC_MODULES),1)
+$(EXECUTABLE): | fixup_twl
+
+ifneq ($(findstring $(MAKEFLAGS),s),s)
+ifneq ($(VERBOSE_BUILD),1)
+ifneq ($(VERBOSE_BUILD),yes)
+QUIET_MAKE_TWL = @echo '   ' TWL '    ' $+;
+endif
+endif
+endif
+
+fixup_twl: audio/libaudio.a image/libimage.a video/libvideo.a
+	$(QUIET_MAKE_TWL)
+	$(QUIET)for f in $+; do \
+		python3 $(srcdir)/dists/ds/make_twl.py $$f; \
+	done
+
+.PHONY: fixup_twl
+endif
 
 # Command to build libmad is:
 # ./configure --host=arm-elf --enable-speed --enable-sso -enable-fpm=arm CFLAGS='-specs=ds_arm9.specs -mthumb-interwork'
diff --git a/dists/ds/make_twl.py b/dists/ds/make_twl.py
new file mode 100755
index 00000000000..6d66e57c5d3
--- /dev/null
+++ b/dists/ds/make_twl.py
@@ -0,0 +1,332 @@
+#!/usr/bin/env python3
+
+"""Converts an archive to make it go into the secondary ROM of the Nintendo DSi.
+
+The archive is modified so that, when linking it, all its contents are recognized
+as DSi-only code by the linker script.
+This works by renaming the archive entries to make their base name end with .twl.
+
+This command takes an existing archive as its first argument and, optionaly, an
+output archive where the result will be written.
+If the second argument is not provided, the input archive is erased with the result,
+if there is any change.
+"""
+
+# ruff: noqa: Q000
+
+from __future__ import annotations
+
+import os
+import pathlib
+import struct
+import sys
+import tempfile
+import typing
+
+if typing.TYPE_CHECKING:
+    import io
+
+
+class ArchiveError(Exception):
+    """An error from Archive format."""
+
+class Entry: # pylint: disable=too-many-instance-attributes
+    """Represents an entry from an Archive file."""
+
+    __slots__ = ('identifier', 'timestamp', 'owner_id', 'group_id', # noqa: RUF023
+                 'mode', 'size', 'original_offset', 'data')
+
+    def __init__(self, identifier: bytes, timestamp: int | None, # noqa: PLR0913 pylint: disable=too-many-arguments,too-many-positional-arguments
+                 owner_id: int | None, group_id: int | None,
+                 mode: int | None, size: int) -> None:
+        """Initialize an Entry from its parsed attributes."""
+        self.identifier = identifier
+        self.timestamp = timestamp
+        self.owner_id = owner_id
+        self.group_id = group_id
+        self.mode = mode
+        self.size = size
+        self.original_offset: int|None = None
+        self.data: bytearray|None = None
+
+    def map_name(self, names: dict[int, bytes] | None) -> None:
+        """Retrieve the long name from the provided nametable.
+
+        The identifier is modified in place.
+        :param names: the nametable where to look at
+        """
+        if self.identifier in {b'/', b'//'}:
+            return
+
+        if not self.identifier.startswith(b'/'):
+            return
+
+        if names is None:
+            msg = "Missing long names entry"
+            raise ArchiveError(msg)
+
+        self.identifier = names[int(self.identifier[1:])]
+
+    def unmap_name(self, names: bytearray) -> None:
+        """Store a long identifier in the provided nametable.
+
+        Names shorter or equal than 16 don't need it.
+        :param names: the nametable where to place the long name
+        """
+        if len(self.identifier) <= 16: # noqa: PLR2004
+            return
+
+        identifier = self.identifier
+        self.identifier = b'/' + str(len(names)).encode('ascii')
+        names.extend(identifier + b'\n')
+
+    def make_twl(self) -> bool:
+        """Transform the identifier in a TWL one.
+
+        This means adding .twl at the end of the base name.
+        :returns: if the name was changed
+        """
+        if b'.' not in self.identifier:
+            return False
+
+        base, ext = self.identifier.rsplit(b'.', maxsplit=1)
+        if base.endswith(b'.twl'):
+            return False
+
+        self.identifier = base + b'.twl.' + ext
+        return True
+
+    def tobytes(self) -> bytes:
+        """Render the entry as bytes for writing."""
+        return (self.identifier[:16].ljust(16) +
+                gen_int(self.timestamp, 12) +
+                gen_int(self.owner_id, 6) +
+                gen_int(self.group_id, 6) +
+                gen_int(self.mode, 8) +
+                gen_int(self.size, 10) + b'`\n')
+
+    def __repr__(self) -> str:
+        """Return a represation of the Entry."""
+        return (f"Entry({self.identifier!r}, {self.timestamp}, {self.owner_id}, "
+                f"{self.group_id}, {self.mode}, {self.size})")
+
+def parse_int(v: bytes) -> int | None:
+    """Parse an int from a byte string.
+
+    :param v: the value as a byte string
+    :returns: the value as an int or None if the string was empty
+    """
+    v = v.rstrip()
+    return int(v) if v else None
+
+def gen_int(v: int | None, sz: int) -> bytes:
+    """Render an int into a byte string.
+
+    :param v: the value as an int or None
+    :param sz: the size of the resulting byte string
+    :returns: the value as a byte string
+    """
+    if v is None:
+        return b' ' * sz
+    sv = str(v).encode('ascii')
+    return sv[:sz].ljust(sz)
+
+def read_nametable(f: io.BufferedReader, size: int) -> dict[int, bytes]:
+    """Read and parse a name table.
+
+    :param f: the archive to read the names from
+    :param size: the size of the table
+    :returns: a mapping between the name index (the offset) and the name
+    """
+    names = {}
+    data = f.read(size)
+    i = 0
+    while True:
+        ni = data.find(b'\n', i)
+        if ni == -1:
+            break
+        names[i] = data[i:ni]
+        i = ni + 1
+    return names
+
+def set_nametable(entries: list[Entry], names: bytearray) -> int:
+    """Set a new name table for the archive.
+
+    :param entries: the entries list used to find the table
+    :param names: the names byte array to set
+    :returns: the size offset between the old and the new name tables
+    """
+    if len(names) == 0:
+        # New nametable is empty: remove the old one if it exists
+        for i, entry in enumerate(entries):
+            if entry != b'//':
+                continue
+            del entries[i]
+            return -(60 + entry.size + entry.size & 1)
+        return 0
+
+    # First entry may be the AR map
+    i = 0
+    if len(entries) > i and entries[i].identifier == b'/':
+        i += 1
+
+    # The second entry is an existing name table: replace it
+    if len(entries) > i and entries[i].identifier == b'//':
+        entries[i].data = names
+        oldsize = entries[i].size
+        newsize = len(names)
+        entries[i].size = newsize
+        return (newsize + (newsize & 1)) - (oldsize + (oldsize & 1))
+
+    # This is a new entry
+    entry = Entry(b'//', None, None, None, None, len(names))
+    entry.data = names
+    entries.insert(i, entry)
+    return 60 + entry.size + entry.size & 1
+
+def fixup_armap(f: io.BufferedReader, entries: list[Entry], offset: int) -> None:
+    """Fix the AR index map present at the start.
+
+    When patching the name table, we get an offset which must be
+    applied back to all entries in the index.
+    :param f: the input file from where to read the index
+    :param entries: the entries list used to find the index
+    :param offset: the offset to apply to every entry in the list
+    """
+    if len(entries) == 0:
+        return
+    if entries[0].identifier != b'/':
+        return
+
+    entry = entries[0]
+
+    if entry.original_offset is None:
+        msg = "Entry has no offset into original file"
+        raise RuntimeError(msg)
+
+    f.seek(entry.original_offset)
+    data = bytearray(entry.size)
+    f.readinto(data)
+
+    map_size, = struct.unpack('>L', data[:4])
+    for i in range(map_size):
+        value, = struct.unpack('>L', data[4+i*4:4+i*4+4])
+        value += offset
+        data[4+i*4:4+i*4+4] = struct.pack('>L', value)
+
+    entry.data = data
+
+def read_entry(f: io.BufferedReader) -> Entry | None:
+    """Read an entry from the archive.
+
+    :param f: the input file
+    :returns: an Entry or None if EOF is reached
+    """
+    file_header = f.read(60)
+    if not file_header:
+        return None
+
+    identifier, timestamp, owner_id, group_id, mode, size, end = struct.unpack(
+            '16s12s6s6s8s10s2s', file_header)
+    if end != b'`\n':
+        msg = "Invalid file entry end"
+        raise ArchiveError(msg)
+
+    identifier = identifier.rstrip()
+    timestamp = parse_int(timestamp)
+    owner_id = parse_int(owner_id)
+    group_id = parse_int(group_id)
+    mode = parse_int(mode)
+    size = parse_int(size)
+    if size is None:
+        msg = "Invalid size in entry"
+        raise ArchiveError(msg)
+
+    return Entry(identifier, timestamp, owner_id, group_id, mode, size)
+
+def read_entries(f: io.BufferedReader) -> list[Entry]:
+    """Read the archive entries.
+
+    Also parse the nametable to give entries their long name.
+    :param f: the input file
+    :returns: a list of the entries from the file
+    """
+    names = None
+    entries = []
+
+    magic = f.read(8)
+    if magic != b'!<arch>\n':
+        msg = "Invalid archive magic"
+        raise ArchiveError(msg)
+
+    while True:
+        entry = read_entry(f)
+        if entry is None:
+            break
+
+        entry.map_name(names)
+        entry.original_offset = f.tell()
+        entries.append(entry)
+
+        if entry.identifier == b'//':
+            names = read_nametable(f, entry.size)
+        else:
+            f.seek(entry.size, os.SEEK_CUR)
+        f.read(entry.size & 1)
+
+    return entries
+
+def write_entries(f: io.BufferedReader, of: io.BufferedWriter,
+                  entries: list[Entry]) -> None:
+    """Write all the archive entries to the output file.
+
+    It either uses the data linked to the entry or the original data
+    in the input file.
+    :param f: the input file
+    :param of: the output file
+    :param entries: the list of entries which must be written
+    """
+    of.write(b'!<arch>\n')
+    for entry in entries:
+        of.write(entry.tobytes())
+        if entry.data is not None:
+            of.write(entry.data)
+        elif entry.original_offset is not None:
+            f.seek(entry.original_offset)
+            of.write(f.read(entry.size))
+        else:
+            msg = "Entry has no data nor offset into original file"
+            raise RuntimeError(msg)
+        if entry.size & 0x1:
+            of.write(b'\n')
+
+def main() -> None:
+    """Do the job."""
+    tf = None
+    archive_file = pathlib.Path(sys.argv[1])
+    with archive_file.open('rb') as f:
+        entries = read_entries(f)
+        names = bytearray()
+        changed = False
+        for entry in entries:
+            changed |= entry.make_twl()
+            entry.unmap_name(names)
+
+        offset = set_nametable(entries, names)
+        fixup_armap(f, entries, offset)
+
+        if len(sys.argv) >= 3: # noqa: PLR2004
+            h: pathlib.Path|int = pathlib.Path(sys.argv[2])
+        else:
+            if not changed:
+                return
+            h, tf_ = tempfile.mkstemp(dir=archive_file.parent)
+            tf = pathlib.Path(tf_)
+        with open(h, 'wb') as of: # noqa: PTH123
+            write_entries(f, of, entries)
+
+    if tf is not None:
+        tf.replace(archive_file)
+
+if __name__ == '__main__':
+    main()




More information about the Scummvm-git-logs mailing list