3dslibris: Portar un lector de ebooks de Nintendo DS a Nintendo 3DS

De la DS a la 3DS: por qué existe este proyecto
En 2007, Ray Haleblian creó dslibris, un lector de EPUB para la Nintendo DS. Era un proyecto impresionante para su época: parseaba EPUB 2.0, renderizaba texto con FreeType2, gestionaba preferencias persistentes y usaba directamente la VRAM del DS. Pero se quedó estancado en 2020, sin soporte para otros formatos y con el repositorio sin mantenimiento en 2025.
3dslibris es el port de ese lector a la Nintendo 3DS como aplicación homebrew. Pero es eso, un port: implica reescribir la capa gráfica completa, adaptar el sistema de input, añadir soporte para nuevos formatos, y resolver problemas de memoria y rendimiento en un dispositivo con solo 64 MB de RAM (128MB en el caso de la New 3DS) y un ARM11 a 268 MHz (con un segundo núcleo a 134 MHz en la New 3DS).
El proyecto está disponible en GitHub bajo licencia GPL v2 o posterior (con una sección AGPL para builds con MuPDF). La versión actual es la 2.0.0.

Banner de 3dslibris — Lector de ebooks para Nintendo 3DS
Qué se reutilizó y qué se escribió desde cero
Código heredado del DS original
Los siguientes módulos del dslibris original se portaron con modificaciones:
| Módulo | Líneas | Cambios principales |
|---|---|---|
book.cpp | 675 | Añadidos campos de portada (coverPixels, coverImagePath) |
text.cpp | 728 | Reescrito BlitToFramebuffer() para framebuffer RGB8 rotado 90° |
page.cpp | 225 | Reescrito DrawNumber() para doble pantalla con reloj y % de progreso |
epub.cpp | 416 | Añadida extracción de portadas (epub_extract_cover()) |
app.cpp | 338 | Reescrito completamente para libctru |
prefs.cpp | 289 | Añadido campo time24h |
font.cpp | 184 | Adaptados paths a sdmc:/ |
No obstante, todos estos componentes han sido reestructurados y redistribuidos para adaptarse a la arquitectura por dominios del repositorio actual.
Componentes nuevos escritos desde cero
Entry point (
main.cpp) — Inicialización de servicios 3DS, creación de directorios, instancia de App y bucle principal con manejo de errores.Blitting a framebuffer 3DS — El DS accedía directamente a VRAM en formato RGB555 con orientación normal. La 3DS usa un framebuffer software en formato RGB8 (24 bits) rotado 90°. La función
BlitToFramebuffer()convierte RGB565 → RGB8 y aplica la rotación al copiar píxeles.Sistema de portadas — Durante el parseo del OPF se busca la imagen de portada (EPUB 2:
<meta name="cover">, EPUB 3:<item properties="cover-image">). Se extrae del ZIP, se decodifica con stb_image, se escala a 95×55 píxeles y se almacena como buffer RGB565.Biblioteca en cuadrícula — El DS original usaba una lista vertical. La 3DS muestra una cuadrícula de 2×3 con portadas, títulos, indicadores de progreso y navegación táctil + D-Pad.
Reloj y porcentaje de lectura — En la parte inferior de la pantalla izquierda se muestra la hora actual y el porcentaje de lectura del libro.
stb_image — Librería de Sean Barrett embebida para decodificar JPEG y PNG, configurada con
STBI_NO_STDIOy solo los decoders necesarios para minimizar el binario.
Librerías vendorizadas
| Componente | Líneas | Propósito |
|---|---|---|
| Expat XML | ~15.000 | Parseo XML (EPUB, preferencias) |
| minizip | ~2.200 | Lectura de archivos dentro del ZIP del EPUB |
| stb_image | ~8.000 | Decodificación de imágenes para portadas |
| libunibreak | ~10.000 | Line breaking Unicode-aware (UAX #14) |
| utf8proc | ~8.000 | Normalización y propiedades Unicode |
Arquitectura actual del código
El proyecto original tenía toda la lógica metida en un directorio source/core/. Tras el refactor refactor: project layout into domain-based modules, el código se reorganizó por dominios funcionales para que cada zona del proyecto tenga responsabilidad clara:
3dslibris/
├── source/
│ ├── main.cpp # Entry point 3DS
│ ├── app/ # Orquestador principal
│ │ └── app.cpp # App::Run(), gestión de modos, job queue
│ ├── library/ # Biblioteca / navegador de libros
│ │ └── app_browser.cpp # Grid view, warmup, cover cache
│ ├── reader/ # Vista de lectura (texto reflowable)
│ │ └── app_book.cpp # Book mode, deferred reflow
│ ├── settings/ # Preferencias y fuentes
│ │ ├── app_prefs.cpp # Menú de ajustes
│ │ ├── prefs.cpp # Persistencia XML
│ │ └── font.cpp # Carga de fuentes FreeType
│ ├── book/ # Modelo de dominio del libro
│ │ ├── book.cpp # Metadata, páginas, capítulos
│ │ ├── page.cpp # Página individual de texto
│ │ ├── book_fixed_layout.cpp # Viewport y deferred work (PDF/CBZ)
│ │ ├── book_inline_image.cpp # Carga y cache de imágenes inline
│ │ ├── heading_layout.cpp # Keep-with-next para headings
│ │ ├── inline_image_layout.cpp # Planificador inline/band/page
│ │ ├── layout_reflow.cpp # Repaginación diferida
│ │ └── reflow_worker.cpp # Worker thread New 3DS
│ ├── formats/ # Parsers por formato
│ │ ├── common/ # Utilidades compartidas
│ │ │ ├── book_io.cpp # I/O genérica de libros
│ │ │ ├── buffered_status_log.cpp # Logging bufferizado
│ │ │ ├── epub_image_utils.cpp # Utilidades de imagen EPUB
│ │ │ ├── file_read_utils.cpp # Lectura segura de archivos
│ │ │ ├── page_cache_utils.cpp # Cache persistente de páginas
│ │ │ └── xml_parse_utils.cpp # Wrapper de Expat
│ │ ├── epub/ # Parser EPUB
│ │ ├── fb2/ # Parser FictionBook 2
│ │ ├── mobi/ # Parser MOBI completo
│ │ │ ├── mobi.cpp # Pipeline principal MOBI
│ │ │ ├── mobi_record_decode.cpp # Decodificación de records (HUFF/CDIC)
│ │ │ ├── mobi_record_scan.cpp # Escaneo acotado de records
│ │ │ ├── mobi_text_cleanup.cpp # Limpieza de texto y wrap fix
│ │ │ ├── mobi_heading_markers.cpp # Marcado semántico de headings
│ │ │ ├── mobi_cover_meta_cache.cpp # Cache de metadatos de portada
│ │ │ ├── mobi_markup_tag.cpp # Tags de markup MOBI
│ │ │ └── mobi_position_map.cpp # Mapeo de posiciones
│ │ ├── mupdf/ # Backend MuPDF (PDF/XPS)
│ │ │ ├── mupdf_common.cpp # Contexto compartido
│ │ │ ├── mupdf_document.cpp # Apertura de documento
│ │ │ ├── mupdf_render.cpp # Renderizado de página
│ │ │ ├── mupdf_view.cpp # Viewport y progressive rendering
│ │ │ └── mupdf_worker.cpp # Worker thread de renderizado
│ │ ├── pdf/ # Wrapper PDF sobre MuPDF
│ │ ├── cbz/ # Comic Book Archive
│ │ │ ├── cbz.cpp # Detección y entrada
│ │ │ ├── cbz_archive.cpp # Gestión del archivo ZIP
│ │ │ ├── cbz_decode.cpp # Decodificación de imágenes
│ │ │ ├── cbz_document.cpp # Inicialización de documento
│ │ │ ├── cbz_view.cpp # Vista y viewport
│ │ │ └── cbz_worker.cpp # Worker thread de renderizado
│ │ └── common/ # (ver arriba)
│ ├── menus/ # Menús paginados
│ │ ├── menu.cpp # Base de menú
│ │ ├── chapter_menu.cpp # Índice de capítulos
│ │ ├── bookmark_menu.cpp # Marcadores
│ │ └── paged_list_menu.cpp # Lista paginada genérica
│ ├── ui/ # Capa de interfaz
│ │ ├── text.cpp # Motor de texto (FreeType + blit)
│ │ ├── button.cpp # Botones UI
│ │ ├── browser_nav.cpp # Navegación de biblioteca
│ │ ├── touch_utils.cpp # Hit-testing táctil
│ │ └── ui_button_skin.cpp # Skin procedural de botones
│ ├── core/ # Core mínimo
│ │ ├── parse.cpp # Inicialización de parseo XML
│ │ └── stb_image_impl.cpp # Implementación stb_image
│ └── shared/ # Utilidades transversales
│ ├── app_flow_utils.cpp # Control de flujo entre modos
│ ├── utf8_utils.cpp # Conversión y reparación UTF-8
│ ├── text_unicode_utils.cpp # Text runs Unicode-aware
│ ├── text_layout_utils.cpp # Layout de texto (pre/code, etc.)
│ ├── pdf_view_utils.cpp # Helpers de vista PDF
│ └── ... # (19 headers en total)
├── include/ # Headers (misma estructura)
├── third_party/ # Dependencias externas
│ ├── libunibreak/ # UAX #14 line breaking
│ └── utf8proc/ # Unicode properties
└── tests/ # Tests unitarios por móduloFlujo de ejecución
main()→ init gfx, crear directorios,App::Run()App::Run()→ cargar fuentes, encontrar libros, parsear metadatos (+ extraer portadas), mostrar biblioteca- Modo BROWSER (
library/app_browser.cpp): Cuadrícula de libros con portadas. Touch/A abre libro. SELECT/Y abre settings. - Modo BOOK (
reader/app_book.cpp): Renderiza páginas con FreeType. D-Pad/A/B pasa páginas. START vuelve a biblioteca. - Modo FIXED-LAYOUT (PDF/CBZ/XPS): Renderizado vía MuPDF con zoom, preview en pantalla inferior y navegación por outline.
- Modo PREFS (
settings/app_prefs.cpp): Botones de configuración en pantalla derecha. Controles en pantalla izquierda. - Cada frame:
Text::BlitToFramebuffer()copia los buffers internos al framebuffer real de la 3DS.
El sistema de renderizado de pantalla
Buffers internos
screenleft[400×400] — Pantalla superior (top), usada como 400×240
screenright[400×400] — Pantalla inferior (bottom), usada como 320×240
offscreen[400×400] — Buffer temporalLos buffers son cuadrados (400×400) por compatibilidad con el indexado del DS original. Se desperdicia ~40% del espacio, pero cambiarlo requeriría reescribir todo el motor de texto (que asume buffers cuadrados) y el sistema de blitting. Es algo que tocará hacer en el futuro.
Formato de píxel y rotación
| Etapa | Formato |
|---|---|
| Buffer interno | RGB565 (16 bits) |
| Framebuffer 3DS real | RGB8 (24 bits) |
| Conversión | BlitToFramebuffer() hace RGB565 → RGB8 + rotación 90° |
La 3DS almacena el framebuffer rotado 90° respecto a cómo lo vemos. Una pantalla de 400×240 se almacena como un buffer de 240×400. BlitToFramebuffer() maneja esta transformación píxel a píxel.
Text runs Unicode-aware
El motor de texto original del DS trabajaba byte a byte, lo que causaba problemas con caracteres multi-byte en UTF-8 (acentos, eñes, caracteres CJK). Se integraron tanto libunibreak (UAX #14 para line breaking correcto) como utf8proc (propiedades Unicode) para que el motor de texto entienda codepoints reales en vez de bytes individuales. Esto afecta directamente a cómo se calculan los saltos de línea, el wrapping y la paginación en libros con contenido no-ASCII.
Clamp de tamaño de texto
Se añadió un límite seguro al tamaño de texto configurable (text_limits.h). Sin este clamp, valores extremos de tamaño de fuente podían causar desbordamientos de buffer y corrupción visual (como pasaba en builds antiguas). Los límites se aplican tanto en la UI de preferencias como en el renderizado directo.
Parseo de EPUB y pipeline de formatos
Soporte de formatos
| Formato | Nivel de soporte | Motor |
|---|---|---|
| EPUB (2/3) | Fuerte | Parser propio + FreeType |
| FB2 | Bueno | Parser XML propio |
| TXT / RTF / ODT | Bueno | Parser propio |
| MOBI | Experimental | Parser propio con pipeline de imágenes inline |
| Viewer | MuPDF | |
| CBZ | Viewer | MuPDF |
| XPS | Viewer | MuPDF |
Pipeline EPUB
EPUB (ZIP) → META-INF/container.xml → rootfile (.opf)
│
┌──────┴──────┐
│ Metadata │
│ (título, │
│ autor, │
│ portada) │
└──────┬──────┘
│
┌──────┴──────┐
│ Manifest │
│ (archivos) │
└──────┬──────┘
│
┌──────┴──────┐
│ Spine │
│ (orden) │
└──────┬──────┘
│
▼
XHTML de cada capítulo
→ parseo XML → Page objectsLayout inteligente de imágenes embebidas
Antes, cualquier <img> embebida en EPUB/FB2 se trataba como bloque de pantalla completa. Eso era demasiado agresivo: líneas musicales, separadores horizontales e iconos pequeños ocupaban una pantalla entera.
La versión actual usa un planificador compartido (inline_image_layout.cpp) entre paginación y render que clasifica cada imagen en uno de tres modos:
inline: imagen muy pequeña, se comporta como un glifo grande dentro del flujo de líneaband: imagen ancha o icono de bloque, consume sólo la altura necesaria y el texto sigue debajopage: imagen grande, mantiene el comportamiento clásico de bloque a pantalla completa
Además, el parser marca cuándo una imagen es la primera pieza de un párrafo, lo que permite tratar mejor EPUBs donde los iconos (ADVERTENCIA, INFORMACIÓN TÉCNICA, etc.) forman parte semántica del mismo párrafo que el texto.
Optimización del sondeo de imágenes
El primer prototipo del layout inteligente penalizaba mucho la apertura de EPUBs con muchas imágenes (~50 segundos en pruebas locales). La solución:
- Se mantiene un índice de entradas ZIP por libro
- Durante el parseo del
spinese reutiliza ununzFileauxiliar - La sonda de metadatos intenta leer sólo un prefijo de la imagen cuando es suficiente
Resultado: los mismos EPUBs bajaron de ~50 s a ~5 s de apertura.
Bloques <pre> y <code> en XHTML
Los bloques <pre> y <code> en EPUBs técnicos necesitan un tratamiento especial: el texto no debe hacer wrap como el flujo normal, pero tampoco debe romper el buffer de página. Se implementó una ruta de layout dedicada que respeta los saltos de línea literales dentro de estos bloques sin afectar al resto del flujo de texto.
La integración de MuPDF: PDF, CBZ y XPS
La mayor novedad de la versión 2.0.0 es la incorporación de MuPDF como motor de renderizado para formatos de layout fijo. Esto no fue trivial:
Arquitectura del stack de layout fijo
MuPDF se integró como un backend aislado (refactor: isolate fixed-layout backends) que comparte la infraestructura de visualización con el resto del lector pero mantiene su propio pipeline de renderizado:
- Pantalla superior: región zoom de la página actual
- Pantalla inferior: preview de página completa con caja de viewport
- Controles unificados:
A/Bzoom,Left/Rightcambiar página,Up/Downnavegar outline, touch para mover viewport
El stack se divide en tres capas:
- Document (
mupdf_document.cpp): apertura, contexto, outline - Render (
mupdf_render.cpp): renderizado de página a bitmap - View (
mupdf_view.cpp): viewport, progressive rendering, interacción táctil
Renderizado progresivo
En lugar de esperar a que una página se renderice completamente antes de mostrarla, se usa un pipeline de preview-first:
- Se muestra inmediatamente una versión de baja resolución
- Se refina en background a mayor calidad
- El usuario puede interactuar con la página mientras se refina
Worker thread en New 3DS
La New Nintendo 3DS tiene un segundo núcleo de procesador. El renderizado de PDF/CBZ se delega a un hilo dedicado en el segundo core (reflow_worker.cpp para texto, mupdf_worker.cpp / cbz_worker.cpp para fixed-layout), manteniendo la UI responsive. La Old 3DS usa un fallback síncrono automático.
Caché y prefetch inteligente
- El prefetch de páginas se difiere hasta que el usuario cambia de página, evitando trabajo innecesario
- Se aceleró la reutilización de caché de renderizado estabilizando el viewport
- Se trackean dirty rectangles con precisión para evitar redraws innecesarios
- Se corrigieron artefactos de redraw en fixed-layout que causaban parpadeo al navegar entre páginas
Serialización de generación mínima de MuPDF
Para evitar condiciones de carrera durante el build, la generación de los archivos mínimos de MuPDF se serializó en el Makefile, asegurando que las dependencias se resuelvan en orden correcto incluso con compilación paralela (make -j2).
Cumplimiento AGPL
MuPDF se distribuye bajo AGPL-3.0-or-later. Esto implicó:
- Añadir un modelo de licencia dual al repositorio (GPL v2+ para el código base, AGPL para builds con MuPDF)
- Documentar el flujo de source release para cumplir con los requisitos de la AGPL
- Separar los artefactos de release para que quede claro qué build lleva qué licencia
MOBI: el formato más difícil
MOBI es probablemente el formato más complejo de soportar correctamente. A diferencia de EPUB (que es esencialmente un ZIP con XHTML), MOBI usa un formato binario propietario con compresión HUFF/CDIC, records encadenados y una estructura de TOC que depende enormemente de cómo se generó el archivo.
Decodificación de records y soporte HUFF/CDIC
El decoder MOBI original tenía bugs en la decodificación de records que causaban corrupción en libros con compresión HUFF/CDIC. Se reescribió el módulo de decodificación (mobi_record_decode.cpp) con un enfoque más robusto:
- Escaneo acotado de records de imagen para no barrer sin control libros grandes
- Decodificación correcta de tablas HUFF/CDIC
- Reintento RGB para ciertos JPEG inline que fallan en el decode inicial
Apertura asíncrona (New 3DS)
En la versión 2.0.0, la apertura de MOBI en New 3DS es asíncrona: el usuario puede empezar a leer mientras el resto del libro se pagina en background. Esto elimina el bloqueo de UI que antes podía durar varios segundos en libros grandes.
Pipeline de imágenes inline
Las imágenes inline en MOBI (<img recindex="N">) usan el mismo pipeline inline / band / page que EPUB/FB2, con:
- Reintento RGB para ciertos JPEG que fallan en decode inicial
- Preservación de tokens binarios de imagen durante las fases de cleanup de texto
- Caché persistente de metadatos de portada
line wrap fix por libro
Muchos MOBI mal convertidos tienen hard-wrap de líneas (cada línea de prosa termina con un salto de línea real). Se implementó un fix opcional por libro que limpia estos saltos sin destruir los marcadores de imagen inline.
Además, se restauró el comportamiento legacy de plain-text wrap para MOBI que se había perdido durante refactorizaciones anteriores, asegurando que la prosa sin markup se comporte correctamente.
Índice MOBI heurístico
La resolución del TOC en MOBI usa un mapeo html→texto→página con dos tablas de lookup. Es heurístico y depende del archivo fuente, pero bastante más preciso que la aproximación inicial.
Optimizaciones para hardware real
Memoria
| Componente | Memoria estimada |
|---|---|
| Screen buffers (×3) | ~940 KB |
| Fuentes FreeType (×4) | ~800 KB |
| XML/EPUB buffers | ~200 KB |
| 20 libros con portada | ~200 KB |
| 500 páginas de texto | ~1 MB |
| Total estimado | ~3.1 MB |
Muy por debajo de los 64 MB disponibles en una Old 3DS, lo que deja margen para buffers temporales, cachés y el heap de MuPDF.
CPU
| Optimización | Detalle |
|---|---|
-O2 | Compilación con optimización nivel 2 |
-ffunction-sections | Dead code elimination por secciones |
-fno-rtti -fno-exceptions | Sin RTTI ni excepciones C++ |
-march=armv6k -mtune=mpcore | Optimizado para ARM11 de la 3DS |
| Doble buffer lazy | Solo se redibuja cuando view_dirty cambia |
| Glyph advance cache (LRU) | Cache de avance de glifos para evitar rasterización repetida |
| EPUB page cache | Cache persistente de páginas para reaperturas rápidas |
Reflow diferido
Cambios de fuente, tamaño, espaciado u orientación no repaginan el libro en caliente. El ajuste se guarda, el libro queda marcado como desactualizado, y la repaginación se aplica al reabrirlo. Esto evita estados inconsistentes y fallos de maquetación.
Optimizaciones de la biblioteca
La biblioteca (browser) recibió varias optimizaciones tardías:
- Carga lazy de portadas: las portadas de libros no visibles no se decodifican hasta que entran en viewport
- Warmup selectivo: el libro seleccionado recibe prioridad de warmup tras un período breve de inactividad
- Queue de jobs: las tareas de metadata, extracción de portada y resolución de TOC se encolan y ejecutan con presupuesto de tiempo (
budget_ms), evitando bloquear el frame - Poda de caché de portadas: las portadas no visibles se liberan de memoria para mantener el footprint bajo
- Framebuffer blit optimizado: las copias al framebuffer de la 3DS se hacen con tracking preciso de regiones dirty, evitando copias completas innecesarias
Reducción de churn de buffers
Se redujo significativamente la creación y destrucción de buffers temporales en los flujos de parseo y render:
- Los buffers de página se reutilizan mediante un sistema de owned buffers (
page_buffer_utils.h) en vez de allocar/deallocar en cada parseo - El churn de buffers en el flujo de I/O de libros se redujo con aliasing de buffers en vez de copias
- La limpieza de texto RTF se optimizó con un parser de control words dedicado (
rtf_control_word_utils.h) que evita crear strings intermedios
Logging bufferizado
En builds de depuración, el logging de parseo se bufferiza en memoria (buffered_status_log.cpp) en vez de escribir línea a línea a fichero. Esto reduce drásticamente la I/O de estado y evita que el logging interfiera con el rendimiento de la paginación.
Optimización de UTF-8
Se optimizó el decoding de UTF-8 en los flujos de renderizado de texto, reduciendo el coste de conversión en cada frame. La reparación de mojibake (caracteres mal codificados) se centralizó en utf8_utils.cpp con rutas optimizadas para los patrones más comunes (fullwidth bytes, soft-hyphen corruption).
Empaquetado y distribución
Formatos de distribución
| Artefacto | Propósito |
|---|---|
3dslibris.3dsx | Homebrew Launcher |
3dslibris-debug.3dsx | Build con logging verbose (3dslibris.log) |
3dslibris.cia | Instalable directo (incluye fonts y recursos via romfs) |
3dslibris-sdmc.zip | Paquete SD con estructura de directorios |
Bundling de assets en el .cia
Originalmente, el .cia requería que el usuario extrajera manualmente 3dslibris-sdmc.zip en la SD para tener acceso a fuentes y recursos de la UI. Esto se corrigió empaquetando los assets de runtime (font/, resources/) dentro del romfs de la aplicación. Ahora un .cia install funciona sin pasos adicionales.
El fallback a SD sigue existiendo: si hay archivos en sdmc:/3ds/3dslibris/, esos tienen prioridad sobre los empaquetados, permitiendo personalización.
Flujo .cia con Docker
El empaquetado .cia usa makerom y bannertool, alineado con el flujo de Universal-Updater. Se construye dentro de un contenedor Docker para evitar problemas de permisos:
docker build -f docker/Dockerfile.cia -t 3dslibris-build .
docker run --rm \
-v "$(pwd):/project" -w /project \
-e DEVKITPRO=/opt/devkitpro \
-e DEVKITARM=/opt/devkitpro/devkitARM \
3dslibris-build \
sh -lc 'make clean && make -j2 && make zip-sdmc && make debug-3dsx && make cia && make source-release'CI/CD con GitHub Actions
Al hacer push de un tag v*, el workflow release.yml compila y adjunta automáticamente todos los artefactos en GitHub Releases.
Estadísticas del proyecto
| Métrica | Valor |
|---|---|
| Líneas de código propio (C/C++) | ~7.000 |
| Librerías vendorizadas | ~43.000 (Expat + minizip + stb_image + libunibreak + utf8proc) |
| Tamaño del binario (.3dsx) | ~1.44 MB |
| Formatos soportados | 8 (EPUB, FB2, TXT, RTF, ODT, MOBI, PDF, CBZ, XPS) |
| Commits totales | 173+ |
| Tiempo de desarrollo activo | ~2 meses intensivos |
Módulos en shared/ | 19 headers de utilidades transversales |
| Tests unitarios | 20+ suites con scripts de validación |
Lo que aprendí
Sobre hardware limitado
Programar para la 3DS te obliga a pensar en cada byte. No hay garbage collector, no hay memoria virtual, no hay swap. Si allocas 2 MB de más, el sistema simplemente no arranca. Cada optimización hay que pensarla. Eso incluye desde reutilizar el estado del ZIP hasta bufferizar logs en vez de escribir línea a línea. Todo tiene un impacto medible.
Sobre código legacy
Portar código de 2007 escrito para una arquitectura completamente diferente es un ejercicio de arqueología software. El dslibris original asume VRAM directa, botones mapeados de forma específica y una pantalla de 256×192. Adaptarlo a la 3DS implicó entender cada asunción implícita y reemplazarla sin romper la lógica central de paginación.
Sobre arquitectura de código
El refactor de source/core/ (un directorio monolítico con 20+ archivos) a una estructura por dominios (app/, library/, reader/, book/, formats/, ui/, shared/) fue una de las mejores decisiones del proyecto. No solo hizo el código más navegable, sino que forzó a pensar en las dependencias entre módulos y a extraer utilidades transversales a shared/.
Sobre licencias
Integrar MuPDF me obligó a entender las implicaciones reales de la AGPL. No basta con decir “es open source”: hay que documentar el source release, separar los componentes, y ser transparente con los usuarios sobre qué build lleva qué obligaciones.
Enlaces
- Repositorio en GitHub
- Releases
- Guía de contribución
- dslibris original por Ray Haleblian