Archives

Produit de topologie en Coq

Voici l’exercice qu’on va abordé dans ce billet :

Soit \((X, T )\) un espace topologique séparé. Montrer que la diagonale \(\Delta\) de \(X ×X\) est fermée dans
\(X × X\).

http://math.univ-lille1.fr/~bodin/exolic1/exolic.pdf

Aparté sur les topologies initiales

Mon livre de topologie définit les produits de topologies comme la topologie initiale des fonctions de projections \(p_i = (x_1, x_2) \in X, X \to x_i\). Ce qui nous amène à regarder ce qu’est une topologie initiale.

Pour être transparent, je vois également le terme de « topologie faible » utilisé ; c’est justement le terme utilisé par coq-topology… Je n’ai pour l’instant pas trouvé s’il y a une différence entre les 2 concepts, dans la suite je pars du principe qu’il s’agit de la même chose.

Une topologie initiale permet de construire une topologie sur un espace qui n’en dispose pas à partir de fonctions de cet espace sur un espace qui, lui, est topologique. Ainsi, si on dispose d’un espace X, et de fonctions \(f_i:X \to Y_i\) avec \(Y_i\) topologique, on définit une topologie sur X comme étant l’ensemble des intersections des \(f_i^{-1}(O_i)\) avec \(O_i\) ouvert de \(Y_i\). Ceci permet d’assurer que, par construction, ces fonctions sont continues.

Dans le cas des fonctions produits, les ouverts sont donc de la forme \(O_1 x O_2\) avec \(O_1\) un ouvert de \(Y_1\) et \(O_2\) un ouvert de \(Y_2\).

Preuve en Coq

Le module coq-topology dispose d’un module ProductTopology qui lui-même se base sur le module WeakTopology qui implémente les définitions du paragraphe précédent.

Comme souvent la principale difficulté n’est pas à trouver dans les mathématiques, il ne s’agit que de dérouler des définitions ; en revanche, ces définitions sont particulièrement peu digestes en Coq.

On va utiliser également les axiomes de séparations déjà définis dans coq-topology.

From Topology Require Import TopologicalSpaces.
From Topology Require Import InteriorsClosures.
From Topology Require Import Neighborhoods.
From Topology Require Import ProductTopology.
From Topology Require Import SeparatednessAxioms.

On dispose de quelques fonctions dans coq-topology pour faciliter l’écriture de produit de 2 topologies, qui se basent sur les fonctions built-in fst et snd de Coq. L’énoncé devient :


Theorem exo (X:TopologicalSpace):
  Hausdorff X -> closed (fun x:point_set (ProductTopology2 X X) => fst x = snd x).
Proof.

On va déplier les définitions :

  unfold Hausdorff.
  intros HypSep.
  unfold closed.

Pour plus de lisibilité, on va associer à l’ensemble \(\Delta\) une variable :

  remember (Complement (fun x : point_set (ProductTopology2 X X) => fst x = snd x)) as ExtDiagonale.

On passe ensuite par un résultat intermédiaire sur cette diagonale pour caractériser les éléments qui y appartiennent :

  assert (forall x y, not (x=y) <-> In ExtDiagonale (x,y)).
  split;intros.
  - rewrite HeqExtDiagonale. eauto with sets.
  - rewrite HeqExtDiagonale in H. eauto with sets.

Pour résoudre l’exercice, on va utiliser une base des ouverts sur le produit de topologie.

  - refine (coverable_by_open_basis_impl_open (ProductTopology2_basis X X) _ _ _). apply ProductTopology2_basis_is_basis.
    intros x0 x0inExtDiag. assert (x0 = (fst x0, snd x0)). apply surjective_pairing. rewrite H0 in x0inExtDiag. pose (tmp:=H (fst x0) (snd x0)). rewrite <- tmp in x0inExtDiag. destruct (HypSep (fst x0) (snd x0) x0inExtDiag) as [U [V ]].
    exists (fun x => In U (fst x) /\ In V (snd x)). inversion H1 as [openU [openV [x0InU [x0inV InterUVEmpty]]]].
    split.
    + replace (fun x : point_set X * point_set X => In U (fst x) /\ In V (snd x)) with [p : point_set (ProductTopology2 X X) | let (x, y) := p in In U x /\ In V y]. constructor ; repeat assumption. simpl. apply functional_extensionality. intros. Search ([_:_ | _]). rewrite characteristic_function_to_ensemble_is_identity. autounfold with *. assert (x=(fst x, snd x)). apply surjective_pairing. rewrite H2. simpl. trivial.
    + split.
      * autounfold with *. intros. pose (tmp2:=surjective_pairing x). rewrite tmp2. rewrite <- H. unfold In in H2. inversion H2. unfold not. intro. rewrite H5 in H3. assert(In (Intersection U V) (snd x)). eauto with *. rewrite InterUVEmpty in H6. Search Empty_set. apply Noone_in_empty in H6. assumption.
      * autounfold with *. eauto with *.
Qed.

Séparation en Coq

On continue dans la série des espaces topologiques avec un exercice sur les axiomes de séparation :

Soit \((X, T )\) un espace topologique.
Montrer que les conditions suivantes sont équivalentes :
(i) \(\forall x, y \in X, x \neq y, \exists V \text{voisinage de}x, y \notin V .\)
(ii) \(\forall x \in X, {x} fermé.\)
(iii) \(\forall x \in X, {V ; V \text{voisinage de }x} = {x}.\)
Soit \((X, T )\) ainsi et \(A \subset X\) tel que \(\overline{A} \neq A\). Montrer que si \(x \in \overline{A}\backslash A\), tout voisinage de \(x\) coupe \(\)A en une infinité de points.

Apparté sur la séparation

Cet exercice m’a donné l’occasion de découvrir le concept de séparation en topologie que je ne connaissais que très vaguement. L’idée derrière la séparation est de caractériser le niveau d’ « indépendance » des voisinages de deux points distincts. Plus exactement, on distingue 3 niveaux de séparations notés T0, T1 et T2:

  • T0 signifie que si on prend deux points distincts, on a au moins un voisinage de l’un qui ne contient pas l’autre,
  • T1 signifie que si on prend deux points distincts, chacun a au moins un voisinage ne contenant pas l’autre,
  • Enfin T2 est le niveau au dessus : si on prend deux points distincts, on a au moins un couple de voisinage respectif qui sont disjoints.

La plupart des espaces usuels (les réels, complexes, espaces vectoriels…) sont T2. Parmi les espaces qui ne sont pas séparés il y a les topologies grossières (ie l’ensemble entier et l’ensemble vide). J’avoue que je ne connais pas la motivation derrière les niveaux T0 et T1 et je n’ai pas d’exemples d’espace vérifiant ces niveaux mais pas les suivants.

Le trio d’équivalence est semble-t-il une propriété fondamentale des axiomes de séparation puisqu’il est directement présenté comme propriété dans mon livre de topologie, qui le démontre de bas en haut. J’ai essayé d’effectuer la démonstration en Coq de haut en bas mais je me suis retrouvé bloquer en cherchant à passer de (ii) à (iii) ; ayant ensuite décidé qu’il était peut-être plus sage de suivre une voie connue, j’ai finalement opté pour la démonstration de bas en haut. Il s’avère que le passage de (iii) à (ii) est aussi compliqué et que dans les autres cas les preuves sont plus ou moins des « miroirs » de celles que j’avais faite dans ma première tentative, j’imagine aisément qu’il est malgré tout possible d’effectuer la preuve dans n’importe quel sens.

Préalable

Une propriété particulièrement utile pour cette exercice est la caractérisation de l’adhérence d’un ensemble, appelé closure_impl_meets_every_open_neighborhood dans coq-topology : un point est dans l’adhérence d’un ensemble si tous ses voisinages rencontrent cet ensemble, ou dit autrement si l’intersection de ses voisinages avec cet ensemble n’est jamais vide.

Du point de vue plus pratique, on continue à utiliser quelques astuces qui simplifient la vie sur le long terme : on définit une tactic ensemble_proof_intro pour automatiquement nous fournir les 2 inclusions de l’égalité de l’ensemble à prouver, on se sert d’inversion plutôt que de destruct, et on nomme autant que possible les hypothèses/variables pour faciliter la lecture.

A noter qu’à un moment j’ai eu besoin de définir un axiome sur la décidabilité sur l’égalité de 2 éléments d’un ensemble. Par défaut si deux éléments d’un ensemble ne sont pas égaux, Coq ne déduit pas automatiquement qu’ils sont différents ; en effet la logique constructive sur laquelle est basée Coq se passe de l’axiome du tiers exclu qui stipule qu’une propriété (comme ici l’égalité) est vraie ou sa négation est vraie. Il faut donc manuellement indiqué que dans notre cas, c’est vrai.

Trio d’équivalence

On commence par les imports de circonstances et l’écriture des propriétés :

From Topology Require Import TopologicalSpaces.
From Topology Require Import InteriorsClosures.
From Topology Require Import Neighborhoods.
Require Import Decidable.

Definition P1 (X:TopologicalSpace) :=
  forall x y:point_set X, not (x = y) -> exists N, neighborhood N x /\ not (In N y).

Definition P2 (X:TopologicalSpace) :=
  forall x:point_set X, closed (Singleton x).

Definition P3 (X:TopologicalSpace) :=
  forall x:point_set X, FamilyIntersection [V:Ensemble (point_set X) | neighborhood V x] = Singleton x.

Ltac ensemble_proof_intro:=apply Extensionality_Ensembles; unfold Same_set; split.

On aura également besoin de montrer que si un singleton est contenu dans un autre, alors leur élément est égal :

Theorem Singleton_Inclusion (T:Type): forall x y:T, Included (Singleton x) (Singleton y) -> x = y.
Proof.
  intros.
  unfold Included in H. symmetry. apply Singleton_inv. apply H. eauto with sets.
Qed.

On commence ensuite par l’implication la plus compliqué, P3 vers P2 :

Theorem P3_implies_P2 {X:TopologicalSpace}:
  P3 X -> P2 X.
Proof.
  unfold P3. unfold P2.
  
  intros P3 x0.

La stratégie pour montrer que le singleton x est fermé, est de montrer qu’il est égal à son adhérence. Cette stratégie part de l’intuition que l’écriture de l’ensemble des voisinages est proche d’une intersection en ensemble de fermé contenant un ensemble.

  assert (Included (closure (Singleton x0)) (Singleton x0)).

  unfold Included.
  intros x xInClosure.

On va se servir de la propriété sur la caractérisation des adhérences. Quand on regarde les hypothèses de cette propriété, on voit qu’il va falloir trouver un ouvert, qui contient x, et on en déduira que x0 est dans V (car l’intersection du singleton x0 et V est non vide).

pose (tmp2:=closure_impl_meets_every_open_neighborhood  _ (Singleton x0) x xInClosure).

  assert (forall V, open_neighborhood V x -> Inhabited (Intersection (Singleton x0) V)).
  intros V VNeighbor. inversion VNeighbor. unfold open_neighborhood in VNeighbor.
  apply tmp2. 
  assumption. assumption.
  assert(forall V, open_neighborhood V x -> In V x0).
  intros. pose (tmp3:=H V H0). inversion tmp3. inversion H1. inversion H2. subst. assumption.

Pour rappel, on cherche à montrer ici que x est dans le singleton x0, ie que x = x0. Pour l’instant on n’a pas encore utilisé l’hypothèse P3. Pour se simplifier la vie on peut remarquer qu’on peut se servir des voisinages ouverts plutôt que des voisinages quelconques :


  assert( forall x : point_set X,  FamilyIntersection [V : Ensemble (point_set X) | neighborhood V x] =
                              FamilyIntersection [V : Ensemble (point_set X) | open_neighborhood V x]).
  {
    intros x1. ensemble_proof_intro.
   - intros  S SinInters. inversion SinInters.
    constructor. intros. apply H1. inversion H3. constructor. apply open_neighborhood_is_neighborhood. apply H4.
  - intros y yinIntersOpen. inversion yinIntersOpen.
    constructor. intros S SInInters. inversion SInInters as [[S' [OpenS' S'InS]]].
    refine (S'InS _ _). apply H1. split. assumption.
  }

Muni de ce résultat on peut prouver l’égalité:


  assert(Included (Singleton x0) (Singleton x)).
  unfold Included. intros y yisx0. inversion yisx0.
  rewrite <- (P3 x).
  rewrite (H1 x).
  constructor.
  intros. inversion H3. subst. eauto.

et donc l’inclusion inverse :

  apply Singleton_intro.
  refine (Singleton_Inclusion _ _ _ _). assumption.

La suite est relativement directe:


  assert (closure (Singleton x0) = Singleton x0).
  ensemble_proof_intro.
  apply H. apply closure_inflationary.
  rewrite <- H0. apply closure_closed.
Qed.

L’inclusion P2 vers P1 est plus simple:

Theorem P2_implies_P1 {X:TopologicalSpace}:
  P2 X -> P1 X.
Proof.
  unfold P1, P2.
  intros P2 x y xNeqy.
  unfold closed in P2.

  exists (Complement (Singleton y)). split.
  unfold neighborhood. exists (Complement (Singleton y)). split. unfold open_neighborhood. split. apply (P2 y).
  unfold Complement. unfold not. unfold In. intros. destruct xNeqy. inversion H. trivial.
  eauto with sets.
  eauto with sets.
Qed.

Celle de P1 vers P3 n’est pas beaucoup plus compliqué, mais nécessite quand même la décidabilité dont on a préalablement parlé, pour faire une disjonction de cas :

Theorem P1_implies_P3 {X:TopologicalSpace}:
  P1 X -> P3 X.
Proof.
  unfold P1. unfold P3.
  intros P1 x0.
  ensemble_proof_intro.
  - unfold Included. intros x xInIntersNeigh.
    destruct (decT X x0 x).
    + eauto with sets.
    + inversion xInIntersNeigh. destruct (P1 x0 x H). inversion H2. pose (tmp:= H0 x2). autounfold with sets in tmp.
      assert([V : Ensemble (point_set X) | neighborhood V x0] x2).
      constructor. assumption.
      apply tmp in H5. destruct (H4 H5).
  - unfold Included. intros x xIsx0.
    inversion xIsx0.
    constructor. intros. inversion H0. inversion H1. inversion H2. unfold Included in H4. apply H4. inversion H3. assumption.
Qed.

Seconde propriété

Alors là c’est vraiment costaud à démontrer, la faute à la démonstration de l’infinité. Coq dispose de quelques résultats sur les ensembles infinis, mais malheureusement peu nombreux et que nous n’utiliserons donc pas.
En revanche il est beaucoup plus simple de travailler sur les ensembles finis, on va donc procéder par l’absurde.

En particulier une propriété des ouverts qui est vraie dans le cas des ensembles finies mais qui ne l’est plus dans les ensembles infinis est la stabilité par intersection. On va donc prouver cette propriété, par récursion sur la taille de l’ensemble :

Theorem intersection_finite (X:TopologicalSpace):
  forall F:Family (point_set X), Finite _ F /\ (forall S, In F S -> open S) -> open (FamilyIntersection  F).
Proof.
  intros F [F_Finite F_Set_Of_Opens].
  apply finite_cardinal in F_Finite.
  destruct F_Finite as [n cardFn].
  generalize dependent F.
  induction n ; intros F cardFn F_Set_Of_Opens.
  - inversion cardFn. Search FamilyIntersection. rewrite empty_family_intersection.
    Print TopologicalSpace. apply open_full.
  - inversion cardFn. pose (tmp:= IHn A H0).
    assert(FamilyIntersection (Add A x) = Intersection (FamilyIntersection A) x).
    ensemble_proof_intro; unfold Included; intros x0.
    + intros x0InIntersAx.
      inversion x0InIntersAx.
      constructor.
      * constructor. eauto with sets.
      * eauto with sets.
    + intros x0InInters. inversion x0InInters. inversion H3. constructor. unfold Add.
      intros. inversion H8.
      * eauto with sets.
      *  inversion H9. subst. eauto with sets.

    + rewrite H3. apply open_intersection2.

      * apply tmp. intros. apply F_Set_Of_Opens. Search Add.
        unfold Add in H1. rewrite <- H1. eauto with sets.
      * apply F_Set_Of_Opens. rewrite <- H1. unfold Add. eauto with sets.
Qed.   

Un autre résultat qui m’a été utile est que l’égalité est conservée par passage au complément :


Theorem Complement_inv (X:Type) : forall (S1 S2: Ensemble X), Complement S1 = Complement S2 -> S1 = S2.
Proof.
  intros. ensemble_proof_intro; rewrite <- Complement_Complement with (A:=S1); rewrite <- Complement_Complement with (A:=S2); apply complement_inclusion; rewrite H; eauto with sets.
Qed.

On peut ensuite passer à la démonstration en elle-même :

Theorem part2 (X:TopologicalSpace): P2 X -> forall A: Ensemble (point_set X), forall x, In (Setminus (closure A) A) x -> forall N, neighborhood N x -> not (Finite _ (Intersection N A)).
Proof.
  intros P2 A x0 x0inclAminA.
  unfold Top.P2 in P2.
  destruct x0inclAminA as [x0InClA x0NotInA].
  intros N [O [[OpenO x0inO] OInN]].
  
  pose (tmp:=closure_impl_meets_every_open_neighborhood _ A x0 x0InClA).

  unfold not. intros NInterAFinite.
  
  remember (fun E => exists x, In (Intersection N A) x /\ E = Complement (Singleton x)) as SetOfElems.
  (**pose (tmp3:=intersection_finite _ SetOfElems).**)
  assert(Finite _ SetOfElems).
  generalize dependent SetOfElems.
  induction NInterAFinite; intros.
  - assert (SetOfElems = Empty_set). rewrite HeqSetOfElems. ensemble_proof_intro.
    + unfold Included. intros. unfold In in H. destruct H. destruct H. destruct H.
    + eauto with sets.
    + rewrite H. eauto with sets.


  - assert (SetOfElems = Add (fun E : Ensemble (point_set X) => exists x0 : point_set X, In A0 x0 /\ E = Complement (Singleton x0)) (Complement (Singleton x))).
    ensemble_proof_intro ; unfold Included; rewrite HeqSetOfElems.
    + intros. inversion H0. inversion H1.  unfold In. inversion H2 as [x2InA0|]. 
      * left. exists x2. split. assumption. assumption.
      * right. inversion H4. subst. eauto with sets.
    + intros. unfold In. inversion H0.
      * inversion H1. inversion H3. exists x3. split. left. assumption. assumption.
      * exists x. inversion H1. split. right. eauto with sets. trivial.


    + rewrite H0. constructor.
      * remember (fun E : Ensemble (point_set X) => exists x1 : point_set X, In A0 x1 /\ E = Complement (Singleton x1)) as P0.
        apply IHNInterAFinite. trivial.
      * unfold In. unfold not. intros. inversion H1. inversion H2.
        apply Complement_inv in H4. 
        apply Singleton_equal in H4. subst. destruct (H H3).
    
   
  - remember (FamilyIntersection SetOfElems) as ContraGen.
    assert (open ContraGen).
    rewrite HeqContraGen.
    apply intersection_finite. split.
    + assumption.
    + intros S SInSetOfElems. rewrite HeqSetOfElems in SInSetOfElems. unfold In in SInSetOfElems. inversion SInSetOfElems.
      inversion H0. rewrite H2. refine (P2 _).
    + pose (contra:= tmp (Intersection ContraGen O) (open_intersection2 ContraGen O H0 OpenO)).
      assert (In (Intersection ContraGen O) x0).
      rewrite HeqContraGen.
      constructor. constructor. rewrite HeqSetOfElems. intros. unfold In in H1. inversion H1. inversion H2. rewrite H4. Search Intersection. apply Intersection_decreases_r in H3. unfold In. unfold Complement. unfold not. intros. inversion H5. subst. contradiction.
      assumption.

      apply contra in H1. inversion H1. inversion H2. inversion H4. rewrite HeqContraGen in H6. rewrite HeqSetOfElems in H6. unfold In in H6. inversion H6. pose (contra2:= H9 (Complement (Singleton x))). unfold In in contra2.
      assert(exists x0 : point_set X, Intersection N A x0 /\ Complement (Singleton x) = Complement (Singleton x0)). exists x. split. constructor.
      eauto with sets. eauto with sets. trivial. apply contra2 in H11. destruct H11. constructor.
Qed.

Clang-tidy

Au bureau, on nous a cédé le code source d’un logiciel relativement ancien et dont le précédent propriétaire ne souhaitait pas continuer la maintenance.Si d’ m point de vue business il s’ agissait d’une opportunité des plus lucratives, puisqu’on a hérité d’une base de clients colossales, travailler sur du code qui a dormi parfois pendant près de 10 ans est particulièrement difficile.

Pour dresser une image plus précise, le projet utilise une version antédiluvienne de WxWidgets et de boost, en réimplémentant toutefois les smart pointers (qui étaient probablement encore à l’état de standardisation à l’époque ). On ressent assez rapidement le fait que plusieurs personnes sont passés dessus, avec un étrange mélange de pratique douteuses de développement logiciel (des fichiers entiers copié et collés) qui côtoient les design pattern classiques d’une base de code de taille respectable.

Et bien entendu, il n’y a pas ou peu de tests unitaires rendant chaque tentative de refactoring hasardeuse.

La récente série de posts autour de la customisation de clang-tidy sur le blog de visual c++ tombe à bien nommé :

  • https://blogs.msdn.microsoft.com/vcblog/2018/10/19/exploring-clang-tooling-part-1-extending-clang-tidy/
  • https://blogs.msdn.microsoft.com/vcblog/2018/10/23/exploring-clang-tooling-part-2-examining-the-clang-ast-with-clang-query/
  • https://blogs.msdn.microsoft.com/vcblog/2018/11/06/exploring-clang-tooling-part-3-rewriting-code-with-clang-tidy/

Pour référence pour plus tard je partage le bout de code qui permet de détecter les types std::string, qu’ils apparaissent explicitement ou via un using ou typedef:

auto typeParamMatcher =
		varDecl(hasType(hasUnqualifiedDesugaredType(
     recordType(hasDeclaration(cxxRecordDecl(hasName("basic_string")))))));

Si on supprime le hasUnqualifiedDesugaredType, on peut ne matcher que l’apparition explicite de ce type:

varDecl(hasType(cxxRecordDecl(matchesName("string"))))

EMGU (OpenCV pour .Net) à la rescousse

Lors de mon précédent billet j’avais présenté comment utiliser les Windows Forms pour afficher une image. J’ai depuis découvert l’existence d’EMGU, un wrapper pour OpenCV qui fournit pas mal de fonctions utilitaires dont certaines pour charger et afficher des images. La plupart des fonctions usuelles de la bibliothèque sont disponibles de façon directe sous le namespace CvInvoke. Ainsi pour charger une image on utilisera imread comme suit :

open Emgu.CV

let img = CvInvoke.Imread(@ »C:\Users\vljno\Desktop\Canardcomission.png »)

Et pour l’afficher on utilisera imshow comme suit :

CvInvoke.Imshow(« nom de fenêtre », img)

CvInvoke.WaitKey(0)

Ce qui ne surprendra pas les personnes ayant déjà utilisé OpenCV dans d’autres langage.

Pour récupérer les données d’une image on utilisera le membre GetData de l’objet Mat retourné par Imread qui donnera un tableau 1d de bytes de l’image. A l’inverse, le membre SetTo permettra de changer le contenu de l’image.

img.GetData ([||])

img.SetTo someArray

Traitement d’image avec Fsharp et OpenCL

En ce moment je découvre les possibilités de F# en matière de scientific computing. F# (prononcé « Fsharp ») est un langage calqué sur OCaml dont il partage la syntaxe de base et une grosse partie de la bibliothèque standard. Quelques fonctionnalités disparaissent (les foncteurs et plus généralement tout ce qui concerne les modules, les types algébriques généralisés), des fonctionnalités sont légèrement retouchées (le quotation mark devient <@… @>, indentation non libre…), en contrepartie le langage a accès à la totalité de l’écosystème DotNet ou leur équivalent Mono et NetCore. Ces environnements étant particulièrement populaires il n’est pas étonnant de voir qu’il existe déjà des bibliothèques pour le calcul statistique, la recherche opérationnelle, les divers algorithmes d’intelligence artificielle, et pour ce qui nous concerne ici l’interfaçage avec des APIs de GPGPU comme OpenCL. Ce qui me donne l’occasion de tester les capacités de F# en tant que langage pour le traitement d’image face à un Python ou à un Matlab.

Commençons déjà par le code permettant de charger une image. De base DotNet fournit la classe Bitmap permettant de charger à peu près n’importe quel format courant et nous proposant d’accéder à ses pixels. Cette dernière est présente dans l’espace System.Drawing (dont il faudra référencer l’assembly) :

open System.Drawing
[<EntryPoint>]
let main argv =
    let img = new Bitmap(@"C:\Users\moi\Documents\fichier.jpg")
    // ….
    0

Afin de vérifier que tout se passe comme attendu il est souhaitable d’afficher cette image. Pour cela nous allons créer un System.Windows.Forms et dessiner l’image dedans via ce code :

open System.Windows.Forms
// ….
let form = new Form(Visible=true)
form.Paint.Add(function e-> e.Graphics.DrawImage(img, e.ClipRectangle, e.ClipRectangle, GraphicsUnit.Pixel))
System.Windows.Forms.Application.Run(form)

Si tout se passe bien vous devriez voir apparaitre votre image à l’écran dans une fenêtre relativement simple.

Maintenant que nous avons chargé notre image nous pouvons nous pencher sur l’utilisation d’OpenCL. Pour cela nous allons passer par la bibliothèque Brahma.OpenCL qui fournit à Fsharp une interface de relativement haut niveau autour d’OpenCL.Net. Cette dernière est disponible via Nuget.
L’initialisation de Brahma est relativement simple, il faut deux objets, un provider (qui représente votre installation OpenCL sur votre machine) et une CommandQueue à laquelle nous soumettrons les tâches que nous voulons effectuer :

open Brahma.OpenCL
open Brahma.FSharp.OpenCL.Core
open Brahma.FSharp.OpenCL.Extensions
open OpenCL.Net
open FSharp.Core
// ….
let provider = ComputeProvider.Create("*", DeviceType.Gpu)
let mutable commandQueue = new Brahma.OpenCL.CommandQueue(provider, provider.Devices |> Seq.head)

Ici nous utiliserons le premier GPU disponible, d’où le Seq.head.

L’avantage de Brahma.OpenCL est qu’elle repose sur les quotations marks pour la création de kernel OpenCL. Une quotation mark est un code F# sous forme d’arbre de syntaxe abstrait, ie une expression qui peut ensuite être passée comme argument à une fonction F#, par exemple pour la modifier, l’interpréter, ou dans le cas de Brahma, la traduire en code OpenCL. Ici nous allons écrire un filtre de Sobel (norme du gradient) prenant en argument un tableau 1d source et un tableau 1d destination (le gradient a besoin des informations des pixels l’entourant, il n’est pas possible de modifier « inplace » les données d’un tableau de façon concurrente. A noter que les quotations mark peuvent capturer des variables comme ici stride.

open Microsoft.FSharp.Quotations
// ….
let stride = img.Height;
let command = <@ fun (range:_2D) (buf:array<byte>) (dst:array<byte>) ->
    let i = range.GlobalID0
    let j = range.GlobalID1
    let mutable h = float32 0.
    if (i > 0 && i < stride) then
        let left = float32 buf.[i - 1 + stride * j]
        let right = float32 buf.[i + 1 + stride * j]
        h <- abs (left - right)
    let mutable v = float32 0.
    if (j > 0) then
        let top = float32 buf.[i + stride * (j - 1)]
        let bottom = float32 buf.[i + stride * (j + 1)]
        v <- abs (top - bottom)
    dst.[i + stride * j] <- byte (sqrt (h * h + v * v)) @>

Bien entendu il n’est pas possible d’écrire n’importe quel code, les kernels OpenCL ne pouvant pas être récursifs par exemple ou appeler des lambdas. La compilation d’une quotation mark sur du code qui ne peut être traduit génèrera une exception.

let kernel, kernelprepare, kernelrun = provider.Compile command

Les valeurs kernelprepare et kernelrun de la compilation permettent respectivement de mettre à disposition les dimensions et arguments (ici les tableaux buf et dst) du kernel au GPU, et d’exécuter ce code kernel via la syntaxe suivante :

kernelprepare d src dst
commandQueue.Add(kernelrun()) |> ignore
commandQueue.Add(dst.ToHost provider).Finish() |> ignore

d est un objet contenant les dimensions du workgroup (global et local) à utilizer pour le kernel, src et dst sont des Arrays F# classiques. On aura au préalable transféré le contenu 2d de img dans src 1d :

let d = _2D(img.Width, img.Height, 8, 8)
let src = Array.init (img.Width * img.Height) (function i -> img.GetPixel(i / stride, i % stride).R)
let dst = Array.zeroCreate (img.Width * img.Height)

Enfin une fois le kernel execute nous copions le contenu de dst dans img :

Array.iteri (fun i (v:byte) -> img.SetPixel(i / stride, i % stride, Color.FromArgb(255, int(v), int(v), int(v)))) dst

Voilà !

Emacs mode for VS 2017

Visual Studio 2008 had an Emacs mode that added most commonly used Emacs keybindings in the IDE. VS 2010 later made the feature available as an optional extension which unfortunaly wasn’t carried over on VS 2013. However the source of the extensions were published so that the communit could carry over : https://github.com/zbrad/EmacsKeys

Porting the extension to the latest version of Visual Studio is actually quite easy. It requires enabling Desktop DotNet and Visual Studio SDK feature in the installer though.

Out of the box the solution won’t compile because of missing assemblies : EnvDTE, Microsoft.VisualStudio.Shell.10*. EnvDTE references should be manually replaced by a reference to the envdte.dll assembly on the disk (in Program Files(x86)\Common Files\Microsoft Shared\MSEnv\PublicAssemblies\) ; I’m not sure why VS2017 doesn’t find it automatically to be honest seems it’s registered in a rather standard location.
Microsoft.VisualStudio.Shell.15 (there’s 2 versions available in the reference browser, both are needed) and Microsoft.VisualStudio.Shell.15.Framework will replace the reference to the older Shell.10 ones ; doing so will make the project target the 4.6.2 Framework.

The last piece of the port is to add a Prerequisites in the vsixmanifest file. I suggest adding the base IDE as the sole one.

Next the extension should build properly.

A note on Android physical security

My Android phone was stolen 2 weeks ago and unfortunatly the thieves were able to see my unlock pattern and thus to unlock it and access my gmail account. I could retrieve my account back thanks to google allowing password reset in case of dubious password change. I also hadn’t double authentication enabled which in such circumstance would have been annoying since I obviously couldn’t look at my phone messages anymore.

This leads me to write this small blog post gathering some advices in how to secure your accounts against physical phone access:

  • Use the fingerprints readers. I know it’s completely unreliable and fails nearly half of the time to detect your fingerprint but at least it’s unlikely to unlock without your own fingers.
  • Get a secondary mail account whose sole purpose is to serve as a password recovery option for your critical web accounts and NEVER access it from your phone or from a device you don’t own.
  • Critical web account obviously includes Samsung and/or Google one: since modern phone can be tracked remotely and includes a kill switch it’s the first thing that will be targeted. At least it happened in my case since I could see password reset notification from my Samsung account when I was back home and could restore my access to GMail.
  • Two-way authentication is becoming increasingly more popular. It’s probably very efficient to prevent security breach when accessing your account from a third party’s device but it’s also very dangerous if one has access to your phone. Having a spare phone line whose sole purpose is to serve as a second way authentication is likely costly; on the other hand, there are some physical alternative (like Yubi keys) which may be used instead. If you don’t accidently loose it.
  • Do regular backup since online service doesn’t always store every data or may need some obscure configuration to work as you may expect. For instance, I lost some of my text messages.
  • Don’t use the paypal mobile app. Never.
  • Don’t store any password PIN or credit card number as a clear text on your phone. If you have issue remembering a password, try to create one as a visual pattern on a keyboard. Muscle memory often works great in such case.
  • Even if unlock pattern are more secure than PIN number since there are at least 9! potential combination instead of 1000 numbers they are also easier to observe and harder to hide ; prefer good old password if possible (for instance as fingerprint fallback option).

 

Small gamedev companies lacks interest in Vulkan

Last month I got interviews for a 3d dev position in a couple of companies near Paris. I mentioned in my resume that I already wrote DX12 and Vulkan code and as expected at some point the interviewers asked me my opinion on DX12 and if I got some performance improvements by using the API. I admitted that my experience is biased since RPCS3 initial GL implementation was not optimal so the performance figure I got were not representative of what can be achieved with DX12.

What surprised me is that my interviewers didn’t show lots of interest in Vulkan. Gamedev studio in Paris can be considered as middle sized ones, between 20 and 70 full time dev who works on several (often niche) titles at the same times for bigger editor. Although they are big enough to maintain their own 3d engine they can’t invest as much money as the big name in the industry. Hence technical choices are often dictated by time and budget constraints.

The first company I was interviewed by (which ships games on PC, PS4 and X1) explained that adding a new rendering backend is costly and that they won’t support Vulkan except if it was the API of the at that time unnamed Nintendo new console (it turns out it’s not the case, the Nintendo Switch is using a custom API, VNM. As far as I know Nintendo always used custom gfx API). With around 1% of market share Linux wasn’t considered as a viable target; neither was Android despite being the first or second most used platform. I suspect the poor state of 3d graphic in the Android ecosystem is to blame here and the web is plenty of horror
story when it comes to OpenGL ES programming. I think that a lot of the enthousiasm surrounding Vulkan comes from the promises of more reliable drivers on Android thanks to the thinner layer of abstraction and the externalization of debug checks. SoC vendors also seems more inclined to improve the situation with Samsung heavily promoting Vulkan support though footage of early demo of Unreal Engine 4 and their partnership with editors to bring Vulkan version of NFS No Limits and Vain Glory on Google Play.

The second company’s engine already has an OpenGL codepath enabling Mac support. While OpenGL and Vulkan are very different in design but they at least share the same shader language (GLSL) which ease any potential porting work (which is still a big task, Vulkan’s being a very verbose API). However, Vulkan was never talked about during the interview; the interviewers were rather interested about what DX12 could bring. The catch is that there’s probably as much work to port a DX11 engine to DX12 as to port it to Vulkan, both Vulkan and DX12 relying on the same gpu abstraction; and while DX12 has a one year head start in term of tool and driver support it’s hindered by the OS support. Although Windows 10 has already gained around 25% of OS market share 60% of the market is still running Windows 7 or 8.x and there isn’t free upgrade offer anymore which makes the adoption rate slower.

On the other hand in both interviews it was clear that support for a low overhead API wasn’t a very high priority. Their engine has made the switch on DX11 very recently to reach parity with PS4 and Xbox One and the dev doesn’t want to reenter a new development cycle right now. At least for Windows; the only way to use Compute Shader, the most popular feature of DX11 hardware gen, on OSX is through Metal API. OpenGL on OSX is supported up to version 4.1 since 2012 and there is no sign that it will change soon. Increasing Metal popularity among will reinforce iOS lead in the smartphone gaming market meaning Apple has no incentive to support the low level API used of their main competitor.

RSX in RPCS3

I’m assuming the reader has some knowledge in graphic programming (with D3D or GL) and generally knows how modern CPU works.

The specs of RSX

RSX is the graphic processor of the PS3. The acronym stands for Reality Synthetiser according to Wikipedia. It’s actually based from Nvidia own Geforce 7800 Gtx, a directX9 class gpu slightly modified to allow Cell’s SPU to do image processing. It has 256 MB of local memory (PS3 terminology for « video memory ») with a 22.4 GB/s bandwidth according to Wikipedia again. It’s processing power is 228 Gflops/s and supports up to 4 render targets.
The aforementioned customisations of RSX allows it to access the main memory (shared by PPU and SPU) at 20 GB/s in read direction and 15 GB/s in write direction. This means that even if it’s slower to render a scene in main memory instead of local memory, the bandwidth hit is rouhly 30%.

Now let’s compare these numbers with the ones from a 2015 PC architecture :
A Geforce 970 has 4 GB of video memory (x16)  with a 200 GB/s bandwidth (x10), can process 5 Teraflop of data (x25) and support 8 render targets. However the theorical peak bandwidth of DD3 is 20 GB/s which is barely what the PS3 did offer in 2007.
The implication for emulation are strong : we can’t afford extra memory transfers between main memory and video memory.

RSX and Cell interaction

In all modern architecture CPU and GPU are executing independently from each others and PS3 is no exception.
RSX commands are 32 bits instructions puts in command buffers in a sequential maneer by Cell. There are 3 special command that are used to break the sequential flow of RSX, namely JUMP (mostly used to move from one command buffer to another) and CALL/RETURN pair (used to implement subroutines). Of course Cell needs to be able to prevent RSX to read commands faster than it can fill command buffer and thus RSX provides a « get » and a « put » register accessible from Cell. « Get » contains the memory address of the command the RSX is currently reading. « Put » can be written to by Cell, and is used as a « barrier », ie the RSX reads command only if Get and Put are different. Put register’s purpose is similar to glFlush in OpenGL.

RSX commands can be sorted in 3 categories :

  • Commands that set RSX register. Like most ancient GPU the RSX doesn’t fetch non buffer inputs in memory but in hardware registers. This includes textures, buffers, render surface… description (their location, their format, their stride, …), vertex constants (RSX has 512 4×32 registers storage for vertex constants), some pipeline state related to blending operation or depth testing. I didn’t mention fragment constants (or pixel shader constant if you prefer D3D terminology) because it looks like there is no true storage for them, the cell has to « patch » fragment program/pixel shader in memory.
  • Commands that issues actual rendering operations. So far I only saw 3 of them, a « clear surface » command that clear render targets (the clear value being stored in register), a « draw » command that issues an unindexed rendering call, an an « indexed draw » command that issues an indexed rendering call.
  • Commands managing semaphores. Semaphore provide a more powerful sync mechanism than the get/put register. Semaphore are basically location in memory associated with a 32 bits values. A wait command can be used to make RSX hold execution until the semaphore values is the same as the expected one, for instance to allow Cell to complete buffer filling task. A release command can be used to make RSX writes a specific value to the semaphore location ; this way a Cell thread can be notified that RSX has finished with a given rendering command and is free to update buffers.