add basic map rendering with gl
authorfkaa
Fri, 22 Mar 2019 18:01:08 +0200
changeset 14707 29dbe9ce8b7d
parent 14706 5e2c892b0222
child 14708 81030dcbd2d8
add basic map rendering with gl
rust/hwphysics/src/collision.rs
rust/hwrunner/Cargo.toml
rust/hwrunner/src/main.rs
rust/land2d/src/lib.rs
rust/lib-hedgewars-engine/Cargo.toml
rust/lib-hedgewars-engine/src/instance.rs
rust/lib-hedgewars-engine/src/lib.rs
rust/lib-hedgewars-engine/src/render/gl.rs
rust/lib-hedgewars-engine/src/render/map.rs
rust/lib-hedgewars-engine/src/render/mod.rs
rust/lib-hedgewars-engine/src/world.rs
rust/mapgen/src/lib.rs
rust/vec2d/src/lib.rs
--- a/rust/hwphysics/src/collision.rs	Thu Mar 21 01:23:05 2019 +0300
+++ b/rust/hwphysics/src/collision.rs	Fri Mar 22 18:01:08 2019 +0200
@@ -125,4 +125,4 @@
     fn add(&mut self, gear_id: GearId, gear_data: CollisionData) {
         self.grid.insert_static(gear_id, &gear_data.bounds);
     }
-}
\ No newline at end of file
+}
--- a/rust/hwrunner/Cargo.toml	Thu Mar 21 01:23:05 2019 +0300
+++ b/rust/hwrunner/Cargo.toml	Fri Mar 22 18:01:08 2019 +0200
@@ -5,8 +5,9 @@
 edition = "2018"
 
 [dependencies]
-gfx = "0.17"
-glutin = "0.18"
-gfx_window_glutin = "0.26"
+#gfx = "0.17"
+glutin = "0.20"
+gl = "0.11"
+#gfx_window_glutin = "0.26"
 
 lib-hedgewars-engine = { path = "../lib-hedgewars-engine" }
--- a/rust/hwrunner/src/main.rs	Thu Mar 21 01:23:05 2019 +0300
+++ b/rust/hwrunner/src/main.rs	Fri Mar 22 18:01:08 2019 +0200
@@ -1,24 +1,21 @@
 use glutin::{
-    dpi::LogicalSize,
+    dpi,
     Event,
     WindowEvent,
+    DeviceEvent,
+    ElementState,
+    MouseButton,
+    MouseScrollDelta,
     EventsLoop,
-    GlWindow,
-    GlContext
+    WindowedContext,
+    GlRequest,
+    GlProfile,
+    ContextTrait,
 };
 
-use gfx::{
-    texture,
-    format,
-    Encoder,
-    Device
-};
-
-use gfx_window_glutin::init_existing;
-
 use hedgewars_engine::instance::EngineInstance;
 
-fn init(event_loop: &EventsLoop, size: LogicalSize) -> GlWindow {
+fn init(event_loop: &EventsLoop, size: dpi::LogicalSize) -> WindowedContext {
     use glutin::{
         ContextBuilder,
         WindowBuilder
@@ -28,41 +25,96 @@
         .with_title("hwengine")
         .with_dimensions(size);
 
-    let context = ContextBuilder::new();
-    GlWindow::new(window, context, event_loop).unwrap()
+    let cxt = ContextBuilder::new()
+        .with_gl(GlRequest::Latest)
+        .with_gl_profile(GlProfile::Core)
+        .build_windowed(window, &event_loop).ok().unwrap();
+
+    unsafe {
+        cxt.make_current().unwrap();
+        gl::load_with(|ptr| cxt.get_proc_address(ptr) as *const _);
+        
+        if let Some(sz) = cxt.get_inner_size() {
+            let phys = sz.to_physical(cxt.get_hidpi_factor());
+            
+            gl::Viewport(0, 0, phys.width as i32, phys.height as i32);
+        }
+    }
+
+    cxt
 }
 
 fn main() {
     let mut event_loop = EventsLoop::new();
-    let window = init(&event_loop, LogicalSize::new(1024.0, 768.0));
+    let (w, h) = (1024.0, 768.0);
+    let window = init(&event_loop, dpi::LogicalSize::new(w, h));
 
-    let (mut device, mut factory, color_view, depth_view) =
-        init_existing::<format::Rgba8, format::Depth>(&window);
+    let mut engine = EngineInstance::new();
 
-    let mut encoder: Encoder<_, _> = factory.create_command_buffer().into();
+    // dirty dirty code follows; DO NOT USE
+    let mut zoom = 1f32;
+    let mut dragging = false;
+    let mut x = 0f32;
+    let mut y = 0f32;
 
-    let engine = EngineInstance::new();
+    use std::time::Instant;
 
+    let mut now = Instant::now();
+    
     let mut is_running = true;
     while is_running {
+        let curr = Instant::now();
+        let delta = curr - now;
+        now = curr;
+        let ms = delta.as_secs() as f64 * 1000.0 + delta.subsec_millis() as f64;
+        window.set_title(&format!("hwengine {:.3}ms", ms));
+        
         event_loop.poll_events(|event| {
             match event {
                 Event::WindowEvent { event, ..} => match event {
                     WindowEvent::CloseRequested => {
                         is_running = false;
                     },
+                    WindowEvent::MouseInput { button, state, .. } => {
+                        if let MouseButton::Right = button {
+                            if let ElementState::Pressed = state {
+                                dragging = true;
+                            } else {
+                                dragging = false;
+                            }
+                        }
+                    }
+                    WindowEvent::MouseWheel { delta, .. } => {
+                        match delta {
+                            MouseScrollDelta::LineDelta(x, y) => {
+                                zoom += y as f32 * 0.1f32;
+                            }
+                            MouseScrollDelta::PixelDelta(delta) => {
+                                let physical = delta.to_physical(window.get_hidpi_factor());
+                                zoom += physical.y as f32 * 0.1f32;
+                            }
+                        }
+                    }
                     _ => ()
                 },
+                Event::DeviceEvent { event, .. } => match event {
+                    DeviceEvent::MouseMotion { delta } => {
+                        if dragging {
+                            x -= delta.0 as f32;
+                            y -= delta.1 as f32;
+                        }
+                    }
+                    _ => {}
+                }
                 _ => ()
             }
         });
 
-        encoder.clear(&color_view, [0.5, 0.0, 0.0, 1.0]);
-        engine.render(&mut encoder, &color_view);
+        unsafe { window.make_current().unwrap() };
 
-        encoder.flush(&mut device);
+        // temporary params.. dont actually handle input here
+        engine.render(x, y, w as f32 * zoom, h as f32 * zoom);
 
         window.swap_buffers().unwrap();
-        device.cleanup();
     }
 }
--- a/rust/land2d/src/lib.rs	Thu Mar 21 01:23:05 2019 +0300
+++ b/rust/land2d/src/lib.rs	Fri Mar 22 18:01:08 2019 +0200
@@ -30,6 +30,12 @@
         &self.pixels.as_slice()
     }
 
+    pub fn raw_pixel_bytes(&self) -> &[u8] {
+        unsafe {
+            self.pixels.as_bytes()
+        }
+    }
+
     #[inline]
     pub fn width(&self) -> usize {
         self.pixels.width()
--- a/rust/lib-hedgewars-engine/Cargo.toml	Thu Mar 21 01:23:05 2019 +0300
+++ b/rust/lib-hedgewars-engine/Cargo.toml	Fri Mar 22 18:01:08 2019 +0200
@@ -5,8 +5,9 @@
 edition = "2018"
 
 [dependencies]
-gfx = "0.17"
-gfx_device_gl = "0.15"
+gl = "0.11"
+#gfx = "0.17"
+#gfx_device_gl = "0.15"
 netbuf = "0.4"
 
 fpnum = { path = "../fpnum" }
@@ -16,6 +17,8 @@
 landgen = { path = "../landgen" }
 hedgewars-engine-messages = { path = "../hedgewars-engine-messages" }
 hwphysics = { path = "../hwphysics" }
+mapgen = { path = "../mapgen" }
+vec2d = { path = "../vec2d" }
 
 [lib]
 name = "hedgewars_engine"
--- a/rust/lib-hedgewars-engine/src/instance.rs	Thu Mar 21 01:23:05 2019 +0300
+++ b/rust/lib-hedgewars-engine/src/instance.rs	Fri Mar 22 18:01:08 2019 +0200
@@ -3,45 +3,43 @@
     UnorderedEngineMessage::*, UnsyncedEngineMessage::*, *,
 };
 
-use self::gfx_gl::{CommandBuffer, Resources};
-use gfx::format::{Unorm, D24, R8_G8_B8_A8};
-use gfx_device_gl as gfx_gl;
+use landgen::outline_template::OutlineTemplate;
+use integral_geometry::{Point, Rect, Size};
 
 use super::{ipc::IPC, world::World};
 
-pub struct EngineGlContext {
-    pub device: gfx_gl::Device,
-    pub factory: gfx_gl::Factory,
-    pub render_target: gfx::handle::RenderTargetView<Resources, (R8_G8_B8_A8, Unorm)>,
-    pub depth_buffer: gfx::handle::DepthStencilView<Resources, (D24, Unorm)>,
-    pub command_buffer: gfx::Encoder<Resources, CommandBuffer>,
-}
-
 pub struct EngineInstance {
     pub world: World,
     pub ipc: IPC,
-    pub gl_context: Option<EngineGlContext>,
 }
 
 impl EngineInstance {
     pub fn new() -> Self {
-        let world = World::new();
+        let mut world = World::new();
+
+        fn template() -> OutlineTemplate {
+            let mut template = OutlineTemplate::new(Size::new(4096*1, 2048*1));
+            template.islands = vec![vec![
+                Rect::from_size_coords(100, 2050, 1, 1),
+                Rect::from_size_coords(100, 500, 400, 1200),
+                Rect::from_size_coords(3600, 500, 400, 1200),
+                Rect::from_size_coords(3900, 2050, 1, 1),
+            ]];
+            template.fill_points = vec![Point::new(1, 0)];
+
+            template
+        }
+        
+        world.init(template());
+        
         Self {
             world,
             ipc: IPC::new(),
-            gl_context: None,
         }
     }
 
-    pub fn render<R, C>(
-        &self,
-        command_buffer: &mut gfx::Encoder<R, C>,
-        render_target: &gfx::handle::RenderTargetView<R, gfx::format::Rgba8>,
-    ) where
-        R: gfx::Resources,
-        C: gfx::CommandBuffer<R>,
-    {
-        command_buffer.clear(render_target, [0.0, 0.5, 0.0, 1.0]);
+    pub fn render(&mut self, x: f32, y: f32, w: f32, h: f32) {
+        self.world.render(x, y, w, h);
     }
 
     fn process_unordered_message(&mut self, message: &UnorderedEngineMessage) {
--- a/rust/lib-hedgewars-engine/src/lib.rs	Thu Mar 21 01:23:05 2019 +0300
+++ b/rust/lib-hedgewars-engine/src/lib.rs	Fri Mar 22 18:01:08 2019 +0200
@@ -3,7 +3,6 @@
 mod render;
 mod world;
 
-use gfx::{format::Formatted, Encoder};
 use std::{
     ffi::CString,
     io::{Read, Write},
@@ -11,9 +10,7 @@
     os::raw::{c_char, c_void},
 };
 
-use gfx_device_gl as gfx_gl;
-
-use self::instance::{EngineGlContext, EngineInstance};
+use self::instance::{EngineInstance};
 
 #[repr(C)]
 #[derive(Copy, Clone)]
@@ -84,36 +81,11 @@
     height: u16,
     gl_loader: extern "C" fn(*const c_char) -> *const c_void,
 ) {
-    let (device, mut factory) = gfx_gl::create(|name| {
-        let c_name = CString::new(name).unwrap();
-        gl_loader(c_name.as_ptr())
-    });
-
-    let dimensions = (width, height, 1u16, gfx::texture::AaMode::Single);
-    let (render_target, depth_buffer) = gfx_gl::create_main_targets_raw(
-        dimensions,
-        gfx::format::Rgba8::get_format().0,
-        gfx::format::Depth::get_format().0,
-    );
-
-    let mut command_buffer: Encoder<_, _> = factory.create_command_buffer().into();
-
-    engine_state.gl_context = Some(EngineGlContext {
-        device,
-        factory,
-        render_target: gfx::memory::Typed::new(render_target),
-        depth_buffer: gfx::memory::Typed::new(depth_buffer),
-        command_buffer,
-    })
 }
 
 #[no_mangle]
 pub extern "C" fn render_frame(engine_state: &mut EngineInstance) {
-    let mut context = replace(&mut engine_state.gl_context, None);
-    if let Some(ref mut c) = context {
-        engine_state.render(&mut c.command_buffer, &mut c.render_target)
-    }
-    replace(&mut engine_state.gl_context, context);
+    //engine_state.render()
 }
 
 #[no_mangle]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rust/lib-hedgewars-engine/src/render/gl.rs	Fri Mar 22 18:01:08 2019 +0200
@@ -0,0 +1,419 @@
+ use integral_geometry::Rect;
+
+use std::{
+    mem,
+    slice,
+    ptr,
+    ffi,
+    ffi::CString,
+};
+
+#[derive(Debug)]
+pub struct Texture2D {
+    pub handle: u32,
+}
+
+impl Drop for Texture2D {
+    fn drop(&mut self) {
+        if self.handle != 0 {
+            unsafe {
+                gl::DeleteTextures(1, &self.handle);
+            }
+        }
+    }
+}
+
+impl Texture2D {
+    pub fn with_data(
+        data: &[u8],
+        data_stride: u32,
+        width: u32,
+        height: u32,
+        internal_format: u32,
+        format: u32,
+        ty: u32,
+        filter: u32
+    ) -> Self {
+        let mut handle = 0;
+        
+        unsafe {
+            gl::GenTextures(1, &mut handle);
+            
+            gl::BindTexture(gl::TEXTURE_2D, handle);
+            gl::PixelStorei(gl::UNPACK_ROW_LENGTH, data_stride as i32);
+            gl::TexImage2D(
+                gl::TEXTURE_2D,
+                0,
+                internal_format as i32,
+                width as i32,
+                height as i32,
+                0,
+                format as u32,
+                ty,
+                data.as_ptr() as *const _
+            );
+
+            gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_WRAP_S, gl::CLAMP_TO_EDGE as i32);
+            gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_WRAP_T, gl::CLAMP_TO_EDGE as i32);
+            gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MIN_FILTER, filter as i32);
+            gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MAG_FILTER, filter as i32);
+        }
+
+        Texture2D {
+            handle
+        }
+    }
+
+    pub fn update(&self, region: Rect, data: &[u8], data_stride: u32, format: u32, ty: u32) {
+        unsafe {
+            gl::BindTexture(gl::TEXTURE_2D, self.handle);
+            gl::PixelStorei(gl::UNPACK_ROW_LENGTH, data_stride as i32);
+            gl::TexSubImage2D(
+                gl::TEXTURE_2D,
+                0, // texture level
+                region.left(), // texture region
+                region.top(),
+                region.width() as i32 - 1,
+                region.height() as i32 - 1,
+                format, // data format
+                ty, // data type
+                data.as_ptr() as *const _, // data ptr
+            );
+        }
+    }
+}
+
+#[derive(Debug)]
+pub struct Buffer {
+    pub handle: u32,
+    pub ty: u32,
+    pub usage: u32,
+}
+
+impl Buffer {
+    pub fn empty(
+        ty: u32,
+        usage: u32,
+        //size: isize
+    ) -> Buffer {
+        let mut buffer = 0;
+
+        unsafe {
+            gl::GenBuffers(1, &mut buffer);
+            gl::BindBuffer(ty, buffer);
+            //gl::BufferData(ty, size, ptr::null_mut(), usage);
+        }
+
+        Buffer {
+            handle: buffer,
+            ty,
+            usage,
+        }
+    }
+    
+    fn with_data(
+        ty: u32,
+        usage: u32,
+        data: &[u8]
+    ) -> Buffer {
+        let mut buffer = 0;
+
+        unsafe {
+            gl::GenBuffers(1, &mut buffer);
+            gl::BindBuffer(ty, buffer);
+            gl::BufferData(ty, data.len() as isize, data.as_ptr() as _, usage);
+        }
+
+        Buffer {
+            handle: buffer,
+            ty,
+            usage
+        }
+    }
+
+    pub fn ty(&self) -> u32 {
+        self.ty
+    }
+
+    pub fn handle(&self) -> u32 {
+        self.handle
+    }
+
+    pub fn write_typed<T>(&self, data: &[T]) {
+        unsafe {
+            let data = slice::from_raw_parts(
+                data.as_ptr() as *const u8,
+                data.len() * mem::size_of::<T>(),
+            );
+            
+            gl::BindBuffer(self.ty, self.handle);
+            gl::BufferData(self.ty, data.len() as isize, data.as_ptr() as *const _ as *const _, self.usage);
+        }
+    }
+    
+    pub fn write(&self, data: &[u8]) {        
+        unsafe {
+            gl::BindBuffer(self.ty, self.handle);
+            gl::BufferData(self.ty, data.len() as isize, data.as_ptr() as *const _ as *const _, self.usage);
+        }
+    }
+}
+
+impl Drop for Buffer {
+    fn drop(&mut self) {
+        unsafe {
+            gl::DeleteBuffers(1, &self.handle);
+        }
+    }
+}
+
+#[derive(Debug)]
+pub enum VariableBinding<'a> {
+    Attribute(&'a str, u32),
+    Uniform(&'a str, u32),
+    UniformBlock(&'a str, u32),
+    Sampler(&'a str, u32),
+}
+
+#[derive(Debug)]
+pub struct Shader {
+    pub program: u32,
+}
+
+impl Drop for Shader {
+    fn drop(&mut self) {
+        unsafe {
+            gl::DeleteProgram(self.program);
+        }
+    }
+}
+
+impl Shader {
+    pub fn new<'a>(
+        vs: &str,
+        ps: Option<&str>,
+        bindings: &[VariableBinding<'a>]
+    ) -> Result<Self, String> {
+        unsafe fn compile_shader(ty: u32, shdr: &str) -> Result<u32, String> {
+            let shader = gl::CreateShader(ty);
+            let len = shdr.len() as i32;
+            let shdr = shdr.as_ptr() as *const i8;
+            gl::ShaderSource(shader, 1, &shdr, &len);
+            gl::CompileShader(shader);
+
+            let mut success = 0i32;
+            gl::GetShaderiv(shader, gl::COMPILE_STATUS, &mut success as _);
+
+            if success == gl::FALSE as i32 {
+                let mut log_size = 0i32;
+                gl::GetShaderiv(shader, gl::INFO_LOG_LENGTH, &mut log_size as _);
+
+                let mut log = vec![0u8; log_size as usize];
+                gl::GetShaderInfoLog(shader, log_size, ptr::null_mut(), log.as_mut_ptr() as _);
+
+                gl::DeleteShader(shader);
+                Err(String::from_utf8_unchecked(log))
+            } else {
+                Ok(shader)
+            }
+        }
+        
+        let vs = unsafe { compile_shader(gl::VERTEX_SHADER, vs)? };
+        let ps = if let Some(ps) = ps {
+            Some(unsafe { compile_shader(gl::FRAGMENT_SHADER, ps)? })
+        } else {
+            None
+        };
+
+        unsafe {
+            let program = gl::CreateProgram();
+            
+            gl::AttachShader(program, vs);
+            if let Some(ps) = ps {
+                gl::AttachShader(program, ps);
+            }
+
+            for bind in bindings {
+                match bind {
+                    &VariableBinding::Attribute(ref name, id) => {
+                        let c_str = CString::new(name.as_bytes()).unwrap();
+                        gl::BindAttribLocation(program, id, c_str.to_bytes_with_nul().as_ptr() as *const _);     
+                    },
+                    _ => {}
+                }
+            }
+
+            gl::LinkProgram(program);
+
+            let mut success = 0i32;
+            gl::GetProgramiv(program, gl::LINK_STATUS, &mut success);
+            if success == gl::FALSE as i32 {
+                let mut log_size = 0i32;
+                gl::GetProgramiv(program, gl::INFO_LOG_LENGTH, &mut log_size as _);
+
+                let mut log = vec![0u8; log_size as usize];
+                gl::GetProgramInfoLog(program, log_size, ptr::null_mut(), log.as_mut_ptr() as _);
+
+                gl::DeleteProgram(program);
+                return Err(String::from_utf8_unchecked(log));
+            }
+
+            //gl::DetachShader(program, vs);
+            if let Some(ps) = ps {
+                //gl::DetachShader(program, ps);
+            }
+
+            gl::UseProgram(program);
+
+            // after linking we setup sampler bindings as specified in the shader
+            for bind in bindings {
+                match bind {
+                    VariableBinding::Uniform(name, id) => {
+                        let c_str = CString::new(name.as_bytes()).unwrap();
+                        let index = gl::GetUniformLocation(program, c_str.to_bytes_with_nul().as_ptr() as *const _);
+
+                        // TODO: impl for block?
+                    },
+                    VariableBinding::UniformBlock(name, id) => {
+                        let c_str = CString::new(name.as_bytes()).unwrap();
+                        let index = gl::GetUniformBlockIndex(program, c_str.to_bytes_with_nul().as_ptr() as *const _);
+
+                        gl::UniformBlockBinding(program, index, *id);
+                    }
+                    VariableBinding::Sampler(name, id) => {
+                        let c_str = CString::new(name.as_bytes()).unwrap();
+                        let index = gl::GetUniformLocation(program, c_str.to_bytes_with_nul().as_ptr() as *const _);
+                        
+                        gl::Uniform1i(index, *id as i32);
+                    },
+                    _ => {}
+                }
+            }
+
+            Ok(Shader {
+                program
+            })
+        }
+    }
+
+    pub fn bind(&self) {
+        unsafe {
+            gl::UseProgram(self.program);
+        }
+    }
+
+    pub fn set_matrix(&self, name: &str, matrix: *const f32) {
+        unsafe {
+            let c_str = CString::new(name).unwrap();
+            let index = gl::GetUniformLocation(self.program, c_str.to_bytes_with_nul().as_ptr() as *const _);
+            
+            gl::UniformMatrix4fv(index, 1, gl::FALSE, matrix);
+        }
+    }
+
+    pub fn bind_texture_2d(&self, index: u32, texture: &Texture2D) {
+        self.bind();
+        
+        unsafe {
+            gl::ActiveTexture(gl::TEXTURE0 + index);
+            gl::BindTexture(gl::TEXTURE_2D, texture.handle);
+        }
+    }
+}
+
+pub enum InputFormat {
+    Float(u32, bool),
+    Integer(u32),
+}
+
+pub struct InputElement {
+    pub shader_slot: u32,
+    pub buffer_slot: u32,
+    pub format: InputFormat,
+    pub components: u32,
+    pub stride: u32,
+    pub offset: u32,
+}
+
+// TODO: 
+pub struct InputLayout {
+    pub elements: Vec<InputElement>,
+}
+
+pub struct LayoutGuard {
+    vao: u32
+}
+
+impl Drop for LayoutGuard {
+    fn drop(&mut self) {
+        unsafe {
+            gl::DeleteVertexArrays(1, [self.vao].as_ptr());
+            gl::BindVertexArray(0);
+        }
+    }
+}
+
+impl InputLayout {
+    pub fn new(elements: Vec<InputElement>) -> Self {
+        InputLayout {
+            elements,
+        }
+    }
+
+    pub fn bind(&mut self, buffers: &[(u32, &Buffer)], index_buffer: Option<&Buffer>) -> LayoutGuard {
+        let mut vao = 0;
+        
+        unsafe {
+            gl::GenVertexArrays(1, &mut vao);
+            gl::BindVertexArray(vao);
+        }
+        
+        for &(slot, ref buffer) in buffers {
+            unsafe {
+                gl::BindBuffer(buffer.ty(), buffer.handle());
+            }
+            
+            for attr in self.elements.iter().filter(|a| a.buffer_slot == slot) {
+                unsafe {
+                    gl::EnableVertexAttribArray(attr.shader_slot);
+                    match attr.format {
+                        InputFormat::Float(fmt, normalized) => {
+                            gl::VertexAttribPointer(
+                                attr.shader_slot,
+                                attr.components as i32,
+                                fmt,
+                                if normalized {
+                                    gl::TRUE
+                                } else {
+                                    gl::FALSE
+                                },
+                                attr.stride as i32,
+                                attr.offset as *const _
+                            );
+                        }
+                        InputFormat::Integer(fmt) => {
+                            gl::VertexAttribIPointer(
+                                attr.shader_slot,
+                                attr.components as i32,
+                                fmt,
+                                attr.stride as i32,
+                                attr.offset as *const _
+                            );
+                        }
+                    }
+
+                }
+            }
+        }
+
+        if let Some(buf) = index_buffer {
+            unsafe {
+                gl::BindBuffer(gl::ELEMENT_ARRAY_BUFFER, buf.handle());
+            }
+        }
+
+        LayoutGuard {
+            vao
+        }
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rust/lib-hedgewars-engine/src/render/map.rs	Fri Mar 22 18:01:08 2019 +0200
@@ -0,0 +1,300 @@
+use integral_geometry::{Point, Rect, Size};
+use land2d::{Land2D};
+use vec2d::{Vec2D};
+
+use super::gl::{
+    Texture2D,
+    Buffer,
+    Shader,
+    InputLayout,
+    VariableBinding,
+    InputElement,
+    InputFormat,
+};
+
+// TODO: temp
+const VERTEX_SHADER: &'static str = r#"
+#version 150
+
+in vec2 Position;
+in vec3 Uv;
+
+out vec3 a_Uv;
+
+//uniform Common {
+uniform mat4 Projection;
+//};
+
+void main()
+{
+	a_Uv = Uv;
+	gl_Position = Projection * vec4(Position, 0.0, 1.0);
+}
+"#;
+
+const PIXEL_SHADER: &'static str = r#"
+#version 150
+
+in vec3 a_Uv;
+
+uniform sampler2D Texture;
+
+out vec4 Target;
+
+void main()
+{
+	 Target = texture2D(Texture, a_Uv.xy);
+}
+"#;
+
+pub struct MapTile {
+    // either index into GL texture array or emulated [Texture; N]
+    texture_index: u32,
+
+    width: u32,
+    height: u32,
+}
+
+#[repr(C)]
+#[derive(Copy, Clone)]
+pub struct TileVertex {
+    pos: [f32; 2],
+    // doesn't hurt to include another float, just in case..
+    uv: [f32; 3],
+}
+
+pub struct DrawTile {
+    texture_index: u32,
+    index_len: u32,
+}
+
+pub struct MapRenderer {
+    tiles: Vec<MapTile>,
+    textures: Vec<Texture2D>,
+
+    tile_vertex_buffer: Buffer,
+    tile_index_buffer: Buffer,
+    tile_vertices: Vec<TileVertex>,
+    tile_indices: Vec<u16>,
+    tile_draw_calls: Vec<DrawTile>,
+    index_offset: u16,
+    tile_shader: Shader,
+    tile_layout: InputLayout,
+    
+    tile_width: u32,
+    tile_height: u32,
+    num_tile_x: i32,
+}
+
+impl MapRenderer {
+    pub fn new(tile_width: u32, tile_height: u32) -> Self {
+        let tile_shader = Shader::new(
+            VERTEX_SHADER,
+            Some(PIXEL_SHADER),
+            &[
+                VariableBinding::Attribute("Position", 0),
+                VariableBinding::Attribute("Uv", 1),
+                VariableBinding::Sampler("Texture", 0),
+            ]
+        ).unwrap();
+
+        let tile_layout = InputLayout::new(vec![
+            // position
+            InputElement {
+                shader_slot: 0,
+                buffer_slot: 0,
+                format: InputFormat::Float(gl::FLOAT, false),
+                components: 2,
+                stride: 20,
+                offset: 0
+            },
+            // uv
+            InputElement {
+                shader_slot: 1,
+                buffer_slot: 0,
+                format: InputFormat::Float(gl::FLOAT, false),
+                components: 3,
+                stride: 20,
+                offset: 8
+            },
+        ]);
+        
+        MapRenderer {
+            tiles: Vec::new(),
+            textures: Vec::new(),
+            
+            tile_vertex_buffer: Buffer::empty(gl::ARRAY_BUFFER, gl::DYNAMIC_DRAW),
+            tile_index_buffer: Buffer::empty(gl::ELEMENT_ARRAY_BUFFER, gl::DYNAMIC_DRAW),
+            tile_vertices: Vec::new(),
+            tile_indices: Vec::new(),
+            index_offset: 0,
+
+            tile_draw_calls: Vec::new(),
+            tile_shader,
+            tile_layout,
+
+            tile_width,
+            tile_height,
+            num_tile_x: 0,
+        }
+    }
+
+    pub fn init(&mut self, land: &Vec2D<u32>) {
+        // clear tiles, but keep our textures for potential re-use
+        self.tiles.clear();
+
+        let tw = self.tile_width as usize;
+        let th = self.tile_height as usize;
+        let lw = land.width();
+        let lh = land.height();
+        let num_tile_x = lw / tw + if lw % tw != 0 { 1 } else { 0 };
+        let num_tile_y = lh / th + if lh % th != 0 { 1 } else { 0 };
+
+        self.num_tile_x = num_tile_x as i32;
+
+        for y in 0..num_tile_y {
+            for x in 0..num_tile_x {
+                let idx = x + y * num_tile_x;
+
+                let (data, stride) = {
+                    let bpp = 4;
+
+                    let offset = x * tw * bpp + y * th * lw * bpp;
+
+                    let data = unsafe { &land.as_bytes()[offset..] };
+                    let stride = land.width();
+
+                    (data, stride as u32)
+                };
+                
+                let texture_index = if idx >= self.textures.len() {
+                    let texture = Texture2D::with_data(
+                        data,
+                        stride,
+                        self.tile_width,
+                        self.tile_height,
+                        gl::RGBA8,
+                        gl::RGBA,
+                        gl::UNSIGNED_BYTE,
+                        gl::NEAREST
+                    );
+
+                    let texture_index = self.textures.len();
+                    self.textures.push(texture);
+
+                    texture_index
+                } else {
+                    let texture_region = Rect::new(
+                        Point::new(0, 0),
+                        Point::new(self.tile_width as i32, self.tile_height as i32)
+                    );
+
+                    self.textures[idx].update(texture_region, data, stride, gl::RGBA, gl::UNSIGNED_BYTE);
+                    idx
+                };
+
+                let tile = MapTile {
+                    texture_index: texture_index as u32,
+                    
+                    // TODO: are there ever non-power of two textures?
+                    width: self.tile_width,
+                    height: self.tile_height,
+                };
+                self.tiles.push(tile);
+            }
+        }
+    }
+
+    pub fn update(&mut self, land: &Land2D<u32>, region: Rect) {
+
+    }
+
+    pub fn render(&mut self, viewport: Rect) {
+        self.tile_vertices.clear();
+        self.tile_indices.clear();
+        self.tile_draw_calls.clear();
+        self.index_offset = 0;
+        
+        for (idx, tile) in self.tiles.iter().enumerate() {
+            let tile_x = idx as i32 % self.num_tile_x;
+            let tile_y = idx as i32 / self.num_tile_x;
+            let tile_w = self.tile_width as i32;
+            let tile_h = self.tile_height as i32;
+
+            let origin = Point::new(tile_x * tile_w, tile_y * tile_h);
+            let tile_rect = Rect::new(origin, origin + Point::new(tile_w, tile_h));
+
+            if viewport.intersects(&tile_rect) {
+                // lazy
+                //dbg!(origin);
+                let tile_x = origin.x as f32;
+                let tile_y = origin.y as f32;
+                let tile_w = tile_x + tile_w as f32;
+                let tile_h = tile_y + tile_h as f32;
+                let uv_depth = tile.texture_index as f32;
+
+                //dbg!(tile_x);
+                let tl = TileVertex { pos: [tile_x, tile_y], uv: [0f32, 0f32, uv_depth] };
+                let bl = TileVertex { pos: [tile_x, tile_h], uv: [0f32, 1f32, uv_depth] };
+                let br = TileVertex { pos: [tile_w, tile_h], uv: [1f32, 1f32, uv_depth] };
+                let tr = TileVertex { pos: [tile_w, tile_y], uv: [1f32, 0f32, uv_depth] };
+
+                self.tile_vertices.extend(&[tl, bl, br, tr]);
+
+                let i = self.index_offset;
+                self.tile_indices.extend(&[
+                    i + 0, i + 1, i + 2,
+                    i + 2, i + 3, i + 0,
+                ]);
+                self.index_offset += 4;
+
+                self.tile_draw_calls.push(DrawTile {
+                    texture_index: tile.texture_index,
+                    index_len: 6
+                });
+            }
+        }
+
+        self.tile_vertex_buffer.write_typed(&self.tile_vertices);
+        self.tile_index_buffer.write_typed(&self.tile_indices);
+
+        let _g = self.tile_layout.bind(&[
+            (0, &self.tile_vertex_buffer)
+        ], Some(&self.tile_index_buffer));
+
+        let ortho = {
+            let l = viewport.left() as f32;
+            let r = viewport.right() as f32;
+            let b = viewport.bottom() as f32;
+            let t = viewport.top() as f32;
+
+            [
+                2f32 / (r - l),    0f32,              0f32,   0f32,
+                0f32,              2f32 / (t - b),    0f32,   0f32,
+                0f32,              0f32,              0.5f32, 0f32,
+                (r + l) / (l - r), (t + b) / (b - t), 0.5f32, 1f32,
+            ]
+        };
+
+        self.tile_shader.bind();
+        self.tile_shader.set_matrix("Projection", ortho.as_ptr());
+        
+        let mut draw_offset = 0;
+        for draw_call in &self.tile_draw_calls {
+            unsafe {
+                self.tile_shader.bind_texture_2d(0, &self.textures[draw_call.texture_index as usize]);
+                
+                gl::DrawElements(
+                    gl::TRIANGLES,
+                    draw_call.index_len as i32,
+                    gl::UNSIGNED_SHORT,
+                    draw_offset as *const _
+                );
+            }
+
+            draw_offset += draw_call.index_len * 2;
+        }
+    }
+}
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rust/lib-hedgewars-engine/src/render/mod.rs	Fri Mar 22 18:01:08 2019 +0200
@@ -0,0 +1,5 @@
+mod map;
+mod gl;
+
+pub use self::map::*;
+use self::gl::*;
--- a/rust/lib-hedgewars-engine/src/world.rs	Thu Mar 21 01:23:05 2019 +0300
+++ b/rust/lib-hedgewars-engine/src/world.rs	Fri Mar 22 18:01:08 2019 +0200
@@ -10,6 +10,8 @@
 use lfprng::LaggedFibonacciPRNG;
 use hwphysics as hwp;
 
+use crate::render::MapRenderer;
+
 struct GameState {
     land: Land2D<u32>,
     physics: hwp::World,
@@ -28,6 +30,7 @@
     random_numbers_gen: LaggedFibonacciPRNG,
     preview: Option<Land2D<u8>>,
     game_state: Option<GameState>,
+    renderer: MapRenderer,
 }
 
 impl World {
@@ -36,6 +39,7 @@
             random_numbers_gen: LaggedFibonacciPRNG::new(&[]),
             preview: None,
             game_state: None,
+            renderer: MapRenderer::new(512, 512),
         }
     }
 
@@ -77,12 +81,38 @@
         let landgen = TemplatedLandGenerator::new(template);
         let land = landgen.generate_land(&params, &mut self.random_numbers_gen);
 
+        use mapgen::{
+            MapGenerator,
+            theme::{Theme, slice_u32_to_u8}
+        };
+
+        use std::path::Path;
+        
+        let theme = Theme::load(Path::new("../../share/hedgewars/Data/Themes/Cheese/")).unwrap();
+        let texture = MapGenerator::new().make_texture32(&land, &theme);
+        self.renderer.init(&texture);
+        
         self.game_state = Some(GameState::new(land, physics));
     }
 
+    pub fn render(&mut self, x: f32, y: f32, w: f32, h: f32) {
+        unsafe {
+            gl::ClearColor(0.4f32, 0f32, 0.2f32, 1f32);
+            gl::Clear(gl::COLOR_BUFFER_BIT);
+        }
+
+        self.renderer.render(Rect::new(
+            Point::new(x as _, y as _),
+            Point::new((x + w) as _, (y + h) as _),
+        ));
+    }
+
     pub fn step(&mut self) {
         if let Some(ref mut state) = self.game_state {
             state.physics.step(fp!(1), &state.land);
         }
     }
 }
+
+
+
--- a/rust/mapgen/src/lib.rs	Thu Mar 21 01:23:05 2019 +0300
+++ b/rust/mapgen/src/lib.rs	Fri Mar 22 18:01:08 2019 +0200
@@ -170,6 +170,72 @@
 
         texture
     }
+
+    // TODO: no way to pass both u8 & u32?
+    pub fn make_texture32(&self, land: &Land2D<u32>, theme: &Theme) -> Vec2D<u32> {
+        let mut texture = Vec2D::new(land.size(), 0);
+
+        if let Some(land_sprite) = theme.land_texture() {
+            for (row_index, (land_row, tex_row)) in land.rows()
+                .zip(texture.rows_mut())
+                .enumerate()
+            {
+                let sprite_row = land_sprite.get_row(row_index % land_sprite.height());
+                let mut x_offset = 0;
+                while sprite_row.len() < land.width() - x_offset {
+                    let copy_range = x_offset..x_offset + sprite_row.len();
+                    tex_row_copy32(
+                        &land_row[copy_range.clone()],
+                        &mut tex_row[copy_range],
+                        sprite_row
+                    );
+
+                    x_offset += land_sprite.width()
+                }
+
+                if x_offset < land.width() {
+                    let final_range = x_offset..land.width();
+                    tex_row_copy32(
+                        &land_row[final_range.clone()],
+                        &mut tex_row[final_range],
+                        &sprite_row[..land.width() - x_offset]
+                    );
+                }
+            }
+        }
+
+        if let Some(border_sprite) = theme.border_texture() {
+            assert!(border_sprite.height() <= 512);
+            let border_width = (border_sprite.height() / 2) as u8;
+            let border_sprite = border_sprite.to_tiled();
+
+            let mut offsets = vec![255u8; land.width()];
+
+            land_border_pass32(
+                land.rows().rev().zip(texture.rows_mut().rev()),
+                &mut offsets,
+                border_width,
+                |x, y| border_sprite.get_pixel(
+                    x % border_sprite.width(),
+                    border_sprite.height() - 1 - y,
+                )
+            );
+
+            offsets.iter_mut().for_each(|v| *v = 255);
+
+            land_border_pass32(
+                land.rows().zip(texture.rows_mut()),
+                &mut offsets,
+                border_width,
+                |x, y| border_sprite.get_pixel(
+                    x % border_sprite.width(),
+                    y,
+                )
+            );
+        }
+
+        texture
+    }
 }
 
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -238,6 +304,31 @@
     }
 }
 
+fn land_border_pass32<'a, T, F>(rows: T, offsets: &mut [u8], border_width: u8, pixel_getter: F)
+    where T: Iterator<Item = (&'a [u32], &'a mut [u32])>,
+          F: (Fn(usize, usize) -> u32)
+{
+    for (land_row, tex_row) in rows {
+        for (x, ((land_v, tex_v), offset_v)) in land_row.iter()
+            .zip(tex_row.iter_mut())
+            .zip(offsets.iter_mut())
+            .enumerate()
+        {
+            *offset_v = if *land_v == 0 {
+                if *offset_v < border_width {
+                    *tex_v = blend(
+                        pixel_getter(x, *offset_v as usize),
+                        *tex_v,
+                    )
+                }
+                offset_v.saturating_add(1)
+            } else {
+                0
+            }
+        }
+    }
+}
+
 fn tex_row_copy(land_row: &[u8], tex_row: &mut [u32], sprite_row: &[u32]) {
     for ((land_v, tex_v), sprite_v) in
         land_row.iter().zip(tex_row.iter_mut()).zip(sprite_row)
@@ -250,6 +341,18 @@
     }
 }
 
+fn tex_row_copy32(land_row: &[u32], tex_row: &mut [u32], sprite_row: &[u32]) {
+    for ((land_v, tex_v), sprite_v) in
+        land_row.iter().zip(tex_row.iter_mut()).zip(sprite_row)
+    {
+        *tex_v = if *land_v == 0 {
+            *sprite_v
+        } else {
+            0
+        }
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use crate::{
--- a/rust/vec2d/src/lib.rs	Thu Mar 21 01:23:05 2019 +0300
+++ b/rust/vec2d/src/lib.rs	Fri Mar 22 18:01:08 2019 +0200
@@ -95,6 +95,19 @@
         let width = self.width();
         self.data.chunks_exact_mut(width)
     }
+
+    #[inline]
+    pub unsafe fn as_bytes(&self) -> &[u8] {
+        use std::{
+            slice,
+            mem
+        };
+        
+        slice::from_raw_parts(
+            self.data.as_ptr() as *const u8,
+            self.data.len() * mem::size_of::<T>(),
+        )
+    }
 }
 
 impl<T: Copy> AsRef<[T]> for Vec2D<T> {