Examen final
corrigé

Titre de ROM

Dans l'ordre, les différentes opérations que la méthode setRomTitle doit effectuer sont :

  1. créer des flots d'entrée et de sortie pour les fichiers ROM d'entrée et de sortie,
  2. lire la totalité des octets du flot d'entrée, qui constituent la ROM à modifier,
  3. modifier les octets contenant le titre de la ROM,
  4. recalculer et stocker la somme de contrôle de la ROM,
  5. écrire le contenu de la ROM dans le flot de sortie,
  6. fermer les flots.

La notation try-with-resource est très utile pour correctement ouvrir et fermer les deux flots. Le reste de la méthode est simple et consiste en une traduction très directe des consignes de l'énoncé.

public static void setRomTitle(String inFileName,
			       String outFileName,
			       String title)
  throws IOException {
  try (InputStream inS = new FileInputStream(inFileName);
       OutputStream outS = new FileOutputStream(outFileName)){
    byte[] rom = inS.readAllBytes();

    // Write title
    for (int i = 0; i < 15; ++i)
      rom[308 + i] = 0;
    for (int i = 0; i < Math.min(15, title.length()); ++i) {
      char c = title.charAt(i);
      rom[308 + i] = (byte) ((0x20 <= c && c < 0x60)
			     ? c
			     : ' ');
    }

    // Fix checksum
    int sum = 0;
    for (int i = 308; i <= 332; ++i)
      sum += rom[i] + 1;
    rom[333] = (byte) -sum;

    outS.write(rom);
  }
}

Vecteur de bits

Cet exercice est très similaire à la série Images continues, dans les deux cas l'idée est de représenter une donnée infinie au moyen d'une fonction mathématique.

Partie 1

L'inversion des bits d'un vecteur s'obtient simplement en inversant les bits produits par sa méthode testBit.

public default BitVector not() {
  return i -> ! testBit(i);
}

Partie 2

La conjonction et la disjonction sont quasi-identiques, et s'obtiennent en effectuant la conjonction et la disjonction des bits individuels produits par testBit.

public default BitVector and(BitVector that) {
  return i -> this.testBit(i) && that.testBit(i);
}

public default BitVector or(BitVector that) {
  return i -> this.testBit(i) || that.testBit(i);
}

Partie 3

Le décalage se fait simplement en changeant l'index passé à la méthode testBit du vecteur à décaler. Il faut juste penser à traiter séparément le cas où le décalage est nul, comme demandé dans l'énoncé.

public default BitVector shift(int d) {
  return d == 0 ? this : i -> testBit(i - d);
}

Partie 4

La jointure se fait simplement en comparant l'index du bit demandé et en obtenant le bit de l'un ou l'autre des deux vecteurs en fonction de si cet index est inférieur ou non à celui de jointure.

public default BitVector join(BitVector that, int j) {
  return i -> (i < j ? this : that).testBit(i);
}

Partie 5

Pour faire en sorte que les bits du vecteur se répètent à l'infini, il faut bien entendu utiliser le reste de la division entière par la longueur de l'intervalle de répétition. Il faut toutefois prendre garde à utiliser la bonne définition de la division entière, à savoir la division entière par défaut (floored division), faute de quoi on obtient des résultats incorrects lorsque le numérateur est négatif.

public default BitVector cycling(int s, int l) {
  if (! (0 < l))
    throw new IllegalArgumentException();
  return i -> testBit(s + Math.floorMod(i - s, l));
}

Partie 6

La conversion en chaîne peut se faire très simplement en parcourant les index de l'intervalle pour lequel la conversion a été demandée, en ordre décroissant afin que les bits apparaissent dans le bon ordre.

public default String toString(int s, int l) {
  if (! (0 <= l))
    throw new IllegalArgumentException();

  StringBuilder b = new StringBuilder();
  for (int i = s + l - 1; i >= s; --i)
    b.append(testBit(i) ? '1' : '0');
  return b.toString();
}

Partie 7

La conversion d'un tableau d'octets en vecteur se fait en distinguant deux cas, en fonction de si l'index du bit demandé fait partie de la plage couverte par le tableau ou non.

S'il n'en fait pas partie, alors il suffit de retourner la valeur par défaut, d.

S'il en fait partie, alors il suffit de calculer l'index de l'octet contenant le bit, et l'index du bit dans cet octet. Cela se fait très facilement au moyen de la division entière et de son reste. Une fois encore, il faut toutefois penser à utiliser la division entière par défaut (les méthode floorDiv et floorMod) et pas la division tronquée (les opérateurs / et %), car cette dernière ne produit pas les résultats désirés avec les index négatifs.

public static BitVector ofBits(byte[] b, boolean d) {
  return i -> {
    int cI = Math.floorDiv(i, Byte.SIZE);
    if (0 <= cI && cI < b.length) {
      int bI = Math.floorMod(i, Byte.SIZE);
      return (b[cI] & (1 << bI)) != 0;
    } else {
      return d;
    }
  };
}

Fondu au blanc

Sachant que sur le Game Boy les couleurs sont numérotées de la plus claire (blanc, qui vaut 0) à la plus foncée (noir, qui vaut 3), un fondu au blanc s'obtient en rendant graduellement chaque couleur plus claire, c-à-d en décrémentant progressivement sa valeur.

Les 3 palettes s'obtiennent donc en décrémentant d'une unité leur couleur leur couleur à chaque étape, sauf dans le cas où la couleur vaut déjà 0, auquel cas on la laisse telle quelle. On obtient donc :

Image Palette
1 0b11_10_01_00
2 0b10_01_00_00
3 0b01_00_00_00
4 0b00_00_00_00

Dessin d'image

Etant donné que SCY vaut 1 et LY vaut 2, la ligne en cours de dessin est la celle d'index 3 (c-à-d la quatrième) de l'image de fond.

Comme les images des tuiles font 8 pixels de haut, la quatrième ligne de l'image de fond est constituée de la quatrième ligne des tuiles de la première rangée. Sachant que SCX vaut 4, les 16 premiers pixels affichés à l'écran sont ceux d'index 4 à 19 de cette ligne-là. Ces pixels proviennent de trois tuiles différentes :

  • les 4 premiers proviennent de la première tuile,
  • les 8 suivants proviennent de la seconde tuile,
  • les 4 derniers proviennent de la troisième tuile.

Pour déterminer la couleur de ces pixels, il faut donc savoir à quoi ressemblent les trois premières tuiles de la première rangée. En consultant le contenu de la mémoire à partir de l'adresse 980016, on constate que ces tuiles ont respectivement les index 0, 1 et 2. Il ne reste plus qu'à déterminer à quoi ressemble leur quatrième ligne.

L'image de la tuile d'index 0 commence à l'adresse 800016, et sa quatrième ligne occupe les octets aux adresses 800816 (bits de poids faible) et 800916 (bits de poids fort). Ces deux octets valent 0, donc la couleur de chacun des pixels de la quatrième ligne de la ligne 0 ont la couleur 0.

L'image de la tuile d'index 1 commence à l'adresse 801016, et sa quatrième ligne occupe les octets aux adresses 801816 (bits de poids faible) et 801916 (bits de poids fort). Le premier de ces octets vaut FF16, le second 0. En d'autres termes, tous les pixels de cette ligne ont la même couleur, qui est la couleur 1.

Finalement, l'image de la tuile d'index 2 commence à l'adresse 802016, et sa quatrième ligne occupe les octets aux adresses 802816 (bits de poids faible) et 802916 (bits de poids fort). Le premier de ces octets vaut 0, le second FF16. En d'autres termes, tous les pixels de cette ligne ont la même couleur, qui est la couleur 2.

En combinant ces différentes informations, on en déduit que les pixels demandés ont les couleurs suivantes :

Pixels 0 à 3 4 à 11 12 à 15
Couleur 0 1 2

Conversion d'image

Partie 1

L'introduction du cache ne pose pas de problème particulier : il suffit de le consulter pour voir s'il ne contient pas encore d'entrée pour la ligne à convertir. Dans ce cas, on convertit la ligne au moyen de reallyConvertLine puis on insère l'entrée correspondante dans le cache. Une fois cela fait, on sait qu'une entrée se trouve dans le cache, et il ne reste plus qu'à la retourner.

private static int[] convertLine(LcdImageLine l) {
  if (! cache.containsKey(l))
    cache.put(l, reallyConvertLine(l));
  return cache.get(l);
}

A noter qu'en utilisant la méthode computeIfAbsent de Map et une référence de méthode, convertLine s'écrit de manière encore plus simple :

private static int[] convertLine(LcdImageLine l) {
  return cache.computeIfAbsent(l,
    ImageConverter::reallyConvertLine);
}

Partie 2

Pour supprimer aléatoirement 100 éléments lorsque le cache en contient 1000, il suffit d'ajouter un test au début de la méthode convertLine qui détermine si la purge est nécessaire et, dans ce cas :

  1. place les clefs du cache dans une liste,
  2. mélange aléatoirement cette liste,
  3. supprime du cache les 100 premiers éléments de cette liste mélangée.
private static int[] convertLine(LcdImageLine l) {
  if (cache.size() == 1000) {
    List<LcdImageLine> ks = new ArrayList<>(cache.keySet());
    Collections.shuffle(ks);
    for (int i = 0; i < 100; ++i)
      cache.remove(ks.get(i));
  }
  return cache.computeIfAbsent(l,
    ImageConverter2::reallyConvertLine);
}

Partie 3

Comme expliqué dans l'énoncé, pour pouvoir supprimer les éléments les moins récemment utilisés du cache, il faut savoir à quand remonte la dernière utilisation de chacun d'entre eux.

Pour ce faire, on peut incrémenter un compteur à chaque appel à convertLine et utiliser la valeur de ce compteur comme une notion abstraite de temps. A chaque élément du cache est associée la valeur de ce compteur au moment de sa dernière utilisation.

Pour pouvoir stocker cette information supplémentaire, il faut définir une classe représentant une paire composée de la ligne convertie, et de la valeur du compteur au moment de la dernière utilisation. Cela se fait très facilement :

private static final class CacheEntry {
  public int lastUse;
  public int[] data;
  public CacheEntry(int[] data) {
    this.data = data;
  }
}

Cela fait, il faut également changer le type du cache, dont les valeurs ont désormais le type CacheEntry :

private static Map<LcdImageLine, CacheEntry> cache =
  new HashMap<>();

Finalement, il faut modifier convertLine pour gérer correctement le compteur et mettre à jour le temps de dernière utilisation de chaque élément obtenu du cache. Tout comme pour la partie 2, il faut supprimer 100 éléments du cache lorsque celui-ci en contient 1000, la seule différence étant que ces éléments sont obtenus en triant par dernière utilisation la liste des clefs plutôt qu'en la mélangeant.

private static int convertCount;

private static int[] convertLine(LcdImageLine l) {
  if (cache.size() == 1000) {
    List<LcdImageLine> lru = new ArrayList<>(cache.keySet());
    Collections.sort(lru, (l1, l2) ->
      Integer.compare(cache.get(l1).lastUse,
		      cache.get(l2).lastUse));
    for (int i = 0; i < 100; ++i)
      cache.remove(lru.get(i));
  }
  CacheEntry c = cache.computeIfAbsent(l, k ->
    new CacheEntry(reallyConvertLine(l)));
  c.lastUse = convertCount++;
  return c.data;
}