📜 ⬆️ ⬇️

We write our simplified OpenGL on Rust - part 1 (draw a line)

Continued :
We write our simplified OpenGL on Rust - part 2 (wire rendering) .
We write our simplified OpenGL on Rust - part 3 (rasterizer)

Probably, few people in Habré are not aware of what Rust is - a new programming language from Mozilla. Already, it has attracted a lot of interest, and recently the first stable version of Rust 1.0 has finally come out, which marks the stabilization of language capabilities. I have always been impressed with system PL, and the idea of ​​a system language offering security that exceeds superior high-level languages ​​is even more interesting. I wanted to try a new language in business and, at the same time, have an interesting time, programming something exciting. While I was thinking what to write about such a thing, I recalled a recent series of articles on computer graphics, which I just skimmed. And it would be very interesting to try to still write all these beautiful things on your own. This is how the idea of ​​this hobby project, as well as this article, was born.

Since the original article thoroughly chews all the nuances related to programming directly the graphic component, I will focus in my article series mainly on what concerns Rust directly. I will try to describe those rakes, which had a chance to stumble, as well as how to solve the problems that arise. I will tell about personal impressions from acquaintance with language. And, of course, I will mention the list of resources that I used during development. So, anyone interested, welcome under cat.
')
Warning: the article is written from the perspective of a novice and describes the stupid mistakes of a novice. If you are a rasta pros, perhaps looking at my attempts, you will want to hurt me with something heavy. In this case, I recommend to refrain from reading it.


Here is the Rust, which i hope to get at the end. (pun, Rust in English "rust")

Training


I will not describe the installation of the compiler, everything is quite obvious there. The installation package from the official site was installed on my Ubuntu 14.04 pair of commands. It should only be noted that there is a repository for Ubuntu, using which you can also theoretically install Rust. However, I have not wondered with this repository. For some reason, Rust was installed, but without Cargo. Synaptic did not even show Cargo in the list of available packages. Rust without Cargo is a pretty pointless thing, so I did not use the specified PPA.

So, as always, we begin our acquaintance with the new language with Hello World. Create the file main.rs:

Hello world
fn main() { println!("Hello World!"); } 

The result of running this in the console is obvious:
 user@user-All-Series:~$ rustc main.rs user@user-All-Series:~$ ./main Hello World! user@user-All-Series:~$ 


You can write code using gedit. Rust syntax coloring for this editor can be found at the following link . It did not work right away , but there was a bug report in the tracker that explained the cause of the problem. A simple pull request solved the problem, so now you have to earn the colors without any extra dancing with a tambourine.

Usually, a new project is created using the cargo new rust_project --bin , but I still didn’t know about it, so I created the whole structure manually, since it’s not difficult:

The structure of the new project
 Cargo.toml src/main.rs 

Content Cargo.toml:
 [package] name = "rust_project" version = "0.0.1" authors = [ "Cepreu <cepreu.mail@gmail.com>" ] 

This project is launched by the cargo run team.

To display the image, I decided not to rewrite the TGAImage, which is provided by the author of the original article. I wanted to output the result using SDL. In fact, we only need from this library to create a window and display a point with the specified color according to the given coordinates. We draw all the other graphic primitives ourselves, so if you wish, you can change the image output backend simply by implementing 2 functions: set(x, y, color); and new(xsize, ysize) . Why SDL? In the future, I would like to be able to change the point of view from the keyboard. It may even be a simple toy to write ... TGA will not allow this.

The very first link to Google led me to the site of the project Rust-SDL2 - binding to SDL2 for Rust. To enable this library, add the following dependency declaration to the end of cargo.toml:

 [dependencies] sdl2 = "0.5.0" 

This adds to the project so-called. container (crate) sdl2. The library uses SDL header files for compilation, so you need to remember to install them. On Ubuntu 14.04, the command does this:

 sudo apt-get install libsdl2-dev 

After this, the cargo build team will successfully cargo build us a project with all the necessary dependencies.

 user@user-All-Series:~/temp/rust_project$ cargo build Updating registry `https://github.com/rust-lang/crates.io-index` Downloading num v0.1.25 Downloading rustc-serialize v0.3.15 Compiling sdl2-sys v0.5.0 Compiling rustc-serialize v0.3.15 Compiling libc v0.1.8 Compiling bitflags v0.2.1 Compiling rand v0.3.8 Compiling num v0.1.25 Compiling sdl2 v0.5.0 Compiling rust_project v0.1.0 (file:///home/user/temp/rust_project) user@user-All-Series:~/temp/rust_project$ 

SDL Experiments


I decided to bring all the functionality related to the interaction with SDL (or another graphic library in perspective) into a separate class, canvas.rs. At first, just to test the library, I copied the contents of the test there from the Rust-SDL2 repository, going to write a finished class on the basis of it.

Here I had the first rake. It turned out that any extern crate package_name in the library should also be duplicated in the main.rs application. It took time to figure it out, but after all the torment, I finally got a project that you can see in the snapshot on github .

As a result, the following code in the canvas.rs file was:

 // Copyright (C) Cepreu <cepreu.mail@gmail.com> under GPLv2 and higher extern crate sdl2; use sdl2::pixels::PixelFormatEnum; use sdl2::rect::Rect; use sdl2::keyboard::Keycode; pub fn test() { let mut sdl_context = sdl2::init().video().unwrap(); let window = sdl_context.window("rust-sdl2 demo: Video", 800, 600) .position_centered() .opengl() .build() .unwrap(); let mut renderer = window.renderer().build().unwrap(); // FIXME: rework it let mut texture = renderer.create_texture_streaming(PixelFormatEnum::RGB24, (256, 256)).unwrap(); // Create a red-green gradient texture.with_lock(None, |buffer: &mut [u8], pitch: usize| { for y in (0..256) { for x in (0..256) { let offset = y*pitch + x*3; buffer[offset + 0] = x as u8; buffer[offset + 1] = y as u8; buffer[offset + 2] = 0; } } }).unwrap(); renderer.clear(); renderer.copy(&texture, None, Some(Rect::new_unwrap(100, 100, 256, 256))); renderer.copy_ex(&texture, None, Some(Rect::new_unwrap(450, 100, 256, 256)), 30.0, None, (false, false)); renderer.present(); let mut running = true; while running { for event in sdl_context.event_pump().poll_iter() { use sdl2::event::Event; match event { Event::Quit {..} | Event::KeyDown { keycode: Some(Keycode::Escape), .. } => { running = false }, _ => {} } } } } 

This code displays such a wonderful window:
Screenshot


Write the code


If you look at the code itself, there are obvious 3 parts: initialization, drawing, waiting for input. The next item we have processing of the specified code in order to obtain a Canvas object, which has:

I know that keyboard input is not the most suitable function for the Canvas class, but the problem was that sdl_context is used to wait for input, which after such a change would become a Canvas field. Which in any case leads to the fact that either the Keyboard object will depend on the Canvas, or both of them will have to depend on some 3rd class. In order not to complicate everything excessively, while he stopped at how to leave everything in one class. If the load on the Canvas increases, then I will take some of the functions out of it and still do the work described above.

It is worth noting that before this, my development process was well characterized by the words “Stack Overflow Driven Development”. I just pulled different pieces of code from different places and put them together without even knowing the syntax of the language. This is a typical way for me to learn a new language or technology - just start writing code. I refer to various resources as a reference book in order to understand how to make a specific piece I need. Need a cycle, read the article about cycles in the language. It is necessary IoC, the good answer on Stackoverflow. Well, you understand. Most languages ​​and technologies are quite similar, so there are no problems with this approach. You can start programming right away, even without really knowing the language. With Rust, this number did not work. The language is rather peculiar and already trying to understand how to make a class or an object, a plug has occurred. In the book of Rasta there were no articles with the name Classes / Objects / Constructors, etc. It became clear that at first it was necessary to learn a little about the hardware. Therefore, in order to get a general impression of the syntax of the language, the lessons of Guessing Game and Dining Philosophers from Rust’s official documentation were taken. She is in English, it is worth noting the existence of a translation into Russian . But then I did not know this, I read it in the original language.

In general, after that, the work went. Although still had to make war with the system of ownership and borrowing Rust. The official documentation notes that every beginner goes through this stage, but encourages us that, as far as practice goes, the code can be written easily and naturally, getting used to the restrictions imposed by the compiler. The introduction states that the complexity of learning is the price that has to be paid for security without overhead. Specifically, the problem was that in the new () method that creates the structure, the fields of the newly created structure need to be initialized with several objects from the SDL in order to use them in the structure methods. If I tried to initialize the field with a link, the compiler cursed what variable borrowing could not do. Here in this code:

 pub struct Canvas<'_> { renderer: &'_ mut Renderer<'_>, } ... pub fn new(x: u32, y: u32) -> Canvas<'_> { ... let mut renderer = window.renderer().build().unwrap(); Canvas { renderer: &mut renderer } } 

By the way, on Habré, the syntax highlighting of Rust is broken, which is why the code above does not color up, although it should. The problem with this piece is: <'_> . If you remove it, then everything is colored normal. But you can’t remove it here. This designation of lifetime (lifetime) is quite a correct syntax for Rust. I do not know where to write it, so I wrote here. Hope repaired.
Just name: mut type in structures cannot be written. Then I tried to make the variable immutable, but this broke another code:

 pub struct Canvas<'_> { renderer: Renderer<'_>, } ... pub fn new(x: u32, y: u32) -> Canvas<'_> { ... let renderer = window.renderer().build().unwrap(); Canvas { renderer: renderer } } pub fn point(&self, x: u32, y: u32) { ... self.renderer.clear(); ... } 

The compiler complained that it cannot borrow the immutable field renderer as changeable ( error: cannot borrow immutable field `self.renderer` as mutable ). Casket opened simply. It turns out in Rust all fields in the structure are considered as changeable or immutable, depending on whether the structure itself was passed to the method as changeable or unchangeable. So the correct code is:

 pub struct Canvas<'_> { renderer: Renderer<'_>, } ... pub fn new(x: u32, y: u32) -> Canvas<'_> { ... let renderer = window.renderer().build().unwrap(); Canvas { renderer: renderer } } pub fn point(&mut self, x: u32, y: u32) { ... self.renderer.clear(); ... } 

As you can see, here I have changed the signature of the point method.

Now a little more about the rake:
There were great difficulties in working with code interacting with the Rust-SDL2 library. She has documentation, but so far there is little to say, and the existence of some methods is generally silent. For example, there is nothing about the sdl2 :: init () method in the documentation. As if it does not exist. The automatic type inference system Rust simplifies and speeds up writing code, but it also played a cruel joke on me when I needed to figure out what type the call returns to sdl2 :: init (). Video (). Unwrap (), because this the result had to be stored in the field in the structure, and there the type is clearly indicated always. I had to read the library source, although a little later I found a less laborious way out. You simply indicate any arbitrary type of the field, Rust when compiling swears at the type mismatch, displaying the type that should be in the error message. Voila!

Special mention deserves such a thing as the lifetime (lifetime) in Rust. I fought with her for a long time. Generally speaking in Rust each variable and link has its lifetime. Simply, it is output automatically by the compiler based on certain rules. However, sometimes it needs to be explicitly indicated Reading an article about the lifetime from the book of rasta did not clarify anything for me. (although I re-read it 3 times) I did not understand why in my case Rust asked for a lifetime. In fact, I simply added these strange <'_> wherever the compiler pointed out an error with an unspecified lifetime, without understanding why he needed it from me. If there are knowledgeable people, I will be glad if you enlighten in the comments. Why exactly underlined, and not some other sign after the apostrophe? Just in the error message about the type mismatch was sdl2::render::Renderer<'_> . At first, I tried to designate the field as just renderer: Renderer , but the compiler scolded me: error: wrong number of lifetime parameters: expected 1, found 0 . UPD: Googolplex explained this point in his comments . The call to window.renderer().build().unwrap() returns sdl2::video::Renderer<'static> . The focus on the lifetime parameter is' static. This is a special lifetime, denoting something that can live to the end of the entire program. Thus, the correct structure declaration looks like this:
 pub struct Canvas { renderer: Renderer<'static>, ... } 

In other parts of the code, all references to the lifetime can be removed.



I think my troubles may seem to someone as experiments of a monkey with a grenade. But I warned you.

Write the line


In general, after solving all the problems described above, the result of my work was what you can see in the repository section . Oh, that feeling of delight when I finally saw on the screen a black window with a small white dot in the center ...
Point


Then it went easier, because serious interaction with the Rust-SDL2 library was no longer required. Its unobvious and undocumented, sometimes even unfinished API was a constant source of difficulties. Any arithmetic and control constructs in Rust are not too different from other PLs. Therefore, the line was written in just a few hours (as opposed to a few days in the previous stages). And most of the work was due to the fact that I decided not to implement the finished algorithm, but to write my own, using the equation of the straight line y = kx + b , as a basis, and deriving all the other formulas myself. This is the function I got in the end:

  pub fn line(&mut self, x1: i32, y1: i32, x2: i32, y2: i32, color: u32) { if (x1 == x2) && (y1 == y2) { self.set(x1, y1, color); } if (x1-x2).abs() > (y1-y2).abs() { if x1 < x2 { let mut yi = y1; for xi in x1..x2+1 { let p = (y2-y1)*xi + x2*y1 - x1*y2; if yi*(x2-x1) < p + (y2-y1)/2 { yi = yi+1; } self.set(xi, yi, color); } } else { let mut yi = y2; for xi in x2..x1+1 { let p = (y1-y2)*xi + x1*y2 - x2*y1; if yi*(x1-x2) < p + (y1-y2)/2 { yi = yi+1; } self.set(xi, yi, color); } } } else { if y1 < y2 { let mut xi = x1; for yi in y1..y2+1 { let p = yi*(x2-x1) - x2*y1 + x1*y2; if xi*(y2-y1) < p + (y2-y1)/2 { xi = xi+1; } self.set(xi, yi, color); } } else { let mut xi = x2; for yi in y2..y1+1 { let p = yi*(x1-x2) - x1*y2 + x2*y1; if xi*(y1-y2) < p + (y1-y2)/2 { xi = xi+1; } self.set(xi, yi, color); } } } } 

I know she is ugly, long, ineffective. But his own - my own. :) I almost immediately came to write the integer version without using real arithmetic. Rust has no implicit type conversions, and explicit ones are very verbose. Therefore, I refused to write the idea of ​​writing the real version first. It is a lot of code, and then all the same as a result to rewrite it, so that it does not slow down.

But the photo of my calculations on a piece of paper (for history)

Connoisseurs of mathematics, please do not kick. Everything was done just for fun.

In the course of writing this footcloth, I needed logging, as there is no time-tested IDE for Rust. There are some fresh ones, but I didn’t want to stick to their beta (alpha?) Test. If someone has used it and saw that it is stable and works well, please post it in the comments. Using a debugger from the console is not my joy. And in general logging thing is useful. I try to train myself to use the logs instead of the debugger, because it helps to write logs, which then really understand what the end user has a problem (at least if he can run debug in the logging mode).

So logging. For logging, Rust has a log library. But this is just an abstract API, and you have to choose the concrete implementation of it yourself. I used env_logger, which is offered in the log documentation. We write the following in cargo.toml:

 [dependencies] sdl2 = "0.5.0" log = "0.3" env_logger = "0.3" 

Using this set is very simple:

 #[macro_use] extern crate log; extern crate env_logger; fn main() { env_logger::init().unwrap(); info!("starting up"); 

You just need to remember that the container (crate) env_logger is configured using environment variables. I use the following command before starting the project:

 set RUST_LOG=rust_project=debug 

This sets the rust_project module with a debug logging level. All other modules remain with the default level, so the log when the program starts is not clogged with any debugging garbage from cargo or sdl. The result of work at this stage can be seen in the repository section . When launching, our program displays this beauty:



Just what was needed in accordance with the original article. Only the color of the second line I changed to blue. The article has already turned out quite long, so for today I’m finishing. Future plans:

Finally, a small portion of personal impressions of the language.

Impressions


All these are my subjective pros and cons. I do not write about what I read somewhere. And only with what he personally encountered during the development. For example, I know that Rust is safe, but so far this chip of his shot has not saved me anywhere in the leg (as far as I can tell), so far I can not appreciate it.
Cons programming on Rust:

Pros:


At last


If I do not weaken, do not tear, do not be angry, I will write a sequel. :) And, of course, if I’m not sent to Read-only for this article.

Source: https://habr.com/ru/post/261629/


All Articles